Merge branch 'dev' into feat/lam-1291/stress-testing
* dev: (85 commits) chore: console.log debug leftovers fix: third level navigation links fix: show subheader on refresh fix: machines/:id routing fix: customer route chore: update wallet nodes feat: shorten long addresses in funding page feat: shorten long addresses refactor: support copied text different from presented text chore: udpate react, downshift and routing refactor: use Wizard component on first route fix: autocomplete component rendering feat: skip2fa option on .env fix: drop contraint before dropping index chore: stop using alias imports fix: re-instate urlResolver chore: server code formatting chore: reformat code chore: adding eslint and prettier config chore: typo ...
This commit is contained in:
commit
e10493abc6
1398 changed files with 60329 additions and 157527 deletions
|
|
@ -1,5 +1,4 @@
|
|||
./node_modules
|
||||
./new-lamassu-admin/node_modules
|
||||
**/node_modules
|
||||
.git
|
||||
.direnv
|
||||
.envrc
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -1,10 +1,9 @@
|
|||
node_modules
|
||||
**/node_modules
|
||||
**/.env
|
||||
|
||||
.idea/
|
||||
.settings/
|
||||
|
||||
certs/
|
||||
tests/stress/machines
|
||||
tests/stress/config.json
|
||||
|
||||
.env
|
||||
packages/server/certs/
|
||||
packages/server/tests/stress/machines
|
||||
packages/server/tests/stress/config.json
|
||||
|
|
|
|||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
|
|
@ -11,7 +11,7 @@ sudo -u postgres createdb lamassu
|
|||
sudo -u postgres psql postgres
|
||||
```
|
||||
|
||||
In ``psql``, run the following and set password to ``postgres123``:
|
||||
In `psql`, run the following and set password to `postgres123`:
|
||||
|
||||
```
|
||||
\password postgres
|
||||
|
|
@ -20,14 +20,12 @@ ctrl-d
|
|||
|
||||
### Starting up environment
|
||||
|
||||
shell.nix script provided, all you need to do to setup the environment is to run `nix-shell` on the folder.
|
||||
shell.nix script provided, all you need to do to setup the environment is to run `nix-shell` on the folder.
|
||||
|
||||
## Installation
|
||||
|
||||
### Install node modules
|
||||
|
||||
Make sure you're running NodeJS 8.3 or higher. Ignore any warnings.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
|
@ -35,25 +33,25 @@ npm install
|
|||
### Generate certificates
|
||||
|
||||
```
|
||||
bash tools/cert-gen.sh
|
||||
bash packages/server/tools/cert-gen.sh
|
||||
```
|
||||
|
||||
Notes:
|
||||
- This will create a ``.lamassu`` directory in your home directory.
|
||||
|
||||
Notes:
|
||||
|
||||
- This will create a `.lamassu` directory in your home directory.
|
||||
|
||||
### Set up database
|
||||
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway.
|
||||
|
||||
```
|
||||
node bin/lamassu-migrate
|
||||
node packages/server/bin/lamassu-migrate
|
||||
```
|
||||
|
||||
### Run new-lamassu-admin
|
||||
|
||||
```
|
||||
cd new-lamassu-admin/
|
||||
npm install
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
|
||||
|
|
@ -62,7 +60,7 @@ npm run start
|
|||
In a second terminal window:
|
||||
|
||||
```
|
||||
node bin/lamassu-admin-server --dev
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
### Register admin user
|
||||
|
|
@ -70,7 +68,7 @@ node bin/lamassu-admin-server --dev
|
|||
In a third terminal window:
|
||||
|
||||
```
|
||||
node bin/lamassu-register admin@example.com superuser
|
||||
node packages/server/bin/lamassu-register admin@example.com superuser
|
||||
```
|
||||
|
||||
You'll use this generated URL in the brower in a moment.
|
||||
|
|
@ -86,32 +84,32 @@ Go to all the required, unconfigured red fields and choose some values. Choose m
|
|||
### Run lamassu-server
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockScoring
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
### Add a lamassu-machine
|
||||
|
||||
Click on ``+ Add Machine`` in the sidebar. Type in a name for your machine and click **Pair**. Open up development tools to show the JavaScript console and copy the totem. You will use this to run lamassu-machine. This pairing totem expires after an hour.
|
||||
Click on `+ Add Machine` in the sidebar. Type in a name for your machine and click **Pair**. Open up development tools to show the JavaScript console and copy the totem. You will use this to run lamassu-machine. This pairing totem expires after an hour.
|
||||
|
||||
Now continue with lamassu-machine instructions from the ``INSTALL.md`` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine).
|
||||
Now continue with lamassu-machine instructions from the `INSTALL.md` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine).
|
||||
|
||||
## Subsequent runs
|
||||
|
||||
To start the Lamassu server run:
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockScoring
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
To start the Lamassu Admin run:
|
||||
|
||||
```
|
||||
node bin/lamassu-admin-server --dev
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```
|
||||
cd new-lamassu-admin/
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
|
|
|
|||
54
INSTALL.md
54
INSTALL.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Preliminaries for Ubuntu 16.04
|
||||
|
||||
Installation for other distros may be slightly different. This assumes NodeJS 8.3 or higher and npm 5.6 are already installed. All of this is done in the lamassu-server directory.
|
||||
Installation for other distros may be slightly different. This assumes NodeJS 22 or higher is already installed. All of this is done in the lamassu-server directory.
|
||||
|
||||
### Packages
|
||||
|
||||
|
|
@ -18,33 +18,18 @@ sudo -u postgres createdb lamassu
|
|||
sudo -u postgres psql postgres
|
||||
```
|
||||
|
||||
In ``psql``, run the following and set password to ``postgres123``:
|
||||
In `psql`, run the following and set password to `postgres123`:
|
||||
|
||||
```
|
||||
\password postgres
|
||||
ctrl-d
|
||||
```
|
||||
|
||||
## Preliminaries for MacOS
|
||||
|
||||
### Postgres
|
||||
|
||||
Use Postgres.app: https://postgresapp.com/
|
||||
|
||||
**psql** is automatically installed. You won't need to set up users.
|
||||
|
||||
### NodeJS
|
||||
|
||||
```
|
||||
curl -L https://git.io/n-install | bash -s -- -y lts
|
||||
. ~/.bash_profile
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Install node modules
|
||||
|
||||
Make sure you're running NodeJS 8.3 or higher. Ignore any warnings.
|
||||
Make sure you're running NodeJS 22 or higher. Ignore any warnings.
|
||||
|
||||
```
|
||||
npm install
|
||||
|
|
@ -53,25 +38,25 @@ npm install
|
|||
### Generate certificates
|
||||
|
||||
```
|
||||
bash tools/cert-gen.sh
|
||||
bash packages/server/tools/cert-gen.sh
|
||||
```
|
||||
|
||||
Notes:
|
||||
- This will create a ``.lamassu`` directory in your home directory.
|
||||
|
||||
Notes:
|
||||
|
||||
- This will create a `.lamassu` directory in your home directory.
|
||||
|
||||
### Set up database
|
||||
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway.
|
||||
|
||||
```
|
||||
node bin/lamassu-migrate
|
||||
node packages/server/bin/lamassu-migrate
|
||||
```
|
||||
|
||||
### Run new-lamassu-admin
|
||||
|
||||
```
|
||||
cd new-lamassu-admin/
|
||||
npm install
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
|
||||
|
|
@ -80,7 +65,7 @@ npm run start
|
|||
In a second terminal window:
|
||||
|
||||
```
|
||||
node bin/lamassu-admin-server --dev
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
### Register admin user
|
||||
|
|
@ -88,7 +73,7 @@ node bin/lamassu-admin-server --dev
|
|||
In a third terminal window:
|
||||
|
||||
```
|
||||
node bin/lamassu-register admin@example.com superuser
|
||||
node packages/server/bin/lamassu-register admin@example.com superuser
|
||||
```
|
||||
|
||||
You'll use this generated URL in the brower in a moment.
|
||||
|
|
@ -104,39 +89,38 @@ Go to all the required, unconfigured red fields and choose some values. Choose m
|
|||
### Run lamassu-server
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockScoring
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
### Add a lamassu-machine
|
||||
|
||||
Click on ``+ Add Machine`` in the sidebar. Type in a name for your machine and click **Pair**. Open up development tools to show the JavaScript console and copy the totem. You will use this to run lamassu-machine. This pairing totem expires after an hour.
|
||||
Click on `+ Add Machine` in the sidebar. Type in a name for your machine and click **Pair**. Open up development tools to show the JavaScript console and copy the totem. You will use this to run lamassu-machine. This pairing totem expires after an hour.
|
||||
|
||||
Now continue with lamassu-machine instructions from the ``INSTALL.md`` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine).
|
||||
Now continue with lamassu-machine instructions from the `INSTALL.md` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine).
|
||||
|
||||
### Run a local coin node (BTC supported)
|
||||
|
||||
Run `node bin/lamassu-coins` in the project root and select `Bitcoin`. This process will require the existence of certain environment variables that the setup will warn about.
|
||||
Run `node packages/server/bin/lamassu-coins` in the project root and select `Bitcoin`. This process will require the existence of certain environment variables that the setup will warn about.
|
||||
|
||||
Once that is done, the node needs to be run in a terminal with the following command `<YOUR_BLOCKCHAIN_DIR_ENV_VAR>/bin/bitcoind -datadir=<YOUR_BLOCKCHAIN_DIR_ENV_VAR>/bitcoin`
|
||||
|
||||
|
||||
## Subsequent runs
|
||||
|
||||
To start the Lamassu server run:
|
||||
|
||||
```
|
||||
node bin/lamassu-server --mockScoring
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
To start the Lamassu Admin run:
|
||||
|
||||
```
|
||||
node bin/lamassu-admin-server --dev
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```
|
||||
cd new-lamassu-admin/
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,3 +1,14 @@
|
|||
FROM node:22-alpine AS build-ui
|
||||
RUN apk add --no-cache npm git curl build-base python3
|
||||
|
||||
COPY ["packages/admin-ui/package.json", "package-lock.json", "./"]
|
||||
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install
|
||||
|
||||
COPY packages/admin-ui/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM ubuntu:20.04 as base
|
||||
|
||||
ARG VERSION
|
||||
|
|
@ -20,14 +31,11 @@ RUN apt-get install nodejs -y -q
|
|||
|
||||
WORKDIR lamassu-server
|
||||
|
||||
COPY ["package.json", "package-lock.json", "./"]
|
||||
COPY ["packages/server/package.json", "package-lock.json", "./"]
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install --production
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN cd new-lamassu-admin && npm install && npm run build
|
||||
RUN mv new-lamassu-admin/build public/
|
||||
RUN rm -rf new-lamassu-admin/node_modules
|
||||
COPY ./packages/server/ ./
|
||||
COPY --from=build-ui /build /lamassu-server/public
|
||||
|
||||
RUN cd .. && tar -zcvf lamassu-server.tar.gz ./lamassu-server
|
||||
|
|
@ -3,11 +3,11 @@ RUN apk add --no-cache npm git curl build-base net-tools python3 postgresql-dev
|
|||
|
||||
WORKDIR /lamassu-server
|
||||
|
||||
COPY ["package.json", "package-lock.json", "./"]
|
||||
COPY ["packages/server/package.json", "package-lock.json", "./"]
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install --production
|
||||
|
||||
COPY . ./
|
||||
COPY packages/server/ ./
|
||||
|
||||
|
||||
FROM node:22-alpine AS l-s-base
|
||||
|
|
@ -30,12 +30,12 @@ RUN apk add --no-cache npm git curl build-base python3
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ["new-lamassu-admin/package.json", "new-lamassu-admin/package-lock.json", "./"]
|
||||
COPY ["packages/admin-ui/package.json", "package-lock.json", "./"]
|
||||
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install
|
||||
|
||||
COPY new-lamassu-admin/ ./
|
||||
COPY packages/admin-ui/ ./
|
||||
RUN npm run build
|
||||
|
||||
|
||||
|
|
@ -46,4 +46,4 @@ RUN chmod +x /lamassu-server/bin/lamassu-admin-server-entrypoint.sh
|
|||
|
||||
EXPOSE 443
|
||||
|
||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-admin-server-entrypoint.sh" ]
|
||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-admin-server-entrypoint.sh" ]
|
||||
74
eslint.config.mjs
Normal file
74
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import pluginReact from 'eslint-plugin-react'
|
||||
import json from '@eslint/json'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
||||
import pluginJest from 'eslint-plugin-jest'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
'**/build',
|
||||
'**/package.json',
|
||||
'**/package-lock.json',
|
||||
'**/currencies.json',
|
||||
'**/countries.json',
|
||||
'**/languages.json',
|
||||
]),
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx}'],
|
||||
plugins: { js },
|
||||
extends: ['js/recommended'],
|
||||
},
|
||||
{
|
||||
files: ['packages/admin-ui/**/*.{js,mjs,jsx}'],
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
process: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/server/**/*.{js,cjs}'],
|
||||
languageOptions: { sourceType: 'commonjs', globals: globals.node },
|
||||
},
|
||||
{
|
||||
...pluginReact.configs.flat.recommended,
|
||||
settings: { react: { version: 'detect' } },
|
||||
files: ['packages/admin-ui/**/*.{jsx,js}'],
|
||||
},
|
||||
{ ...reactCompiler.configs.recommended },
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
plugins: { json },
|
||||
language: 'json/json',
|
||||
extends: ['json/recommended'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'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',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=759670
|
||||
// for the documentation about the jsconfig.json format
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"jspm_packages",
|
||||
"tmp",
|
||||
"temp"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
const axios = require("axios");
|
||||
|
||||
const getSatBEstimateFee = () => {
|
||||
return axios.get('https://mempool.space/api/v1/fees/recommended')
|
||||
.then(r => r.data.hourFee)
|
||||
}
|
||||
|
||||
const getSatBEstimateFees = () => {
|
||||
return axios.get('https://mempool.space/api/v1/fees/recommended')
|
||||
.then(r => r.data)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSatBEstimateFees,
|
||||
getSatBEstimateFee
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
const BN = require('../bn')
|
||||
const T = require('../time')
|
||||
const logger = require('../logger')
|
||||
const E = require('../error')
|
||||
|
||||
const PENDING_INTERVAL_MS = 60 * T.minutes
|
||||
|
||||
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
|
||||
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
|
||||
|
||||
const massage = _.flow(_.omit(massageFields),
|
||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
||||
|
||||
const massageUpdates = _.flow(_.omit(massageUpdateFields),
|
||||
convertBigNumFields, _.mapKeys(_.snakeCase))
|
||||
|
||||
module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
|
||||
|
||||
function convertBigNumFields (obj) {
|
||||
const convert = value =>
|
||||
value && BN.isBigNumber(value)
|
||||
? value.toString()
|
||||
: value
|
||||
return _.mapValues(convert, obj)
|
||||
}
|
||||
|
||||
function toObj (row) {
|
||||
if (!row) return null
|
||||
|
||||
const keys = _.keys(row)
|
||||
let newObj = {}
|
||||
|
||||
keys.forEach(key => {
|
||||
const objKey = _.camelCase(key)
|
||||
if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'commission_percentage', 'raw_ticker_price'])) {
|
||||
newObj[objKey] = new BN(row[key])
|
||||
return
|
||||
}
|
||||
|
||||
newObj[objKey] = row[key]
|
||||
})
|
||||
|
||||
newObj.direction = 'cashIn'
|
||||
|
||||
return newObj
|
||||
}
|
||||
|
||||
function upsert (t, dbTx, preProcessedTx) {
|
||||
if (!dbTx) {
|
||||
return insert(t, preProcessedTx)
|
||||
.then(tx => ({dbTx, tx}))
|
||||
}
|
||||
|
||||
return update(t, dbTx, diff(dbTx, preProcessedTx))
|
||||
.then(tx => ({dbTx, tx}))
|
||||
}
|
||||
|
||||
function insert (t, tx) {
|
||||
const dbTx = massage(tx)
|
||||
const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *'
|
||||
return t.one(sql)
|
||||
.then(toObj)
|
||||
}
|
||||
|
||||
function update (t, tx, changes) {
|
||||
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
||||
|
||||
const dbChanges = isFinalTxStage(changes) ? massage(changes) : massageUpdates(changes)
|
||||
const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
|
||||
pgp.as.format(' where id=$1', [tx.id]) + ' returning *'
|
||||
|
||||
return t.one(sql)
|
||||
.then(toObj)
|
||||
}
|
||||
|
||||
function diff (oldTx, newTx) {
|
||||
let updatedTx = {}
|
||||
|
||||
if (!oldTx) throw new Error('oldTx must not be null')
|
||||
if (!newTx) throw new Error('newTx must not be null')
|
||||
|
||||
_.forEach(fieldKey => {
|
||||
const oldField = oldTx[fieldKey]
|
||||
const newField = newTx[fieldKey]
|
||||
if (fieldKey === 'bills') return
|
||||
if (_.isEqualWith(nilEqual, oldField, newField)) return
|
||||
|
||||
if (!ensureRatchet(oldField, newField, fieldKey)) {
|
||||
logger.warn('Value from lamassu-machine would violate ratchet [%s]', fieldKey)
|
||||
logger.warn('Old tx: %j', oldTx)
|
||||
logger.warn('New tx: %j', newTx)
|
||||
throw new E.RatchetError('Value from lamassu-machine would violate ratchet')
|
||||
}
|
||||
|
||||
updatedTx[fieldKey] = newField
|
||||
}, _.keys(newTx))
|
||||
|
||||
return updatedTx
|
||||
}
|
||||
|
||||
function ensureRatchet (oldField, newField, fieldKey) {
|
||||
const monotonic = ['cryptoAtoms', 'fiat', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion', 'batched', 'discount']
|
||||
const free = ['sendPending', 'error', 'errorCode', 'customerId', 'discountSource']
|
||||
|
||||
if (_.isNil(oldField)) return true
|
||||
if (_.includes(fieldKey, monotonic)) return isMonotonic(oldField, newField, fieldKey)
|
||||
|
||||
if (_.includes(fieldKey, free)) {
|
||||
if (_.isNil(newField)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
if (_.isNil(newField)) return false
|
||||
if (BN.isBigNumber(oldField) && BN.isBigNumber(newField)) return new BN(oldField).eq(newField)
|
||||
if (oldField.toString() === newField.toString()) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isMonotonic (oldField, newField, fieldKey) {
|
||||
if (_.isNil(newField)) return false
|
||||
if (_.isBoolean(oldField)) return oldField === newField || !oldField
|
||||
if (BN.isBigNumber(oldField)) return oldField.lte(newField)
|
||||
if (_.isNumber(oldField)) return oldField <= newField
|
||||
|
||||
throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`)
|
||||
}
|
||||
|
||||
function nilEqual (a, b) {
|
||||
if (_.isNil(a) && _.isNil(b)) return true
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function isClearToSend (oldTx, newTx) {
|
||||
const now = Date.now()
|
||||
|
||||
return (newTx.send || newTx.batched) &&
|
||||
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
|
||||
(newTx.created > now - PENDING_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function isFinalTxStage (txChanges) {
|
||||
return txChanges.send || txChanges.batched
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
module.exports = {logDispense, logActionById, logAction, logError}
|
||||
|
||||
function logDispense (t, tx) {
|
||||
const baseRec = {error: tx.error, error_code: tx.errorCode}
|
||||
const rec = _.merge(mapDispense(tx), baseRec)
|
||||
const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError'
|
||||
return logAction(t, action, rec, tx)
|
||||
}
|
||||
|
||||
function logActionById (t, action, _rec, txId) {
|
||||
const rec = _.assign(_rec, {action, tx_id: txId, redeem: false})
|
||||
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
|
||||
|
||||
return t.none(sql)
|
||||
}
|
||||
|
||||
function logAction (t, action, _rec, tx) {
|
||||
const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem, device_id: tx.deviceId})
|
||||
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
|
||||
|
||||
return t.none(sql)
|
||||
.then(_.constant(tx))
|
||||
}
|
||||
|
||||
function logError (t, action, err, tx) {
|
||||
return logAction(t, action, {
|
||||
error: err.message,
|
||||
error_code: err.name
|
||||
}, tx)
|
||||
}
|
||||
|
||||
function mapDispense (tx) {
|
||||
const bills = tx.bills
|
||||
|
||||
if (_.isEmpty(bills)) return {}
|
||||
|
||||
const res = {}
|
||||
|
||||
_.forEach(it => {
|
||||
const suffix = _.snakeCase(bills[it].name.replace(/cassette/gi, ''))
|
||||
res[`provisioned_${suffix}`] = bills[it].provisioned
|
||||
res[`denomination_${suffix}`] = bills[it].denomination
|
||||
res[`dispensed_${suffix}`] = bills[it].dispensed
|
||||
res[`rejected_${suffix}`] = bills[it].rejected
|
||||
}, _.times(_.identity(), _.size(bills)))
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
const db = require('../db')
|
||||
const E = require('../error')
|
||||
const logger = require('../logger')
|
||||
|
||||
const helper = require('./cash-out-helper')
|
||||
const cashOutActions = require('./cash-out-actions')
|
||||
const cashOutLow = require('./cash-out-low')
|
||||
|
||||
const toObj = helper.toObj
|
||||
|
||||
module.exports = { atomic }
|
||||
|
||||
function atomic (tx, pi, fromClient) {
|
||||
const TransactionMode = pgp.txMode.TransactionMode
|
||||
const isolationLevel = pgp.txMode.isolationLevel
|
||||
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
||||
function transaction (t) {
|
||||
const sql = 'SELECT * FROM cash_out_txs WHERE id=$1 FOR UPDATE'
|
||||
|
||||
return t.oneOrNone(sql, [tx.id])
|
||||
.then(toObj)
|
||||
.then(oldTx => {
|
||||
const isStale = fromClient && oldTx && (oldTx.txVersion >= tx.txVersion)
|
||||
if (isStale) throw new E.StaleTxError({ txId: tx.id })
|
||||
|
||||
// Server doesn't bump version, so we just prevent from version being older.
|
||||
const isStaleFromServer = !fromClient && oldTx && (oldTx.txVersion > tx.txVersion)
|
||||
if (isStaleFromServer) throw new Error('Stale Error: server triggered', tx.id)
|
||||
|
||||
return preProcess(t, oldTx, tx, pi)
|
||||
.then(preProcessedTx => cashOutLow.upsert(t, oldTx, preProcessedTx))
|
||||
})
|
||||
}
|
||||
return db.tx({ mode }, transaction)
|
||||
}
|
||||
|
||||
function preProcess (t, oldTx, newTx, pi) {
|
||||
if (!oldTx) {
|
||||
return pi.isHd(newTx)
|
||||
.then(isHd => nextHd(t, isHd, newTx))
|
||||
.then(newTxHd => {
|
||||
return pi.newAddress(newTxHd)
|
||||
.then(_.merge(newTxHd))
|
||||
})
|
||||
.then(addressedTx => {
|
||||
const rec = {
|
||||
to_address: addressedTx.toAddress,
|
||||
layer_2_address: addressedTx.layer2Address
|
||||
}
|
||||
|
||||
return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx)
|
||||
})
|
||||
.catch(err => {
|
||||
pi.notifyOperator(newTx, { isRedemption: false, error: 'Error while provisioning address' })
|
||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
||||
return cashOutActions.logError(t, 'provisionAddress', err, newTx)
|
||||
.then(() => { throw err })
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve(updateStatus(oldTx, newTx))
|
||||
.then(updatedTx => {
|
||||
if (updatedTx.status !== oldTx.status) {
|
||||
const isZeroConf = pi.isZeroConf(updatedTx)
|
||||
updatedTx.justAuthorized = wasJustAuthorized(oldTx, updatedTx, isZeroConf)
|
||||
|
||||
const rec = {
|
||||
to_address: updatedTx.toAddress,
|
||||
tx_hash: updatedTx.txHash
|
||||
}
|
||||
|
||||
return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx)
|
||||
}
|
||||
|
||||
const hasError = !oldTx.error && newTx.error
|
||||
const hasDispenseOccurred = !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
|
||||
|
||||
if (hasError || hasDispenseOccurred) {
|
||||
return cashOutActions.logDispense(t, updatedTx)
|
||||
.then(it => updateCassettes(t, updatedTx).then(() => it) )
|
||||
.then((t) => {
|
||||
pi.notifyOperator(updatedTx, { isRedemption: true })
|
||||
.catch((err) => logger.error('Failure sending transaction notification', err))
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
if (!oldTx.phone && newTx.phone) {
|
||||
return cashOutActions.logAction(t, 'addPhone', {}, updatedTx)
|
||||
}
|
||||
|
||||
if (!oldTx.redeem && newTx.redeem) {
|
||||
return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx)
|
||||
}
|
||||
|
||||
return updatedTx
|
||||
})
|
||||
}
|
||||
|
||||
function nextHd (t, isHd, tx) {
|
||||
if (!isHd) return Promise.resolve(tx)
|
||||
|
||||
return t.one("select nextval('hd_indices_seq') as hd_index")
|
||||
.then(row => _.set('hdIndex', row.hd_index, tx))
|
||||
}
|
||||
|
||||
function updateCassettes (t, tx) {
|
||||
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
|
||||
|
||||
const billsStmt = _.join(', ')(_.map(it => `${tx.bills[it].name} = ${tx.bills[it].name} - $${it + 1}`)(_.range(0, _.size(tx.bills))))
|
||||
const returnStmt = _.join(', ')(_.map(bill => `${bill.name}`)(tx.bills))
|
||||
|
||||
const sql = `UPDATE devices SET ${billsStmt} WHERE device_id = $${_.size(tx.bills) + 1} RETURNING ${returnStmt}`
|
||||
|
||||
const values = []
|
||||
|
||||
_.forEach(it => values.push(
|
||||
tx.bills[it].dispensed + tx.bills[it].rejected
|
||||
), _.times(_.identity(), _.size(tx.bills)))
|
||||
|
||||
values.push(tx.deviceId)
|
||||
|
||||
return t.one(sql, values)
|
||||
}
|
||||
|
||||
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
|
||||
const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published', 'rejected']) &&
|
||||
_.includes(newTx.status, ['authorized', 'instant', 'confirmed'])
|
||||
|
||||
const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized', 'rejected']) &&
|
||||
_.includes(newTx.status, ['instant', 'confirmed'])
|
||||
|
||||
return isZeroConf ? isAuthorized() : isConfirmed()
|
||||
}
|
||||
|
||||
function isPublished (status) {
|
||||
return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed'])
|
||||
}
|
||||
|
||||
function isConfirmed (status) {
|
||||
return status === 'confirmed'
|
||||
}
|
||||
|
||||
function updateStatus (oldTx, newTx) {
|
||||
const oldStatus = oldTx.status
|
||||
const newStatus = ratchetStatus(oldStatus, newTx.status)
|
||||
|
||||
const publishedAt = !oldTx.publishedAt && isPublished(newStatus)
|
||||
? 'now()^'
|
||||
: undefined
|
||||
|
||||
const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus)
|
||||
? 'now()^'
|
||||
: undefined
|
||||
|
||||
const updateRec = {
|
||||
publishedAt,
|
||||
confirmedAt,
|
||||
status: newStatus
|
||||
}
|
||||
|
||||
return _.merge(newTx, updateRec)
|
||||
}
|
||||
|
||||
function ratchetStatus (oldStatus, newStatus) {
|
||||
const statusOrder = ['notSeen', 'published', 'rejected',
|
||||
'authorized', 'instant', 'confirmed']
|
||||
|
||||
if (oldStatus === newStatus) return oldStatus
|
||||
if (newStatus === 'insufficientFunds') return newStatus
|
||||
|
||||
const idx = Math.max(statusOrder.indexOf(oldStatus), statusOrder.indexOf(newStatus))
|
||||
return statusOrder[idx]
|
||||
}
|
||||
|
||||
function dispenseOccurred (bills) {
|
||||
if (_.isEmpty(bills)) return false
|
||||
return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills)
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
function getBackwardsCompatibleTriggers (triggers) {
|
||||
const filtered = _.filter(_.matches({ triggerType: 'txVolume', direction: 'both', thresholdDays: 1 }))(triggers)
|
||||
const grouped = _.groupBy(_.prop('requirement'))(filtered)
|
||||
return _.mapValues(_.compose(_.get('threshold'), _.minBy('threshold')))(grouped)
|
||||
}
|
||||
|
||||
function hasSanctions (triggers) {
|
||||
return _.some(_.matches({ requirement: 'sanctions' }))(triggers)
|
||||
}
|
||||
|
||||
function maxDaysThreshold (triggers) {
|
||||
return _.max(_.map('thresholdDays')(triggers))
|
||||
}
|
||||
|
||||
function getCashLimit (triggers) {
|
||||
const withFiat = _.filter(({ triggerType }) => _.includes(triggerType, ['txVolume', 'txAmount']))
|
||||
const blocking = _.filter(({ requirement }) => _.includes(requirement, ['block', 'suspend']))
|
||||
return _.compose(_.minBy('threshold'), blocking, withFiat)(triggers)
|
||||
}
|
||||
|
||||
const hasRequirement = requirement => _.compose(_.negate(_.isEmpty), _.find(_.matches({ requirement })))
|
||||
|
||||
const hasPhone = hasRequirement('sms')
|
||||
const hasFacephoto = hasRequirement('facephoto')
|
||||
const hasIdScan = hasRequirement('idCardData')
|
||||
|
||||
const AUTH_METHODS = {
|
||||
SMS: 'SMS',
|
||||
EMAIL: 'EMAIL'
|
||||
}
|
||||
|
||||
module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS }
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const uuid = require('uuid')
|
||||
|
||||
const logger = require('./logger')
|
||||
const db = require('./db')
|
||||
const ofac = require('./ofac/index')
|
||||
|
||||
function logSanctionsMatch (deviceId, customer, sanctionsId, alias) {
|
||||
const sql = `insert into sanctions_logs
|
||||
(id, device_id, sanctioned_id, sanctioned_alias_id, sanctioned_alias_full_name, customer_id)
|
||||
values
|
||||
($1, $2, $3, $4, $5, $6)`
|
||||
|
||||
return db.none(sql, [uuid.v4(), deviceId, sanctionsId, alias.id, alias.fullName, customer.id])
|
||||
}
|
||||
|
||||
function logSanctionsMatches (deviceId, customer, results) {
|
||||
const logAlias = resultId => alias => logSanctionsMatch(deviceId, customer, resultId, alias)
|
||||
const logResult = result => _.map(logAlias(result.id), result.aliases)
|
||||
|
||||
return Promise.all(_.flatMap(logResult, results))
|
||||
}
|
||||
|
||||
function matchOfac (deviceId, customer) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// Probably because we haven't asked for ID yet
|
||||
if (!_.isPlainObject(customer.idCardData)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const nameParts = {
|
||||
firstName: customer.idCardData.firstName,
|
||||
lastName: customer.idCardData.lastName
|
||||
}
|
||||
|
||||
if (_.some(_.isNil, _.values(nameParts))) {
|
||||
logger.error(new Error(`Insufficient idCardData while matching OFAC for: ${customer.id}`))
|
||||
return true
|
||||
}
|
||||
|
||||
const birthDate = customer.idCardData.dateOfBirth
|
||||
|
||||
if (_.isNil(birthDate)) {
|
||||
logger.error(new Error(`No birth date while matching OFAC for: ${customer.id}`))
|
||||
return true
|
||||
}
|
||||
|
||||
const options = {
|
||||
threshold: 0.85,
|
||||
fullNameThreshold: 0.95,
|
||||
debug: false
|
||||
}
|
||||
|
||||
const results = ofac.match(nameParts, birthDate, options)
|
||||
|
||||
return logSanctionsMatches(deviceId, customer, results)
|
||||
.then(() => !_.isEmpty(results))
|
||||
})
|
||||
}
|
||||
|
||||
// BACKWARDS_COMPATIBILITY 7.5
|
||||
// machines before 7.5 need to test sanctionsActive here
|
||||
function validateOfac (deviceId, sanctionsActive, customer) {
|
||||
if (!sanctionsActive) return Promise.resolve(true)
|
||||
if (customer.sanctionsOverride === 'blocked') return Promise.resolve(false)
|
||||
if (customer.sanctionsOverride === 'verified') return Promise.resolve(true)
|
||||
|
||||
return matchOfac(deviceId, customer)
|
||||
.then(didMatch => !didMatch)
|
||||
}
|
||||
|
||||
function validationPatch (deviceId, sanctionsActive, customer) {
|
||||
return validateOfac(deviceId, sanctionsActive, customer)
|
||||
.then(sanctions =>
|
||||
_.isNil(customer.sanctions) || customer.sanctions !== sanctions ?
|
||||
{ sanctions } :
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {validationPatch}
|
||||
25
lib/email.js
25
lib/email.js
|
|
@ -1,25 +0,0 @@
|
|||
const ph = require('./plugin-helper')
|
||||
|
||||
function sendMessage (settings, rec) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
return plugin.sendMessage(account, rec)
|
||||
})
|
||||
}
|
||||
|
||||
function sendCustomerMessage (settings, rec) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const pluginCode = settings.config.notifications_thirdParty_email || 'mailgun'
|
||||
const plugin = ph.load(ph.EMAIL, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
return plugin.sendMessage(account, rec)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {sendMessage, sendCustomerMessage}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const { ALL_CRYPTOS } = require('@lamassu/coins')
|
||||
|
||||
const configManager = require('./new-config-manager')
|
||||
const ccxt = require('./plugins/exchange/ccxt')
|
||||
const mockExchange = require('./plugins/exchange/mock-exchange')
|
||||
const accounts = require('./new-admin/config/accounts')
|
||||
|
||||
function lookupExchange (settings, cryptoCode) {
|
||||
const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange
|
||||
if (exchange === 'no-exchange') return null
|
||||
return exchange
|
||||
}
|
||||
|
||||
function fetchExchange (settings, cryptoCode) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const exchangeName = lookupExchange(settings, cryptoCode)
|
||||
if (!exchangeName) throw new Error('No exchange set')
|
||||
const account = settings.accounts[exchangeName]
|
||||
|
||||
return { exchangeName, account }
|
||||
})
|
||||
}
|
||||
|
||||
function buy (settings, tradeEntry) {
|
||||
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
||||
return fetchExchange(settings, cryptoCode)
|
||||
.then(r => {
|
||||
if (r.exchangeName === 'mock-exchange') {
|
||||
return mockExchange.buy(cryptoAtoms, fiatCode, cryptoCode)
|
||||
}
|
||||
return ccxt.trade('buy', r.account, tradeEntry, r.exchangeName)
|
||||
})
|
||||
}
|
||||
|
||||
function sell (settings, tradeEntry) {
|
||||
const { cryptoAtoms, fiatCode, cryptoCode } = tradeEntry
|
||||
return fetchExchange(settings, cryptoCode)
|
||||
.then(r => {
|
||||
if (r.exchangeName === 'mock-exchange') {
|
||||
return mockExchange.sell(cryptoAtoms, fiatCode, cryptoCode)
|
||||
}
|
||||
return ccxt.trade('sell', r.account, tradeEntry, r.exchangeName)
|
||||
})
|
||||
}
|
||||
|
||||
function active (settings, cryptoCode) {
|
||||
return !!lookupExchange(settings, cryptoCode)
|
||||
}
|
||||
|
||||
function getMarkets () {
|
||||
const filterExchanges = _.filter(it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange')
|
||||
const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST))
|
||||
|
||||
const fetchMarketForExchange = exchange =>
|
||||
ccxt.getMarkets(exchange, ALL_CRYPTOS)
|
||||
.then(markets => ({ exchange, markets }))
|
||||
.catch(error => ({
|
||||
exchange,
|
||||
markets: [],
|
||||
error: error.message
|
||||
}))
|
||||
|
||||
const transformToObject = _.reduce((acc, { exchange, markets }) => ({
|
||||
...acc,
|
||||
[exchange]: markets
|
||||
}), {})
|
||||
|
||||
const promises = _.map(fetchMarketForExchange, availableExchanges)
|
||||
return Promise.all(promises)
|
||||
.then(transformToObject)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchExchange,
|
||||
buy,
|
||||
sell,
|
||||
active,
|
||||
getMarkets
|
||||
}
|
||||
62
lib/forex.js
62
lib/forex.js
|
|
@ -1,62 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const axios = require('axios')
|
||||
const BN = require('./bn')
|
||||
|
||||
const MAX_ROTATIONS = 5
|
||||
|
||||
const getFiatRates = () => axios.get('https://bitpay.com/api/rates').then(response => response.data)
|
||||
|
||||
const API_QUEUE = [
|
||||
{ api: getBitPayFxRate, name: 'bitpay', fiatCodeProperty: 'code', rateProperty: 'rate' },
|
||||
{ api: getCoinCapFxRate, name: 'coincap', fiatCodeProperty: 'symbol', rateProperty: 'rateUsd' }
|
||||
]
|
||||
|
||||
function getBitPayFxRate (fiatCode, fiatCodeProperty, rateProperty) {
|
||||
return axios.get('https://bitpay.com/rates')
|
||||
.then(response => {
|
||||
const fxRates = response.data.data
|
||||
const usdRate = findCurrencyRates(fxRates, 'USD', fiatCodeProperty, rateProperty)
|
||||
const fxRate = findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty).div(usdRate)
|
||||
return {
|
||||
fxRate
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getCoinCapFxRate (fiatCode, fiatCodeProperty, rateProperty) {
|
||||
return axios.get('https://api.coincap.io/v2/rates')
|
||||
.then(response => {
|
||||
const fxRates = response.data.data
|
||||
const fxRate = new BN(1).div(findCurrencyRates(fxRates, fiatCode, fiatCodeProperty, rateProperty))
|
||||
return {
|
||||
fxRate
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function findCurrencyRates (fxRates, fiatCode, fiatCodeProperty, rateProperty) {
|
||||
const rates = _.find(_.matchesProperty(fiatCodeProperty, fiatCode), fxRates)
|
||||
if (!rates || !rates[rateProperty]) throw new Error(`Unsupported currency: ${fiatCode}`)
|
||||
return new BN(rates[rateProperty].toString())
|
||||
}
|
||||
|
||||
const getRate = (retries = 1, fiatCode) => {
|
||||
const selected = _.first(API_QUEUE).name
|
||||
const activeAPI = _.first(API_QUEUE).api
|
||||
const fiatCodeProperty = _.first(API_QUEUE).fiatCodeProperty
|
||||
const rateProperty = _.first(API_QUEUE).rateProperty
|
||||
|
||||
if (!activeAPI) throw new Error(`FOREX api ${selected} does not exist.`)
|
||||
|
||||
return activeAPI(fiatCode, fiatCodeProperty, rateProperty)
|
||||
.catch(() => {
|
||||
// Switch service
|
||||
const erroredService = API_QUEUE.shift()
|
||||
API_QUEUE.push(erroredService)
|
||||
if (retries >= MAX_ROTATIONS) throw new Error(`FOREX API error from ${erroredService.name}`)
|
||||
|
||||
return getRate(++retries, fiatCode)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { getFiatRates, getRate }
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
const gql = require('graphql-tag')
|
||||
|
||||
module.exports = gql`
|
||||
type Coin {
|
||||
cryptoCode: String!
|
||||
cryptoCodeDisplay: String!
|
||||
display: String!
|
||||
minimumTx: String!
|
||||
cashInFee: String!
|
||||
cashOutFee: String!
|
||||
cashInCommission: String!
|
||||
cashOutCommission: String!
|
||||
cryptoNetwork: String!
|
||||
cryptoUnits: String!
|
||||
batchable: Boolean!
|
||||
isCashInOnly: Boolean!
|
||||
}
|
||||
|
||||
type LocaleInfo {
|
||||
country: String!
|
||||
fiatCode: String!
|
||||
languages: [String!]!
|
||||
}
|
||||
|
||||
type OperatorInfo {
|
||||
name: String!
|
||||
phone: String!
|
||||
email: String!
|
||||
website: String!
|
||||
companyNumber: String!
|
||||
}
|
||||
|
||||
type MachineInfo {
|
||||
deviceId: String! @deprecated(reason: "unused by the machine")
|
||||
deviceName: String
|
||||
numberOfCassettes: Int
|
||||
numberOfRecyclers: Int
|
||||
}
|
||||
|
||||
type ReceiptInfo {
|
||||
paper: Boolean!
|
||||
automaticPrint: Boolean!
|
||||
sms: Boolean!
|
||||
operatorWebsite: Boolean!
|
||||
operatorEmail: Boolean!
|
||||
operatorPhone: Boolean!
|
||||
companyNumber: Boolean!
|
||||
machineLocation: Boolean!
|
||||
customerNameOrPhoneNumber: Boolean!
|
||||
exchangeRate: Boolean!
|
||||
addressQRCode: Boolean!
|
||||
}
|
||||
|
||||
type MachineScreenOptions {
|
||||
rates: RateScreenOptions!
|
||||
}
|
||||
|
||||
type RateScreenOptions {
|
||||
active: Boolean!
|
||||
}
|
||||
|
||||
type SpeedtestFile {
|
||||
url: String!
|
||||
size: Int!
|
||||
}
|
||||
|
||||
enum TriggerAutomationType {
|
||||
Automatic
|
||||
Manual
|
||||
}
|
||||
|
||||
type CustomTriggersAutomation {
|
||||
id: ID!
|
||||
type: TriggerAutomationType!
|
||||
}
|
||||
|
||||
type TriggersAutomation {
|
||||
sanctions: TriggerAutomationType!
|
||||
idCardPhoto: TriggerAutomationType!
|
||||
idCardData: TriggerAutomationType!
|
||||
facephoto: TriggerAutomationType!
|
||||
usSsn: TriggerAutomationType!
|
||||
custom: [CustomTriggersAutomation]!
|
||||
}
|
||||
|
||||
type CustomScreen {
|
||||
text: String!
|
||||
title: String!
|
||||
}
|
||||
|
||||
type CustomInput {
|
||||
type: String!
|
||||
constraintType: String!
|
||||
label1: String
|
||||
label2: String
|
||||
choiceList: [String]
|
||||
}
|
||||
|
||||
type CustomRequest {
|
||||
name: String!
|
||||
input: CustomInput!
|
||||
screen1: CustomScreen!
|
||||
screen2: CustomScreen!
|
||||
}
|
||||
|
||||
type CustomInfoRequest {
|
||||
id: String!
|
||||
enabled: Boolean!
|
||||
customRequest: CustomRequest!
|
||||
}
|
||||
|
||||
type Trigger {
|
||||
id: String!
|
||||
direction: String!
|
||||
requirement: String!
|
||||
triggerType: String!
|
||||
|
||||
suspensionDays: Float
|
||||
threshold: Int
|
||||
thresholdDays: Int
|
||||
customInfoRequestId: String @deprecated(reason: "use customInfoRequest.id")
|
||||
customInfoRequest: CustomInfoRequest
|
||||
externalService: String
|
||||
}
|
||||
|
||||
type TermsDetails {
|
||||
tcPhoto: Boolean!
|
||||
delay: Boolean!
|
||||
title: String!
|
||||
accept: String!
|
||||
cancel: String!
|
||||
}
|
||||
|
||||
type Terms {
|
||||
hash: String!
|
||||
text: String
|
||||
details: TermsDetails
|
||||
}
|
||||
|
||||
enum CustomerAuthentication {
|
||||
EMAIL
|
||||
SMS
|
||||
}
|
||||
|
||||
type StaticConfig {
|
||||
configVersion: Int!
|
||||
|
||||
coins: [Coin!]!
|
||||
enablePaperWalletOnly: Boolean!
|
||||
hasLightning: Boolean!
|
||||
serverVersion: String!
|
||||
timezone: Int!
|
||||
twoWayMode: Boolean!
|
||||
customerAuthentication: CustomerAuthentication!
|
||||
|
||||
localeInfo: LocaleInfo!
|
||||
operatorInfo: OperatorInfo
|
||||
machineInfo: MachineInfo!
|
||||
receiptInfo: ReceiptInfo
|
||||
screenOptions: MachineScreenOptions
|
||||
|
||||
speedtestFiles: [SpeedtestFile!]!
|
||||
urlsToPing: [String!]!
|
||||
|
||||
triggersAutomation: TriggersAutomation!
|
||||
triggers: [Trigger!]!
|
||||
}
|
||||
|
||||
type DynamicCoinValues {
|
||||
# NOTE: Doesn't seem to be used anywhere outside of lib/plugins.js.
|
||||
# However, it can be used to generate the cache key, if we ever move to an
|
||||
# actual caching mechanism.
|
||||
#timestamp: String!
|
||||
|
||||
cryptoCode: String!
|
||||
balance: String!
|
||||
|
||||
# Raw rates
|
||||
ask: String!
|
||||
bid: String!
|
||||
|
||||
# Rates with commissions applied
|
||||
cashIn: String!
|
||||
cashOut: String!
|
||||
|
||||
zeroConfLimit: Int!
|
||||
}
|
||||
|
||||
type PhysicalCassette {
|
||||
name: String!
|
||||
denomination: Int!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
type PhysicalRecycler {
|
||||
name: String!
|
||||
number: Int!
|
||||
denomination: Int!
|
||||
count: Int!
|
||||
}
|
||||
|
||||
type Cassettes {
|
||||
physical: [PhysicalCassette!]!
|
||||
virtual: [Int!]!
|
||||
}
|
||||
|
||||
type Recyclers {
|
||||
physical: [PhysicalRecycler!]!
|
||||
virtual: [Int!]!
|
||||
}
|
||||
|
||||
type DynamicConfig {
|
||||
areThereAvailablePromoCodes: Boolean!
|
||||
cassettes: Cassettes
|
||||
recyclers: Recyclers
|
||||
coins: [DynamicCoinValues!]!
|
||||
reboot: Boolean!
|
||||
shutdown: Boolean!
|
||||
restartServices: Boolean!
|
||||
emptyUnit: Boolean!
|
||||
refillUnit: Boolean!
|
||||
diagnostics: Boolean!
|
||||
}
|
||||
|
||||
type Configs {
|
||||
static: StaticConfig
|
||||
dynamic: DynamicConfig!
|
||||
}
|
||||
|
||||
type Query {
|
||||
configs(currentConfigVersion: Int): Configs!
|
||||
terms(currentHash: String, currentConfigVersion: Int): Terms
|
||||
}
|
||||
`
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
const configManager = require('./new-config-manager')
|
||||
const ph = require('./plugin-helper')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
function fetch (settings, cryptoCode) {
|
||||
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2
|
||||
|
||||
if (_.isEmpty(plugin) || plugin === 'no-layer2') return Promise.resolve()
|
||||
|
||||
const layer2 = ph.load(ph.LAYER2, plugin)
|
||||
const account = settings.accounts[plugin]
|
||||
|
||||
return Promise.resolve({layer2, account})
|
||||
}
|
||||
|
||||
function newAddress (settings, info) {
|
||||
return fetch(settings, info.cryptoCode)
|
||||
.then(r => {
|
||||
if (!r) return
|
||||
return r.layer2.newAddress(r.account, info)
|
||||
})
|
||||
}
|
||||
|
||||
function getStatus (settings, tx) {
|
||||
const toAddress = tx.layer2Address
|
||||
if (!toAddress) return Promise.resolve({status: 'notSeen'})
|
||||
|
||||
return fetch(settings, tx.cryptoCode)
|
||||
.then(r => {
|
||||
if (!r) return {status: 'notSeen'}
|
||||
return r.layer2.getStatus(r.account, toAddress, tx.cryptoAtoms, tx.cryptoCode)
|
||||
})
|
||||
}
|
||||
|
||||
function cryptoNetwork (settings, cryptoCode) {
|
||||
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).layer2
|
||||
const layer2 = ph.load(ph.LAYER2, plugin)
|
||||
const account = settings.accounts[plugin]
|
||||
|
||||
if (!layer2.cryptoNetwork) return Promise.resolve(false)
|
||||
return layer2.cryptoNetwork(account, cryptoCode)
|
||||
}
|
||||
|
||||
function isLayer2Address (address) {
|
||||
return address.split(':').length >= 2
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isLayer2Address,
|
||||
newAddress,
|
||||
getStatus,
|
||||
cryptoNetwork
|
||||
}
|
||||
|
|
@ -1,534 +0,0 @@
|
|||
const fsPromises = require('fs').promises
|
||||
const path = require('path')
|
||||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
const uuid = require('uuid')
|
||||
const makeDir = require('make-dir')
|
||||
|
||||
const batching = require('./cashbox-batches')
|
||||
const db = require('./db')
|
||||
const pairing = require('./pairing')
|
||||
const { checkPings, checkStuckScreen } = require('./notifier')
|
||||
const dbm = require('./postgresql_interface')
|
||||
const configManager = require('./new-config-manager')
|
||||
const notifierUtils = require('./notifier/utils')
|
||||
const notifierQueries = require('./notifier/queries')
|
||||
const { GraphQLError } = require('graphql');
|
||||
const { loadLatestConfig } = require('./new-settings-loader')
|
||||
const logger = require('./logger')
|
||||
|
||||
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||
const stuckStatus = { label: 'Stuck', type: 'error' }
|
||||
const OPERATOR_DATA_DIR = process.env.OPERATOR_DATA_DIR
|
||||
|
||||
const MACHINE_WITH_CALCULATED_FIELD_SQL = `
|
||||
select d.*, COALESCE(emptybills, 0) + COALESCE(regularbills, 0) as cashbox from devices d
|
||||
left join (
|
||||
select count(*) as emptyBills, eub.device_id
|
||||
from empty_unit_bills eub
|
||||
where eub.cashbox_batch_id is null
|
||||
group by eub.device_id
|
||||
) as nebills on nebills.device_id = d.device_id
|
||||
left join (
|
||||
select count(*) as regularBills, cit.device_id from bills b
|
||||
left join cash_in_txs cit on b.cash_in_txs_id = cit.id
|
||||
where b.cashbox_batch_id is null and b.destination_unit = 'cashbox'
|
||||
group by cit.device_id
|
||||
) as nbills on nbills.device_id = d.device_id`
|
||||
|
||||
function toMachineObject (r) {
|
||||
return {
|
||||
deviceId: r.device_id,
|
||||
cashUnits: {
|
||||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
cassette2: r.cassette2,
|
||||
cassette3: r.cassette3,
|
||||
cassette4: r.cassette4,
|
||||
recycler1: r.recycler1,
|
||||
recycler2: r.recycler2,
|
||||
recycler3: r.recycler3,
|
||||
recycler4: r.recycler4,
|
||||
recycler5: r.recycler5,
|
||||
recycler6: r.recycler6
|
||||
},
|
||||
numberOfCassettes: r.number_of_cassettes,
|
||||
numberOfRecyclers: r.number_of_recyclers,
|
||||
version: r.version,
|
||||
model: r.model,
|
||||
diagnostics: {
|
||||
timestamp: r.diagnostics_timestamp? new Date(r.diagnostics_timestamp) : null,
|
||||
scanTimestamp: r.diagnostics_scan_timestamp? new Date(r.diagnostics_scan_timestamp) : null,
|
||||
frontTimestamp: r.diagnostics_front_timestamp? new Date(r.diagnostics_front_timestamp) : null
|
||||
},
|
||||
pairedAt: new Date(r.created),
|
||||
lastPing: new Date(r.last_online),
|
||||
name: r.name,
|
||||
paired: r.paired
|
||||
// TODO: we shall start using this JSON field at some point
|
||||
// location: r.location,
|
||||
}
|
||||
}
|
||||
|
||||
function getMachineIds () {
|
||||
const sql = 'select device_id from devices'
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
function getMachines () {
|
||||
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} where display=TRUE ORDER BY created`
|
||||
return db.any(sql)
|
||||
.then(rr => rr.map(toMachineObject))
|
||||
}
|
||||
|
||||
function getUnpairedMachines () {
|
||||
return db.any('SELECT * FROM unpaired_devices')
|
||||
.then(_.map(r =>
|
||||
_.flow(
|
||||
_.set('deviceId', _.get('device_id', r)),
|
||||
_.unset('device_id')
|
||||
)(r)
|
||||
))
|
||||
}
|
||||
|
||||
function getConfig (defaultConfig) {
|
||||
return defaultConfig ? Promise.resolve(defaultConfig) : loadLatestConfig()
|
||||
}
|
||||
|
||||
const getStatus = (ping, stuck) => {
|
||||
if (ping && ping.age) return unresponsiveStatus
|
||||
|
||||
if (stuck && stuck.age) return stuckStatus
|
||||
|
||||
return fullyFunctionalStatus
|
||||
}
|
||||
|
||||
function addName (pings, events, config) {
|
||||
return machine => {
|
||||
const cashOutConfig = configManager.getCashOut(machine.deviceId, config)
|
||||
|
||||
const cashOut = !!cashOutConfig.active
|
||||
|
||||
const statuses = [
|
||||
getStatus(
|
||||
_.first(pings[machine.deviceId]),
|
||||
_.first(checkStuckScreen(events, machine))
|
||||
)
|
||||
]
|
||||
|
||||
return _.assign(machine, { cashOut, statuses })
|
||||
}
|
||||
}
|
||||
|
||||
function getMachineNames (config) {
|
||||
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
|
||||
.then(([rawMachines, config, heartbeat, performance]) => Promise.all(
|
||||
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
|
||||
))
|
||||
.then(([rawMachines, pings, events, config, heartbeat, performance]) => {
|
||||
const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
|
||||
const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)
|
||||
|
||||
return machines.map(addName(pings, events, config))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the machine id, get the machine name
|
||||
*
|
||||
* @name getMachineName
|
||||
* @function
|
||||
* @async
|
||||
*
|
||||
* @param {string} machineId machine id
|
||||
* @returns {string} machine name
|
||||
*/
|
||||
function getMachineName (machineId) {
|
||||
const sql = 'SELECT name FROM devices WHERE device_id=$1'
|
||||
return db.oneOrNone(sql, [machineId])
|
||||
.then(it => it.name)
|
||||
}
|
||||
|
||||
function getMachine (machineId, config) {
|
||||
const sql = `${MACHINE_WITH_CALCULATED_FIELD_SQL} WHERE d.device_id = $1`
|
||||
|
||||
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => {
|
||||
if (r === null) throw new GraphQLError('Resource doesn\'t exist', { extensions: { code: 'NOT_FOUND' } })
|
||||
else return toMachineObject(r)
|
||||
})
|
||||
|
||||
return Promise.all([queryMachine, dbm.machineEvents(), config, getNetworkHeartbeatByDevice(machineId), getNetworkPerformanceByDevice(machineId)])
|
||||
.then(([machine, events, config, heartbeat, performance]) => {
|
||||
const pings = checkPings([machine])
|
||||
const mergedMachine = {
|
||||
...machine,
|
||||
responseTime: _.get('responseTime', heartbeat),
|
||||
packetLoss: _.get('packetLoss', heartbeat),
|
||||
downloadSpeed: _.get('downloadSpeed', performance),
|
||||
}
|
||||
|
||||
return addName(pings, events, config)(mergedMachine)
|
||||
})
|
||||
}
|
||||
|
||||
function renameMachine (rec) {
|
||||
const sql = 'UPDATE devices SET name=$1 WHERE device_id=$2'
|
||||
return db.none(sql, [rec.newName, rec.deviceId])
|
||||
}
|
||||
|
||||
function resetCashOutBills (rec) {
|
||||
const detailB = notifierUtils.buildDetail({ deviceId: rec.deviceId })
|
||||
const { cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6 } = rec.cashUnits
|
||||
const sql = `UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11;`
|
||||
return db.none(sql, [cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, rec.deviceId]).then(() => notifierQueries.invalidateNotification(detailB, 'fiatBalance'))
|
||||
}
|
||||
|
||||
function setCassetteBills (rec) {
|
||||
const { cashbox, cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6 } = rec.cashUnits
|
||||
return getMachine(rec.deviceId)
|
||||
.then(machine => {
|
||||
const oldCashboxCount = machine?.cashUnits?.cashbox
|
||||
if (_.isNil(oldCashboxCount) || cashbox.toString() === oldCashboxCount.toString()) {
|
||||
const sql = `
|
||||
UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4,
|
||||
recycler1=coalesce($5, recycler1), recycler2=coalesce($6, recycler2), recycler3=coalesce($7, recycler3),
|
||||
recycler4=coalesce($8, recycler4), recycler5=coalesce($9, recycler5), recycler6=coalesce($10, recycler6)
|
||||
WHERE device_id=$11`
|
||||
return db.none(sql, [cassette1, cassette2, cassette3, cassette4, recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, rec.deviceId])
|
||||
}
|
||||
|
||||
return batching.updateMachineWithBatch({ ...rec, oldCashboxValue: oldCashboxCount })
|
||||
})
|
||||
}
|
||||
|
||||
function emptyMachineUnits ({ deviceId, newUnits, fiatCode }) {
|
||||
return loadLatestConfig()
|
||||
.then(config => Promise.all([getMachine(deviceId), configManager.getCashOut(deviceId, config)]))
|
||||
.then(([machine, cashoutSettings]) => {
|
||||
const movedBills = _.reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[value]: {
|
||||
operationName: `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`,
|
||||
delta: newUnits[value] - machine.cashUnits[value],
|
||||
denomination: value !== 'cashbox' ? cashoutSettings[value] : null
|
||||
}
|
||||
}),
|
||||
{},
|
||||
_.keys(newUnits)
|
||||
)
|
||||
|
||||
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cashbox'], movedBills)))
|
||||
const operationsToCreate = _.map(it => ({
|
||||
id: uuid.v4(),
|
||||
device_id: deviceId,
|
||||
operation_type: it
|
||||
}))(operationNames)
|
||||
|
||||
const billArr = _.reduce(
|
||||
(acc, value) => {
|
||||
const unit = movedBills[value]
|
||||
return _.concat(acc, _.times(() => ({
|
||||
id: uuid.v4(),
|
||||
fiat: unit.denomination,
|
||||
fiat_code: fiatCode,
|
||||
device_id: deviceId
|
||||
// TODO: Uncomment this if we decide to keep track of bills across multiple operations. For now, we'll just create the emptying operations for each unit affected, but not relate these events with individual bills and just use the field for the cashbox batch event
|
||||
// cash_unit_operation_id: _.find(it => it.operation_type === `cash-${_.replace(/(cassette|recycler)/g, '$1-')(value)}-empty`, operationsToCreate).id
|
||||
}), Math.abs(unit.delta)))
|
||||
},
|
||||
[],
|
||||
_.keys(_.omit(['cashbox'], movedBills))
|
||||
)
|
||||
|
||||
// This occurs when an empty unit is called when the units are already empty, hence, no bills moved around
|
||||
if (_.isEmpty(billArr) && _.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 q2Cols = ['id', 'fiat', 'fiat_code', 'device_id']
|
||||
const q2 = t.none(pgp.helpers.insert(billArr, q2Cols, 'empty_unit_bills'))
|
||||
const q3 = t.none(`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, [
|
||||
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
||||
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
||||
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
||||
_.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4),
|
||||
_.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1),
|
||||
_.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2),
|
||||
_.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3),
|
||||
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
||||
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
||||
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
||||
deviceId
|
||||
])
|
||||
|
||||
return t.batch([q1, q2, q3])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function refillMachineUnits ({ deviceId, newUnits }) {
|
||||
return getMachine(deviceId)
|
||||
.then(machine => {
|
||||
const movedBills = _.reduce(
|
||||
(acc, value) => ({
|
||||
...acc,
|
||||
[value]: {
|
||||
operationName: `cash-${_.replace(/(recycler)/g, '$1-')(value)}-refill`,
|
||||
delta: newUnits[value] - machine.cashUnits[value]
|
||||
}
|
||||
}),
|
||||
{},
|
||||
_.keys(newUnits)
|
||||
)
|
||||
|
||||
const operationNames = _.mapValues(it => it.operationName)(_.filter(it => Math.abs(it.delta) > 0)(_.omit(['cassette1', 'cassette2', 'cassette3', 'cassette4'], movedBills)))
|
||||
const operationsToCreate = _.map(it => ({
|
||||
id: uuid.v4(),
|
||||
device_id: deviceId,
|
||||
operation_type: it
|
||||
}))(operationNames)
|
||||
|
||||
// This occurs when a refill unit is called when the loading boxes are empty, hence, no bills moved around
|
||||
if (_.isEmpty(operationsToCreate)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return db.tx(t => {
|
||||
const q1Cols = ['id', 'device_id', 'operation_type']
|
||||
const q1= t.none(pgp.helpers.insert(operationsToCreate, q1Cols, 'cash_unit_operation'))
|
||||
const q2 = t.none(`UPDATE devices SET cassette1=$1, cassette2=$2, cassette3=$3, cassette4=$4, recycler1=$5, recycler2=$6, recycler3=$7, recycler4=$8, recycler5=$9, recycler6=$10 WHERE device_id=$11`, [
|
||||
_.defaultTo(machine.cashUnits.cassette1, newUnits.cassette1),
|
||||
_.defaultTo(machine.cashUnits.cassette2, newUnits.cassette2),
|
||||
_.defaultTo(machine.cashUnits.cassette3, newUnits.cassette3),
|
||||
_.defaultTo(machine.cashUnits.cassette4, newUnits.cassette4),
|
||||
_.defaultTo(machine.cashUnits.recycler1, newUnits.recycler1),
|
||||
_.defaultTo(machine.cashUnits.recycler2, newUnits.recycler2),
|
||||
_.defaultTo(machine.cashUnits.recycler3, newUnits.recycler3),
|
||||
_.defaultTo(machine.cashUnits.recycler4, newUnits.recycler4),
|
||||
_.defaultTo(machine.cashUnits.recycler5, newUnits.recycler5),
|
||||
_.defaultTo(machine.cashUnits.recycler6, newUnits.recycler6),
|
||||
deviceId
|
||||
])
|
||||
|
||||
return t.batch([q1, q2])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function unpair (rec) {
|
||||
return pairing.unpair(rec.deviceId)
|
||||
}
|
||||
|
||||
function reboot (rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'reboot',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)])
|
||||
}
|
||||
|
||||
function shutdown (rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'shutdown',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)])
|
||||
}
|
||||
|
||||
function restartServices (rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'restartServices',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)])
|
||||
}
|
||||
|
||||
function emptyUnit (rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'emptyUnit',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)])
|
||||
}
|
||||
|
||||
function refillUnit (rec) {
|
||||
return db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'refillUnit',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)])
|
||||
}
|
||||
|
||||
function diagnostics (rec) {
|
||||
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${rec.deviceId}/`
|
||||
const sql = `UPDATE devices
|
||||
SET diagnostics_timestamp = NULL,
|
||||
diagnostics_scan_updated_at = NULL,
|
||||
diagnostics_front_updated_at = NULL
|
||||
WHERE device_id = $1`
|
||||
|
||||
const scanPath = path.join(directory, 'scan.jpg')
|
||||
const frontPath = path.join(directory, 'front.jpg')
|
||||
|
||||
const removeFiles = [scanPath, frontPath].map(filePath => {
|
||||
return fsPromises.unlink(filePath).catch(err => {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
// File doesn't exist, no problem
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.all(removeFiles)
|
||||
.then(() => db.none(sql, [rec.deviceId]))
|
||||
.then(() => db.none('NOTIFY $1:name, $2', ['machineAction', JSON.stringify(
|
||||
{
|
||||
action: 'diagnostics',
|
||||
value: _.pick(['deviceId', 'operatorId', 'action'], rec)
|
||||
}
|
||||
)]))
|
||||
}
|
||||
|
||||
function setMachine (rec, operatorId) {
|
||||
rec.operatorId = operatorId
|
||||
switch (rec.action) {
|
||||
case 'rename': return renameMachine(rec)
|
||||
case 'resetCashOutBills': return resetCashOutBills(rec)
|
||||
case 'setCassetteBills': return setCassetteBills(rec)
|
||||
case 'unpair': return unpair(rec)
|
||||
case 'reboot': return reboot(rec)
|
||||
case 'shutdown': return shutdown(rec)
|
||||
case 'restartServices': return restartServices(rec)
|
||||
case 'emptyUnit': return emptyUnit(rec)
|
||||
case 'refillUnit': return refillUnit(rec)
|
||||
case 'diagnostics': return diagnostics(rec)
|
||||
default: throw new Error('No such action: ' + rec.action)
|
||||
}
|
||||
}
|
||||
|
||||
function updateNetworkPerformance (deviceId, data) {
|
||||
if (_.isEmpty(data)) return Promise.resolve(true)
|
||||
const downloadSpeed = _.head(data)
|
||||
const dbData = {
|
||||
device_id: deviceId,
|
||||
download_speed: downloadSpeed.speed,
|
||||
created: new Date()
|
||||
}
|
||||
const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'],
|
||||
{ table: 'machine_network_performance' })
|
||||
const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' +
|
||||
cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] })
|
||||
const upsert = pgp.helpers.insert(dbData, cs) + onConflict
|
||||
return db.none(upsert)
|
||||
}
|
||||
|
||||
function updateNetworkHeartbeat (deviceId, data) {
|
||||
if (_.isEmpty(data)) return Promise.resolve(true)
|
||||
const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data)
|
||||
const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data)
|
||||
const dbData = {
|
||||
id: uuid.v4(),
|
||||
device_id: deviceId,
|
||||
average_response_time: avgResponseTime,
|
||||
average_packet_loss: avgPacketLoss
|
||||
}
|
||||
const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat')
|
||||
return db.none(sql)
|
||||
}
|
||||
|
||||
function getNetworkPerformance () {
|
||||
const sql = `SELECT device_id, download_speed FROM machine_network_performance`
|
||||
return db.manyOrNone(sql)
|
||||
.then(res => _.map(_.mapKeys(_.camelCase))(res))
|
||||
}
|
||||
|
||||
function getNetworkHeartbeat () {
|
||||
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
|
||||
FROM machine_network_heartbeat
|
||||
GROUP BY device_id`
|
||||
return db.manyOrNone(sql)
|
||||
.then(res => _.map(_.mapKeys(_.camelCase))(res))
|
||||
}
|
||||
|
||||
function getNetworkPerformanceByDevice (deviceId) {
|
||||
const sql = `SELECT device_id, download_speed FROM machine_network_performance WHERE device_id = $1`
|
||||
return db.manyOrNone(sql, [deviceId])
|
||||
.then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res)))
|
||||
}
|
||||
|
||||
function getNetworkHeartbeatByDevice (deviceId) {
|
||||
const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id
|
||||
FROM machine_network_heartbeat WHERE device_id = $1
|
||||
GROUP BY device_id`
|
||||
return db.manyOrNone(sql, [deviceId])
|
||||
.then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res)))
|
||||
}
|
||||
|
||||
function updateDiagnostics (deviceId, images) {
|
||||
const sql = `UPDATE devices
|
||||
SET diagnostics_timestamp = NOW(),
|
||||
diagnostics_scan_updated_at = CASE WHEN $2 THEN NOW() ELSE diagnostics_scan_updated_at END,
|
||||
diagnostics_front_updated_at = CASE WHEN $3 THEN NOW() ELSE diagnostics_front_updated_at END
|
||||
WHERE device_id = $1`
|
||||
|
||||
const directory = `${OPERATOR_DATA_DIR}/diagnostics/${deviceId}/`
|
||||
const { scan, front } = images
|
||||
|
||||
return updatePhotos(directory, [['scan.jpg', scan], ['front.jpg', front]])
|
||||
.then(() => db.none(sql, [deviceId, !!scan, !!front]))
|
||||
.catch(err => logger.error('while running machine diagnostics: ', err))
|
||||
}
|
||||
|
||||
const updateFailedQRScans = (deviceId, frames) => {
|
||||
const timestamp = (new Date()).toISOString()
|
||||
const directory = `${OPERATOR_DATA_DIR}/failedQRScans/${deviceId}/`
|
||||
const filenames = _.map(no => `${timestamp}-${no}.jpg`, _.range(0, _.size(frames)))
|
||||
return updatePhotos(directory, _.zip(filenames, frames))
|
||||
}
|
||||
|
||||
function createPhoto (name, data, dir) {
|
||||
if (!data) {
|
||||
logger.error(`Diagnostics error: No data to save for ${name} photo`)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const decodedImageData = Buffer.from(data, 'base64')
|
||||
const filename = path.join(dir, name)
|
||||
return fsPromises.writeFile(filename, decodedImageData)
|
||||
}
|
||||
|
||||
function updatePhotos (dir, photoPairs) {
|
||||
const dirname = path.join(dir)
|
||||
_.attempt(() => makeDir.sync(dirname))
|
||||
return Promise.all(photoPairs.map(
|
||||
([filename, data]) => createPhoto(filename, data, dirname)
|
||||
))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMachineName,
|
||||
getMachines,
|
||||
getUnpairedMachines,
|
||||
getMachine,
|
||||
getMachineNames,
|
||||
setMachine,
|
||||
updateNetworkPerformance,
|
||||
updateNetworkHeartbeat,
|
||||
getNetworkPerformance,
|
||||
getNetworkHeartbeat,
|
||||
getMachineIds,
|
||||
emptyMachineUnits,
|
||||
refillMachineUnits,
|
||||
updateDiagnostics,
|
||||
updateFailedQRScans
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
const { COINS, ALL_CRYPTOS } = require('@lamassu/coins')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const { ALL } = require('../../plugins/common/ccxt')
|
||||
|
||||
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR, LN, TRX, USDT_TRON, USDC } = COINS
|
||||
const { bitpay, itbit, bitstamp, kraken, binanceus, cex, binance, bitfinex } = ALL
|
||||
|
||||
const TICKER = 'ticker'
|
||||
const WALLET = 'wallet'
|
||||
const LAYER_2 = 'layer2'
|
||||
const EXCHANGE = 'exchange'
|
||||
const SMS = 'sms'
|
||||
const ID_VERIFIER = 'idVerifier'
|
||||
const EMAIL = 'email'
|
||||
const ZERO_CONF = 'zeroConf'
|
||||
const WALLET_SCORING = 'wallet_scoring'
|
||||
const COMPLIANCE = 'compliance'
|
||||
|
||||
const ALL_ACCOUNTS = [
|
||||
{ code: 'bitfinex', display: 'Bitfinex', class: TICKER, cryptos: bitfinex.CRYPTO },
|
||||
{ code: 'bitfinex', display: 'Bitfinex', class: EXCHANGE, cryptos: bitfinex.CRYPTO },
|
||||
{ code: 'binance', display: 'Binance', class: TICKER, cryptos: binance.CRYPTO },
|
||||
{ code: 'binanceus', display: 'Binance.us', class: TICKER, cryptos: binanceus.CRYPTO },
|
||||
{ code: 'cex', display: 'CEX.IO', class: TICKER, cryptos: cex.CRYPTO },
|
||||
{ code: 'bitpay', display: 'Bitpay', class: TICKER, cryptos: bitpay.CRYPTO },
|
||||
{ code: 'kraken', display: 'Kraken', class: TICKER, cryptos: kraken.CRYPTO },
|
||||
{ code: 'bitstamp', display: 'Bitstamp', class: TICKER, cryptos: bitstamp.CRYPTO },
|
||||
{ code: 'itbit', display: 'itBit', class: TICKER, cryptos: itbit.CRYPTO },
|
||||
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
|
||||
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
||||
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
||||
{ code: 'infura', display: 'Infura/Alchemy', class: WALLET, cryptos: [ETH, USDT, USDC] },
|
||||
{ code: 'trongrid', display: 'Trongrid', class: WALLET, cryptos: [TRX, USDT_TRON] },
|
||||
{ code: 'geth', display: 'geth (deprecated)', class: WALLET, cryptos: [ETH, USDT, USDC] },
|
||||
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
||||
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
||||
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
||||
{ code: 'monerod', display: 'monerod', class: WALLET, cryptos: [XMR] },
|
||||
{ code: 'bitcoincashd', display: 'bitcoincashd', class: WALLET, cryptos: [BCH] },
|
||||
{ code: 'bitgo', display: 'BitGo', class: WALLET, cryptos: [BTC, ZEC, LTC, BCH, DASH] },
|
||||
{ code: 'galoy', display: 'Galoy', class: WALLET, cryptos: [LN] },
|
||||
{ code: 'bitstamp', display: 'Bitstamp', class: EXCHANGE, cryptos: bitstamp.CRYPTO },
|
||||
{ code: 'itbit', display: 'itBit', class: EXCHANGE, cryptos: itbit.CRYPTO },
|
||||
{ code: 'kraken', display: 'Kraken', class: EXCHANGE, cryptos: kraken.CRYPTO },
|
||||
{ code: 'binance', display: 'Binance', class: EXCHANGE, cryptos: binance.CRYPTO },
|
||||
{ code: 'binanceus', display: 'Binance.us', class: EXCHANGE, cryptos: binanceus.CRYPTO },
|
||||
{ code: 'cex', display: 'CEX.IO', class: EXCHANGE, cryptos: cex.CRYPTO },
|
||||
{ code: 'mock-wallet', display: 'Mock (Caution!)', class: WALLET, cryptos: ALL_CRYPTOS, dev: true },
|
||||
{ code: 'no-exchange', display: 'No exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS },
|
||||
{ code: 'mock-exchange', display: 'Mock exchange', class: EXCHANGE, cryptos: ALL_CRYPTOS, dev: true },
|
||||
{ code: 'mock-sms', display: 'Mock SMS', class: SMS, dev: true },
|
||||
{ code: 'mock-id-verify', display: 'Mock ID verifier', class: ID_VERIFIER, dev: true },
|
||||
{ code: 'twilio', display: 'Twilio', class: SMS },
|
||||
{ code: 'telnyx', display: 'Telnyx', class: SMS },
|
||||
{ code: 'vonage', display: 'Vonage', class: SMS },
|
||||
{ code: 'inforu', display: 'InforU', class: SMS },
|
||||
{ code: 'mailgun', display: 'Mailgun', class: EMAIL },
|
||||
{ code: 'mock-email', display: 'Mock Email', class: EMAIL, dev: true },
|
||||
{ code: 'none', display: 'None', class: ZERO_CONF, cryptos: ALL_CRYPTOS },
|
||||
{ code: 'blockcypher', display: 'Blockcypher', class: ZERO_CONF, cryptos: [BTC] },
|
||||
{ code: 'mock-zero-conf', display: 'Mock 0-conf', class: ZERO_CONF, cryptos: ALL_CRYPTOS, dev: true },
|
||||
{ code: 'scorechain', display: 'Scorechain', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, DASH, USDT, USDC, USDT_TRON, TRX] },
|
||||
{ code: 'elliptic', display: 'Elliptic', class: WALLET_SCORING, cryptos: [BTC, ETH, LTC, BCH, USDT, USDC, USDT_TRON, TRX, ZEC] },
|
||||
{ code: 'mock-scoring', display: 'Mock scoring', class: WALLET_SCORING, cryptos: ALL_CRYPTOS, dev: true },
|
||||
{ code: 'sumsub', display: 'Sumsub', class: COMPLIANCE },
|
||||
{ code: 'mock-compliance', display: 'Mock Compliance', class: COMPLIANCE, dev: true },
|
||||
]
|
||||
|
||||
const flags = require('minimist')(process.argv.slice(2))
|
||||
const devMode = flags.dev || flags.lamassuDev
|
||||
const ACCOUNT_LIST = devMode ? ALL_ACCOUNTS : _.filter(it => !it.dev)(ALL_ACCOUNTS)
|
||||
|
||||
module.exports = { ACCOUNT_LIST }
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,255 +0,0 @@
|
|||
{
|
||||
"attribute": {"name":0, "nativeName":1},
|
||||
"rtl": {"ar":1,"dv":1,"fa":1,"ha":1,"he":1,"ks":1,"ku":1,"ps":1,"ur":1,"yi":1},
|
||||
"lang": {
|
||||
"aa":["Afar","Afar"],
|
||||
"ab":["Abkhazian","Аҧсуа"],
|
||||
"af":["Afrikaans","Afrikaans"],
|
||||
"ak":["Akan","Akana"],
|
||||
"am":["Amharic","አማርኛ"],
|
||||
"an":["Aragonese","Aragonés"],
|
||||
"ar":["Arabic","العربية"],
|
||||
"as":["Assamese","অসমীয়া"],
|
||||
"av":["Avar","Авар"],
|
||||
"ay":["Aymara","Aymar"],
|
||||
"az":["Azerbaijani","Azərbaycanca / آذربايجان"],
|
||||
"ba":["Bashkir","Башҡорт"],
|
||||
"be":["Belarusian","Беларуская"],
|
||||
"bg":["Bulgarian","Български"],
|
||||
"bh":["Bihari","भोजपुरी"],
|
||||
"bi":["Bislama","Bislama"],
|
||||
"bm":["Bambara","Bamanankan"],
|
||||
"bn":["Bengali","বাংলা"],
|
||||
"bo":["Tibetan","བོད་ཡིག / Bod skad"],
|
||||
"br":["Breton","Brezhoneg"],
|
||||
"bs":["Bosnian","Bosanski"],
|
||||
"ca":["Catalan","Català"],
|
||||
"ce":["Chechen","Нохчийн"],
|
||||
"ch":["Chamorro","Chamoru"],
|
||||
"co":["Corsican","Corsu"],
|
||||
"cr":["Cree","Nehiyaw"],
|
||||
"cs":["Czech","Česky"],
|
||||
"cu":["Old Church Slavonic / Old Bulgarian","словѣньскъ / slověnĭskŭ"],
|
||||
"cv":["Chuvash","Чăваш"],
|
||||
"cy":["Welsh","Cymraeg"],
|
||||
"da":["Danish","Dansk"],
|
||||
"de":["German","Deutsch"],
|
||||
"dv":["Divehi","ދިވެހިބަސް"],
|
||||
"dz":["Dzongkha","ཇོང་ཁ"],
|
||||
"ee":["Ewe","Ɛʋɛ"],
|
||||
"el":["Greek","Ελληνικά"],
|
||||
"en":["English","English"],
|
||||
"eo":["Esperanto","Esperanto"],
|
||||
"es":["Spanish","Español"],
|
||||
"et":["Estonian","Eesti"],
|
||||
"eu":["Basque","Euskara"],
|
||||
"fa":["Persian","فارسی"],
|
||||
"ff":["Peul","Fulfulde"],
|
||||
"fi":["Finnish","Suomi"],
|
||||
"fj":["Fijian","Na Vosa Vakaviti"],
|
||||
"fo":["Faroese","Føroyskt"],
|
||||
"fr":["French","Français"],
|
||||
"fy":["West Frisian","Frysk"],
|
||||
"ga":["Irish","Gaeilge"],
|
||||
"gd":["Scottish Gaelic","Gàidhlig"],
|
||||
"gl":["Galician","Galego"],
|
||||
"gn":["Guarani","Avañe'ẽ"],
|
||||
"gu":["Gujarati","ગુજરાતી"],
|
||||
"gv":["Manx","Gaelg"],
|
||||
"ha":["Hausa","هَوُسَ"],
|
||||
"he":["Hebrew","עברית"],
|
||||
"hi":["Hindi","हिन्दी"],
|
||||
"ho":["Hiri Motu","Hiri Motu"],
|
||||
"hr":["Croatian","Hrvatski"],
|
||||
"ht":["Haitian","Krèyol ayisyen"],
|
||||
"hu":["Hungarian","Magyar"],
|
||||
"hy":["Armenian","Հայերեն"],
|
||||
"hz":["Herero","Otsiherero"],
|
||||
"ia":["Interlingua","Interlingua"],
|
||||
"id":["Indonesian","Bahasa Indonesia"],
|
||||
"ie":["Interlingue","Interlingue"],
|
||||
"ig":["Igbo","Igbo"],
|
||||
"ii":["Sichuan Yi","ꆇꉙ / 四川彝语"],
|
||||
"ik":["Inupiak","Iñupiak"],
|
||||
"io":["Ido","Ido"],
|
||||
"is":["Icelandic","Íslenska"],
|
||||
"it":["Italian","Italiano"],
|
||||
"iu":["Inuktitut","ᐃᓄᒃᑎᑐᑦ"],
|
||||
"ja":["Japanese","日本語"],
|
||||
"jv":["Javanese","Basa Jawa"],
|
||||
"ka":["Georgian","ქართული"],
|
||||
"kg":["Kongo","KiKongo"],
|
||||
"ki":["Kikuyu","Gĩkũyũ"],
|
||||
"kj":["Kuanyama","Kuanyama"],
|
||||
"kk":["Kazakh","Қазақша"],
|
||||
"kl":["Greenlandic","Kalaallisut"],
|
||||
"km":["Cambodian","ភាសាខ្មែរ"],
|
||||
"kn":["Kannada","ಕನ್ನಡ"],
|
||||
"ko":["Korean","한국어"],
|
||||
"kr":["Kanuri","Kanuri"],
|
||||
"ks":["Kashmiri","कश्मीरी / كشميري"],
|
||||
"ku":["Kurdish","Kurdî / كوردی"],
|
||||
"kv":["Komi","Коми"],
|
||||
"kw":["Cornish","Kernewek"],
|
||||
"ky":["Kirghiz","Kırgızca / Кыргызча"],
|
||||
"la":["Latin","Latina"],
|
||||
"lb":["Luxembourgish","Lëtzebuergesch"],
|
||||
"lg":["Ganda","Luganda"],
|
||||
"li":["Limburgian","Limburgs"],
|
||||
"ln":["Lingala","Lingála"],
|
||||
"lo":["Laotian","ລາວ / Pha xa lao"],
|
||||
"lt":["Lithuanian","Lietuvių"],
|
||||
"lv":["Latvian","Latviešu"],
|
||||
"mg":["Malagasy","Malagasy"],
|
||||
"mh":["Marshallese","Kajin Majel / Ebon"],
|
||||
"mi":["Maori","Māori"],
|
||||
"mk":["Macedonian","Македонски"],
|
||||
"ml":["Malayalam","മലയാളം"],
|
||||
"mn":["Mongolian","Монгол"],
|
||||
"mo":["Moldovan","Moldovenească"],
|
||||
"mr":["Marathi","मराठी"],
|
||||
"ms":["Malay","Bahasa Melayu"],
|
||||
"mt":["Maltese","bil-Malti"],
|
||||
"my":["Burmese","Myanmasa"],
|
||||
"na":["Nauruan","Dorerin Naoero"],
|
||||
"nd":["North Ndebele","Sindebele"],
|
||||
"ne":["Nepali","नेपाली"],
|
||||
"ng":["Ndonga","Oshiwambo"],
|
||||
"nl":["Dutch","Nederlands"],
|
||||
"nn":["Norwegian Nynorsk","Norsk (nynorsk)"],
|
||||
"no":["Norwegian","Norsk (bokmål / riksmål)"],
|
||||
"nr":["South Ndebele","isiNdebele"],
|
||||
"nv":["Navajo","Diné bizaad"],
|
||||
"ny":["Chichewa","Chi-Chewa"],
|
||||
"oc":["Occitan","Occitan"],
|
||||
"oj":["Ojibwa","ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"],
|
||||
"om":["Oromo","Oromoo"],
|
||||
"or":["Oriya","ଓଡ଼ିଆ"],
|
||||
"os":["Ossetian / Ossetic","Иронау"],
|
||||
"pa":["Panjabi / Punjabi","ਪੰਜਾਬੀ / पंजाबी / پنجابي"],
|
||||
"pi":["Pali","Pāli / पाऴि"],
|
||||
"pl":["Polish","Polski"],
|
||||
"ps":["Pashto","پښتو"],
|
||||
"pt":["Portuguese","Português"],
|
||||
"qu":["Quechua","Runa Simi"],
|
||||
"rm":["Raeto Romance","Rumantsch"],
|
||||
"rn":["Kirundi","Kirundi"],
|
||||
"ro":["Romanian","Română"],
|
||||
"ru":["Russian","Русский"],
|
||||
"rw":["Rwandi","Kinyarwandi"],
|
||||
"sa":["Sanskrit","संस्कृतम्"],
|
||||
"sc":["Sardinian","Sardu"],
|
||||
"sd":["Sindhi","सिनधि"],
|
||||
"se":["Northern Sami","Sámegiella"],
|
||||
"sg":["Sango","Sängö"],
|
||||
"sh":["Serbo-Croatian","Srpskohrvatski / Српскохрватски"],
|
||||
"si":["Sinhalese","සිංහල"],
|
||||
"sk":["Slovak","Slovenčina"],
|
||||
"sl":["Slovenian","Slovenščina"],
|
||||
"sm":["Samoan","Gagana Samoa"],
|
||||
"sn":["Shona","chiShona"],
|
||||
"so":["Somalia","Soomaaliga"],
|
||||
"sq":["Albanian","Shqip"],
|
||||
"sr":["Serbian","Српски"],
|
||||
"ss":["Swati","SiSwati"],
|
||||
"st":["Southern Sotho","Sesotho"],
|
||||
"su":["Sundanese","Basa Sunda"],
|
||||
"sv":["Swedish","Svenska"],
|
||||
"sw":["Swahili","Kiswahili"],
|
||||
"ta":["Tamil","தமிழ்"],
|
||||
"te":["Telugu","తెలుగు"],
|
||||
"tg":["Tajik","Тоҷикӣ"],
|
||||
"th":["Thai","ไทย / Phasa Thai"],
|
||||
"ti":["Tigrinya","ትግርኛ"],
|
||||
"tk":["Turkmen","Туркмен / تركمن"],
|
||||
"tl":["Tagalog / Filipino","Tagalog"],
|
||||
"tn":["Tswana","Setswana"],
|
||||
"to":["Tonga","Lea Faka-Tonga"],
|
||||
"tr":["Turkish","Türkçe"],
|
||||
"ts":["Tsonga","Xitsonga"],
|
||||
"tt":["Tatar","Tatarça"],
|
||||
"tw":["Twi","Twi"],
|
||||
"ty":["Tahitian","Reo Mā`ohi"],
|
||||
"ug":["Uyghur","Uyƣurqə / ئۇيغۇرچە"],
|
||||
"uk":["Ukrainian","Українська"],
|
||||
"ur":["Urdu","اردو"],
|
||||
"uz":["Uzbek","Ўзбек"],
|
||||
"ve":["Venda","Tshivenḓa"],
|
||||
"vi":["Vietnamese","Tiếng Việt"],
|
||||
"vo":["Volapük","Volapük"],
|
||||
"wa":["Walloon","Walon"],
|
||||
"wo":["Wolof","Wollof"],
|
||||
"xh":["Xhosa","isiXhosa"],
|
||||
"yi":["Yiddish","ייִדיש"],
|
||||
"yo":["Yoruba","Yorùbá"],
|
||||
"za":["Zhuang","Cuengh / Tôô / 壮语"],
|
||||
"zh":["Chinese","中文"],
|
||||
"zu":["Zulu","isiZulu"]
|
||||
},
|
||||
"supported": [
|
||||
"en-US",
|
||||
"en-CA",
|
||||
"fr-QC",
|
||||
"ach-UG",
|
||||
"af-ZA",
|
||||
"ar-SA",
|
||||
"bg-BG",
|
||||
"ca-ES",
|
||||
"cs-CZ",
|
||||
"cy-GB",
|
||||
"de-DE",
|
||||
"de-AT",
|
||||
"de-CH",
|
||||
"da-DK",
|
||||
"el-GR",
|
||||
"en-GB",
|
||||
"en-AU",
|
||||
"en-HK",
|
||||
"en-IE",
|
||||
"en-NZ",
|
||||
"en-PR",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"et-EE",
|
||||
"fi-FI",
|
||||
"fr-FR",
|
||||
"fr-CH",
|
||||
"fur-IT",
|
||||
"ga-IE",
|
||||
"gd-GB",
|
||||
"he-IL",
|
||||
"hr-HR",
|
||||
"hu-HU",
|
||||
"hy-AM",
|
||||
"id-ID",
|
||||
"it-CH",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"ka-GE",
|
||||
"ko-KR",
|
||||
"ky-KG",
|
||||
"lt-LT",
|
||||
"nb-NO",
|
||||
"nl-BE",
|
||||
"nl-NL",
|
||||
"pt-PT",
|
||||
"pt-BR",
|
||||
"pl-PL",
|
||||
"ro-RO",
|
||||
"ru-RU",
|
||||
"sco-GB",
|
||||
"sh-HR",
|
||||
"sk-SK",
|
||||
"sl-SI",
|
||||
"sr-SP",
|
||||
"sv-SE",
|
||||
"th-TH",
|
||||
"tr-TR",
|
||||
"uk-UA",
|
||||
"vi-VN",
|
||||
"zh-CN",
|
||||
"zh-HK",
|
||||
"zh-SG",
|
||||
"zh-TW"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
const simpleWebauthn = require('@simplewebauthn/server')
|
||||
const base64url = require('base64url')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const userManagement = require('../userManagement')
|
||||
const credentials = require('../../../../hardware-credentials')
|
||||
const T = require('../../../../time')
|
||||
const users = require('../../../../users')
|
||||
|
||||
const devMode = require('minimist')(process.argv.slice(2)).dev
|
||||
|
||||
const REMEMBER_ME_AGE = 90 * T.day
|
||||
|
||||
const generateAttestationOptions = (session, options) => {
|
||||
return users.getUserById(options.userId).then(user => {
|
||||
return Promise.all([credentials.getHardwareCredentialsByUserId(user.id), user])
|
||||
}).then(([userDevices, user]) => {
|
||||
const opts = simpleWebauthn.generateAttestationOptions({
|
||||
rpName: 'Lamassu',
|
||||
rpID: options.domain,
|
||||
userName: user.username,
|
||||
userID: user.id,
|
||||
timeout: 60000,
|
||||
attestationType: 'indirect',
|
||||
excludeCredentials: userDevices.map(dev => ({
|
||||
id: dev.data.credentialID,
|
||||
type: 'public-key',
|
||||
transports: ['usb', 'ble', 'nfc', 'internal']
|
||||
})),
|
||||
authenticatorSelection: {
|
||||
userVerification: 'discouraged',
|
||||
requireResidentKey: false
|
||||
}
|
||||
})
|
||||
|
||||
session.webauthn = {
|
||||
attestation: {
|
||||
challenge: opts.challenge
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
})
|
||||
}
|
||||
|
||||
const generateAssertionOptions = (session, options) => {
|
||||
return userManagement.authenticateUser(options.username, options.password).then(user => {
|
||||
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
|
||||
const opts = simpleWebauthn.generateAssertionOptions({
|
||||
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 webauthnData = session.webauthn.attestation
|
||||
const expectedChallenge = webauthnData.challenge
|
||||
|
||||
return Promise.all([
|
||||
users.getUserById(options.userId),
|
||||
simpleWebauthn.verifyAttestationResponse({
|
||||
credential: options.attestationResponse,
|
||||
expectedChallenge: `${expectedChallenge}`,
|
||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||
expectedRPID: options.domain
|
||||
})
|
||||
])
|
||||
.then(([user, verification]) => {
|
||||
const { verified, attestationInfo } = verification
|
||||
|
||||
if (!(verified || attestationInfo)) {
|
||||
session.webauthn = null
|
||||
return false
|
||||
}
|
||||
|
||||
const {
|
||||
counter,
|
||||
credentialPublicKey,
|
||||
credentialID
|
||||
} = attestationInfo
|
||||
|
||||
return credentials.getHardwareCredentialsByUserId(user.id)
|
||||
.then(userDevices => {
|
||||
const existingDevice = userDevices.find(device => device.data.credentialID === credentialID)
|
||||
|
||||
if (!existingDevice) {
|
||||
const newDevice = {
|
||||
counter,
|
||||
credentialPublicKey,
|
||||
credentialID
|
||||
}
|
||||
credentials.createHardwareCredential(user.id, newDevice)
|
||||
}
|
||||
|
||||
session.webauthn = null
|
||||
return verified
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const validateAssertion = (session, options) => {
|
||||
return userManagement.authenticateUser(options.username, options.password).then(user => {
|
||||
const expectedChallenge = session.webauthn.assertion.challenge
|
||||
|
||||
return credentials.getHardwareCredentialsByUserId(user.id).then(devices => {
|
||||
const dbAuthenticator = _.find(dev => {
|
||||
return Buffer.from(dev.data.credentialID).compare(base64url.toBuffer(options.assertionResponse.rawId)) === 0
|
||||
}, devices)
|
||||
|
||||
if (!dbAuthenticator.data) {
|
||||
throw new Error(`Could not find authenticator matching ${options.assertionResponse.id}`)
|
||||
}
|
||||
|
||||
const convertedAuthenticator = _.merge(
|
||||
dbAuthenticator.data,
|
||||
{ credentialPublicKey: Buffer.from(dbAuthenticator.data.credentialPublicKey) }
|
||||
)
|
||||
|
||||
let verification
|
||||
try {
|
||||
verification = simpleWebauthn.verifyAssertionResponse({
|
||||
credential: options.assertionResponse,
|
||||
expectedChallenge: `${expectedChallenge}`,
|
||||
expectedOrigin: `https://${options.domain}${devMode ? `:3001` : ``}`,
|
||||
expectedRPID: options.domain,
|
||||
authenticator: convertedAuthenticator
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
const { verified, assertionInfo } = verification
|
||||
|
||||
if (!verified) {
|
||||
session.webauthn = null
|
||||
return false
|
||||
}
|
||||
|
||||
dbAuthenticator.data.counter = assertionInfo.newCounter
|
||||
return credentials.updateHardwareCredential(dbAuthenticator)
|
||||
.then(() => {
|
||||
const finalUser = { id: user.id, username: user.username, role: user.role }
|
||||
session.user = finalUser
|
||||
if (options.rememberMe) session.cookie.maxAge = REMEMBER_ME_AGE
|
||||
|
||||
session.webauthn = null
|
||||
return verified
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateAttestationOptions,
|
||||
generateAssertionOptions,
|
||||
validateAttestation,
|
||||
validateAssertion
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
const { parseAsync } = require('json2csv')
|
||||
const cashbox = require('../../../cashbox-batches')
|
||||
const logDateFormat = require('../../../logs').logDateFormat
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
cashboxBatches: () => cashbox.getBatches(),
|
||||
cashboxBatchesCsv: (...[, { from, until, timezone }]) => cashbox.getBatches(from, until)
|
||||
.then(data => parseAsync(logDateFormat(timezone, cashbox.logFormatter(data), ['created'])))
|
||||
},
|
||||
Mutation: {
|
||||
createBatch: (...[, { deviceId, cashboxCount }]) => cashbox.createCashboxBatch(deviceId, cashboxCount),
|
||||
editBatch: (...[, { id, performedBy }]) => cashbox.editBatchById(id, performedBy)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
const { accounts: accountsConfig, countries, languages } = require('../../config')
|
||||
|
||||
const resolver = {
|
||||
Query: {
|
||||
countries: () => countries,
|
||||
languages: () => languages,
|
||||
accountsConfig: () => accountsConfig
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolver
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
const authentication = require('../modules/userManagement')
|
||||
const queries = require('../../services/customInfoRequests')
|
||||
const DataLoader = require('dataloader')
|
||||
|
||||
const customerCustomInfoRequestsLoader = new DataLoader(ids => queries.batchGetAllCustomInfoRequestsForCustomer(ids), { cache: false })
|
||||
|
||||
const customInfoRequestLoader = new DataLoader(ids => queries.batchGetCustomInfoRequest(ids), { cache: false })
|
||||
|
||||
const resolvers = {
|
||||
Customer: {
|
||||
customInfoRequests: parent => customerCustomInfoRequestsLoader.load(parent.id)
|
||||
},
|
||||
CustomRequestData: {
|
||||
customInfoRequest: parent => customInfoRequestLoader.load(parent.infoRequestId)
|
||||
},
|
||||
Query: {
|
||||
customInfoRequests: (...[, { onlyEnabled }]) => queries.getCustomInfoRequests(onlyEnabled),
|
||||
customerCustomInfoRequests: (...[, { customerId }]) => queries.getAllCustomInfoRequestsForCustomer(customerId),
|
||||
customerCustomInfoRequest: (...[, { customerId, infoRequestId }]) => queries.getCustomInfoRequestForCustomer(customerId, infoRequestId)
|
||||
},
|
||||
Mutation: {
|
||||
insertCustomInfoRequest: (...[, { customRequest }]) => queries.addCustomInfoRequest(customRequest),
|
||||
removeCustomInfoRequest: (...[, { id }]) => queries.removeCustomInfoRequest(id),
|
||||
editCustomInfoRequest: (...[, { id, customRequest }]) => queries.editCustomInfoRequest(id, customRequest),
|
||||
setAuthorizedCustomRequest: (...[, { customerId, infoRequestId, override }, context]) => {
|
||||
const token = authentication.getToken(context)
|
||||
return queries.setAuthorizedCustomRequest(customerId, infoRequestId, override, token)
|
||||
},
|
||||
setCustomerCustomInfoRequest: (...[, { customerId, infoRequestId, data }]) => queries.setCustomerData(customerId, infoRequestId, data)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
const loyalty = require('../../../loyalty')
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
promoCodes: () => loyalty.getAvailablePromoCodes(),
|
||||
individualDiscounts: () => loyalty.getAvailableIndividualDiscounts()
|
||||
},
|
||||
Mutation: {
|
||||
createPromoCode: (...[, { code, discount }]) => loyalty.createPromoCode(code, discount),
|
||||
deletePromoCode: (...[, { codeId }]) => loyalty.deletePromoCode(codeId),
|
||||
createIndividualDiscount: (...[, { customerId, discount }]) => loyalty.createIndividualDiscount(customerId, discount),
|
||||
deleteIndividualDiscount: (...[, { discountId }]) => loyalty.deleteIndividualDiscount(discountId)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
const { DateTimeISOResolver, JSONResolver, JSONObjectResolver } = require('graphql-scalars')
|
||||
|
||||
const resolvers = {
|
||||
JSON: JSONResolver,
|
||||
JSONObject: JSONObjectResolver,
|
||||
DateTimeISO: DateTimeISOResolver
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
const DataLoader = require('dataloader')
|
||||
const { parseAsync } = require('json2csv')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const filters = require('../../filters')
|
||||
const cashOutTx = require('../../../cash-out/cash-out-tx')
|
||||
const cashInTx = require('../../../cash-in/cash-in-tx')
|
||||
const transactions = require('../../services/transactions')
|
||||
const anonymous = require('../../../constants').anonymousCustomer
|
||||
const logDateFormat = require('../../../logs').logDateFormat
|
||||
|
||||
const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids), { cache: false })
|
||||
|
||||
const resolvers = {
|
||||
Customer: {
|
||||
transactions: parent => transactionsLoader.load(parent.id)
|
||||
},
|
||||
Transaction: {
|
||||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||
},
|
||||
Query: {
|
||||
transactions: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
|
||||
transactions.batch(from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
|
||||
transactions.batch(from, until, limit, offset, null, txClass, deviceId, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified)
|
||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
|
||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||
transactions.getTx(id, txClass).then(data =>
|
||||
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime', 'publishedAt']))
|
||||
),
|
||||
txAssociatedDataCsv: (...[, { id, txClass, timezone }]) =>
|
||||
transactions.getTxAssociatedData(id, txClass).then(data =>
|
||||
parseAsync(logDateFormat(timezone, data, ['created']))
|
||||
),
|
||||
transactionFilters: () => filters.transaction()
|
||||
},
|
||||
Mutation: {
|
||||
cancelCashOutTransaction: (...[, { id }]) => cashOutTx.cancel(id),
|
||||
cancelCashInTransaction: (...[, { id }]) => cashInTx.cancel(id)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
const authentication = require('../modules/authentication')
|
||||
const userManagement = require('../modules/userManagement')
|
||||
const users = require('../../../users')
|
||||
const sessionManager = require('../../../session-manager')
|
||||
|
||||
const getAttestationQueryOptions = variables => {
|
||||
switch (authentication.CHOSEN_STRATEGY) {
|
||||
case 'FIDO2FA':
|
||||
return { userId: variables.userID, domain: variables.domain }
|
||||
case 'FIDOPasswordless':
|
||||
return { userId: variables.userID, domain: variables.domain }
|
||||
case 'FIDOUsernameless':
|
||||
return { userId: variables.userID, domain: variables.domain }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const getAssertionQueryOptions = variables => {
|
||||
switch (authentication.CHOSEN_STRATEGY) {
|
||||
case 'FIDO2FA':
|
||||
return { username: variables.username, password: variables.password, domain: variables.domain }
|
||||
case 'FIDOPasswordless':
|
||||
return { username: variables.username, domain: variables.domain }
|
||||
case 'FIDOUsernameless':
|
||||
return { domain: variables.domain }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const getAttestationMutationOptions = variables => {
|
||||
switch (authentication.CHOSEN_STRATEGY) {
|
||||
case 'FIDO2FA':
|
||||
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
|
||||
case 'FIDOPasswordless':
|
||||
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
|
||||
case 'FIDOUsernameless':
|
||||
return { userId: variables.userID, attestationResponse: variables.attestationResponse, domain: variables.domain }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const getAssertionMutationOptions = variables => {
|
||||
switch (authentication.CHOSEN_STRATEGY) {
|
||||
case 'FIDO2FA':
|
||||
return { username: variables.username, password: variables.password, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
|
||||
case 'FIDOPasswordless':
|
||||
return { username: variables.username, rememberMe: variables.rememberMe, assertionResponse: variables.assertionResponse, domain: variables.domain }
|
||||
case 'FIDOUsernameless':
|
||||
return { assertionResponse: variables.assertionResponse, domain: variables.domain }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const resolver = {
|
||||
Query: {
|
||||
users: () => users.getUsers(),
|
||||
sessions: () => sessionManager.getSessions(),
|
||||
userSessions: (...[, { username }]) => sessionManager.getSessionsByUsername(username),
|
||||
userData: (...[, {}, context]) => userManagement.getUserData(context),
|
||||
get2FASecret: (...[, { username, password }]) => userManagement.get2FASecret(username, password),
|
||||
confirm2FA: (...[, { code }, context]) => userManagement.confirm2FA(code, context),
|
||||
validateRegisterLink: (...[, { token }]) => userManagement.validateRegisterLink(token),
|
||||
validateResetPasswordLink: (...[, { token }]) => userManagement.validateResetPasswordLink(token),
|
||||
validateReset2FALink: (...[, { token }]) => userManagement.validateReset2FALink(token),
|
||||
generateAttestationOptions: (...[, variables, context]) => authentication.strategy.generateAttestationOptions(context.req.session, getAttestationQueryOptions(variables)),
|
||||
generateAssertionOptions: (...[, variables, context]) => authentication.strategy.generateAssertionOptions(context.req.session, getAssertionQueryOptions(variables))
|
||||
},
|
||||
Mutation: {
|
||||
enableUser: (...[, { confirmationCode, id }, context]) => userManagement.enableUser(confirmationCode, id, context),
|
||||
disableUser: (...[, { confirmationCode, id }, context]) => userManagement.disableUser(confirmationCode, id, context),
|
||||
deleteSession: (...[, { sid }, context]) => userManagement.deleteSession(sid, context),
|
||||
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
|
||||
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => userManagement.changeUserRole(confirmationCode, id, newRole, context),
|
||||
login: (...[, { username, password }]) => userManagement.login(username, password),
|
||||
input2FA: (...[, { username, password, rememberMe, code }, context]) => userManagement.input2FA(username, password, rememberMe, code, context),
|
||||
setup2FA: (...[, { username, password, rememberMe, codeConfirmation }, context]) => userManagement.setup2FA(username, password, rememberMe, codeConfirmation, context),
|
||||
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => userManagement.createResetPasswordToken(confirmationCode, userID, context),
|
||||
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => userManagement.createReset2FAToken(confirmationCode, userID, context),
|
||||
createRegisterToken: (...[, { username, role }]) => userManagement.createRegisterToken(username, role),
|
||||
register: (...[, { token, username, password, role }]) => userManagement.register(token, username, password, role),
|
||||
resetPassword: (...[, { token, userID, newPassword }, context]) => userManagement.resetPassword(token, userID, newPassword, context),
|
||||
reset2FA: (...[, { token, userID, code }, context]) => userManagement.reset2FA(token, userID, code, context),
|
||||
validateAttestation: (...[, variables, context]) => authentication.strategy.validateAttestation(context.req.session, getAttestationMutationOptions(variables)),
|
||||
validateAssertion: (...[, variables, context]) => authentication.strategy.validateAssertion(context.req.session, getAssertionMutationOptions(variables))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolver
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
const gql = require('graphql-tag')
|
||||
|
||||
const typeDef = gql`
|
||||
type MachineLog {
|
||||
id: ID!
|
||||
logLevel: String!
|
||||
timestamp: DateTimeISO!
|
||||
message: String!
|
||||
}
|
||||
|
||||
type ServerLog {
|
||||
id: ID!
|
||||
logLevel: String!
|
||||
timestamp: DateTimeISO!
|
||||
message: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
machineLogs(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [MachineLog] @auth
|
||||
machineLogsCsv(deviceId: ID!, from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth
|
||||
serverLogs(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int): [ServerLog] @auth
|
||||
serverLogsCsv(from: DateTimeISO, until: DateTimeISO, limit: Int, offset: Int, timezone: String): String @auth
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = typeDef
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const session = require('express-session')
|
||||
const PgSession = require('connect-pg-simple')(session)
|
||||
const db = require('../../db')
|
||||
const { USER_SESSIONS_TABLE_NAME } = require('../../constants')
|
||||
const { getOperatorId } = require('../../operator')
|
||||
|
||||
router.use('*', async (req, res, next) => getOperatorId('authentication').then(operatorId => session({
|
||||
store: new PgSession({
|
||||
pgPromise: db,
|
||||
tableName: USER_SESSIONS_TABLE_NAME
|
||||
}),
|
||||
name: 'lamassu_sid',
|
||||
secret: operatorId,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: true
|
||||
}
|
||||
})(req, res, next))
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const BN = require('../../bn')
|
||||
const settingsLoader = require('../../new-settings-loader')
|
||||
const configManager = require('../../new-config-manager')
|
||||
const wallet = require('../../wallet')
|
||||
const ticker = require('../../ticker')
|
||||
const txBatching = require('../../tx-batching')
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
function computeCrypto (cryptoCode, _balance) {
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
||||
return new BN(_balance).shiftedBy(-unitScale).decimalPlaces(5)
|
||||
}
|
||||
|
||||
function computeFiat (rate, cryptoCode, _balance) {
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
||||
return new BN(_balance).shiftedBy(-unitScale).times(rate).decimalPlaces(5)
|
||||
}
|
||||
|
||||
function getSingleCoinFunding (settings, fiatCode, cryptoCode) {
|
||||
const promises = [
|
||||
wallet.newFunding(settings, cryptoCode),
|
||||
ticker.getRates(settings, fiatCode, cryptoCode),
|
||||
txBatching.getOpenBatchCryptoValue(cryptoCode)
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(([fundingRec, ratesRec, batchRec]) => {
|
||||
const rates = ratesRec.rates
|
||||
const rate = (rates.ask.plus(rates.bid)).div(2)
|
||||
const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance
|
||||
const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance)
|
||||
const pending = fundingRec.fundingPendingBalance.minus(batchRec)
|
||||
const fiatPending = computeFiat(rate, cryptoCode, pending)
|
||||
const fundingAddress = fundingRec.fundingAddress
|
||||
const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress)
|
||||
|
||||
return {
|
||||
cryptoCode,
|
||||
fundingAddress,
|
||||
fundingAddressUrl,
|
||||
confirmedBalance: computeCrypto(cryptoCode, fundingConfirmedBalance).toFormat(5),
|
||||
pending: computeCrypto(cryptoCode, pending).toFormat(5),
|
||||
fiatConfirmedBalance: fiatConfirmedBalance,
|
||||
fiatPending: fiatPending,
|
||||
fiatCode
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Promise.allSettled not running on current version of node
|
||||
const reflect = p => p.then(value => ({ value, status: 'fulfilled' }), error => ({ error: error.toString(), status: 'rejected' }))
|
||||
|
||||
function getFunding () {
|
||||
return settingsLoader.loadLatest().then(settings => {
|
||||
const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config)
|
||||
const fiatCode = configManager.getGlobalLocale(settings.config).fiatCurrency
|
||||
const pareCoins = c => _.includes(c.cryptoCode, cryptoCodes)
|
||||
const cryptoCurrencies = coinUtils.cryptoCurrencies()
|
||||
const cryptoDisplays = _.filter(pareCoins, cryptoCurrencies)
|
||||
|
||||
const promises = cryptoDisplays.map(it => getSingleCoinFunding(settings, fiatCode, it.cryptoCode))
|
||||
return Promise.all(promises.map(reflect))
|
||||
.then((response) => {
|
||||
const mapped = response.map(it => _.merge({ errorMsg: it.error }, it.value))
|
||||
return _.toArray(_.merge(mapped, cryptoDisplays))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { getFunding }
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
const db = require('../../db')
|
||||
|
||||
function validateUser (username, password) {
|
||||
return db.tx(t => {
|
||||
const q1 = t.one('SELECT * FROM users WHERE username=$1 AND password=$2', [username, password])
|
||||
const q2 = t.none('UPDATE users SET last_accessed = now() WHERE username=$1', [username])
|
||||
|
||||
return t.batch([q1, q2])
|
||||
.then(([user]) => user)
|
||||
.catch(() => false)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateUser
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
const fs = require('fs')
|
||||
const pify = require('pify')
|
||||
const readFile = pify(fs.readFile)
|
||||
const crypto = require('crypto')
|
||||
const baseX = require('base-x')
|
||||
const { parse, NIL } = require('uuid')
|
||||
|
||||
const db = require('../../db')
|
||||
const pairing = require('../../pairing')
|
||||
|
||||
const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
|
||||
const bsAlpha = baseX(ALPHA_BASE)
|
||||
|
||||
const CA_PATH = process.env.CA_PATH
|
||||
const HOSTNAME = process.env.HOSTNAME
|
||||
|
||||
const unpair = pairing.unpair
|
||||
|
||||
function totem (name) {
|
||||
return readFile(CA_PATH)
|
||||
.then(data => {
|
||||
const caHash = crypto.createHash('sha256').update(data).digest()
|
||||
const token = crypto.randomBytes(32)
|
||||
const hexToken = token.toString('hex')
|
||||
const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex')
|
||||
const buf = Buffer.concat([caHash, token, Buffer.from(HOSTNAME)])
|
||||
const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)'
|
||||
|
||||
return db.none(sql, [hexToken, caHexToken, name])
|
||||
.then(() => bsAlpha.encode(buf))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { totem, unpair }
|
||||
|
|
@ -1,328 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const configManager = require('../new-config-manager')
|
||||
const logger = require('../logger')
|
||||
const queries = require('./queries')
|
||||
const settingsLoader = require('../new-settings-loader')
|
||||
const customers = require('../customers')
|
||||
|
||||
const notificationCenter = require('./notificationCenter')
|
||||
const utils = require('./utils')
|
||||
const emailFuncs = require('./email')
|
||||
const smsFuncs = require('./sms')
|
||||
const webhookFuncs = require('./webhook')
|
||||
const { STALE, STALE_STATE } = require('./codes')
|
||||
|
||||
function buildMessage (alerts, notifications) {
|
||||
const smsEnabled = utils.isActive(notifications.sms)
|
||||
const emailEnabled = utils.isActive(notifications.email)
|
||||
|
||||
let rec = {}
|
||||
if (smsEnabled) {
|
||||
rec = _.set(['sms', 'body'])(
|
||||
smsFuncs.printSmsAlerts(alerts, notifications.sms)
|
||||
)(rec)
|
||||
}
|
||||
if (emailEnabled) {
|
||||
rec = _.set(['email', 'subject'])(
|
||||
emailFuncs.alertSubject(alerts, notifications.email)
|
||||
)(rec)
|
||||
rec = _.set(['email', 'body'])(
|
||||
emailFuncs.printEmailAlerts(alerts, notifications.email)
|
||||
)(rec)
|
||||
}
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
function checkNotification (plugins) {
|
||||
const notifications = plugins.getNotificationConfig()
|
||||
const smsEnabled = utils.isActive(notifications.sms)
|
||||
const emailEnabled = utils.isActive(notifications.email)
|
||||
const notificationCenterEnabled = utils.isActive(notifications.notificationCenter)
|
||||
|
||||
if (!(notificationCenterEnabled || smsEnabled || emailEnabled)) return Promise.resolve()
|
||||
|
||||
return getAlerts(plugins)
|
||||
.then(alerts => {
|
||||
notifyIfActive('errors', 'errorAlertsNotify', alerts)
|
||||
const currentAlertFingerprint = utils.buildAlertFingerprint(
|
||||
alerts,
|
||||
notifications
|
||||
)
|
||||
if (!currentAlertFingerprint) {
|
||||
const inAlert = !!utils.getAlertFingerprint()
|
||||
// variables for setAlertFingerprint: (fingerprint = null, lastAlertTime = null)
|
||||
utils.setAlertFingerprint(null, null)
|
||||
if (inAlert) return utils.sendNoAlerts(plugins, smsEnabled, emailEnabled)
|
||||
}
|
||||
if (utils.shouldNotAlert(currentAlertFingerprint)) return
|
||||
|
||||
const message = buildMessage(alerts, notifications)
|
||||
utils.setAlertFingerprint(currentAlertFingerprint, Date.now())
|
||||
return plugins.sendMessage(message)
|
||||
})
|
||||
.then(results => {
|
||||
if (results && results.length > 0) {
|
||||
logger.debug('Successfully sent alerts')
|
||||
}
|
||||
})
|
||||
.catch(logger.error)
|
||||
}
|
||||
|
||||
function getAlerts (plugins) {
|
||||
return Promise.all([
|
||||
plugins.checkBalances(),
|
||||
queries.machineEvents(),
|
||||
plugins.getMachineNames()
|
||||
]).then(([balances, events, devices]) => {
|
||||
notifyIfActive('balance', 'balancesNotify', balances)
|
||||
return buildAlerts(checkPings(devices), balances, events, devices)
|
||||
})
|
||||
}
|
||||
|
||||
function buildAlerts (pings, balances, events, devices) {
|
||||
const alerts = { devices: {}, deviceNames: {} }
|
||||
alerts.general = _.filter(r => !r.deviceId, balances)
|
||||
_.forEach(device => {
|
||||
const deviceId = device.deviceId
|
||||
const ping = pings[deviceId] || []
|
||||
const stuckScreen = checkStuckScreen(events, device)
|
||||
|
||||
alerts.devices = _.set([deviceId, 'balanceAlerts'], _.filter(
|
||||
['deviceId', deviceId],
|
||||
balances
|
||||
), alerts.devices)
|
||||
alerts.devices[deviceId].deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
|
||||
|
||||
alerts.deviceNames[deviceId] = device.name
|
||||
}, devices)
|
||||
|
||||
return alerts
|
||||
}
|
||||
|
||||
function checkPings (devices) {
|
||||
const deviceIds = _.map('deviceId', devices)
|
||||
const pings = _.map(utils.checkPing, devices)
|
||||
return _.zipObject(deviceIds)(pings)
|
||||
}
|
||||
|
||||
function checkStuckScreen (deviceEvents, machine) {
|
||||
const lastEvent = _.pipe(
|
||||
_.filter(e => e.device_id === machine.deviceId),
|
||||
_.sortBy(utils.getDeviceTime),
|
||||
_.map(utils.parseEventNote),
|
||||
_.last
|
||||
)(deviceEvents)
|
||||
|
||||
if (!lastEvent) return []
|
||||
|
||||
const state = lastEvent.note.state
|
||||
const isIdle = lastEvent.note.isIdle
|
||||
|
||||
if (isIdle) return []
|
||||
|
||||
const age = Math.floor(lastEvent.age)
|
||||
const machineName = machine.name
|
||||
if (age > STALE_STATE) return [{ code: STALE, state, age, machineName }]
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function transactionNotify (tx, rec) {
|
||||
return settingsLoader.loadLatestConfig().then(config => {
|
||||
const notifSettings = configManager.getGlobalNotifications(config)
|
||||
const highValueTx = tx.fiat.gt(notifSettings.highValueTransaction || Infinity)
|
||||
const isCashOut = tx.direction === 'cashOut'
|
||||
|
||||
// for notification center
|
||||
const directionDisplay = isCashOut ? 'cash-out' : 'cash-in'
|
||||
const readyToNotify = !isCashOut || (tx.direction === 'cashOut' && rec.isRedemption)
|
||||
// awaiting for redesign. notification should not be sent if toggle in the settings table is disabled,
|
||||
// but currently we're sending notifications of high value tx even with the toggle disabled
|
||||
if (readyToNotify && !highValueTx) {
|
||||
notifyIfActive('transactions', 'notifCenterTransactionNotify', highValueTx, directionDisplay, tx.fiat, tx.fiatCode, tx.deviceId, tx.toAddress)
|
||||
} else if (readyToNotify && highValueTx) {
|
||||
notificationCenter.notifCenterTransactionNotify(highValueTx, directionDisplay, tx.fiat, tx.fiatCode, tx.deviceId, tx.toAddress)
|
||||
}
|
||||
|
||||
// alert through sms or email any transaction or high value transaction, if SMS || email alerts are enabled
|
||||
const walletSettings = configManager.getWalletSettings(tx.cryptoCode, config)
|
||||
const zeroConfLimit = walletSettings.zeroConfLimit || 0
|
||||
const zeroConf = isCashOut && tx.fiat.lte(zeroConfLimit)
|
||||
const notificationsEnabled = notifSettings.sms.transactions || notifSettings.email.transactions
|
||||
const customerPromise = tx.customerId ? customers.getById(tx.customerId) : Promise.resolve({})
|
||||
|
||||
if (!notificationsEnabled && !highValueTx) return Promise.resolve()
|
||||
if (zeroConf && isCashOut && !rec.isRedemption && !rec.error) return Promise.resolve()
|
||||
if (!zeroConf && rec.isRedemption) return sendRedemptionMessage(tx.id, rec.error)
|
||||
|
||||
return Promise.all([
|
||||
queries.getMachineName(tx.deviceId),
|
||||
customerPromise
|
||||
]).then(([machineName, customer]) => {
|
||||
return utils.buildTransactionMessage(tx, rec, highValueTx, machineName, customer)
|
||||
}).then(([msg, highValueTx]) => sendTransactionMessage(msg, highValueTx))
|
||||
})
|
||||
}
|
||||
|
||||
function complianceNotify (settings, customer, deviceId, action, period) {
|
||||
const timestamp = (new Date()).toLocaleString()
|
||||
return queries.getMachineName(deviceId)
|
||||
.then(machineName => {
|
||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||
|
||||
const msgCore = {
|
||||
BLOCKED: `was blocked`,
|
||||
SUSPENDED: `was suspended for ${!!period && period} days`,
|
||||
PENDING_COMPLIANCE: `is waiting for your manual approval`,
|
||||
}
|
||||
|
||||
const rec = {
|
||||
sms: {
|
||||
body: `Customer ${customer.phone} ${msgCore[action]} - ${machineName}. ${timestamp}`
|
||||
},
|
||||
email: {
|
||||
subject: `Customer compliance`,
|
||||
body: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}. ${timestamp}`
|
||||
},
|
||||
webhook: {
|
||||
topic: `Customer compliance`,
|
||||
content: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}. ${timestamp}`
|
||||
}
|
||||
}
|
||||
|
||||
const promises = []
|
||||
|
||||
const emailActive =
|
||||
notifications.email.active &&
|
||||
notifications.email.compliance
|
||||
|
||||
const smsActive =
|
||||
notifications.sms.active &&
|
||||
notifications.sms.compliance
|
||||
|
||||
const webhookActive = true
|
||||
|
||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||
if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec))
|
||||
|
||||
notifyIfActive('compliance', 'customerComplianceNotify', customer, deviceId, action, machineName, period)
|
||||
|
||||
return Promise.all(promises)
|
||||
.catch(err => console.error(`An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`))
|
||||
})
|
||||
}
|
||||
|
||||
function sendRedemptionMessage (txId, error) {
|
||||
const subject = `Here's an update on transaction ${txId}`
|
||||
const body = error
|
||||
? `Error: ${error}`
|
||||
: 'It was just dispensed successfully'
|
||||
|
||||
const rec = {
|
||||
sms: {
|
||||
body: `${subject} - ${body}`
|
||||
},
|
||||
email: {
|
||||
subject,
|
||||
body
|
||||
},
|
||||
webhook: {
|
||||
topic: `Transaction update`,
|
||||
content: body
|
||||
}
|
||||
}
|
||||
return sendTransactionMessage(rec)
|
||||
}
|
||||
|
||||
function sendTransactionMessage (rec, isHighValueTx) {
|
||||
return settingsLoader.loadLatest().then(settings => {
|
||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||
|
||||
const promises = []
|
||||
|
||||
const emailActive =
|
||||
notifications.email.active &&
|
||||
(notifications.email.transactions || isHighValueTx)
|
||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||
|
||||
const smsActive =
|
||||
notifications.sms.active &&
|
||||
(notifications.sms.transactions || isHighValueTx)
|
||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||
|
||||
// TODO: Webhook transaction notifications are dependent on notification settings, due to how transactionNotify() is programmed
|
||||
// As changing it would require structural change to that function and the current behavior is temporary (webhooks will eventually have settings tied to them), it's not worth those changes right now
|
||||
const webhookActive = true
|
||||
if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec))
|
||||
|
||||
return Promise.all(promises)
|
||||
.catch(err => console.error(`An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`))
|
||||
})
|
||||
}
|
||||
|
||||
function cashboxNotify (deviceId) {
|
||||
return Promise.all([
|
||||
settingsLoader.loadLatest(),
|
||||
queries.getMachineName(deviceId)
|
||||
])
|
||||
.then(([settings, machineName]) => {
|
||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||
const rec = {
|
||||
sms: {
|
||||
body: `Cashbox removed - ${machineName}`
|
||||
},
|
||||
email: {
|
||||
subject: `Cashbox removal`,
|
||||
body: `Cashbox removed in machine ${machineName}`
|
||||
},
|
||||
webhook: {
|
||||
topic: `Cashbox removal`,
|
||||
content: `Cashbox removed in machine ${machineName}`
|
||||
}
|
||||
}
|
||||
|
||||
const promises = []
|
||||
|
||||
const emailActive =
|
||||
notifications.email.active &&
|
||||
notifications.email.security
|
||||
|
||||
const smsActive =
|
||||
notifications.sms.active &&
|
||||
notifications.sms.security
|
||||
|
||||
const webhookActive = true
|
||||
|
||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||
if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec))
|
||||
notifyIfActive('security', 'cashboxNotify', deviceId)
|
||||
|
||||
return Promise.all(promises)
|
||||
.catch(err => console.error(`An error occurred when sending a notification. Please check your notification preferences and 3rd party account configuration: ${err.stack}`))
|
||||
})
|
||||
}
|
||||
|
||||
// for notification center, check if type of notification is active before calling the respective notify function
|
||||
const notifyIfActive = (type, fnName, ...args) => {
|
||||
return settingsLoader.loadLatestConfig().then(config => {
|
||||
const notificationSettings = configManager.getGlobalNotifications(config).notificationCenter
|
||||
if (!notificationCenter[fnName]) return Promise.reject(new Error(`Notification function ${fnName} for type ${type} does not exist`))
|
||||
if (!(notificationSettings.active && notificationSettings[type])) return Promise.resolve()
|
||||
return notificationCenter[fnName](...args)
|
||||
}).catch(logger.error)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
transactionNotify,
|
||||
complianceNotify,
|
||||
checkNotification,
|
||||
checkPings,
|
||||
checkStuckScreen,
|
||||
sendRedemptionMessage,
|
||||
cashboxNotify,
|
||||
notifyIfActive
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const queries = require('./queries')
|
||||
const utils = require('./utils')
|
||||
const customers = require('../customers')
|
||||
const {
|
||||
NOTIFICATION_TYPES: {
|
||||
SECURITY,
|
||||
COMPLIANCE,
|
||||
CRYPTO_BALANCE,
|
||||
FIAT_BALANCE,
|
||||
ERROR,
|
||||
HIGH_VALUE_TX,
|
||||
NORMAL_VALUE_TX
|
||||
},
|
||||
|
||||
STALE,
|
||||
PING,
|
||||
|
||||
HIGH_CRYPTO_BALANCE,
|
||||
LOW_CRYPTO_BALANCE,
|
||||
CASH_BOX_FULL,
|
||||
LOW_CASH_OUT,
|
||||
LOW_RECYCLER_STACKER,
|
||||
} = require('./codes')
|
||||
|
||||
const sanctionsNotify = (customer, phone) => {
|
||||
const code = 'SANCTIONS'
|
||||
const detailB = utils.buildDetail({ customerId: customer.id, code })
|
||||
const addNotif = phone =>
|
||||
queries.addNotification(COMPLIANCE, `Blocked customer with phone ${phone} for being on the OFAC sanctions list`, detailB)
|
||||
// if it's a new customer then phone comes as undefined
|
||||
return phone ? addNotif(phone) : customers.getById(customer.id).then(c => addNotif(c.phone))
|
||||
}
|
||||
|
||||
const clearOldCustomerSuspendedNotifications = (customerId, deviceId) => {
|
||||
const detailB = utils.buildDetail({ code: 'SUSPENDED', customerId, deviceId })
|
||||
return queries.invalidateNotification(detailB, 'compliance')
|
||||
}
|
||||
|
||||
const customerComplianceNotify = (customer, deviceId, code, machineName, days = null) => {
|
||||
// code for now can be "BLOCKED", "SUSPENDED"
|
||||
const detailB = utils.buildDetail({ customerId: customer.id, code, deviceId })
|
||||
const date = new Date()
|
||||
if (days) {
|
||||
date.setDate(date.getDate() + days)
|
||||
}
|
||||
const message = code === 'SUSPENDED' ? `Customer ${customer.phone} suspended until ${date.toLocaleString()}` :
|
||||
code === 'BLOCKED' ? `Customer ${customer.phone} blocked` :
|
||||
`Customer ${customer.phone} has pending compliance in machine ${machineName}`
|
||||
|
||||
return clearOldCustomerSuspendedNotifications(customer.id, deviceId)
|
||||
.then(() => queries.getValidNotifications(COMPLIANCE, detailB))
|
||||
.then(res => {
|
||||
if (res.length > 0) return Promise.resolve()
|
||||
return queries.addNotification(COMPLIANCE, message, detailB)
|
||||
})
|
||||
}
|
||||
|
||||
const clearOldFiatNotifications = (balances) => {
|
||||
return queries.getAllValidNotifications(FIAT_BALANCE).then(notifications => {
|
||||
const filterByBalance = _.filter(notification => {
|
||||
const { cassette, deviceId } = notification.detail
|
||||
return !_.find(balance => balance.cassette === cassette && balance.deviceId === deviceId)(balances)
|
||||
})
|
||||
const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(notifications)
|
||||
const notInvalidated = _.filter(notification => {
|
||||
return !_.find(id => notification.id === id)(indexesToInvalidate)
|
||||
}, notifications)
|
||||
return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated)
|
||||
})
|
||||
}
|
||||
|
||||
const fiatBalancesNotify = (fiatWarnings) => {
|
||||
return clearOldFiatNotifications(fiatWarnings).then(notInvalidated => {
|
||||
return fiatWarnings.forEach(balance => {
|
||||
if (_.find(o => {
|
||||
const { cassette, deviceId } = o.detail
|
||||
return cassette === balance.cassette && deviceId === balance.deviceId
|
||||
}, notInvalidated)) return
|
||||
const message = balance.code === LOW_CASH_OUT ?
|
||||
`Cash-out cassette ${balance.cassette} low or empty!` :
|
||||
balance.code === LOW_RECYCLER_STACKER ?
|
||||
`Recycler ${balance.cassette} low or empty!` :
|
||||
balance.code === CASH_BOX_FULL ?
|
||||
`Cash box full or almost full!` :
|
||||
`Cash box full or almost full!` /* Shouldn't happen */
|
||||
const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette })
|
||||
return queries.addNotification(FIAT_BALANCE, message, detailB)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clearOldCryptoNotifications = balances => {
|
||||
return queries.getAllValidNotifications(CRYPTO_BALANCE).then(res => {
|
||||
const filterByBalance = _.filter(notification => {
|
||||
const { cryptoCode, code } = notification.detail
|
||||
return !_.find(balance => balance.cryptoCode === cryptoCode && balance.code === code)(balances)
|
||||
})
|
||||
const indexesToInvalidate = _.compose(_.map('id'), filterByBalance)(res)
|
||||
|
||||
const notInvalidated = _.filter(notification => {
|
||||
return !_.find(id => notification.id === id)(indexesToInvalidate)
|
||||
}, res)
|
||||
return (indexesToInvalidate.length ? queries.batchInvalidate(indexesToInvalidate) : Promise.resolve()).then(() => notInvalidated)
|
||||
})
|
||||
}
|
||||
|
||||
const cryptoBalancesNotify = (cryptoWarnings) => {
|
||||
return clearOldCryptoNotifications(cryptoWarnings).then(notInvalidated => {
|
||||
return cryptoWarnings.forEach(balance => {
|
||||
// if notification exists in DB and wasnt invalidated then don't add a duplicate
|
||||
if (_.find(o => {
|
||||
const { code, cryptoCode } = o.detail
|
||||
return code === balance.code && cryptoCode === balance.cryptoCode
|
||||
}, notInvalidated)) return
|
||||
|
||||
const fiat = utils.formatCurrency(balance.fiatBalance.balance, balance.fiatCode)
|
||||
const message = `${balance.code === HIGH_CRYPTO_BALANCE ? 'High' : 'Low'} balance in ${balance.cryptoCode} [${fiat}]`
|
||||
const detailB = utils.buildDetail({ cryptoCode: balance.cryptoCode, code: balance.code })
|
||||
return queries.addNotification(CRYPTO_BALANCE, message, detailB)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const balancesNotify = (balances) => {
|
||||
const isCryptoCode = c => _.includes(c, [HIGH_CRYPTO_BALANCE, LOW_CRYPTO_BALANCE])
|
||||
const isFiatCode = c => _.includes(c, [LOW_CASH_OUT, CASH_BOX_FULL, LOW_RECYCLER_STACKER])
|
||||
const by = o =>
|
||||
isCryptoCode(o) ? 'crypto' :
|
||||
isFiatCode(o) ? 'fiat' :
|
||||
undefined
|
||||
const warnings = _.flow(
|
||||
_.groupBy(_.flow(_.get(['code']), by)),
|
||||
_.update('crypto', _.defaultTo([])),
|
||||
_.update('fiat', _.defaultTo([])),
|
||||
)(balances)
|
||||
return Promise.all([cryptoBalancesNotify(warnings.crypto), fiatBalancesNotify(warnings.fiat)])
|
||||
}
|
||||
|
||||
const clearOldErrorNotifications = alerts => {
|
||||
return queries.getAllValidNotifications(ERROR)
|
||||
.then(res => {
|
||||
// for each valid notification in DB see if it exists in alerts
|
||||
// if the notification doesn't exist in alerts, it is not valid anymore
|
||||
const filterByAlert = _.filter(notification => {
|
||||
const { code, deviceId } = notification.detail
|
||||
return !_.find(alert => alert.code === code && alert.deviceId === deviceId)(alerts)
|
||||
})
|
||||
const indexesToInvalidate = _.compose(_.map('id'), filterByAlert)(res)
|
||||
if (!indexesToInvalidate.length) return Promise.resolve()
|
||||
return queries.batchInvalidate(indexesToInvalidate)
|
||||
})
|
||||
}
|
||||
|
||||
const errorAlertsNotify = (alertRec) => {
|
||||
const embedDeviceId = deviceId => _.assign({ deviceId })
|
||||
const mapToAlerts = _.map(it => _.map(embedDeviceId(it), alertRec.devices[it].deviceAlerts))
|
||||
const alerts = _.compose(_.flatten, mapToAlerts, _.keys)(alertRec.devices)
|
||||
|
||||
return clearOldErrorNotifications(alerts).then(() => {
|
||||
_.forEach(alert => {
|
||||
switch (alert.code) {
|
||||
case PING: {
|
||||
const detailB = utils.buildDetail({ code: PING, age: alert.age ? alert.age : -1, deviceId: alert.deviceId })
|
||||
return queries.getValidNotifications(ERROR, _.omit(['age'], detailB)).then(res => {
|
||||
if (res.length > 0) return Promise.resolve()
|
||||
const message = `Machine down`
|
||||
return queries.addNotification(ERROR, message, detailB)
|
||||
})
|
||||
}
|
||||
case STALE: {
|
||||
const detailB = utils.buildDetail({ code: STALE, deviceId: alert.deviceId })
|
||||
return queries.getValidNotifications(ERROR, detailB).then(res => {
|
||||
if (res.length > 0) return Promise.resolve()
|
||||
const message = `Machine is stuck on ${alert.state} screen`
|
||||
return queries.addNotification(ERROR, message, detailB)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, alerts)
|
||||
})
|
||||
}
|
||||
|
||||
function notifCenterTransactionNotify (isHighValue, direction, fiat, fiatCode, deviceId, cryptoAddress) {
|
||||
const messageSuffix = isHighValue ? 'High value' : ''
|
||||
const message = `${messageSuffix} ${fiat} ${fiatCode} ${direction} transaction`
|
||||
const detailB = utils.buildDetail({ deviceId: deviceId, direction, fiat, fiatCode, cryptoAddress })
|
||||
return queries.addNotification(isHighValue ? HIGH_VALUE_TX : NORMAL_VALUE_TX, message, detailB)
|
||||
}
|
||||
|
||||
const blacklistNotify = (tx, isAddressReuse) => {
|
||||
const code = isAddressReuse ? 'REUSED' : 'BLOCKED'
|
||||
const name = isAddressReuse ? 'reused' : 'blacklisted'
|
||||
|
||||
const detailB = utils.buildDetail({ cryptoCode: tx.cryptoCode, code, cryptoAddress: tx.toAddress })
|
||||
const message = `Blocked ${name} address: ${tx.cryptoCode} ${tx.toAddress.substr(0, 10)}...`
|
||||
return queries.addNotification(COMPLIANCE, message, detailB)
|
||||
}
|
||||
|
||||
const cashboxNotify = deviceId => {
|
||||
const detailB = utils.buildDetail({ deviceId: deviceId })
|
||||
const message = `Cashbox removed`
|
||||
return queries.addNotification(SECURITY, message, detailB)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sanctionsNotify,
|
||||
customerComplianceNotify,
|
||||
balancesNotify,
|
||||
errorAlertsNotify,
|
||||
notifCenterTransactionNotify,
|
||||
blacklistNotify,
|
||||
cashboxNotify
|
||||
}
|
||||
950
lib/plugins.js
950
lib/plugins.js
|
|
@ -1,950 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const crypto = require('crypto')
|
||||
const pgp = require('pg-promise')()
|
||||
const { getTimezoneOffset } = require('date-fns-tz')
|
||||
const { millisecondsToMinutes } = require('date-fns/fp')
|
||||
|
||||
const BN = require('./bn')
|
||||
const dbm = require('./postgresql_interface')
|
||||
const db = require('./db')
|
||||
const logger = require('./logger')
|
||||
const logs = require('./logs')
|
||||
const T = require('./time')
|
||||
const configManager = require('./new-config-manager')
|
||||
const settingsLoader = require('./new-settings-loader')
|
||||
const ticker = require('./ticker')
|
||||
const wallet = require('./wallet')
|
||||
const walletScoring = require('./wallet-scoring')
|
||||
const exchange = require('./exchange')
|
||||
const sms = require('./sms')
|
||||
const email = require('./email')
|
||||
const cashOutHelper = require('./cash-out/cash-out-helper')
|
||||
const machineLoader = require('./machine-loader')
|
||||
const commissionMath = require('./commission-math')
|
||||
const loyalty = require('./loyalty')
|
||||
const transactionBatching = require('./tx-batching')
|
||||
|
||||
const {
|
||||
CASH_OUT_DISPENSE_READY,
|
||||
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
|
||||
CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS,
|
||||
CASH_UNIT_CAPACITY,
|
||||
CONFIRMATION_CODE,
|
||||
} = require('./constants')
|
||||
|
||||
const notifier = require('./notifier')
|
||||
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
const mapValuesWithKey = _.mapValues.convert({
|
||||
cap: false
|
||||
})
|
||||
|
||||
const TRADE_TTL = 2 * T.minutes
|
||||
const STALE_TICKER = 3 * T.minutes
|
||||
const STALE_BALANCE = 3 * T.minutes
|
||||
const tradesQueues = {}
|
||||
|
||||
function plugins (settings, deviceId) {
|
||||
|
||||
function internalBuildRates (tickers, withCommission = true) {
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
|
||||
const rates = {}
|
||||
|
||||
cryptoCodes.forEach((cryptoCode, i) => {
|
||||
const rateRec = tickers[i]
|
||||
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||
|
||||
if (!rateRec) return
|
||||
|
||||
const cashInCommission = new BN(1).plus(new BN(commissions.cashIn).div(100))
|
||||
|
||||
const cashOutCommission = _.isNil(commissions.cashOut)
|
||||
? undefined
|
||||
: new BN(1).plus(new BN(commissions.cashOut).div(100))
|
||||
|
||||
if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode)
|
||||
const rate = rateRec.rates
|
||||
|
||||
withCommission ? rates[cryptoCode] = {
|
||||
cashIn: rate.ask.times(cashInCommission).decimalPlaces(5),
|
||||
cashOut: cashOutCommission && rate.bid.div(cashOutCommission).decimalPlaces(5)
|
||||
} : rates[cryptoCode] = {
|
||||
cashIn: rate.ask.decimalPlaces(5),
|
||||
cashOut: rate.bid.decimalPlaces(5)
|
||||
}
|
||||
})
|
||||
return rates
|
||||
}
|
||||
|
||||
function buildRatesNoCommission (tickers) {
|
||||
return internalBuildRates(tickers, false)
|
||||
}
|
||||
|
||||
function buildRates (tickers) {
|
||||
return internalBuildRates(tickers, true)
|
||||
}
|
||||
|
||||
function getNotificationConfig () {
|
||||
return configManager.getGlobalNotifications(settings.config)
|
||||
}
|
||||
|
||||
function buildBalances (balanceRecs) {
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
|
||||
const balances = {}
|
||||
|
||||
cryptoCodes.forEach((cryptoCode, i) => {
|
||||
const balanceRec = balanceRecs[i]
|
||||
if (!balanceRec) return logger.warn('No balance for ' + cryptoCode + ' yet')
|
||||
if (Date.now() - balanceRec.timestamp > STALE_BALANCE) return logger.warn('Stale balance for ' + cryptoCode)
|
||||
|
||||
balances[cryptoCode] = balanceRec.balance
|
||||
})
|
||||
|
||||
return balances
|
||||
}
|
||||
|
||||
function isZeroConf (tx) {
|
||||
const walletSettings = configManager.getWalletSettings(tx.cryptoCode, settings.config)
|
||||
const zeroConfLimit = walletSettings.zeroConfLimit || 0
|
||||
return tx.fiat.lte(zeroConfLimit)
|
||||
}
|
||||
|
||||
const accountProvisioned = (cashUnitType, cashUnits, redeemableTxs) => {
|
||||
const kons = (cashUnits, tx) => {
|
||||
// cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations
|
||||
const cashUnitsBills = _.flow(
|
||||
_.get(['bills']),
|
||||
_.filter(it => _.includes(cashUnitType, it.name) && it.denomination > 0),
|
||||
_.zip(cashUnits),
|
||||
)(tx)
|
||||
|
||||
const sameDenominations = ([cashUnit, bill]) => cashUnit?.denomination === bill?.denomination
|
||||
if (!_.every(sameDenominations, cashUnitsBills))
|
||||
throw new Error(`Denominations don't add up, ${cashUnitType}s were changed.`)
|
||||
|
||||
return _.map(
|
||||
([cashUnit, { provisioned }]) => _.set('count', cashUnit.count - provisioned, cashUnit),
|
||||
cashUnitsBills
|
||||
)
|
||||
}
|
||||
|
||||
return _.reduce(kons, cashUnits, redeemableTxs)
|
||||
}
|
||||
|
||||
function computeAvailableCassettes (cassettes, redeemableTxs) {
|
||||
if (_.isEmpty(redeemableTxs)) return cassettes
|
||||
cassettes = accountProvisioned('cassette', cassettes, redeemableTxs)
|
||||
if (_.some(({ count }) => count < 0, cassettes))
|
||||
throw new Error('Negative note count: %j', counts)
|
||||
return cassettes
|
||||
}
|
||||
|
||||
function computeAvailableRecyclers (recyclers, redeemableTxs) {
|
||||
if (_.isEmpty(redeemableTxs)) return recyclers
|
||||
recyclers = accountProvisioned('recycler', recyclers, redeemableTxs)
|
||||
if (_.some(({ count }) => count < 0, recyclers))
|
||||
throw new Error('Negative note count: %j', counts)
|
||||
return recyclers
|
||||
}
|
||||
|
||||
function buildAvailableCassettes (excludeTxId) {
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||
|
||||
if (!cashOutConfig.active) return Promise.resolve()
|
||||
|
||||
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||
.then(([{ counts, numberOfCassettes }, redeemableTxs]) => {
|
||||
redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), redeemableTxs)
|
||||
|
||||
const denominations = _.map(
|
||||
it => cashOutConfig[`cassette${it}`],
|
||||
_.range(1, numberOfCassettes+1)
|
||||
)
|
||||
|
||||
if (counts.length !== denominations.length)
|
||||
throw new Error('Denominations and respective counts do not match!')
|
||||
|
||||
const cassettes = _.map(
|
||||
it => ({
|
||||
name: `cassette${it + 1}`,
|
||||
denomination: parseInt(denominations[it], 10),
|
||||
count: parseInt(counts[it], 10)
|
||||
}),
|
||||
_.range(0, numberOfCassettes)
|
||||
)
|
||||
|
||||
const virtualCassettes = denominations.length ? [Math.max(...denominations) * 2] : []
|
||||
|
||||
try {
|
||||
return {
|
||||
cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
|
||||
virtualCassettes
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return {
|
||||
cassettes,
|
||||
virtualCassettes
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildAvailableRecyclers (excludeTxId) {
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||
|
||||
if (!cashOutConfig.active) return Promise.resolve()
|
||||
|
||||
return Promise.all([dbm.recyclerCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
|
||||
.then(([{ counts, numberOfRecyclers }, redeemableTxs]) => {
|
||||
redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), redeemableTxs)
|
||||
|
||||
const denominations = _.map(
|
||||
it => cashOutConfig[`recycler${it}`],
|
||||
_.range(1, numberOfRecyclers+1)
|
||||
)
|
||||
|
||||
if (counts.length !== denominations.length)
|
||||
throw new Error('Denominations and respective counts do not match!')
|
||||
|
||||
const recyclers = _.map(
|
||||
it => ({
|
||||
number: it + 1,
|
||||
name: `recycler${it + 1}`,
|
||||
denomination: parseInt(denominations[it], 10),
|
||||
count: parseInt(counts[it], 10)
|
||||
}),
|
||||
_.range(0, numberOfRecyclers)
|
||||
)
|
||||
|
||||
const virtualRecyclers = denominations.length ? [Math.max(..._.flatten(denominations)) * 2] : []
|
||||
|
||||
try {
|
||||
return {
|
||||
recyclers: computeAvailableRecyclers(recyclers, redeemableTxs),
|
||||
virtualRecyclers
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return {
|
||||
recyclers,
|
||||
virtualRecyclers
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildAvailableUnits (excludeTxId) {
|
||||
return Promise.all([buildAvailableCassettes(excludeTxId), buildAvailableRecyclers(excludeTxId)])
|
||||
.then(([cassettes, recyclers]) => ({ cassettes: cassettes.cassettes, recyclers: recyclers.recyclers }))
|
||||
}
|
||||
|
||||
function mapCoinSettings (coinParams) {
|
||||
const [ cryptoCode, cryptoNetwork ] = coinParams
|
||||
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||
const minimumTx = new BN(commissions.minimumTx)
|
||||
const cashInFee = new BN(commissions.fixedFee)
|
||||
const cashOutFee = new BN(commissions.cashOutFixedFee)
|
||||
const cashInCommission = new BN(commissions.cashIn)
|
||||
const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
const cryptoUnits = configManager.getCryptoUnits(cryptoCode, settings.config)
|
||||
|
||||
return {
|
||||
cryptoCode,
|
||||
cryptoCodeDisplay: cryptoRec.cryptoCodeDisplay ?? cryptoCode,
|
||||
display: cryptoRec.display,
|
||||
isCashInOnly: Boolean(cryptoRec.isCashinOnly),
|
||||
minimumTx: BN.max(minimumTx, cashInFee),
|
||||
cashInFee,
|
||||
cashOutFee,
|
||||
cashInCommission,
|
||||
cashOutCommission,
|
||||
cryptoNetwork,
|
||||
cryptoUnits
|
||||
}
|
||||
}
|
||||
|
||||
function getTickerRates (fiatCode, cryptoCode) {
|
||||
return ticker.getRates(settings, fiatCode, cryptoCode)
|
||||
}
|
||||
|
||||
function pollQueries () {
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const fiatCode = localeConfig.fiatCurrency
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
const machineScreenOpts = configManager.getAllMachineScreenOpts(settings.config)
|
||||
|
||||
const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c))
|
||||
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
|
||||
const networkPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c))
|
||||
const supportsBatchingPromise = cryptoCodes.map(c => wallet.supportsBatching(settings, c))
|
||||
|
||||
return Promise.all([
|
||||
buildAvailableCassettes(),
|
||||
buildAvailableRecyclers(),
|
||||
settingsLoader.fetchCurrentConfigVersion(),
|
||||
millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)),
|
||||
loyalty.getNumberOfAvailablePromoCodes(),
|
||||
Promise.all(supportsBatchingPromise),
|
||||
Promise.all(tickerPromises),
|
||||
Promise.all(balancePromises),
|
||||
Promise.all(networkPromises)
|
||||
])
|
||||
.then(([
|
||||
cassettes,
|
||||
recyclers,
|
||||
configVersion,
|
||||
timezone,
|
||||
numberOfAvailablePromoCodes,
|
||||
batchableCoins,
|
||||
tickers,
|
||||
balances,
|
||||
networks
|
||||
]) => {
|
||||
const coinsWithoutRate = _.flow(
|
||||
_.zip(cryptoCodes),
|
||||
_.map(mapCoinSettings)
|
||||
)(networks)
|
||||
|
||||
const coins = _.flow(
|
||||
_.map(it => ({ batchable: it })),
|
||||
_.zipWith(
|
||||
_.assign,
|
||||
_.zipWith(_.assign, coinsWithoutRate, tickers)
|
||||
)
|
||||
)(batchableCoins)
|
||||
|
||||
return {
|
||||
cassettes,
|
||||
recyclers: recyclers,
|
||||
rates: buildRates(tickers),
|
||||
balances: buildBalances(balances),
|
||||
coins,
|
||||
configVersion,
|
||||
areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0,
|
||||
timezone,
|
||||
screenOptions: machineScreenOpts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sendCoins (tx) {
|
||||
return wallet.supportsBatching(settings, tx.cryptoCode)
|
||||
.then(supportsBatching => {
|
||||
if (supportsBatching) {
|
||||
return transactionBatching.addTransactionToBatch(tx)
|
||||
.then(() => ({
|
||||
batched: true,
|
||||
sendPending: false,
|
||||
error: null,
|
||||
errorCode: null
|
||||
}))
|
||||
}
|
||||
return wallet.sendCoins(settings, tx)
|
||||
})
|
||||
}
|
||||
|
||||
function recordPing (deviceTime, version, model) {
|
||||
const devices = {
|
||||
version,
|
||||
model,
|
||||
last_online: deviceTime
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
db.none(`insert into machine_pings(device_id, device_time) values($1, $2)
|
||||
ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`, [deviceId, deviceTime]),
|
||||
db.none(pgp.helpers.update(devices, null, 'devices') + 'WHERE device_id = ${deviceId}', {
|
||||
deviceId
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
function pruneMachinesHeartbeat () {
|
||||
const sql = `DELETE FROM machine_network_heartbeat h
|
||||
USING (SELECT device_id, max(created) as lastEntry FROM machine_network_heartbeat GROUP BY device_id) d
|
||||
WHERE d.device_id = h.device_id AND h.created < d.lastEntry`
|
||||
db.none(sql)
|
||||
}
|
||||
|
||||
function isHd (tx) {
|
||||
return wallet.isHd(settings, tx)
|
||||
}
|
||||
|
||||
function getStatus (tx) {
|
||||
return wallet.getStatus(settings, tx, deviceId)
|
||||
}
|
||||
|
||||
function newAddress (tx) {
|
||||
const info = {
|
||||
cryptoCode: tx.cryptoCode,
|
||||
label: 'TX ' + Date.now(),
|
||||
account: 'deposit',
|
||||
hdIndex: tx.hdIndex,
|
||||
cryptoAtoms: tx.cryptoAtoms,
|
||||
isLightning: tx.isLightning
|
||||
}
|
||||
return wallet.newAddress(settings, info, tx)
|
||||
}
|
||||
|
||||
function fiatBalance (fiatCode, cryptoCode) {
|
||||
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||
return Promise.all([
|
||||
getTickerRates(fiatCode, cryptoCode),
|
||||
wallet.balance(settings, cryptoCode)
|
||||
])
|
||||
.then(([rates, balanceRec]) => {
|
||||
if (!rates || !balanceRec) return null
|
||||
|
||||
const rawRate = rates.rates.ask
|
||||
const cashInCommission = new BN(1).minus(new BN(commissions.cashIn).div(100))
|
||||
const balance = balanceRec.balance
|
||||
|
||||
if (!rawRate || !balance) return null
|
||||
|
||||
const rate = rawRate.div(cashInCommission)
|
||||
|
||||
const lowBalanceMargin = new BN(0.95)
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
const unitScale = cryptoRec.unitScale
|
||||
const shiftedRate = rate.shiftedBy(-unitScale)
|
||||
const fiatTransferBalance = balance.times(shiftedRate).times(lowBalanceMargin)
|
||||
|
||||
return {
|
||||
timestamp: balanceRec.timestamp,
|
||||
balance: fiatTransferBalance.integerValue(BN.ROUND_DOWN).toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function notifyConfirmation (tx) {
|
||||
logger.debug('notifyConfirmation')
|
||||
|
||||
const phone = tx.phone
|
||||
|
||||
const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC`
|
||||
return sms.getSms(CASH_OUT_DISPENSE_READY, phone, { timestamp })
|
||||
.then(smsObj => {
|
||||
const rec = {
|
||||
sms: smsObj
|
||||
}
|
||||
|
||||
return sms.sendMessage(settings, rec)
|
||||
.then(() => {
|
||||
const sql = 'UPDATE cash_out_txs SET notified=$1 WHERE id=$2'
|
||||
const values = [true, tx.id]
|
||||
|
||||
return db.none(sql, values)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function notifyOperator (tx, rec) {
|
||||
// notify operator about new transaction and add high volume txs to database
|
||||
return notifier.transactionNotify(tx, rec)
|
||||
}
|
||||
|
||||
function clearOldLogs () {
|
||||
return logs.clearOldLogs()
|
||||
.catch(logger.error)
|
||||
}
|
||||
|
||||
/*
|
||||
* Trader functions
|
||||
*/
|
||||
|
||||
function buy (rec, tx) {
|
||||
return buyAndSell(rec, true, tx)
|
||||
}
|
||||
|
||||
function sell (rec) {
|
||||
return buyAndSell(rec, false)
|
||||
}
|
||||
|
||||
function buyAndSell (rec, doBuy, tx) {
|
||||
const cryptoCode = rec.cryptoCode
|
||||
return exchange.fetchExchange(settings, cryptoCode)
|
||||
.then(_exchange => {
|
||||
const fiatCode = _exchange.account.currencyMarket
|
||||
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated()
|
||||
|
||||
const market = [fiatCode, cryptoCode].join('')
|
||||
|
||||
if (!exchange.active(settings, cryptoCode)) return
|
||||
|
||||
const direction = doBuy ? 'cashIn' : 'cashOut'
|
||||
const internalTxId = tx ? tx.id : rec.id
|
||||
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
|
||||
if (!tradesQueues[market]) tradesQueues[market] = []
|
||||
tradesQueues[market].push({
|
||||
direction,
|
||||
internalTxId,
|
||||
fiatCode,
|
||||
cryptoAtoms,
|
||||
cryptoCode,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function consolidateTrades (cryptoCode, fiatCode) {
|
||||
const market = [fiatCode, cryptoCode].join('')
|
||||
|
||||
const marketTradesQueues = tradesQueues[market]
|
||||
if (!marketTradesQueues || marketTradesQueues.length === 0) return null
|
||||
|
||||
logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length)
|
||||
logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0])
|
||||
|
||||
const t1 = Date.now()
|
||||
|
||||
const filtered = marketTradesQueues
|
||||
.filter(tradeEntry => {
|
||||
return t1 - tradeEntry.timestamp < TRADE_TTL
|
||||
})
|
||||
|
||||
const filteredCount = marketTradesQueues.length - filtered.length
|
||||
|
||||
if (filteredCount > 0) {
|
||||
tradesQueues[market] = filtered
|
||||
logger.debug('[%s] expired %d trades', market, filteredCount)
|
||||
}
|
||||
|
||||
if (filtered.length === 0) return null
|
||||
|
||||
const partitionByDirection = _.partition(({ direction }) => direction === 'cashIn')
|
||||
const [cashInTxs, cashOutTxs] = _.compose(partitionByDirection, _.uniqBy('internalTxId'))(filtered)
|
||||
|
||||
const cryptoAtoms = filtered
|
||||
.reduce((prev, current) => prev.plus(current.cryptoAtoms), new BN(0))
|
||||
|
||||
const timestamp = filtered.map(r => r.timestamp).reduce((acc, r) => Math.max(acc, r), 0)
|
||||
|
||||
const consolidatedTrade = {
|
||||
cashInTxs,
|
||||
cashOutTxs,
|
||||
fiatCode,
|
||||
cryptoAtoms,
|
||||
cryptoCode,
|
||||
timestamp
|
||||
}
|
||||
|
||||
tradesQueues[market] = []
|
||||
|
||||
logger.debug('[%s] consolidated: %j', market, consolidatedTrade)
|
||||
return consolidatedTrade
|
||||
}
|
||||
|
||||
function executeTrades () {
|
||||
return machineLoader.getMachines()
|
||||
.then(devices => {
|
||||
const deviceIds = devices.map(device => device.deviceId)
|
||||
const lists = deviceIds.map(deviceId => {
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
|
||||
return Promise.all(cryptoCodes.map(cryptoCode => {
|
||||
return exchange.fetchExchange(settings, cryptoCode)
|
||||
.then(exchange => ({
|
||||
fiatCode: exchange.account.currencyMarket,
|
||||
cryptoCode
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
return Promise.all(lists)
|
||||
})
|
||||
.then(lists => {
|
||||
return Promise.all(_.uniq(_.flatten(lists))
|
||||
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)))
|
||||
})
|
||||
.catch(logger.error)
|
||||
}
|
||||
|
||||
function executeTradesForMarket (settings, fiatCode, cryptoCode) {
|
||||
if (!exchange.active(settings, cryptoCode)) return
|
||||
|
||||
const market = [fiatCode, cryptoCode].join('')
|
||||
const tradeEntry = consolidateTrades(cryptoCode, fiatCode)
|
||||
|
||||
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
|
||||
|
||||
return executeTradeForType(tradeEntry)
|
||||
.catch(err => {
|
||||
tradesQueues[market].push(tradeEntry)
|
||||
if (err.name === 'orderTooSmall') return logger.debug(err.message)
|
||||
logger.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function executeTradeForType (_tradeEntry) {
|
||||
const expand = te => _.assign(te, {
|
||||
cryptoAtoms: te.cryptoAtoms.abs(),
|
||||
type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell'
|
||||
})
|
||||
|
||||
const tradeEntry = expand(_tradeEntry)
|
||||
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
|
||||
|
||||
return recordTrade(tradeEntry)
|
||||
.then(newEntry => {
|
||||
tradeEntry.tradeId = newEntry.id
|
||||
return execute(settings, tradeEntry)
|
||||
.catch(err => {
|
||||
updateTradeEntry(tradeEntry, newEntry, err)
|
||||
.then(() => {
|
||||
logger.error(err)
|
||||
throw err
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function updateTradeEntry (tradeEntry, newEntry, err) {
|
||||
const data = mergeTradeEntryAndError(tradeEntry, err)
|
||||
const sql = pgp.helpers.update(data, ['error'], 'trades') + ` WHERE id = ${newEntry.id}`
|
||||
return db.none(sql)
|
||||
}
|
||||
|
||||
function recordTradeAndTx (tradeId, { cashInTxs, cashOutTxs }, dbTx) {
|
||||
const columnSetCashIn = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { table: 'cashin_tx_trades' })
|
||||
const columnSetCashOut = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { table: 'cashout_tx_trades' })
|
||||
const mapToEntry = _.map(tx => ({ tx_id: tx.internalTxId, trade_id: tradeId }))
|
||||
const queries = []
|
||||
|
||||
if (!_.isEmpty(cashInTxs)) {
|
||||
const query = pgp.helpers.insert(mapToEntry(cashInTxs), columnSetCashIn)
|
||||
queries.push(dbTx.none(query))
|
||||
}
|
||||
if (!_.isEmpty(cashOutTxs)) {
|
||||
const query = pgp.helpers.insert(mapToEntry(cashOutTxs), columnSetCashOut)
|
||||
queries.push(dbTx.none(query))
|
||||
}
|
||||
return Promise.all(queries)
|
||||
}
|
||||
|
||||
function convertBigNumFields (obj) {
|
||||
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
|
||||
? value.toString()
|
||||
: value
|
||||
|
||||
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
|
||||
? key + '#'
|
||||
: key
|
||||
|
||||
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
||||
}
|
||||
|
||||
function mergeTradeEntryAndError (tradeEntry, error) {
|
||||
if (error && error.message) {
|
||||
return Object.assign({}, tradeEntry, {
|
||||
error: error.message.slice(0, 200)
|
||||
})
|
||||
}
|
||||
return tradeEntry
|
||||
}
|
||||
|
||||
function recordTrade (_tradeEntry, error) {
|
||||
const massage = _.flow(
|
||||
mergeTradeEntryAndError,
|
||||
_.pick(['cryptoCode', 'cryptoAtoms', 'fiatCode', 'type', 'error']),
|
||||
convertBigNumFields,
|
||||
_.mapKeys(_.snakeCase)
|
||||
)
|
||||
const tradeEntry = massage(_tradeEntry, error)
|
||||
const sql = pgp.helpers.insert(tradeEntry, null, 'trades') + 'RETURNING *'
|
||||
return db.tx(t => {
|
||||
return t.oneOrNone(sql)
|
||||
.then(newTrade => {
|
||||
return recordTradeAndTx(newTrade.id, _tradeEntry, t)
|
||||
.then(() => newTrade)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function sendMessage (rec) {
|
||||
const notifications = configManager.getGlobalNotifications(settings.config)
|
||||
|
||||
let promises = []
|
||||
if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec))
|
||||
if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec))
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
function checkDevicesCashBalances (fiatCode, devices) {
|
||||
return _.map(device => checkDeviceCashBalances(fiatCode, device), devices)
|
||||
}
|
||||
|
||||
function getCashUnitCapacity (model, device) {
|
||||
if (!CASH_UNIT_CAPACITY[model]) {
|
||||
return CASH_UNIT_CAPACITY.default[device]
|
||||
}
|
||||
return CASH_UNIT_CAPACITY[model][device]
|
||||
}
|
||||
|
||||
function checkDeviceCashBalances (fiatCode, device) {
|
||||
const deviceId = device.deviceId
|
||||
const machineName = device.name
|
||||
const notifications = configManager.getNotifications(null, deviceId, settings.config)
|
||||
|
||||
const cashInAlerts = device.cashUnits.cashbox > notifications.cashInAlertThreshold
|
||||
? [{
|
||||
code: 'CASH_BOX_FULL',
|
||||
machineName,
|
||||
deviceId,
|
||||
notes: device.cashUnits.cashbox
|
||||
}]
|
||||
: []
|
||||
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||
const cashOutEnabled = cashOutConfig.active
|
||||
const isUnitLow = (have, max, limit) => ((have / max) * 100) < limit
|
||||
|
||||
if (!cashOutEnabled)
|
||||
return cashInAlerts
|
||||
|
||||
const cassetteCapacity = getCashUnitCapacity(device.model, 'cassette')
|
||||
const cassetteAlerts = Array(Math.min(device.numberOfCassettes ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES))
|
||||
.fill(null)
|
||||
.flatMap((_elem, idx) => {
|
||||
const nth = idx + 1
|
||||
const cassetteField = `cassette${nth}`
|
||||
const notes = device.cashUnits[cassetteField]
|
||||
const denomination = cashOutConfig[cassetteField]
|
||||
|
||||
const limit = notifications[`fillingPercentageCassette${nth}`]
|
||||
return isUnitLow(notes, cassetteCapacity, limit) ?
|
||||
[{
|
||||
code: 'LOW_CASH_OUT',
|
||||
cassette: nth,
|
||||
machineName,
|
||||
deviceId,
|
||||
notes,
|
||||
denomination,
|
||||
fiatCode
|
||||
}] :
|
||||
[]
|
||||
})
|
||||
|
||||
const recyclerCapacity = getCashUnitCapacity(device.model, 'recycler')
|
||||
const recyclerAlerts = Array(Math.min(device.numberOfRecyclers ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS))
|
||||
.fill(null)
|
||||
.flatMap((_elem, idx) => {
|
||||
const nth = idx + 1
|
||||
const recyclerField = `recycler${nth}`
|
||||
const notes = device.cashUnits[recyclerField]
|
||||
const denomination = cashOutConfig[recyclerField]
|
||||
|
||||
const limit = notifications[`fillingPercentageRecycler${nth}`]
|
||||
return isUnitLow(notes, recyclerCapacity, limit) ?
|
||||
[{
|
||||
code: 'LOW_RECYCLER_STACKER',
|
||||
cassette: nth, // @see DETAIL_TEMPLATE in /lib/notifier/utils.js
|
||||
machineName,
|
||||
deviceId,
|
||||
notes,
|
||||
denomination,
|
||||
fiatCode
|
||||
}] :
|
||||
[]
|
||||
})
|
||||
|
||||
return [].concat(cashInAlerts, cassetteAlerts, recyclerAlerts)
|
||||
}
|
||||
|
||||
function checkCryptoBalances (fiatCode, devices) {
|
||||
const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes)
|
||||
|
||||
const fetchCryptoCodes = _deviceId => {
|
||||
const localeConfig = configManager.getLocale(_deviceId, settings.config)
|
||||
return localeConfig.cryptoCurrencies
|
||||
}
|
||||
|
||||
const union = _.flow(_.map(fetchCryptoCodes), _.flatten, _.uniq)
|
||||
const cryptoCodes = union(devices)
|
||||
const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode])
|
||||
|
||||
return Promise.all(fiatBalancePromises(cryptoCodes))
|
||||
.then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)))
|
||||
}
|
||||
|
||||
function checkCryptoBalance (fiatCode, rec) {
|
||||
const [cryptoCode, fiatBalance] = rec
|
||||
if (!fiatBalance) return null
|
||||
|
||||
const notifications = configManager.getNotifications(cryptoCode, null, settings.config)
|
||||
const lowAlertThreshold = notifications.cryptoLowBalance
|
||||
const highAlertThreshold = notifications.cryptoHighBalance
|
||||
|
||||
const req = {
|
||||
cryptoCode,
|
||||
fiatBalance,
|
||||
fiatCode
|
||||
}
|
||||
|
||||
if (_.isFinite(lowAlertThreshold) && new BN(fiatBalance.balance).lt(lowAlertThreshold)) {
|
||||
return _.set('code')('LOW_CRYPTO_BALANCE')(req)
|
||||
}
|
||||
|
||||
if (_.isFinite(highAlertThreshold) && new BN(fiatBalance.balance).gt(highAlertThreshold)) {
|
||||
return _.set('code')('HIGH_CRYPTO_BALANCE')(req)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function checkBalances () {
|
||||
const localeConfig = configManager.getGlobalLocale(settings.config)
|
||||
const fiatCode = localeConfig.fiatCurrency
|
||||
|
||||
return machineLoader.getMachines()
|
||||
.then(devices => Promise.all([
|
||||
checkCryptoBalances(fiatCode, devices),
|
||||
checkDevicesCashBalances(fiatCode, devices)
|
||||
]))
|
||||
.then(_.flow(_.flattenDeep, _.compact))
|
||||
}
|
||||
|
||||
function randomCode () {
|
||||
return new BN(crypto.randomBytes(3).toString('hex'), 16).shiftedBy(-6).toFixed(6).slice(-6)
|
||||
}
|
||||
|
||||
function getPhoneCode (phone) {
|
||||
const code = settings.config.notifications_thirdParty_sms === 'mock-sms'
|
||||
? '123'
|
||||
: randomCode()
|
||||
|
||||
const timestamp = `${(new Date()).toISOString().substring(11, 19)} UTC`
|
||||
return sms.getSms(CONFIRMATION_CODE, phone, { code, timestamp })
|
||||
.then(smsObj => {
|
||||
const rec = {
|
||||
sms: smsObj
|
||||
}
|
||||
|
||||
return sms.sendMessage(settings, rec)
|
||||
.then(() => code)
|
||||
})
|
||||
}
|
||||
|
||||
function getEmailCode (toEmail) {
|
||||
const code = settings.config.notifications_thirdParty_email === 'mock-email'
|
||||
? '123'
|
||||
: randomCode()
|
||||
|
||||
const rec = {
|
||||
email: {
|
||||
toEmail,
|
||||
subject: 'Your cryptomat code',
|
||||
body: `Your cryptomat code: ${code}`
|
||||
}
|
||||
}
|
||||
|
||||
return email.sendCustomerMessage(settings, rec)
|
||||
.then(() => code)
|
||||
}
|
||||
|
||||
function sweepHdRow (row) {
|
||||
const txId = row.id
|
||||
const cryptoCode = row.crypto_code
|
||||
|
||||
return wallet.sweep(settings, txId, cryptoCode, row.hd_index)
|
||||
.then(txHash => {
|
||||
if (txHash) {
|
||||
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
|
||||
|
||||
const sql = `update cash_out_txs set swept='t'
|
||||
where id=$1`
|
||||
|
||||
return db.none(sql, row.id)
|
||||
}
|
||||
})
|
||||
.catch(err => logger.error('[%s] [Session ID: %s] Sweep error: %s', cryptoCode, row.id, err.message))
|
||||
}
|
||||
|
||||
function sweepHd () {
|
||||
const sql = `SELECT id, crypto_code, hd_index FROM cash_out_txs
|
||||
WHERE hd_index IS NOT NULL AND NOT swept AND status IN ('confirmed', 'instant') AND created > now() - interval '1 week'`
|
||||
|
||||
return db.any(sql)
|
||||
.then(rows => Promise.all(rows.map(sweepHdRow)))
|
||||
.catch(logger.error)
|
||||
}
|
||||
|
||||
function getMachineNames () {
|
||||
return machineLoader.getMachineNames(settings.config)
|
||||
}
|
||||
|
||||
function getRawRates () {
|
||||
const localeConfig = configManager.getGlobalLocale(settings.config)
|
||||
const fiatCode = localeConfig.fiatCurrency
|
||||
|
||||
const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config)
|
||||
const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c))
|
||||
|
||||
return Promise.all(tickerPromises)
|
||||
}
|
||||
|
||||
function getRates () {
|
||||
return getRawRates()
|
||||
.then(buildRates)
|
||||
}
|
||||
|
||||
function rateAddress (cryptoCode, address) {
|
||||
return walletScoring.rateAddress(settings, cryptoCode, address)
|
||||
}
|
||||
|
||||
function rateTransaction (cryptoCode, address) {
|
||||
return walletScoring.rateTransaction(settings, cryptoCode, address)
|
||||
}
|
||||
|
||||
function isWalletScoringEnabled (tx) {
|
||||
return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode)
|
||||
}
|
||||
|
||||
function probeLN (cryptoCode, address) {
|
||||
return wallet.probeLN(settings, cryptoCode, address)
|
||||
}
|
||||
|
||||
return {
|
||||
getRates,
|
||||
recordPing,
|
||||
buildRates,
|
||||
getRawRates,
|
||||
buildRatesNoCommission,
|
||||
pollQueries,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
isHd,
|
||||
isZeroConf,
|
||||
getStatus,
|
||||
getPhoneCode,
|
||||
getEmailCode,
|
||||
executeTrades,
|
||||
clearOldLogs,
|
||||
notifyConfirmation,
|
||||
sweepHd,
|
||||
sendMessage,
|
||||
checkBalances,
|
||||
getMachineNames,
|
||||
buy,
|
||||
sell,
|
||||
getNotificationConfig,
|
||||
notifyOperator,
|
||||
pruneMachinesHeartbeat,
|
||||
rateAddress,
|
||||
rateTransaction,
|
||||
isWalletScoringEnabled,
|
||||
probeLN,
|
||||
buildAvailableUnits
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = plugins
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const sumsubApi = require('./sumsub.api')
|
||||
const { PENDING, RETRY, APPROVED, REJECTED } = require('../consts')
|
||||
|
||||
const CODE = 'sumsub'
|
||||
|
||||
const getApplicantByExternalId = (account, userId) => {
|
||||
return sumsubApi.getApplicantByExternalId(account, userId)
|
||||
.then(r => r.data)
|
||||
}
|
||||
|
||||
const createApplicant = (account, userId, level) => {
|
||||
return sumsubApi.createApplicant(account, userId, level)
|
||||
.then(r => r.data)
|
||||
.catch(err => {
|
||||
if (err.response.status === 409) return getApplicantByExternalId(account, userId)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
const createLink = (account, userId, level) => {
|
||||
return sumsubApi.createLink(account, userId, level)
|
||||
.then(r => r.data.url)
|
||||
}
|
||||
|
||||
const getApplicantStatus = (account, userId) => {
|
||||
return sumsubApi.getApplicantByExternalId(account, userId)
|
||||
.then(r => {
|
||||
const levelName = _.get('data.review.levelName', r)
|
||||
const reviewStatus = _.get('data.review.reviewStatus', r)
|
||||
const reviewAnswer = _.get('data.review.reviewResult.reviewAnswer', r)
|
||||
const reviewRejectType = _.get('data.review.reviewResult.reviewRejectType', r)
|
||||
|
||||
// if last review was from a different level, return the current level and RETRY
|
||||
if (levelName !== account.applicantLevel) return { level: account.applicantLevel, answer: RETRY }
|
||||
|
||||
let answer = PENDING
|
||||
if (reviewStatus === 'init') answer = RETRY
|
||||
if (reviewAnswer === 'GREEN' && reviewStatus === 'completed') answer = APPROVED
|
||||
if (reviewAnswer === 'RED' && reviewRejectType === 'RETRY') answer = RETRY
|
||||
if (reviewAnswer === 'RED' && reviewRejectType === 'FINAL') answer = REJECTED
|
||||
|
||||
return { level: levelName, answer }
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CODE,
|
||||
createApplicant,
|
||||
getApplicantStatus,
|
||||
createLink
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
const _ = require('lodash/fp')
|
||||
const ccxt = require('ccxt')
|
||||
const mem = require('mem')
|
||||
|
||||
const { buildMarket, ALL, isConfigValid } = require('../common/ccxt')
|
||||
const { ORDER_TYPES } = require('./consts')
|
||||
const logger = require('../../logger')
|
||||
const { currencies } = require('../../new-admin/config')
|
||||
const T = require('../../time')
|
||||
|
||||
const DEFAULT_PRICE_PRECISION = 2
|
||||
const DEFAULT_AMOUNT_PRECISION = 8
|
||||
|
||||
function trade (side, account, tradeEntry, exchangeName) {
|
||||
const { cryptoAtoms, fiatCode, cryptoCode: _cryptoCode, tradeId } = tradeEntry
|
||||
try {
|
||||
const cryptoCode = coinUtils.getEquivalentCode(_cryptoCode)
|
||||
const exchangeConfig = ALL[exchangeName]
|
||||
if (!exchangeConfig) throw Error('Exchange configuration not found')
|
||||
|
||||
const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig
|
||||
if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config')
|
||||
|
||||
const selectedFiatMarket = account.currencyMarket
|
||||
const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName)
|
||||
const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION)
|
||||
const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision)
|
||||
const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {}
|
||||
const withCustomKey = USER_REF ? { [USER_REF]: tradeId } : {}
|
||||
const options = _.assign(accountOptions, withCustomKey)
|
||||
const exchange = new ccxt[exchangeName](loadConfig(account))
|
||||
|
||||
if (ORDER_TYPE === ORDER_TYPES.MARKET) {
|
||||
return exchange.createOrder(symbol, ORDER_TYPES.MARKET, side, amount, null, options)
|
||||
}
|
||||
|
||||
return exchange.fetchOrderBook(symbol)
|
||||
.then(orderBook => {
|
||||
const price = calculatePrice(side, amount, orderBook).toFixed(DEFAULT_PRICE_PRECISION)
|
||||
return exchange.createOrder(symbol, ORDER_TYPES.LIMIT, side, amount, price, options)
|
||||
})
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
function calculatePrice (side, amount, orderBook) {
|
||||
const book = side === 'buy' ? 'asks' : 'bids'
|
||||
let collected = 0.0
|
||||
for (const entry of orderBook[book]) {
|
||||
collected += parseFloat(entry[1])
|
||||
if (collected >= amount) return parseFloat(entry[0])
|
||||
}
|
||||
throw new Error('Insufficient market depth')
|
||||
}
|
||||
|
||||
function _getMarkets (exchangeName, availableCryptos) {
|
||||
const prunedCryptos = _.compose(_.uniq, _.map(coinUtils.getEquivalentCode))(availableCryptos)
|
||||
|
||||
try {
|
||||
const exchange = new ccxt[exchangeName]()
|
||||
const cryptosToQuoteAgainst = ['USDT']
|
||||
const currencyCodes = _.concat(_.map(it => it.code, currencies), cryptosToQuoteAgainst)
|
||||
|
||||
return exchange.fetchMarkets()
|
||||
.then(_.filter(it => (it.type === 'spot' || it.spot)))
|
||||
.then(res =>
|
||||
_.reduce((acc, value) => {
|
||||
if (_.includes(value.base, prunedCryptos) && _.includes(value.quote, currencyCodes)) {
|
||||
if (value.quote === value.base) return acc
|
||||
|
||||
if (_.isNil(acc[value.quote])) {
|
||||
return { ...acc, [value.quote]: [value.base] }
|
||||
}
|
||||
|
||||
acc[value.quote].push(value.base)
|
||||
}
|
||||
return acc
|
||||
}, {}, res)
|
||||
)
|
||||
} catch (e) {
|
||||
logger.debug(`No CCXT exchange found for ${exchangeName}`)
|
||||
}
|
||||
}
|
||||
|
||||
const getMarkets = mem(_getMarkets, {
|
||||
maxAge: T.week,
|
||||
cacheKey: (exchangeName, availableCryptos) => exchangeName
|
||||
})
|
||||
|
||||
module.exports = { trade, getMarkets }
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const { ORDER_TYPES } = require('./consts')
|
||||
const { COINS } = require('@lamassu/coins')
|
||||
|
||||
const ORDER_TYPE = ORDER_TYPES.LIMIT
|
||||
const { BTC, ETH, USDT, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, USDT, LN]
|
||||
const FIAT = ['USD']
|
||||
const DEFAULT_FIAT_MARKET = 'USD'
|
||||
const AMOUNT_PRECISION = 4
|
||||
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
const mapper = {
|
||||
'clientKey': 'apiKey',
|
||||
'clientSecret': 'secret',
|
||||
'userId': 'uid'
|
||||
}
|
||||
const mapped = _.mapKeys(key => mapper[key] ? mapper[key] : key)(_.omit(['walletId'], account))
|
||||
return { ...mapped, timeout: 3000 }
|
||||
}
|
||||
const loadOptions = ({ walletId }) => ({ walletId })
|
||||
|
||||
module.exports = { loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
module.exports = {
|
||||
buy,
|
||||
sell
|
||||
}
|
||||
|
||||
function buy (cryptoAtoms, fiatCode, cryptoCode) {
|
||||
console.log('[mock] buying %s %s for %s', cryptoAtoms.toString(), cryptoCode, fiatCode)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function sell (cryptoAtoms, fiatCode, cryptoCode) {
|
||||
console.log('[mock] selling %s %s for %s', cryptoAtoms.toString(), cryptoCode, fiatCode)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
const Telnyx = require('telnyx')
|
||||
|
||||
const NAME = 'Telnyx'
|
||||
|
||||
function sendMessage (account, rec) {
|
||||
const telnyx = Telnyx(account.apiKey)
|
||||
|
||||
const from = account.fromNumber
|
||||
const text = rec.sms.body
|
||||
const to = rec.sms.toNumber || account.toNumber
|
||||
|
||||
return telnyx.messages.create({ from, to, text })
|
||||
.catch(err => {
|
||||
throw new Error(`Telnyx error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
function getLookup () {
|
||||
throw new Error('Telnyx error: lookup not supported')
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
sendMessage,
|
||||
getLookup
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
const twilio = require('twilio')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const NAME = 'Twilio'
|
||||
|
||||
const BAD_NUMBER_CODES = [21201, 21202, 21211, 21214, 21216, 21217, 21219, 21408,
|
||||
21610, 21612, 21614, 21608]
|
||||
|
||||
function sendMessage (account, rec) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// to catch configuration errors like
|
||||
// "Error: username is required"
|
||||
const client = twilio(account.accountSid, account.authToken)
|
||||
const body = rec.sms.body
|
||||
const _toNumber = rec.sms.toNumber || account.toNumber
|
||||
const from = (_.startsWith('+')(account.fromNumber)
|
||||
|| !_.isNumber(String(account.fromNumber).replace(/\s/g,'')))
|
||||
? account.fromNumber : `+${account.fromNumber}`
|
||||
|
||||
const opts = {
|
||||
body: body,
|
||||
to: _toNumber,
|
||||
from
|
||||
}
|
||||
|
||||
return client.messages.create(opts)
|
||||
})
|
||||
.catch(err => {
|
||||
if (_.includes(err.code, BAD_NUMBER_CODES)) {
|
||||
const badNumberError = new Error(err.message)
|
||||
badNumberError.name = 'BadNumberError'
|
||||
throw badNumberError
|
||||
}
|
||||
|
||||
throw new Error(`Twilio error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
function getLookup (account, number) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const client = twilio(account.accountSid, account.authToken)
|
||||
return client.lookups.v1.phoneNumbers(number)
|
||||
.fetch({ addOns: ['lamassu_ekata'] })
|
||||
})
|
||||
.then(info => info.addOns.results['lamassu_ekata'])
|
||||
.then(info => {
|
||||
if (info.status !== 'successful') {
|
||||
throw new Error(`Twilio error: ${info.message}`)
|
||||
}
|
||||
return info
|
||||
})
|
||||
.catch(err => {
|
||||
if (_.includes(err.code, BAD_NUMBER_CODES)) {
|
||||
const badNumberError = new Error(err.message)
|
||||
badNumberError.name = 'BadNumberError'
|
||||
throw badNumberError
|
||||
}
|
||||
|
||||
throw new Error(`Twilio error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
sendMessage,
|
||||
getLookup
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
const https = require('https')
|
||||
const axios = require('axios').create({
|
||||
// TODO: get rejectUnauthorized true to work
|
||||
baseURL: `${process.env.TICKER_URL}/api/rates/`,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
})
|
||||
|
||||
const BN = require('../../../bn')
|
||||
|
||||
function ticker (account, fiatCode, cryptoCode) {
|
||||
return axios.get(`${cryptoCode}/${fiatCode}`)
|
||||
.then(({ data }) => {
|
||||
if (data.error) throw new Error(JSON.stringify(data.error))
|
||||
return {
|
||||
rates: {
|
||||
ask: BN(data.ask),
|
||||
bid: BN(data.bid),
|
||||
signature: data.signature
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ticker
|
||||
}
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
[
|
||||
{
|
||||
"constant":true,
|
||||
"inputs":[
|
||||
|
||||
],
|
||||
"name":"name",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"",
|
||||
"type":"string"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":false,
|
||||
"inputs":[
|
||||
{
|
||||
"name":"_spender",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"name":"_value",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"name":"approve",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"success",
|
||||
"type":"bool"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":true,
|
||||
"inputs":[
|
||||
|
||||
],
|
||||
"name":"totalSupply",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":false,
|
||||
"inputs":[
|
||||
{
|
||||
"name":"_from",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"name":"_to",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"name":"_value",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"name":"transferFrom",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"success",
|
||||
"type":"bool"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":true,
|
||||
"inputs":[
|
||||
|
||||
],
|
||||
"name":"decimals",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":true,
|
||||
"inputs":[
|
||||
|
||||
],
|
||||
"name":"version",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"",
|
||||
"type":"string"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":true,
|
||||
"inputs":[
|
||||
{
|
||||
"name":"_owner",
|
||||
"type":"address"
|
||||
}
|
||||
],
|
||||
"name":"balanceOf",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"balance",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":true,
|
||||
"inputs":[
|
||||
|
||||
],
|
||||
"name":"symbol",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"",
|
||||
"type":"string"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":false,
|
||||
"inputs":[
|
||||
{
|
||||
"name":"_to",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"name":"_value",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"name":"transfer",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"success",
|
||||
"type":"bool"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":false,
|
||||
"inputs":[
|
||||
{
|
||||
"name":"_spender",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"name":"_value",
|
||||
"type":"uint256"
|
||||
},
|
||||
{
|
||||
"name":"_extraData",
|
||||
"type":"bytes"
|
||||
}
|
||||
],
|
||||
"name":"approveAndCall",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"success",
|
||||
"type":"bool"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"constant":true,
|
||||
"inputs":[
|
||||
{
|
||||
"name":"_owner",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"name":"_spender",
|
||||
"type":"address"
|
||||
}
|
||||
],
|
||||
"name":"allowance",
|
||||
"outputs":[
|
||||
{
|
||||
"name":"remaining",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"payable":false,
|
||||
"type":"function"
|
||||
},
|
||||
{
|
||||
"inputs":[
|
||||
{
|
||||
"name":"_initialAmount",
|
||||
"type":"uint256"
|
||||
},
|
||||
{
|
||||
"name":"_tokenName",
|
||||
"type":"string"
|
||||
},
|
||||
{
|
||||
"name":"_decimalUnits",
|
||||
"type":"uint8"
|
||||
},
|
||||
{
|
||||
"name":"_tokenSymbol",
|
||||
"type":"string"
|
||||
}
|
||||
],
|
||||
"type":"constructor"
|
||||
},
|
||||
{
|
||||
"payable":false,
|
||||
"type":"fallback"
|
||||
},
|
||||
{
|
||||
"anonymous":false,
|
||||
"inputs":[
|
||||
{
|
||||
"indexed":true,
|
||||
"name":"_from",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"indexed":true,
|
||||
"name":"_to",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"indexed":false,
|
||||
"name":"_value",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"name":"Transfer",
|
||||
"type":"event"
|
||||
},
|
||||
{
|
||||
"anonymous":false,
|
||||
"inputs":[
|
||||
{
|
||||
"indexed":true,
|
||||
"name":"_owner",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"indexed":true,
|
||||
"name":"_spender",
|
||||
"type":"address"
|
||||
},
|
||||
{
|
||||
"indexed":false,
|
||||
"name":"_value",
|
||||
"type":"uint256"
|
||||
}
|
||||
],
|
||||
"name":"Approval",
|
||||
"type":"event"
|
||||
}
|
||||
]
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
const NAME = 'FakeScoring'
|
||||
|
||||
const { WALLET_SCORE_THRESHOLD } = require('../../../constants')
|
||||
|
||||
function rateAddress (account, cryptoCode, address) {
|
||||
return new Promise((resolve, _) => {
|
||||
setTimeout(() => {
|
||||
console.log('[WALLET-SCORING] DEBUG: Mock scoring rating wallet address %s', address)
|
||||
return Promise.resolve(2)
|
||||
.then(score => resolve({ address, score, isValid: score < WALLET_SCORE_THRESHOLD }))
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function isWalletScoringEnabled (account, cryptoCode) {
|
||||
return new Promise((resolve, _) => {
|
||||
setTimeout(() => {
|
||||
return resolve(true)
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
rateAddress,
|
||||
rateTransaction:rateAddress,
|
||||
isWalletScoringEnabled
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const jsonRpc = require('../../common/json-rpc')
|
||||
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency('BCH')
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
||||
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
|
||||
|
||||
function fetch (method, params) {
|
||||
return jsonRpc.fetch(rpcConfig, method, params)
|
||||
}
|
||||
|
||||
function errorHandle (e) {
|
||||
const err = JSON.parse(e.message)
|
||||
switch (err.code) {
|
||||
case -6:
|
||||
throw new E.InsufficientFundsError()
|
||||
default:
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function checkCryptoCode (cryptoCode) {
|
||||
if (cryptoCode !== 'BCH') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8)
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('sendtoaddress', [toAddress, coins]))
|
||||
.then((txId) => fetch('gettransaction', [txId]))
|
||||
.then((res) => _.pick(['fee', 'txid'], res))
|
||||
.then((pickedObj) => {
|
||||
return {
|
||||
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
|
||||
txid: pickedObj.txid
|
||||
}
|
||||
})
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
return checkCryptoCode(info.cryptoCode)
|
||||
.then(() => fetch('getnewaddress'))
|
||||
}
|
||||
|
||||
function addressBalance (address, confs) {
|
||||
return fetch('getreceivedbyaddress', [address, confs])
|
||||
.then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function confirmedBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 1))
|
||||
}
|
||||
|
||||
function pendingBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 0))
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoCode } = tx
|
||||
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
})
|
||||
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
|
||||
fundingPendingBalance,
|
||||
fundingConfirmedBalance,
|
||||
fundingAddress
|
||||
}))
|
||||
}
|
||||
|
||||
function cryptoNetwork (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getblockchaininfo'))
|
||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('listreceivedbyaddress', [0, true, true, address]))
|
||||
.then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress))))
|
||||
.then(_.map(({ hash }) => hash))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
cryptoNetwork,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const jsonRpc = require('../../common/json-rpc')
|
||||
const { getSatBEstimateFee } = require('../../../blockexplorers/mempool.space')
|
||||
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
const logger = require('../../../logger')
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
const { isDevMode } = require('../../../environment-helper')
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency('BTC')
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
||||
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
|
||||
|
||||
const SUPPORTS_BATCHING = true
|
||||
|
||||
function fetch (method, params) {
|
||||
return jsonRpc.fetch(rpcConfig, method, params)
|
||||
}
|
||||
|
||||
function errorHandle (e) {
|
||||
const err = JSON.parse(e.message)
|
||||
switch (err.code) {
|
||||
case -5:
|
||||
return logger.error(`${err}`)
|
||||
case -6:
|
||||
throw new E.InsufficientFundsError()
|
||||
default:
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function checkCryptoCode (cryptoCode) {
|
||||
if (cryptoCode !== 'BTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getbalances'))
|
||||
.then(({ mine }) => new BN(mine.trusted).shiftedBy(unitScale).decimalPlaces(0))
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getbalances'))
|
||||
.then(({ mine }) => new BN(mine.untrusted_pending).plus(mine.immature).shiftedBy(unitScale).decimalPlaces(0))
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function estimateFee () {
|
||||
return getSatBEstimateFee()
|
||||
.then(result => BN(result))
|
||||
.catch(err => {
|
||||
logger.error('failure estimating fes', err)
|
||||
})
|
||||
}
|
||||
|
||||
function calculateFeeDiscount (feeMultiplier = 1, unitScale) {
|
||||
// 0 makes bitcoind do automatic fee selection
|
||||
const AUTOMATIC_FEE = 0
|
||||
return estimateFee()
|
||||
.then(estimatedFee => {
|
||||
if (!estimatedFee) {
|
||||
logger.info('failure estimating fee, using bitcoind automatic fee selection')
|
||||
return AUTOMATIC_FEE
|
||||
}
|
||||
// transform from sat/vB to BTC/kvB and apply the multipler
|
||||
const newFee = estimatedFee.shiftedBy(-unitScale+3).times(feeMultiplier)
|
||||
if (newFee.lt(0.00001) || newFee.gt(0.1)) {
|
||||
logger.info('fee outside safety parameters, defaulting to automatic fee selection')
|
||||
return AUTOMATIC_FEE
|
||||
}
|
||||
return newFee.toFixed(8)
|
||||
})
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8)
|
||||
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => calculateFeeDiscount(feeMultiplier, unitScale))
|
||||
.then(newFee => fetch('settxfee', [newFee]))
|
||||
.then(() => fetch('sendtoaddress', [toAddress, coins]))
|
||||
.then((txId) => fetch('gettransaction', [txId]))
|
||||
.then((res) => _.pick(['fee', 'txid'], res))
|
||||
.then((pickedObj) => {
|
||||
return {
|
||||
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
|
||||
txid: pickedObj.txid
|
||||
}
|
||||
})
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function sendCoinsBatch (account, txs, cryptoCode, feeMultiplier) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => calculateFeeDiscount(feeMultiplier, unitScale))
|
||||
.then(newFee => fetch('settxfee', [newFee]))
|
||||
.then(() => _.reduce((acc, value) => ({
|
||||
...acc,
|
||||
[value.toAddress]: _.isNil(acc[value.toAddress])
|
||||
? BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8)
|
||||
: BN(acc[value.toAddress]).plus(BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8))
|
||||
}), {}, txs))
|
||||
.then((obj) => fetch('sendmany', ['', obj]))
|
||||
.then((txId) => fetch('gettransaction', [txId]))
|
||||
.then((res) => _.pick(['fee', 'txid'], res))
|
||||
.then((pickedObj) => ({
|
||||
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
|
||||
txid: pickedObj.txid
|
||||
}))
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
return checkCryptoCode(info.cryptoCode)
|
||||
.then(() => fetch('getnewaddress'))
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function addressBalance (address, confs) {
|
||||
return fetch('getreceivedbyaddress', [address, confs])
|
||||
.then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0))
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function confirmedBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 1))
|
||||
}
|
||||
|
||||
function pendingBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 0))
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
})
|
||||
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
|
||||
fundingPendingBalance,
|
||||
fundingConfirmedBalance,
|
||||
fundingAddress
|
||||
}))
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function cryptoNetwork (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
|
||||
}
|
||||
|
||||
function fetchRBF (txId) {
|
||||
return fetch('getmempoolentry', [txId])
|
||||
.then((res) => {
|
||||
return [txId, res['bip125-replaceable']]
|
||||
})
|
||||
.catch(err => {
|
||||
errorHandle(err)
|
||||
return [txId, true]
|
||||
})
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getblockchaininfo'))
|
||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('listreceivedbyaddress', [0, true, true, address]))
|
||||
.then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress))))
|
||||
.then(_.map(({ hash }) => hash))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
cryptoNetwork,
|
||||
fetchRBF,
|
||||
sendCoinsBatch,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress,
|
||||
fetch,
|
||||
SUPPORTS_BATCHING
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const jsonRpc = require('../../common/json-rpc')
|
||||
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency('DASH')
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
||||
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
|
||||
|
||||
function fetch (method, params) {
|
||||
return jsonRpc.fetch(rpcConfig, method, params)
|
||||
}
|
||||
|
||||
function errorHandle (e) {
|
||||
const err = JSON.parse(e.message)
|
||||
switch (err.code) {
|
||||
case -6:
|
||||
throw new E.InsufficientFundsError()
|
||||
default:
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function checkCryptoCode (cryptoCode) {
|
||||
if (cryptoCode !== 'DASH') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8)
|
||||
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('sendtoaddress', [toAddress, coins]))
|
||||
.then((txId) => fetch('gettransaction', [txId]))
|
||||
.then((res) => _.pick(['fee', 'txid'], res))
|
||||
.then((pickedObj) => {
|
||||
return {
|
||||
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
|
||||
txid: pickedObj.txid
|
||||
}
|
||||
})
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
return checkCryptoCode(info.cryptoCode)
|
||||
.then(() => fetch('getnewaddress'))
|
||||
}
|
||||
|
||||
function addressBalance (address, confs) {
|
||||
return fetch('getreceivedbyaddress', [address, confs])
|
||||
.then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function confirmedBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 1))
|
||||
}
|
||||
|
||||
function pendingBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 0))
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
})
|
||||
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
|
||||
fundingPendingBalance,
|
||||
fundingConfirmedBalance,
|
||||
fundingAddress
|
||||
}))
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getblockchaininfo'))
|
||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('listreceivedbyaddress', [0, true, true, true, address]))
|
||||
.then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress))))
|
||||
.then(_.map(({ hash }) => hash))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
@ -1,377 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const axios = require('axios')
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
const NAME = 'LN'
|
||||
const SUPPORTED_COINS = ['LN']
|
||||
|
||||
const BN = require('../../../bn')
|
||||
|
||||
function request (graphqlQuery, token, endpoint) {
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
'X-API-KEY': token
|
||||
}
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: endpoint,
|
||||
headers: headers,
|
||||
data: graphqlQuery
|
||||
})
|
||||
.then(r => {
|
||||
if (r.error) throw r.error
|
||||
return r.data
|
||||
})
|
||||
.catch(err => {
|
||||
throw new Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function checkCryptoCode (cryptoCode) {
|
||||
if (!SUPPORTED_COINS.includes(cryptoCode)) {
|
||||
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function getTransactionsByAddress (token, endpoint, walletId, address) {
|
||||
const accountInfo = {
|
||||
'operationName': 'me',
|
||||
'query': `query me($walletId: WalletId!, , $address: OnChainAddress!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
transactionsByAddress (address: $address) {
|
||||
edges {
|
||||
node {
|
||||
direction
|
||||
settlementAmount
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'variables': { walletId, address }
|
||||
}
|
||||
return request(accountInfo, token, endpoint)
|
||||
.then(r => {
|
||||
return r.data.me.defaultAccount.walletById.transactionsByAddress
|
||||
})
|
||||
.catch(err => {
|
||||
throw new Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function getGaloyWallet (token, endpoint, walletId) {
|
||||
const accountInfo = {
|
||||
'operationName': 'me',
|
||||
'query': `query me($walletId: WalletId!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
id
|
||||
walletCurrency
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'variables': { walletId }
|
||||
}
|
||||
return request(accountInfo, token, endpoint)
|
||||
.then(r => {
|
||||
return r.data.me.defaultAccount.walletById
|
||||
})
|
||||
.catch(err => {
|
||||
throw new Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function isLnInvoice (address) {
|
||||
return address.toLowerCase().startsWith('lnbc')
|
||||
}
|
||||
|
||||
function isLnurl (address) {
|
||||
return address.toLowerCase().startsWith('lnurl')
|
||||
}
|
||||
|
||||
function sendFundsOnChain (walletId, address, cryptoAtoms, token, endpoint) {
|
||||
const sendOnChain = {
|
||||
'operationName': 'onChainPaymentSend',
|
||||
'query': `mutation onChainPaymentSend($input: OnChainPaymentSendInput!) {
|
||||
onChainPaymentSend(input: $input) {
|
||||
errors {
|
||||
message
|
||||
path
|
||||
}
|
||||
status
|
||||
}
|
||||
}`,
|
||||
'variables': { 'input': { address, amount: cryptoAtoms.toString(), walletId } }
|
||||
}
|
||||
return request(sendOnChain, token, endpoint)
|
||||
.then(result => {
|
||||
return result.data.onChainPaymentSend
|
||||
})
|
||||
}
|
||||
|
||||
function sendFundsLNURL (walletId, lnurl, cryptoAtoms, token, endpoint) {
|
||||
const sendLnNoAmount = {
|
||||
'operationName': 'lnurlPaymentSend',
|
||||
'query': `mutation lnurlPaymentSend($input: LnurlPaymentSendInput!) {
|
||||
lnurlPaymentSend(input: $input) {
|
||||
errors {
|
||||
message
|
||||
path
|
||||
}
|
||||
status
|
||||
}
|
||||
}`,
|
||||
'variables': { 'input': { 'lnurl': `${lnurl}`, 'walletId': `${walletId}`, 'amount': `${cryptoAtoms}` } }
|
||||
}
|
||||
return request(sendLnNoAmount, token, endpoint).then(result => result.data.lnurlPaymentSend)
|
||||
}
|
||||
|
||||
function sendFundsLN (walletId, invoice, cryptoAtoms, token, endpoint) {
|
||||
const sendLnNoAmount = {
|
||||
'operationName': 'lnNoAmountInvoicePaymentSend',
|
||||
'query': `mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) {
|
||||
lnNoAmountInvoicePaymentSend(input: $input) {
|
||||
errors {
|
||||
message
|
||||
path
|
||||
}
|
||||
status
|
||||
}
|
||||
}`,
|
||||
'variables': { 'input': { 'paymentRequest': invoice, walletId, amount: cryptoAtoms.toString() } }
|
||||
}
|
||||
return request(sendLnNoAmount, token, endpoint).then(result => result.data.lnNoAmountInvoicePaymentSend)
|
||||
}
|
||||
|
||||
function sendProbeRequest (walletId, invoice, cryptoAtoms, token, endpoint) {
|
||||
const sendProbeNoAmount = {
|
||||
'operationName': 'lnNoAmountInvoiceFeeProbe',
|
||||
'query': `mutation lnNoAmountInvoiceFeeProbe($input: LnNoAmountInvoiceFeeProbeInput!) {
|
||||
lnNoAmountInvoiceFeeProbe(input: $input) {
|
||||
amount
|
||||
errors {
|
||||
message
|
||||
path
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'variables': { 'input': { paymentRequest: invoice, walletId, amount: cryptoAtoms.toString() } }
|
||||
}
|
||||
return request(sendProbeNoAmount, token, endpoint).then(result => result.data.lnNoAmountInvoiceFeeProbe)
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
if (isLnInvoice(toAddress)) {
|
||||
return sendFundsLN(account.walletId, toAddress, cryptoAtoms, account.apiSecret, account.endpoint)
|
||||
}
|
||||
if (isLnurl(toAddress)) {
|
||||
return sendFundsLNURL(account.walletId, toAddress, cryptoAtoms, account.apiSecret, account.endpoint)
|
||||
}
|
||||
return sendFundsOnChain(account.walletId, toAddress, cryptoAtoms, account.apiSecret, account.endpoint)
|
||||
})
|
||||
.then(result => {
|
||||
switch (result.status) {
|
||||
case 'ALREADY_PAID':
|
||||
throw new Error('Transaction already exists!')
|
||||
case 'FAILURE':
|
||||
throw new Error('Transaction failed!', JSON.stringify(result.errors))
|
||||
case 'SUCCESS':
|
||||
return '<galoy transaction>'
|
||||
case 'PENDING':
|
||||
return '<galoy transaction>'
|
||||
default:
|
||||
throw new Error(`Transaction failed: ${_.head(result.errors).message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function probeLN (account, cryptoCode, invoice) {
|
||||
const probeHardLimits = [200000, 1000000, 2000000]
|
||||
const promises = probeHardLimits.map(limit => {
|
||||
return sendProbeRequest(account.walletId, invoice, limit, account.apiSecret, account.endpoint)
|
||||
.then(r => _.isEmpty(r.errors))
|
||||
})
|
||||
return Promise.all(promises)
|
||||
.then(results => _.zipObject(probeHardLimits, results))
|
||||
}
|
||||
|
||||
function newOnChainAddress (walletId, token, endpoint) {
|
||||
const createOnChainAddress = {
|
||||
'operationName': 'onChainAddressCreate',
|
||||
'query': `mutation onChainAddressCreate($input: OnChainAddressCreateInput!) {
|
||||
onChainAddressCreate(input: $input) {
|
||||
address
|
||||
errors {
|
||||
message
|
||||
path
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'variables': { 'input': { walletId } }
|
||||
}
|
||||
return request(createOnChainAddress, token, endpoint)
|
||||
.then(result => {
|
||||
return result.data.onChainAddressCreate.address
|
||||
})
|
||||
}
|
||||
|
||||
function newNoAmountInvoice (walletId, token, endpoint) {
|
||||
const createInvoice = {
|
||||
'operationName': 'lnNoAmountInvoiceCreate',
|
||||
'query': `mutation lnNoAmountInvoiceCreate($input: LnNoAmountInvoiceCreateInput!) {
|
||||
lnNoAmountInvoiceCreate(input: $input) {
|
||||
errors {
|
||||
message
|
||||
path
|
||||
}
|
||||
invoice {
|
||||
paymentRequest
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'variables': { 'input': { walletId } }
|
||||
}
|
||||
return request(createInvoice, token, endpoint)
|
||||
.then(result => {
|
||||
return result.data.lnNoAmountInvoiceCreate.invoice.paymentRequest
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function newInvoice (walletId, cryptoAtoms, token, endpoint) {
|
||||
const createInvoice = {
|
||||
'operationName': 'lnInvoiceCreate',
|
||||
'query': `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) {
|
||||
lnInvoiceCreate(input: $input) {
|
||||
errors {
|
||||
message
|
||||
path
|
||||
}
|
||||
invoice {
|
||||
paymentRequest
|
||||
}
|
||||
}
|
||||
}`,
|
||||
'variables': { 'input': { walletId, amount: cryptoAtoms.toString() } }
|
||||
}
|
||||
return request(createInvoice, token, endpoint)
|
||||
.then(result => {
|
||||
return result.data.lnInvoiceCreate.invoice.paymentRequest
|
||||
})
|
||||
}
|
||||
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
|
||||
.then(wallet => {
|
||||
return new BN(wallet.balance || 0)
|
||||
})
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
const { cryptoAtoms, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => newInvoice(account.walletId, cryptoAtoms, account.apiSecret, account.endpoint))
|
||||
}
|
||||
|
||||
function getInvoiceStatus (token, endpoint, address) {
|
||||
const query = {
|
||||
'operationName': 'lnInvoicePaymentStatus',
|
||||
'query': `query lnInvoicePaymentStatus($input: LnInvoicePaymentStatusInput!) {
|
||||
lnInvoicePaymentStatus(input: $input) {
|
||||
status
|
||||
}
|
||||
}`,
|
||||
'variables': { input: { paymentRequest: address } }
|
||||
}
|
||||
return request(query, token, endpoint)
|
||||
.then(r => {
|
||||
return r?.data?.lnInvoicePaymentStatus?.status
|
||||
})
|
||||
.catch(err => {
|
||||
throw new Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
const getBalance = _.reduce((acc, value) => {
|
||||
acc[value.node.status] = acc[value.node.status].plus(new BN(value.node.settlementAmount))
|
||||
return acc
|
||||
}, { SUCCESS: new BN(0), PENDING: new BN(0), FAILURE: new BN(0) })
|
||||
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const address = coinUtils.parseUrl(cryptoCode, account.environment, toAddress, false)
|
||||
if (isLnInvoice(address)) {
|
||||
return getInvoiceStatus(account.apiSecret, account.endpoint, address)
|
||||
.then(it => {
|
||||
const isPaid = it === 'PAID'
|
||||
if (isPaid) return { receivedCryptoAtoms: cryptoAtoms, status: 'confirmed' }
|
||||
return { receivedCryptoAtoms: BN(0), status: 'notSeen' }
|
||||
})
|
||||
}
|
||||
// On-chain and intra-ledger transactions
|
||||
return getTransactionsByAddress(account.apiSecret, account.endpoint, account.walletId, address)
|
||||
.then(transactions => {
|
||||
const { SUCCESS: confirmed, PENDING: pending } = getBalance(transactions.edges)
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
// Regular BTC address
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => getGaloyWallet(account.apiSecret, account.endpoint, account.walletId))
|
||||
.then(wallet => {
|
||||
return newOnChainAddress(account.walletId, account.apiSecret, account.endpoint)
|
||||
.then(onChainAddress => [onChainAddress, wallet.balance])
|
||||
})
|
||||
.then(([onChainAddress, balance]) => {
|
||||
return {
|
||||
// with the old api is not possible to get pending balance
|
||||
fundingPendingBalance: new BN(0),
|
||||
fundingConfirmedBalance: new BN(balance),
|
||||
fundingAddress: onChainAddress
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cryptoNetwork (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => account.environment === 'test' ? 'test' : 'main')
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => Promise.resolve('ready'))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
cryptoNetwork,
|
||||
checkBlockchainStatus,
|
||||
probeLN
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const NodeCache = require('node-cache')
|
||||
const base = require('../geth/base')
|
||||
const T = require('../../../time')
|
||||
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('../../../constants')
|
||||
|
||||
const NAME = 'infura'
|
||||
|
||||
function run (account) {
|
||||
if (!account.endpoint) throw new Error('Need to configure API endpoint for Infura')
|
||||
|
||||
const endpoint = _.startsWith('https://')(account.endpoint)
|
||||
? account.endpoint : `https://${account.endpoint}`
|
||||
|
||||
base.connect(endpoint)
|
||||
}
|
||||
|
||||
const txsCache = new NodeCache({
|
||||
stdTTL: T.hour / 1000,
|
||||
checkperiod: T.minute / 1000,
|
||||
deleteOnExpire: true
|
||||
})
|
||||
|
||||
function shouldGetStatus (tx) {
|
||||
const timePassedSinceTx = Date.now() - new Date(tx.created)
|
||||
const timePassedSinceReq = Date.now() - new Date(txsCache.get(tx.id).lastReqTime)
|
||||
|
||||
if (timePassedSinceTx < 3 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 10 * T.seconds
|
||||
if (timePassedSinceTx < 5 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 20 * T.seconds
|
||||
if (timePassedSinceTx < 30 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.minute
|
||||
if (timePassedSinceTx < 1 * T.hour) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 2 * T.minute
|
||||
if (timePassedSinceTx < 3 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 5 * T.minute
|
||||
if (timePassedSinceTx < 1 * T.day) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.hour
|
||||
return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > T.hour
|
||||
}
|
||||
|
||||
// Override geth's getStatus function to allow for different polling timing
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
if (_.isNil(txsCache.get(tx.id))) {
|
||||
txsCache.set(tx.id, { lastReqTime: Date.now() })
|
||||
}
|
||||
|
||||
// return last available response
|
||||
if (!shouldGetStatus(tx)) {
|
||||
return Promise.resolve(txsCache.get(tx.id).res)
|
||||
}
|
||||
|
||||
return base.getStatus(account, tx, requested, settings, operatorId)
|
||||
.then(res => {
|
||||
if (res.status === 'confirmed') {
|
||||
txsCache.del(tx.id) // Transaction reached final status, can trim it from the caching obj
|
||||
} else {
|
||||
txsCache.set(tx.id, { lastReqTime: Date.now(), res })
|
||||
txsCache.ttl(tx.id, T.hour / 1000)
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = _.merge(base, { NAME, run, getStatus, fetchSpeed: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW })
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const jsonRpc = require('../../common/json-rpc')
|
||||
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency('LTC')
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
||||
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
|
||||
|
||||
function fetch (method, params) {
|
||||
return jsonRpc.fetch(rpcConfig, method, params)
|
||||
}
|
||||
|
||||
function errorHandle (e) {
|
||||
const err = JSON.parse(e.message)
|
||||
switch (err.code) {
|
||||
case -6:
|
||||
throw new E.InsufficientFundsError()
|
||||
default:
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function checkCryptoCode (cryptoCode) {
|
||||
if (cryptoCode !== 'LTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8)
|
||||
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('sendtoaddress', [toAddress, coins]))
|
||||
.then((txId) => fetch('gettransaction', [txId]))
|
||||
.then((res) => _.pick(['fee', 'txid'], res))
|
||||
.then((pickedObj) => {
|
||||
return {
|
||||
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
|
||||
txid: pickedObj.txid
|
||||
}
|
||||
})
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
return checkCryptoCode(info.cryptoCode)
|
||||
.then(() => fetch('getnewaddress'))
|
||||
}
|
||||
|
||||
function addressBalance (address, confs) {
|
||||
return fetch('getreceivedbyaddress', [address, confs])
|
||||
.then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function confirmedBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 1))
|
||||
}
|
||||
|
||||
function pendingBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 0))
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
})
|
||||
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
|
||||
fundingPendingBalance,
|
||||
fundingConfirmedBalance,
|
||||
fundingAddress
|
||||
}))
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getblockchaininfo'))
|
||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
throw new Error(`Transactions hash retrieval not implemented for this coin!`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
const NAME = 'FakeWallet'
|
||||
|
||||
const SECONDS = 1000
|
||||
const PUBLISH_TIME = 3 * SECONDS
|
||||
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
|
||||
const CONFIRM_TIME = AUTHORIZE_TIME + 10 * SECONDS
|
||||
const SUPPORTED_COINS = coinUtils.cryptoCurrencies()
|
||||
|
||||
let t0
|
||||
|
||||
const checkCryptoCode = (cryptoCode) => !_.includes(cryptoCode, SUPPORTED_COINS)
|
||||
? Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
: Promise.resolve()
|
||||
|
||||
function _balance (cryptoCode) {
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
const unitScale = cryptoRec.unitScale
|
||||
return new BN(10).shiftedBy(unitScale).decimalPlaces(0)
|
||||
}
|
||||
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return Promise.resolve()
|
||||
.then(() => _balance(cryptoCode))
|
||||
}
|
||||
|
||||
function pendingBalance (account, cryptoCode) {
|
||||
return balance(account, cryptoCode)
|
||||
.then(b => b.times(1.1))
|
||||
}
|
||||
|
||||
function confirmedBalance (account, cryptoCode) {
|
||||
return balance(account, cryptoCode)
|
||||
}
|
||||
|
||||
// Note: This makes it easier to test insufficient funds errors
|
||||
let sendCount = 100
|
||||
|
||||
function isInsufficient (cryptoAtoms, cryptoCode) {
|
||||
const b = _balance(cryptoCode)
|
||||
return cryptoAtoms.gt(b.div(1000).times(sendCount))
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
sendCount++
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (isInsufficient(cryptoAtoms, cryptoCode)) {
|
||||
console.log('[%s] DEBUG: Mock wallet insufficient funds: %s',
|
||||
cryptoCode, cryptoAtoms.toString())
|
||||
return reject(new E.InsufficientFundsError())
|
||||
}
|
||||
|
||||
console.log('[%s] DEBUG: Mock wallet sending %s cryptoAtoms to %s',
|
||||
cryptoCode, cryptoAtoms.toString(), toAddress)
|
||||
return resolve({ txid: '<txHash>', fee: new BN(0) })
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
function sendCoinsBatch (account, txs, cryptoCode) {
|
||||
sendCount = sendCount + txs.length
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
const cryptoSum = _.reduce((acc, value) => acc.plus(value.crypto_atoms), BN(0), txs)
|
||||
if (isInsufficient(cryptoSum, cryptoCode)) {
|
||||
console.log('[%s] DEBUG: Mock wallet insufficient funds: %s',
|
||||
cryptoCode, cryptoSum.toString())
|
||||
return reject(new E.InsufficientFundsError())
|
||||
}
|
||||
|
||||
console.log('[%s] DEBUG: Mock wallet sending %s cryptoAtoms in a batch',
|
||||
cryptoCode, cryptoSum.toString())
|
||||
return resolve({ txid: '<txHash>', fee: BN(0) })
|
||||
}, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
function newAddress () {
|
||||
t0 = Date.now()
|
||||
return Promise.resolve('<Fake address, don\'t send>')
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
const promises = [
|
||||
pendingBalance(account, cryptoCode),
|
||||
confirmedBalance(account, cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
|
||||
fundingPendingBalance,
|
||||
fundingConfirmedBalance,
|
||||
fundingAddress
|
||||
}))
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoCode } = tx
|
||||
const elapsed = Date.now() - t0
|
||||
|
||||
if (elapsed < PUBLISH_TIME) return Promise.resolve({ receivedCryptoAtoms: new BN(0), status: 'notSeen' })
|
||||
if (elapsed < AUTHORIZE_TIME) return Promise.resolve({ receivedCryptoAtoms: requested, status: 'published' })
|
||||
if (elapsed < CONFIRM_TIME) return Promise.resolve({ receivedCryptoAtoms: requested, status: 'authorized' })
|
||||
|
||||
console.log('[%s] DEBUG: Mock wallet has confirmed transaction [%s]', cryptoCode, toAddress.slice(0, 5))
|
||||
|
||||
return Promise.resolve({ status: 'confirmed' })
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
return resolve([]) // TODO: should return something other than empty list?
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => Promise.resolve('ready'))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
balance,
|
||||
sendCoinsBatch,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const _ = require('lodash/fp')
|
||||
const { COINS, utils } = require('@lamassu/coins')
|
||||
const { default: PQueue } = require('p-queue')
|
||||
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
const logger = require('../../../logger')
|
||||
const jsonRpc = require('../../common/json-rpc')
|
||||
|
||||
const BLOCKCHAIN_DIR = process.env.BLOCKCHAIN_DIR
|
||||
|
||||
const cryptoRec = utils.getCryptoCurrency(COINS.XMR)
|
||||
const configPath = utils.configPath(cryptoRec, BLOCKCHAIN_DIR)
|
||||
const walletDir = path.resolve(utils.cryptoDir(cryptoRec, BLOCKCHAIN_DIR), 'wallets')
|
||||
|
||||
const DIGEST_QUEUE = new PQueue({
|
||||
concurrency: 1,
|
||||
interval: 150,
|
||||
})
|
||||
|
||||
function createDigestRequest (account = {}, method, params = []) {
|
||||
return DIGEST_QUEUE.add(() => jsonRpc.fetchDigest(account, method, params)
|
||||
.then(res => {
|
||||
const r = JSON.parse(res)
|
||||
if (r.error) throw r.error
|
||||
return r.result
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function rpcConfig () {
|
||||
try {
|
||||
const config = jsonRpc.parseConf(configPath)
|
||||
return {
|
||||
username: config['rpc-login'].split(':')[0],
|
||||
password: config['rpc-login'].split(':')[1],
|
||||
port: cryptoRec.walletPort || cryptoRec.defaultPort
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Wallet is currently not installed!')
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
port: cryptoRec.walletPort || cryptoRec.defaultPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetch (method, params) {
|
||||
return createDigestRequest(rpcConfig(), method, params)
|
||||
}
|
||||
|
||||
function handleError (error, method) {
|
||||
switch(error.code) {
|
||||
case -13:
|
||||
{
|
||||
if (
|
||||
fs.existsSync(path.resolve(walletDir, 'Wallet')) &&
|
||||
fs.existsSync(path.resolve(walletDir, 'Wallet.keys'))
|
||||
) {
|
||||
logger.debug('Found wallet! Opening wallet...')
|
||||
return openWallet()
|
||||
}
|
||||
logger.debug('Couldn\'t find wallet! Creating...')
|
||||
return createWallet()
|
||||
}
|
||||
case -21:
|
||||
throw new Error('Wallet already exists!')
|
||||
case -22:
|
||||
try {
|
||||
return openWalletWithPassword()
|
||||
} catch {
|
||||
throw new Error('Invalid wallet password!')
|
||||
}
|
||||
case -17:
|
||||
throw new E.InsufficientFundsError()
|
||||
case -37:
|
||||
throw new E.InsufficientFundsError()
|
||||
default:
|
||||
throw new Error(
|
||||
_.join(' ', [
|
||||
`json-rpc::${method} error:`,
|
||||
JSON.stringify(_.get('message', error, '')),
|
||||
JSON.stringify(_.get('response.data.error', error, ''))
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function openWallet () {
|
||||
return fetch('open_wallet', { filename: 'Wallet' })
|
||||
.catch(() => openWalletWithPassword())
|
||||
}
|
||||
|
||||
function openWalletWithPassword () {
|
||||
return fetch('open_wallet', { filename: 'Wallet', password: rpcConfig().password })
|
||||
}
|
||||
|
||||
function createWallet () {
|
||||
return fetch('create_wallet', { filename: 'Wallet', language: 'English' })
|
||||
.then(() => new Promise(() => setTimeout(() => openWallet(), 3000)))
|
||||
.then(() => fetch('auto_refresh'))
|
||||
}
|
||||
|
||||
function checkCryptoCode (cryptoCode) {
|
||||
if (cryptoCode !== 'XMR') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function refreshWallet () {
|
||||
return fetch('refresh')
|
||||
.catch(err => handleError(err, 'refreshWallet'))
|
||||
}
|
||||
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => refreshWallet())
|
||||
.then(() => fetch('get_balance', { account_index: 0, address_indices: [0] }))
|
||||
.then(res => {
|
||||
return BN(res.unlocked_balance).decimalPlaces(0)
|
||||
})
|
||||
.catch(err => handleError(err, 'accountBalance'))
|
||||
}
|
||||
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => refreshWallet())
|
||||
.then(() => fetch('transfer_split', {
|
||||
destinations: [{ amount: cryptoAtoms, address: toAddress }],
|
||||
account_index: 0,
|
||||
subaddr_indices: [],
|
||||
priority: 0,
|
||||
mixin: 6,
|
||||
ring_size: 7,
|
||||
unlock_time: 0,
|
||||
get_tx_hex: false,
|
||||
new_algorithm: false,
|
||||
get_tx_metadata: false
|
||||
}))
|
||||
.then(res => ({
|
||||
fee: BN(res.fee_list[0]).abs(),
|
||||
txid: res.tx_hash_list[0]
|
||||
}))
|
||||
.catch(err => handleError(err, 'sendCoins'))
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
return checkCryptoCode(info.cryptoCode)
|
||||
.then(() => fetch('create_address', { account_index: 0 }))
|
||||
.then(res => res.address)
|
||||
.catch(err => handleError(err, 'newAddress'))
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => refreshWallet())
|
||||
.then(() => fetch('get_address_index', { address: toAddress }))
|
||||
.then(addressRes => fetch('get_transfers', { in: true, pool: true, account_index: addressRes.index.major, subaddr_indices: [addressRes.index.minor] }))
|
||||
.then(transferRes => {
|
||||
const confirmedToAddress = _.filter(it => it.address === toAddress, transferRes.in ?? [])
|
||||
const pendingToAddress = _.filter(it => it.address === toAddress, transferRes.pool ?? [])
|
||||
const confirmed = _.reduce((acc, value) => acc.plus(value.amount), BN(0), confirmedToAddress)
|
||||
const pending = _.reduce((acc, value) => acc.plus(value.amount), BN(0), pendingToAddress)
|
||||
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
.catch(err => handleError(err, 'getStatus'))
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => refreshWallet())
|
||||
.then(() => Promise.all([
|
||||
fetch('get_balance', { account_index: 0, address_indices: [0] }),
|
||||
fetch('create_address', { account_index: 0 }),
|
||||
fetch('get_transfers', { pool: true, account_index: 0 })
|
||||
]))
|
||||
.then(([balanceRes, addressRes, transferRes]) => {
|
||||
const memPoolBalance = _.reduce((acc, value) => acc.plus(value.amount), BN(0), transferRes.pool)
|
||||
return {
|
||||
fundingPendingBalance: BN(balanceRes.balance).minus(balanceRes.unlocked_balance).plus(memPoolBalance),
|
||||
fundingConfirmedBalance: BN(balanceRes.unlocked_balance),
|
||||
fundingAddress: addressRes.address
|
||||
}
|
||||
})
|
||||
.catch(err => handleError(err, 'newFunding'))
|
||||
}
|
||||
|
||||
function cryptoNetwork (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
switch(parseInt(rpcConfig().port, 10)) {
|
||||
case 18082:
|
||||
return 'main'
|
||||
case 28082:
|
||||
return 'test'
|
||||
case 38083:
|
||||
return 'stage'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
try {
|
||||
const config = jsonRpc.parseConf(configPath)
|
||||
|
||||
// Daemon uses a different connection of the wallet
|
||||
const rpcConfig = {
|
||||
username: config['rpc-login'].split(':')[0],
|
||||
password: config['rpc-login'].split(':')[1],
|
||||
port: cryptoRec.defaultPort
|
||||
}
|
||||
|
||||
return jsonRpc.fetchDigest(rpcConfig, 'get_info')
|
||||
.then(res => !!res.synchronized ? 'ready' : 'syncing')
|
||||
} catch (err) {
|
||||
throw new Error('XMR daemon is currently not installed')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
checkCryptoCode(cryptoCode)
|
||||
.then(() => refreshWallet())
|
||||
.then(() => fetch('get_address_index', { address: address }))
|
||||
.then(addressRes => fetch('get_transfers', { in: true, pool: true, pending: true, account_index: addressRes.index.major, subaddr_indices: [addressRes.index.minor] }))
|
||||
.then(_.map(({ txid }) => txid))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
cryptoNetwork,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
const https = require('https')
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const SUPPORTED_COINS = ['BTC']
|
||||
|
||||
const axios = require('axios').create({
|
||||
// TODO: get rejectUnauthorized true to work
|
||||
baseURL: `${process.env.WALLET_URL}/api`,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
})
|
||||
|
||||
const checkCryptoCode = (cryptoCode) => !_.includes(cryptoCode, SUPPORTED_COINS)
|
||||
? Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
: Promise.resolve()
|
||||
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
return axios.post('/balance', {
|
||||
cryptoCode,
|
||||
config: settings.config,
|
||||
operatorId
|
||||
})
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.error) throw new Error(JSON.stringify(data.error))
|
||||
return new BN(data.balance)
|
||||
})
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId) {
|
||||
const { cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
return axios.post('/sendCoins', {
|
||||
tx,
|
||||
config: settings.config,
|
||||
operatorId
|
||||
})
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.error && data.error.errorCode === 'sc-001') throw new E.InsufficientFundsError()
|
||||
else if (data.error) throw new Error(JSON.stringify(data.error))
|
||||
const fee = new BN(data.fee).decimalPlaces(0)
|
||||
const txid = data.txid
|
||||
return { txid, fee }
|
||||
})
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
return checkCryptoCode(info.cryptoCode)
|
||||
.then(() => axios.post('/newAddress', {
|
||||
info,
|
||||
tx,
|
||||
config: settings.config,
|
||||
operatorId
|
||||
}))
|
||||
.then(({ data }) => {
|
||||
if(data.error) throw new Error(JSON.stringify(data.error))
|
||||
return data.newAddress
|
||||
})
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
return checkCryptoCode(tx.cryptoCode)
|
||||
.then(() => axios.get(`/balance/${tx.toAddress}?cryptoCode=${tx.cryptoCode}`))
|
||||
.then(({ data }) => {
|
||||
if (data.error) throw new Error(JSON.stringify(data.error))
|
||||
const confirmed = new BN(data.confirmedBalance)
|
||||
const pending = new BN(data.pendingBalance)
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
throw new E.NotImplementedError()
|
||||
}
|
||||
|
||||
function sweep (account, txId, cryptoCode, hdIndex, settings, operatorId) {
|
||||
throw new E.NotImplementedError()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
newFunding,
|
||||
getStatus,
|
||||
sweep,
|
||||
supportsHd: true,
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pRetry = require('p-retry')
|
||||
const jsonRpc = require('../../common/json-rpc')
|
||||
|
||||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
|
||||
const BN = require('../../../bn')
|
||||
const E = require('../../../error')
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency('ZEC')
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
||||
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
|
||||
|
||||
function fetch (method, params) {
|
||||
return jsonRpc.fetch(rpcConfig, method, params)
|
||||
}
|
||||
|
||||
function errorHandle (e) {
|
||||
const err = JSON.parse(e.message)
|
||||
switch (err.code) {
|
||||
case -6:
|
||||
throw new E.InsufficientFundsError()
|
||||
default:
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function checkCryptoCode (cryptoCode) {
|
||||
if (cryptoCode !== 'ZEC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
function accountBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function accountUnconfirmedBalance (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getwalletinfo'))
|
||||
.then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
// We want a balance that includes all spends (0 conf) but only deposits that
|
||||
// have at least 1 confirmation. getbalance does this for us automatically.
|
||||
function balance (account, cryptoCode, settings, operatorId) {
|
||||
return accountBalance(cryptoCode)
|
||||
}
|
||||
|
||||
function sendCoins (account, tx, settings, operatorId) {
|
||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||
const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8)
|
||||
const checkSendStatus = function (opid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('z_getoperationstatus', [[opid]])
|
||||
.then(res => {
|
||||
const status = _.get('status', res[0])
|
||||
switch (status) {
|
||||
case 'success':
|
||||
resolve(res[0])
|
||||
break
|
||||
case 'failed':
|
||||
throw new pRetry.AbortError(res[0].error)
|
||||
case 'executing':
|
||||
reject(new Error('operation still executing'))
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const checker = opid => pRetry(() => checkSendStatus(opid), { retries: 20, minTimeout: 300, factor: 1.05 })
|
||||
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('z_sendmany', ['ANY_TADDR', [{ address: toAddress, amount: coins }], null, null, 'NoPrivacy']))
|
||||
.then(checker)
|
||||
.then((res) => {
|
||||
return {
|
||||
fee: _.get('params.fee', res),
|
||||
txid: _.get('result.txid', res)
|
||||
}
|
||||
})
|
||||
.then((pickedObj) => {
|
||||
return {
|
||||
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
|
||||
txid: pickedObj.txid
|
||||
}
|
||||
})
|
||||
.catch(errorHandle)
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
return checkCryptoCode(info.cryptoCode)
|
||||
.then(() => fetch('getnewaddress'))
|
||||
}
|
||||
|
||||
function addressBalance (address, confs) {
|
||||
return fetch('getreceivedbyaddress', [address, confs])
|
||||
.then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0))
|
||||
}
|
||||
|
||||
function confirmedBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 1))
|
||||
}
|
||||
|
||||
function pendingBalance (address, cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => addressBalance(address, 0))
|
||||
}
|
||||
|
||||
function getStatus (account, tx, requested, settings, operatorId) {
|
||||
const { toAddress, cryptoCode } = tx
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => confirmedBalance(toAddress, cryptoCode))
|
||||
.then(confirmed => {
|
||||
if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }
|
||||
|
||||
return pendingBalance(toAddress, cryptoCode)
|
||||
.then(pending => {
|
||||
if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
|
||||
if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
|
||||
return { receivedCryptoAtoms: pending, status: 'notSeen' }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function newFunding (account, cryptoCode, settings, operatorId) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => {
|
||||
const promises = [
|
||||
accountUnconfirmedBalance(cryptoCode),
|
||||
accountBalance(cryptoCode),
|
||||
newAddress(account, { cryptoCode })
|
||||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
})
|
||||
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
|
||||
fundingPendingBalance,
|
||||
fundingConfirmedBalance,
|
||||
fundingAddress
|
||||
}))
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (cryptoCode) {
|
||||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getblockchaininfo'))
|
||||
.then(res => !!res['initial_block_download_complete'] ? 'ready' : 'syncing')
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getaddresstxids', [address]))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
const qs = require('querystring')
|
||||
const axios = require('axios')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const { fetchRBF } = require('../../wallet/bitcoind/bitcoind')
|
||||
module.exports = { authorize }
|
||||
|
||||
function highConfidence (confidence, txref, txRBF) {
|
||||
if (txref.double_spend) return 0
|
||||
if (txRBF) return 0
|
||||
if (txref.confirmations > 0 || txref.confidence * 100 >= confidence) return txref.value
|
||||
return 0
|
||||
}
|
||||
|
||||
function authorize (account, toAddress, cryptoAtoms, cryptoCode, isBitcoindAvailable) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
|
||||
|
||||
const query = qs.stringify({
|
||||
token: account.token,
|
||||
includeConfidence: true
|
||||
})
|
||||
|
||||
const confidence = account.confidenceFactor
|
||||
const isRBFEnabled = account.rbf
|
||||
const url = `https://api.blockcypher.com/v1/btc/main/addrs/${toAddress}?${query}`
|
||||
|
||||
return axios.get(url)
|
||||
.then(r => {
|
||||
const data = r.data
|
||||
if (isBitcoindAvailable && isRBFEnabled && data.unconfirmed_txrefs) {
|
||||
const promises = _.map(unconfirmedTxref => fetchRBF(unconfirmedTxref.tx_hash), data.unconfirmed_txrefs)
|
||||
return Promise.all(promises)
|
||||
.then(values => {
|
||||
const unconfirmedTxsRBF = _.fromPairs(values)
|
||||
const sumTxRefs = txrefs => _.sumBy(txref => highConfidence(confidence, txref, unconfirmedTxsRBF[txref.tx_hash]), txrefs)
|
||||
const authorizedValue = sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs)
|
||||
return cryptoAtoms.lte(authorizedValue)
|
||||
})
|
||||
}
|
||||
|
||||
const sumTxRefs = txrefs => _.sumBy(txref => highConfidence(confidence, txref), txrefs)
|
||||
const authorizedValue = sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs)
|
||||
return cryptoAtoms.lte(authorizedValue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
module.exports = {authorize}
|
||||
|
||||
function authorize (account, toAddress, cryptoAtoms, cryptoCode) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
|
||||
|
||||
const isAuthorized = false
|
||||
return isAuthorized
|
||||
})
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const db = require('./db')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
function getInsertQuery (tableName, fields) {
|
||||
// outputs string like: '$1, $2, $3...' with proper No of items
|
||||
const placeholders = fields.map(function (_, i) {
|
||||
return '$' + (i + 1)
|
||||
}).join(', ')
|
||||
|
||||
const query = 'INSERT INTO ' + tableName +
|
||||
' (' + fields.join(', ') + ')' +
|
||||
' VALUES' +
|
||||
' (' + placeholders + ')'
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) {
|
||||
const sql = 'INSERT INTO device_events (device_id, event_type, ' +
|
||||
'note, device_time) VALUES ($1, $2, $3, $4)'
|
||||
const values = [deviceId, event.eventType, event.note,
|
||||
event.deviceTime]
|
||||
|
||||
return db.none(sql, values)
|
||||
}
|
||||
|
||||
exports.cassetteCounts = function cassetteCounts (deviceId) {
|
||||
const sql = 'SELECT cassette1, cassette2, cassette3, cassette4, number_of_cassettes FROM devices ' +
|
||||
'WHERE device_id=$1'
|
||||
|
||||
return db.one(sql, [deviceId])
|
||||
.then(row => {
|
||||
const counts = []
|
||||
_.forEach(it => {
|
||||
counts.push(row[`cassette${it + 1}`])
|
||||
}, _.times(_.identity(), row.number_of_cassettes))
|
||||
|
||||
return { numberOfCassettes: row.number_of_cassettes, counts }
|
||||
})
|
||||
}
|
||||
|
||||
exports.recyclerCounts = function recyclerCounts (deviceId) {
|
||||
const sql = 'SELECT recycler1, recycler2, recycler3, recycler4, recycler5, recycler6, number_of_recyclers FROM devices ' +
|
||||
'WHERE device_id=$1'
|
||||
|
||||
return db.one(sql, [deviceId])
|
||||
.then(row => {
|
||||
const counts = []
|
||||
_.forEach(it => {
|
||||
counts.push(row[`recycler${it + 1}`])
|
||||
}, _.times(_.identity(), row.number_of_recyclers))
|
||||
|
||||
return { numberOfRecyclers: row.number_of_recyclers, counts }
|
||||
})
|
||||
}
|
||||
|
||||
// Note: since we only prune on insert, we'll always have
|
||||
// last known state.
|
||||
exports.machineEvent = function machineEvent (rec) {
|
||||
const fields = ['id', 'device_id', 'event_type', 'note', 'device_time']
|
||||
const sql = getInsertQuery('machine_events', fields)
|
||||
const values = [rec.id, rec.deviceId, rec.eventType, rec.note, rec.deviceTime]
|
||||
|
||||
const deleteSql = `delete from machine_events
|
||||
where created < now() - interval '1 days'`
|
||||
|
||||
return db.none(sql, values)
|
||||
.then(() => db.none(deleteSql))
|
||||
}
|
||||
|
||||
exports.machineEventsByIdBatch = function machineEventsByIdBatch (machineIds) {
|
||||
const formattedIds = _.map(pgp.as.text, machineIds).join(',')
|
||||
const sql = `SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events WHERE device_id IN ($1^) ORDER BY age ASC LIMIT 1`
|
||||
return db.any(sql, [formattedIds]).then(res => {
|
||||
const events = _.map(_.mapKeys(_.camelCase))(res)
|
||||
const eventMap = _.groupBy('deviceId', events)
|
||||
return machineIds.map(id => _.prop([0], eventMap[id]))
|
||||
})
|
||||
}
|
||||
|
||||
exports.machineEvents = function machineEvents () {
|
||||
const sql = 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events'
|
||||
|
||||
return db.any(sql, [])
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const semver = require('semver')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const compliance = require('../compliance')
|
||||
const complianceTriggers = require('../compliance-triggers')
|
||||
const configManager = require('../new-config-manager')
|
||||
const { get, add, getById, update } = require('../customers')
|
||||
const httpError = require('../route-helpers').httpError
|
||||
const plugins = require('../plugins')
|
||||
const Tx = require('../tx')
|
||||
const respond = require('../respond')
|
||||
const loyalty = require('../loyalty')
|
||||
|
||||
function addOrUpdateCustomer (req) {
|
||||
const customerData = req.body
|
||||
const machineVersion = req.query.version
|
||||
const triggers = configManager.getTriggers(req.settings.config)
|
||||
const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
|
||||
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
|
||||
|
||||
return get(customerData.phone)
|
||||
.then(customer => {
|
||||
if (customer) return customer
|
||||
|
||||
return add(req.body)
|
||||
})
|
||||
.then(customer => getById(customer.id))
|
||||
.then(customer => {
|
||||
// BACKWARDS_COMPATIBILITY 7.5
|
||||
// machines before 7.5 expect customer with sanctions result
|
||||
const isOlderMachineVersion = !machineVersion || semver.lt(machineVersion, '7.5.0-beta.0')
|
||||
const shouldRunOfacCompat = !compatTriggers.sanctions && isOlderMachineVersion
|
||||
if (!shouldRunOfacCompat) return customer
|
||||
|
||||
return compliance.validationPatch(req.deviceId, !!compatTriggers.sanctions, customer)
|
||||
.then(patch => {
|
||||
if (_.isEmpty(patch)) return customer
|
||||
return update(customer.id, patch)
|
||||
})
|
||||
})
|
||||
.then(customer => {
|
||||
return Tx.customerHistory(customer.id, maxDaysThreshold)
|
||||
.then(result => {
|
||||
customer.txHistory = result
|
||||
return customer
|
||||
})
|
||||
})
|
||||
.then(customer => {
|
||||
return loyalty.getCustomerActiveIndividualDiscount(customer.id)
|
||||
.then(discount => ({ ...customer, discount }))
|
||||
})
|
||||
}
|
||||
|
||||
function getCustomerWithPhoneCode (req, res, next) {
|
||||
const pi = plugins(req.settings, req.deviceId)
|
||||
const phone = req.body.phone
|
||||
|
||||
return pi.getPhoneCode(phone)
|
||||
.then(code => {
|
||||
return addOrUpdateCustomer(req)
|
||||
.then(customer => respond(req, res, { code, customer }))
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
|
||||
throw err
|
||||
})
|
||||
.catch(next)
|
||||
}
|
||||
|
||||
router.post('/', getCustomerWithPhoneCode)
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
const express = require('express')
|
||||
const nmd = require('nano-markdown')
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
const complianceTriggers = require('../compliance-triggers')
|
||||
const configManager = require('../new-config-manager')
|
||||
const plugins = require('../plugins')
|
||||
const semver = require('semver')
|
||||
const state = require('../middlewares/state')
|
||||
const version = require('../../package.json').version
|
||||
const { batchGetCustomInfoRequest, getCustomInfoRequests } = require('../new-admin/services/customInfoRequests')
|
||||
|
||||
const urlsToPing = [
|
||||
`us.archive.ubuntu.com`,
|
||||
`uk.archive.ubuntu.com`,
|
||||
`za.archive.ubuntu.com`,
|
||||
`cn.archive.ubuntu.com`
|
||||
]
|
||||
|
||||
const speedtestFiles = [
|
||||
{
|
||||
url: 'https://github.com/lamassu/speed-test-assets/raw/main/python-defaults_2.7.18-3.tar.gz',
|
||||
size: 44668
|
||||
}
|
||||
]
|
||||
|
||||
function checkHasLightning (settings) {
|
||||
return configManager.getWalletSettings('BTC', settings.config).layer2 !== 'no-layer2'
|
||||
}
|
||||
|
||||
const createTerms = terms => (terms.active && terms.text) ? ({
|
||||
active: terms.active,
|
||||
title: terms.title,
|
||||
text: nmd(terms.text),
|
||||
accept: terms.acceptButtonText,
|
||||
cancel: terms.cancelButtonText
|
||||
}) : null
|
||||
|
||||
const buildTriggers = (allTriggers) => {
|
||||
const normalTriggers = []
|
||||
const customTriggers = _.filter(o => {
|
||||
if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o)
|
||||
return !_.isNil(o.customInfoRequestId) && !_.isEmpty(o.customInfoRequestId)
|
||||
}, allTriggers)
|
||||
|
||||
return _.flow([_.map(_.get('customInfoRequestId')), batchGetCustomInfoRequest])(customTriggers)
|
||||
.then(res => {
|
||||
res.forEach((details, index) => {
|
||||
// make sure we aren't attaching the details to the wrong trigger
|
||||
if (customTriggers[index].customInfoRequestId !== details.id) return
|
||||
customTriggers[index] = { ...customTriggers[index], customInfoRequest: details }
|
||||
})
|
||||
return [...normalTriggers, ...customTriggers]
|
||||
})
|
||||
}
|
||||
|
||||
function poll (req, res, next) {
|
||||
const machineVersion = req.query.version
|
||||
const machineModel = req.query.model
|
||||
const deviceId = req.deviceId
|
||||
const deviceTime = req.deviceTime
|
||||
const pid = req.query.pid
|
||||
const settings = req.settings
|
||||
const operatorId = res.locals.operatorId
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const zeroConfLimits = _.reduce((acc, cryptoCode) => {
|
||||
acc[cryptoCode] = configManager.getWalletSettings(cryptoCode, settings.config).zeroConfLimit
|
||||
return acc
|
||||
}, {}, localeConfig.cryptoCurrencies)
|
||||
const pi = plugins(settings, deviceId)
|
||||
const hasLightning = checkHasLightning(settings)
|
||||
|
||||
const operatorInfo = configManager.getOperatorInfo(settings.config)
|
||||
const machineInfo = { deviceId: req.deviceId, deviceName: req.deviceName }
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, settings.config)
|
||||
const receipt = configManager.getReceipt(settings.config)
|
||||
const terms = configManager.getTermsConditions(settings.config)
|
||||
const enablePaperWalletOnly = configManager.getCompliance(settings.config).enablePaperWalletOnly
|
||||
|
||||
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
||||
|
||||
// BACKWARDS_COMPATIBILITY 8.1
|
||||
// Machines after 8.1 only need the server version from the initial polling request.
|
||||
if (semver.gte(machineVersion, '8.1.0-beta.0'))
|
||||
return res.json({ version })
|
||||
|
||||
return Promise.all([
|
||||
pi.recordPing(deviceTime, machineVersion, machineModel),
|
||||
pi.pollQueries(),
|
||||
buildTriggers(configManager.getTriggers(settings.config)),
|
||||
configManager.getTriggersAutomation(getCustomInfoRequests(true), settings.config, true),
|
||||
])
|
||||
.then(([_pingRes, results, triggers, triggersAutomation]) => {
|
||||
const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid
|
||||
const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid
|
||||
const restartServices = pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid
|
||||
const emptyUnit = pid && state.emptyUnit?.[operatorId]?.[deviceId] === pid
|
||||
const refillUnit = pid && state.refillUnit?.[operatorId]?.[deviceId] === pid
|
||||
const langs = localeConfig.languages
|
||||
|
||||
const locale = {
|
||||
fiatCode: localeConfig.fiatCurrency,
|
||||
localeInfo: {
|
||||
primaryLocale: langs[0],
|
||||
primaryLocales: langs,
|
||||
country: localeConfig.country
|
||||
}
|
||||
}
|
||||
|
||||
const response = {
|
||||
error: null,
|
||||
locale,
|
||||
version,
|
||||
receiptPrintingActive: receipt.active,
|
||||
automaticReceiptPrint: receipt.automaticPrint,
|
||||
smsReceiptActive: receipt.sms,
|
||||
enablePaperWalletOnly,
|
||||
twoWayMode: cashOutConfig.active,
|
||||
zeroConfLimits,
|
||||
reboot,
|
||||
shutdown,
|
||||
restartServices,
|
||||
emptyUnit,
|
||||
refillUnit,
|
||||
hasLightning,
|
||||
receipt,
|
||||
operatorInfo,
|
||||
machineInfo,
|
||||
triggers,
|
||||
triggersAutomation,
|
||||
speedtestFiles,
|
||||
urlsToPing
|
||||
}
|
||||
|
||||
// BACKWARDS_COMPATIBILITY 7.6
|
||||
// Machines before 7.6 expect a single zeroConfLimit value per machine.
|
||||
if (!semver.gte(machineVersion, '7.6.0-beta.0'))
|
||||
response.zeroConfLimit = _.min(_.values(zeroConfLimits))
|
||||
|
||||
// BACKWARDS_COMPATIBILITY 7.5
|
||||
// machines before 7.5 expect old compliance
|
||||
if (!machineVersion || semver.lt(machineVersion, '7.5.0-beta.0')) {
|
||||
const compatTriggers = complianceTriggers.getBackwardsCompatibleTriggers(triggers)
|
||||
response.smsVerificationActive = !!compatTriggers.sms
|
||||
response.smsVerificationThreshold = compatTriggers.sms
|
||||
response.idCardDataVerificationActive = !!compatTriggers.idCardData
|
||||
response.idCardDataVerificationThreshold = compatTriggers.idCardData
|
||||
response.idCardPhotoVerificationActive = !!compatTriggers.idCardPhoto
|
||||
response.idCardPhotoVerificationThreshold = compatTriggers.idCardPhoto
|
||||
response.sanctionsVerificationActive = !!compatTriggers.sanctions
|
||||
response.sanctionsVerificationThreshold = compatTriggers.sanctions
|
||||
response.frontCameraVerificationActive = !!compatTriggers.facephoto
|
||||
response.frontCameraVerificationThreshold = compatTriggers.facephoto
|
||||
}
|
||||
|
||||
// BACKWARDS_COMPATIBILITY 7.4.9
|
||||
// machines before 7.4.9 expect t&c on poll
|
||||
if (!machineVersion || semver.lt(machineVersion, '7.4.9')) {
|
||||
response.terms = createTerms(terms)
|
||||
}
|
||||
|
||||
return res.json(_.assign(response, results))
|
||||
})
|
||||
.catch(next)
|
||||
}
|
||||
|
||||
router.get('/', poll)
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
const express = require('express')
|
||||
const router = express.Router()
|
||||
|
||||
const plugins = require('../plugins')
|
||||
const settingsLoader = require('../new-settings-loader')
|
||||
|
||||
function probe (req, res, next) {
|
||||
// TODO: why req.settings is undefined?
|
||||
settingsLoader.loadLatest()
|
||||
.then(settings => {
|
||||
const pi = plugins(settings, req.deviceId)
|
||||
return pi.probeLN('LN', req.body.address)
|
||||
.then(r => res.status(200).send({ hardLimits: r }))
|
||||
.catch(next)
|
||||
})
|
||||
}
|
||||
|
||||
router.get('/', probe)
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const ofac = require('./ofac')
|
||||
const T = require('./time')
|
||||
const logger = require('./logger')
|
||||
const customers = require('./customers')
|
||||
|
||||
const sanctionStatus = {
|
||||
loaded: false,
|
||||
timestamp: null
|
||||
}
|
||||
|
||||
const loadOrUpdateSanctions = () => {
|
||||
if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) {
|
||||
logger.info('No sanction lists loaded. Loading sanctions...')
|
||||
return ofac.load()
|
||||
.then(() => {
|
||||
logger.info('OFAC sanction list loaded!')
|
||||
sanctionStatus.loaded = true
|
||||
sanctionStatus.timestamp = Date.now()
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Couldn\'t load OFAC sanction list!', e)
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const checkByUser = (customerId, userToken) => {
|
||||
return Promise.all([loadOrUpdateSanctions(), customers.getCustomerById(customerId)])
|
||||
.then(([, customer]) => {
|
||||
const { firstName, lastName, dateOfBirth } = customer?.idCardData
|
||||
const birthdate = _.replace(/-/g, '')(dateOfBirth)
|
||||
const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false })
|
||||
const isOfacSanctioned = _.size(ofacMatches) > 0
|
||||
customers.updateCustomer(customerId, { sanctions: !isOfacSanctioned }, userToken)
|
||||
|
||||
return { ofacSanctioned: isOfacSanctioned }
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkByUser
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const txBatching = require('./tx-batching')
|
||||
const wallet = require('./wallet')
|
||||
|
||||
function submitBatch (settings, batch) {
|
||||
txBatching.getBatchTransactions(batch)
|
||||
.then(txs => {
|
||||
if (_.isEmpty(txs)) return Promise.resolve()
|
||||
return wallet.sendCoinsBatch(settings, txs, batch.crypto_code)
|
||||
.then(res => txBatching.confirmSentBatch(batch, res))
|
||||
.catch(err => txBatching.setErroredBatch(batch, err.message))
|
||||
})
|
||||
}
|
||||
|
||||
function processBatches (settings, lifecycle) {
|
||||
return txBatching.getBatchesByStatus(['open'])
|
||||
.then(batches => {
|
||||
_.each(batch => {
|
||||
const elapsedMS = batch.time_elapsed * 1000
|
||||
|
||||
if (elapsedMS >= lifecycle) {
|
||||
return txBatching.closeTransactionBatch(batch)
|
||||
.then(() => submitBatch(settings, batch))
|
||||
}
|
||||
}, batches)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = processBatches
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
const ph = require('./plugin-helper')
|
||||
const argv = require('minimist')(process.argv.slice(2))
|
||||
|
||||
// TODO - This function should be rolled back after UI is created for this feature
|
||||
function loadWalletScoring (settings) {
|
||||
if (argv.mockScoring) {
|
||||
const mock = ph.load(ph.WALLET_SCORING, 'mock-scoring')
|
||||
return { plugin: mock, account: {} }
|
||||
}
|
||||
|
||||
const scorechainAccount = settings.accounts['scorechain']
|
||||
if (scorechainAccount?.enabled) {
|
||||
const scorechain = ph.load(ph.WALLET_SCORING, 'scorechain')
|
||||
return { plugin: scorechain, account: scorechainAccount}
|
||||
}
|
||||
|
||||
const ellipticAccount = settings.accounts['elliptic']
|
||||
const elliptic = ph.load(ph.WALLET_SCORING, 'elliptic')
|
||||
|
||||
return { plugin: elliptic, account: ellipticAccount }
|
||||
}
|
||||
|
||||
function rateTransaction (settings, cryptoCode, address) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const { plugin, account } = loadWalletScoring(settings)
|
||||
|
||||
return plugin.rateAddress(account, cryptoCode, address)
|
||||
})
|
||||
}
|
||||
|
||||
function rateAddress (settings, cryptoCode, address) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const { plugin, account } = loadWalletScoring(settings)
|
||||
|
||||
return plugin.rateAddress(account, cryptoCode, address)
|
||||
})
|
||||
}
|
||||
|
||||
function isWalletScoringEnabled (settings, cryptoCode) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const { plugin, account } = loadWalletScoring(settings)
|
||||
|
||||
return plugin.isWalletScoringEnabled(account, cryptoCode)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
rateAddress,
|
||||
rateTransaction,
|
||||
isWalletScoringEnabled
|
||||
}
|
||||
311
lib/wallet.js
311
lib/wallet.js
|
|
@ -1,311 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
const mem = require('mem')
|
||||
const hkdf = require('futoin-hkdf')
|
||||
|
||||
const configManager = require('./new-config-manager')
|
||||
const pify = require('pify')
|
||||
const fs = pify(require('fs'))
|
||||
|
||||
const mnemonicHelpers = require('./mnemonic-helpers')
|
||||
const ph = require('./plugin-helper')
|
||||
const layer2 = require('./layer2')
|
||||
const httpError = require('./route-helpers').httpError
|
||||
const logger = require('./logger')
|
||||
const { getOpenBatchCryptoValue } = require('./tx-batching')
|
||||
const BN = require('./bn')
|
||||
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('./constants')
|
||||
|
||||
const FETCH_INTERVAL = 5000
|
||||
const INSUFFICIENT_FUNDS_CODE = 570
|
||||
const INSUFFICIENT_FUNDS_NAME = 'InsufficientFunds'
|
||||
const ZERO_CONF_EXPIRATION = 60000
|
||||
|
||||
const MNEMONIC_PATH = process.env.MNEMONIC_PATH
|
||||
|
||||
function computeSeed (masterSeed) {
|
||||
return hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' })
|
||||
}
|
||||
|
||||
function computeOperatorId (masterSeed) {
|
||||
return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex')
|
||||
}
|
||||
|
||||
function fetchWallet (settings, cryptoCode) {
|
||||
return fs.readFile(MNEMONIC_PATH, 'utf8')
|
||||
.then(mnemonic => {
|
||||
const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic)
|
||||
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet
|
||||
const wallet = ph.load(ph.WALLET, plugin)
|
||||
const rawAccount = settings.accounts[plugin]
|
||||
const account = _.set('seed', computeSeed(masterSeed), rawAccount)
|
||||
const accountWithMnemonic = _.set('mnemonic', mnemonic, account)
|
||||
if (_.isFunction(wallet.run)) wallet.run(accountWithMnemonic)
|
||||
const operatorId = computeOperatorId(masterSeed)
|
||||
return { wallet, account: accountWithMnemonic, operatorId }
|
||||
})
|
||||
}
|
||||
|
||||
const lastBalance = {}
|
||||
|
||||
function _balance (settings, cryptoCode) {
|
||||
return fetchWallet(settings, cryptoCode)
|
||||
.then(r => r.wallet.balance(r.account, cryptoCode, settings, r.operatorId))
|
||||
.then(balance => Promise.all([balance, getOpenBatchCryptoValue(cryptoCode)]))
|
||||
.then(([balance, reservedBalance]) => ({ balance: BN(balance).minus(reservedBalance), reservedBalance, timestamp: Date.now() }))
|
||||
.then(r => {
|
||||
lastBalance[cryptoCode] = r
|
||||
return r
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error(err)
|
||||
return lastBalance[cryptoCode]
|
||||
})
|
||||
}
|
||||
|
||||
function probeLN (settings, cryptoCode, address) {
|
||||
return fetchWallet(settings, cryptoCode).then(r => {
|
||||
if (!r.wallet.probeLN) return null
|
||||
return r.wallet.probeLN(r.account, cryptoCode, address)
|
||||
})
|
||||
}
|
||||
|
||||
function sendCoins (settings, tx) {
|
||||
return fetchWallet(settings, tx.cryptoCode)
|
||||
.then(r => {
|
||||
const feeMultiplier = new BN(configManager.getWalletSettings(tx.cryptoCode, settings.config).feeMultiplier)
|
||||
return r.wallet.sendCoins(r.account, tx, settings, r.operatorId, feeMultiplier)
|
||||
.then(res => {
|
||||
mem.clear(module.exports.balance)
|
||||
return res
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === INSUFFICIENT_FUNDS_NAME) {
|
||||
throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE)
|
||||
}
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
function sendCoinsBatch (settings, txs, cryptoCode) {
|
||||
return fetchWallet(settings, cryptoCode)
|
||||
.then(r => {
|
||||
const feeMultiplier = new BN(configManager.getWalletSettings(cryptoCode, settings.config).feeMultiplier)
|
||||
return r.wallet.sendCoinsBatch(r.account, txs, cryptoCode, feeMultiplier)
|
||||
.then(res => {
|
||||
mem.clear(module.exports.balance)
|
||||
return res
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === INSUFFICIENT_FUNDS_NAME) {
|
||||
throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE)
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
function newAddress (settings, info, tx) {
|
||||
const walletAddressPromise = fetchWallet(settings, info.cryptoCode)
|
||||
.then(r => r.wallet.newAddress(r.account, info, tx, settings, r.operatorId))
|
||||
|
||||
return Promise.all([
|
||||
walletAddressPromise,
|
||||
layer2.newAddress(settings, info)
|
||||
])
|
||||
.then(([toAddress, layer2Address]) => ({
|
||||
toAddress,
|
||||
layer2Address
|
||||
}))
|
||||
}
|
||||
|
||||
function newFunding (settings, cryptoCode, address) {
|
||||
return fetchWallet(settings, cryptoCode)
|
||||
.then(r => {
|
||||
const wallet = r.wallet
|
||||
const account = r.account
|
||||
|
||||
return wallet.newFunding(account, cryptoCode, settings, r.operatorId)
|
||||
})
|
||||
}
|
||||
|
||||
function mergeStatus (a, b) {
|
||||
if (!a) return b
|
||||
if (!b) return a
|
||||
|
||||
return { receivedCryptoAtoms: a.receivedCryptoAtoms, status: mergeStatusMode(a.status, b.status) }
|
||||
}
|
||||
|
||||
function mergeStatusMode (a, b) {
|
||||
const cleared = ['authorized', 'confirmed', 'instant']
|
||||
if (_.includes(a, cleared)) return a
|
||||
if (_.includes(b, cleared)) return b
|
||||
|
||||
if (a === 'published') return a
|
||||
if (b === 'published') return b
|
||||
|
||||
if (a === 'rejected') return a
|
||||
if (b === 'rejected') return b
|
||||
|
||||
return 'notSeen'
|
||||
}
|
||||
|
||||
function getWalletStatus (settings, tx) {
|
||||
const fudgeFactorEnabled = configManager.getGlobalCashOut(settings.config).fudgeFactorActive
|
||||
const fudgeFactor = fudgeFactorEnabled ? 100 : 0
|
||||
const requested = tx.cryptoAtoms.minus(fudgeFactor)
|
||||
|
||||
const walletStatusPromise = fetchWallet(settings, tx.cryptoCode)
|
||||
.then(r => r.wallet.getStatus(r.account, tx, requested, settings, r.operatorId))
|
||||
|
||||
return Promise.all([
|
||||
walletStatusPromise,
|
||||
layer2.getStatus(settings, tx)
|
||||
])
|
||||
.then(([walletStatus, layer2Status]) => {
|
||||
return mergeStatus(walletStatus, layer2Status)
|
||||
})
|
||||
}
|
||||
|
||||
function authorizeZeroConf (settings, tx, machineId) {
|
||||
const walletSettings = configManager.getWalletSettings(tx.cryptoCode, settings.config)
|
||||
const isBitcoindAvailable = walletSettings.wallet === 'bitcoind'
|
||||
const plugin = walletSettings.zeroConf
|
||||
const zeroConfLimit = walletSettings.zeroConfLimit || 0
|
||||
|
||||
if (!_.isObject(tx.fiat)) {
|
||||
return Promise.reject(new Error('tx.fiat is undefined!'))
|
||||
}
|
||||
|
||||
// TODO: confirm if this treatment is needed for ERC-20 tokens, once their cash-out transactions are enabled
|
||||
if (tx.cryptoCode === 'ETH') {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
if (tx.fiat.gt(zeroConfLimit)) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
if (plugin === 'none') return Promise.resolve(true)
|
||||
|
||||
const zeroConf = ph.load(ph.ZERO_CONF, plugin)
|
||||
const account = settings.accounts[plugin]
|
||||
|
||||
return zeroConf.authorize(account, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode, isBitcoindAvailable)
|
||||
}
|
||||
|
||||
function getStatus (settings, tx, machineId) {
|
||||
return getWalletStatus(settings, tx)
|
||||
.then((statusRec) => {
|
||||
if (statusRec.status === 'authorized') {
|
||||
return authorizeZeroConf(settings, tx, machineId)
|
||||
.then(isAuthorized => {
|
||||
const publishAge = Date.now() - tx.publishedAt
|
||||
|
||||
const unauthorizedStatus = publishAge < ZERO_CONF_EXPIRATION
|
||||
? 'published'
|
||||
: 'rejected'
|
||||
|
||||
// Sanity check to confirm if there's any cryptoatoms for which to dispense bills
|
||||
const authorizedStatus = isAuthorized ? 'authorized' : unauthorizedStatus
|
||||
const status = BN(tx.cryptoAtoms).gt(0) ? authorizedStatus : 'rejected'
|
||||
|
||||
return { receivedCryptoAtoms: statusRec.receivedCryptoAtoms, status }
|
||||
})
|
||||
}
|
||||
|
||||
return statusRec
|
||||
})
|
||||
}
|
||||
|
||||
function sweep (settings, txId, cryptoCode, hdIndex) {
|
||||
return fetchWallet(settings, cryptoCode)
|
||||
.then(r => r.wallet.sweep(r.account, txId, cryptoCode, hdIndex, settings, r.operatorId))
|
||||
}
|
||||
|
||||
function isHd (settings, tx) {
|
||||
return fetchWallet(settings, tx.cryptoCode)
|
||||
.then(r => r.wallet.supportsHd)
|
||||
}
|
||||
|
||||
function cryptoNetwork (settings, cryptoCode) {
|
||||
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet
|
||||
const account = settings.accounts[plugin]
|
||||
return fetchWallet(settings, cryptoCode).then(r => {
|
||||
if (!r.wallet.cryptoNetwork) return Promise.resolve(false)
|
||||
return r.wallet.cryptoNetwork(account, cryptoCode, settings, r.operatorId)
|
||||
})
|
||||
}
|
||||
|
||||
function isStrictAddress (settings, cryptoCode, toAddress) {
|
||||
// Note: For now, only for wallets that specifically check for this.
|
||||
|
||||
return fetchWallet(settings, cryptoCode)
|
||||
.then(r => {
|
||||
if (!r.wallet.isStrictAddress) return true
|
||||
return r.wallet.isStrictAddress(cryptoCode, toAddress, settings, r.operatorId)
|
||||
})
|
||||
}
|
||||
|
||||
function supportsBatching (settings, cryptoCode) {
|
||||
return fetchWallet(settings, cryptoCode).then(r => {
|
||||
return Promise.resolve(!!r.wallet.SUPPORTS_BATCHING && !!configManager.getWalletSettings(cryptoCode, settings.config).allowTransactionBatching)
|
||||
})
|
||||
}
|
||||
|
||||
function checkBlockchainStatus (settings, cryptoCode) {
|
||||
const walletDaemons = {
|
||||
BTC: './plugins/wallet/bitcoind/bitcoind.js',
|
||||
BCH: './plugins/wallet/bitcoincashd/bitcoincashd.js',
|
||||
DASH: './plugins/wallet/dashd/dashd.js',
|
||||
ETH: './plugins/wallet/geth/base.js',
|
||||
LTC: './plugins/wallet/litecoind/litecoind.js',
|
||||
XMR: './plugins/wallet/monerod/monerod.js',
|
||||
ZEC: './plugins/wallet/zcashd/zcashd.js'
|
||||
}
|
||||
|
||||
return Promise.resolve(require(walletDaemons[cryptoCode]))
|
||||
.then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode))
|
||||
}
|
||||
|
||||
const balance = (settings, cryptoCode) => {
|
||||
return fetchWallet(settings, cryptoCode)
|
||||
.then(r => r.wallet.fetchSpeed ?? BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL)
|
||||
.then(multiplier => {
|
||||
switch (multiplier) {
|
||||
case BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL:
|
||||
return balanceNormal(settings, cryptoCode)
|
||||
case BALANCE_FETCH_SPEED_MULTIPLIER.SLOW:
|
||||
return balanceSlow(settings, cryptoCode)
|
||||
default:
|
||||
throw new Error()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const balanceNormal = mem(_balance, {
|
||||
maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL * FETCH_INTERVAL,
|
||||
cacheKey: (settings, cryptoCode) => cryptoCode
|
||||
})
|
||||
|
||||
const balanceSlow = mem(_balance, {
|
||||
maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW * FETCH_INTERVAL,
|
||||
cacheKey: (settings, cryptoCode) => cryptoCode
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
sendCoinsBatch,
|
||||
newAddress,
|
||||
getStatus,
|
||||
isStrictAddress,
|
||||
sweep,
|
||||
isHd,
|
||||
newFunding,
|
||||
cryptoNetwork,
|
||||
supportsBatching,
|
||||
checkBlockchainStatus,
|
||||
probeLN
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sqls = [
|
||||
'CREATE TABLE IF NOT EXISTS user_config ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'type text NOT NULL, ' +
|
||||
'data json NOT NULL ' +
|
||||
')',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS devices ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'fingerprint text NOT NULL UNIQUE, ' +
|
||||
'name text, ' +
|
||||
'authorized boolean, ' +
|
||||
'unpair boolean NOT NULL DEFAULT false' +
|
||||
')',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS pairing_tokens (' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'token text, ' +
|
||||
'created timestamp NOT NULL DEFAULT now() ' +
|
||||
')',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS users ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'userName text NOT NULL UNIQUE, ' +
|
||||
'salt text NOT NULL, ' +
|
||||
'pwdHash text NOT NULL ' +
|
||||
')'
|
||||
]
|
||||
|
||||
db.multi(sqls, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
const sql =
|
||||
['CREATE TABLE bills ( ' +
|
||||
'id uuid PRIMARY KEY, ' +
|
||||
'device_fingerprint text NOT NULL, ' +
|
||||
'denomination integer NOT NULL, ' +
|
||||
'currency_code text NOT NULL, ' +
|
||||
'satoshis integer NOT NULL, ' +
|
||||
'to_address text NOT NULL, ' +
|
||||
'session_id uuid NOT NULL, ' +
|
||||
'device_time bigint NOT NULL, ' +
|
||||
'created timestamp NOT NULL DEFAULT now() )']
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
db.multi(['CREATE TABLE IF NOT EXISTS machine_events ( ' +
|
||||
'id uuid PRIMARY KEY, ' +
|
||||
'device_fingerprint text NOT NULL, ' +
|
||||
'event_type text NOT NULL, ' +
|
||||
'note text, ' +
|
||||
'device_time bigint NOT NULL, ' +
|
||||
'created timestamp NOT NULL DEFAULT now() )'], next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
var db = require('./db')
|
||||
|
||||
function singleQuotify (item) { return '\'' + item + '\'' }
|
||||
|
||||
exports.up = function (next) {
|
||||
var stages = ['initial_request', 'partial_request', 'final_request',
|
||||
'partial_send', 'deposit', 'dispense_request', 'dispense']
|
||||
.map(singleQuotify).join(',')
|
||||
|
||||
var authorizations = ['timeout', 'machine', 'pending', 'rejected',
|
||||
'published', 'authorized', 'confirmed'].map(singleQuotify).join(',')
|
||||
|
||||
var sqls = [
|
||||
'CREATE TYPE transaction_stage AS ENUM (' + stages + ')',
|
||||
'CREATE TYPE transaction_authority AS ENUM (' + authorizations + ')',
|
||||
|
||||
'CREATE TABLE transactions ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'session_id uuid NOT NULL, ' +
|
||||
'device_fingerprint text, ' +
|
||||
'to_address text NOT NULL, ' +
|
||||
'satoshis integer NOT NULL DEFAULT 0, ' +
|
||||
'fiat integer NOT NULL DEFAULT 0, ' +
|
||||
'currency_code text NOT NULL, ' +
|
||||
'fee integer NOT NULL DEFAULT 0, ' +
|
||||
'incoming boolean NOT NULL, ' +
|
||||
'stage transaction_stage NOT NULL, ' +
|
||||
'authority transaction_authority NOT NULL, ' +
|
||||
'tx_hash text, ' +
|
||||
'error text, ' +
|
||||
'created timestamp NOT NULL DEFAULT now(), ' +
|
||||
'UNIQUE (session_id, to_address, stage, authority) ' +
|
||||
')',
|
||||
'CREATE INDEX ON transactions (session_id)',
|
||||
|
||||
'CREATE TABLE pending_transactions ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'device_fingerprint text NOT NULL, ' +
|
||||
'session_id uuid UNIQUE NOT NULL, ' +
|
||||
'incoming boolean NOT NULL, ' +
|
||||
'currency_code text NOT NULL, ' +
|
||||
'to_address text NOT NULL, ' +
|
||||
'satoshis integer NOT NULL, ' +
|
||||
'updated timestamp NOT NULL DEFAULT now() ' +
|
||||
')',
|
||||
|
||||
'CREATE TABLE dispenses ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'device_fingerprint text NOT NULL, ' +
|
||||
'transaction_id integer UNIQUE REFERENCES transactions(id), ' +
|
||||
'dispense1 integer NOT NULL, ' +
|
||||
'reject1 integer NOT NULL, ' +
|
||||
'count1 integer NOT NULL, ' +
|
||||
'dispense2 integer NOT NULL, ' +
|
||||
'reject2 integer NOT NULL, ' +
|
||||
'count2 integer NOT NULL, ' +
|
||||
'refill boolean NOT NULL, ' +
|
||||
'error text, ' +
|
||||
'created timestamp NOT NULL DEFAULT now() ' +
|
||||
')',
|
||||
'CREATE INDEX ON dispenses (device_fingerprint)'
|
||||
]
|
||||
|
||||
db.multi(sqls, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
db.multi(['CREATE TABLE IF NOT EXISTS machine_configs ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'device_fingerprint text NOT NULL, ' +
|
||||
'data json NOT NULL )'], next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
db.multi(['CREATE TABLE IF NOT EXISTS cached_responses ( ' +
|
||||
'id serial PRIMARY KEY, ' +
|
||||
'device_fingerprint text NOT NULL, ' +
|
||||
'session_id uuid NOT NULL, ' +
|
||||
'path text NOT NULL, ' +
|
||||
'method text NOT NULL, ' +
|
||||
'body json NOT NULL, ' +
|
||||
'created timestamptz NOT NULL DEFAULT now(), ' +
|
||||
'UNIQUE (device_fingerprint, session_id, path, method) ' +
|
||||
')'], next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
'ALTER TABLE machine_pings RENAME COLUMN created to updated'
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
"ALTER TABLE customers ADD COLUMN suspended_until timestamptz"
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
'ALTER TABLE user_tokens ADD COLUMN last_accessed timestamptz',
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
const _ = require('lodash/fp')
|
||||
var db = require('../lib/db')
|
||||
const settingsLoader = require('../lib/new-settings-loader')
|
||||
const configManager = require('../lib/new-config-manager')
|
||||
|
||||
exports.up = function (next) {
|
||||
return db.tx(async t => {
|
||||
const settingsPromise = settingsLoader.loadLatestConfig()
|
||||
const machinesPromise = t.any('SELECT device_id FROM devices')
|
||||
const [config, machines] = await Promise.all([settingsPromise, machinesPromise])
|
||||
const cryptoCodes = configManager.getCryptosFromWalletNamespace(config)
|
||||
|
||||
const deviceIds = _.map(_.get('device_id'))(machines)
|
||||
const getZeroConfLimit = _.compose(_.get('zeroConfLimit'), it => configManager.getCashOut(it, config))
|
||||
const zeroConfLimits = _.map(getZeroConfLimit)(deviceIds)
|
||||
|
||||
const configMin = _.min(zeroConfLimits)
|
||||
const smallerZeroConf = _.isFinite(configMin) ? Number(configMin) : 0
|
||||
|
||||
_.forEach(cryptoCode => {
|
||||
const walletConfig = configManager.getWalletSettings(cryptoCode, config)
|
||||
const zeroConfLimit = _.get('zeroConfLimit', walletConfig)
|
||||
|
||||
if (_.isNil(zeroConfLimit)) {
|
||||
config[`wallets_${cryptoCode}_zeroConfLimit`] = smallerZeroConf
|
||||
}
|
||||
}, cryptoCodes)
|
||||
|
||||
_.forEach(deviceId => {
|
||||
const key = `cashOut_${deviceId}_zeroConfLimit`
|
||||
if (_.has(key, config)) {
|
||||
config[key] = null
|
||||
}
|
||||
})(deviceIds)
|
||||
|
||||
return settingsLoader.migrationSaveConfig(config)
|
||||
})
|
||||
.then(() => next())
|
||||
.catch(err => next(err))
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
const { migrationSaveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||
|
||||
exports.up = function (next) {
|
||||
const newConfig = {
|
||||
cashIn_cashboxReset: 'Manual'
|
||||
}
|
||||
return loadLatest()
|
||||
.then(config => {
|
||||
return migrationSaveConfig(newConfig)
|
||||
.then(() => next())
|
||||
.catch(err => {
|
||||
if (err.message === 'lamassu-server is not configured') {
|
||||
return next()
|
||||
}
|
||||
console.log(err.message)
|
||||
return next(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
var db = require('./db')
|
||||
const pify = require('pify')
|
||||
const fs = pify(require('fs'))
|
||||
const hkdf = require('futoin-hkdf')
|
||||
|
||||
const state = require('../lib/middlewares/state')
|
||||
const mnemonicHelpers = require('../lib/mnemonic-helpers')
|
||||
|
||||
function computeOperatorId (masterSeed) {
|
||||
return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex')
|
||||
}
|
||||
|
||||
function getMnemonic () {
|
||||
if (state.mnemonic) return Promise.resolve(state.mnemonic)
|
||||
return fs.readFile(process.env.MNEMONIC_PATH, 'utf8').then(mnemonic => {
|
||||
state.mnemonic = mnemonic
|
||||
return mnemonic
|
||||
})
|
||||
}
|
||||
|
||||
function generateOperatorId () {
|
||||
return getMnemonic().then(mnemonic => {
|
||||
return computeOperatorId(mnemonicHelpers.toEntropyBuffer(mnemonic))
|
||||
}).catch(e => {
|
||||
console.error('Error while computing operator id\n' + e)
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
exports.up = function (next) {
|
||||
return generateOperatorId()
|
||||
.then(operatorId => {
|
||||
const sql = [
|
||||
`CREATE TABLE operator_ids (
|
||||
id serial PRIMARY KEY,
|
||||
operator_id TEXT NOT NULL,
|
||||
service TEXT NOT NULL
|
||||
)`,
|
||||
`INSERT INTO operator_ids (operator_id, service) VALUES ('${operatorId}','middleware')`,
|
||||
`INSERT INTO operator_ids (operator_id, service) VALUES ('${operatorId}','coinatmradar')`,
|
||||
`INSERT INTO operator_ids (operator_id, service) VALUES ('${operatorId}','authentication')`
|
||||
]
|
||||
db.multi(sql, next)
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
let sql = [
|
||||
'ALTER TABLE customers ADD COLUMN last_auth_attempt timestamptz'
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
db.multi(['ALTER TABLE customers ADD COLUMN last_used_machine TEXT REFERENCES devices (device_id)'], next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
db.multi(['ALTER TABLE customers DROP CONSTRAINT customers_last_used_machine_fkey;'], next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = next => db.multi([
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN fixed_fee numeric(14, 5) NOT NULL DEFAULT 0;'
|
||||
], next)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const { saveConfig } = require('../lib/new-settings-loader')
|
||||
|
||||
exports.up = next => saveConfig({ 'commissions_cashOutFixedFee': 0 })
|
||||
.then(next)
|
||||
.catch(next)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = next => db.multi([
|
||||
'DROP TABLE aggregated_machine_pings;',
|
||||
'DROP TABLE cash_in_refills;',
|
||||
'DROP TABLE cash_out_refills;',
|
||||
'DROP TABLE customer_compliance_persistence;',
|
||||
'DROP TABLE compliance_overrides_persistence;',
|
||||
'DROP TABLE server_events;',
|
||||
], next)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = next => db.multi([
|
||||
'ALTER TABLE bills ADD CONSTRAINT cash_in_txs_id FOREIGN KEY (cash_in_txs_id) REFERENCES cash_in_txs(id);',
|
||||
'CREATE INDEX bills_cash_in_txs_id_idx ON bills USING btree (cash_in_txs_id);',
|
||||
`CREATE INDEX bills_null_cashbox_batch_id_idx ON bills (cash_in_txs_id) WHERE cashbox_batch_id IS NULL AND destination_unit = 'cashbox';`,
|
||||
'CREATE INDEX cash_in_txs_device_id_idx ON cash_in_txs USING btree (device_id);'
|
||||
], next)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = next => db.multi([
|
||||
'ALTER TABLE public.blacklist DROP CONSTRAINT IF EXISTS blacklist_pkey;',
|
||||
'ALTER TABLE public.blacklist ADD PRIMARY KEY (address);',
|
||||
'DROP INDEX IF EXISTS blacklist_temp_address_key;',
|
||||
'CREATE UNIQUE INDEX blacklist_address_idx ON public.blacklist USING btree (address);',
|
||||
|
||||
], next)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
SKIP_PREFLIGHT_CHECK=true
|
||||
HTTPS=true
|
||||
REACT_APP_TYPE_CHECK_SANCTUARY=false
|
||||
PORT=3001
|
||||
5
new-lamassu-admin/.vscode/settings.json
vendored
5
new-lamassu-admin/.vscode/settings.json
vendored
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import globals from 'globals'
|
||||
import pluginJs from '@eslint/js'
|
||||
import pluginReact from 'eslint-plugin-react'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx}'],
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.browser,
|
||||
process: 'readonly'
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: '16'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'react-compiler': reactCompiler
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react-compiler/react-compiler': 'warn'
|
||||
}
|
||||
},
|
||||
eslintConfigPrettier
|
||||
]
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
location /graphql {
|
||||
proxy_pass https://lamassu-admin-server/graphql;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300;
|
||||
proxy_connect_timeout 300;
|
||||
proxy_send_timeout 300;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
}
|
||||
8372
new-lamassu-admin/package-lock.json
generated
8372
new-lamassu-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +0,0 @@
|
|||
diff --git a/node_modules/react-scripts/config/webpack.config.js b/node_modules/react-scripts/config/webpack.config.js
|
||||
index 80c6ac2..3420936 100644
|
||||
--- a/node_modules/react-scripts/config/webpack.config.js
|
||||
+++ b/node_modules/react-scripts/config/webpack.config.js
|
||||
@@ -752,6 +752,7 @@ module.exports = function (webpackEnv) {
|
||||
formatter: require.resolve('react-dev-utils/eslintFormatter'),
|
||||
eslintPath: require.resolve('eslint'),
|
||||
context: paths.appSrc,
|
||||
+ cache: true,
|
||||
// ESLint class options
|
||||
cwd: paths.appPath,
|
||||
resolvePluginsRelativeTo: __dirname,
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import Slide from '@material-ui/core/Slide'
|
||||
import {
|
||||
StylesProvider,
|
||||
jssPreset,
|
||||
MuiThemeProvider,
|
||||
makeStyles
|
||||
} from '@material-ui/core/styles'
|
||||
import gql from 'graphql-tag'
|
||||
import { create } from 'jss'
|
||||
import extendJss from 'jss-plugin-extend'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import {
|
||||
useLocation,
|
||||
useHistory,
|
||||
BrowserRouter as Router
|
||||
} from 'react-router-dom'
|
||||
import Header from 'src/components/layout/Header'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { tree, hasSidebar, Routes, getParent } from 'src/routing/routes'
|
||||
import ApolloProvider from 'src/utils/apollo'
|
||||
|
||||
import AppContext from 'src/AppContext'
|
||||
import global from 'src/styling/global'
|
||||
import theme from 'src/styling/theme'
|
||||
import { backgroundColor, mainWidth } from 'src/styling/variables'
|
||||
|
||||
const jss = create({
|
||||
plugins: [extendJss(), ...jssPreset().plugins]
|
||||
})
|
||||
|
||||
const fill = '100%'
|
||||
const flexDirection = 'column'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
...global,
|
||||
root: {
|
||||
backgroundColor,
|
||||
width: fill,
|
||||
minHeight: fill,
|
||||
display: 'flex',
|
||||
flexDirection
|
||||
},
|
||||
wrapper: {
|
||||
width: mainWidth,
|
||||
height: fill,
|
||||
margin: '0 auto',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection
|
||||
},
|
||||
grid: {
|
||||
flex: 1,
|
||||
height: '100%'
|
||||
},
|
||||
contentWithSidebar: {
|
||||
flex: 1,
|
||||
marginLeft: 48,
|
||||
paddingTop: 15
|
||||
},
|
||||
contentWithoutSidebar: {
|
||||
width: mainWidth
|
||||
}
|
||||
})
|
||||
|
||||
const GET_USER_DATA = gql`
|
||||
query userData {
|
||||
userData {
|
||||
id
|
||||
username
|
||||
role
|
||||
enabled
|
||||
last_accessed
|
||||
last_accessed_from
|
||||
last_accessed_address
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Main = () => {
|
||||
const classes = useStyles()
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
const { wizardTested, userData, setUserData } = useContext(AppContext)
|
||||
|
||||
const { loading } = useQuery(GET_USER_DATA, {
|
||||
onCompleted: userResponse => {
|
||||
if (!userData && userResponse?.userData)
|
||||
setUserData(userResponse.userData)
|
||||
}
|
||||
})
|
||||
|
||||
const route = location.pathname
|
||||
|
||||
const sidebar = hasSidebar(route)
|
||||
const parent = sidebar ? getParent(route) : {}
|
||||
|
||||
const is404 = location.pathname === '/404'
|
||||
|
||||
const isSelected = it => location.pathname === it.route
|
||||
|
||||
const onClick = it => history.push(it.route)
|
||||
|
||||
const contentClassName = sidebar
|
||||
? classes.contentWithSidebar
|
||||
: classes.contentWithoutSidebar
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{!is404 && wizardTested && userData && (
|
||||
<Header tree={tree} user={userData} />
|
||||
)}
|
||||
<main className={classes.wrapper}>
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<Slide direction="left" in={true} mountOnEnter unmountOnExit>
|
||||
<div>
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
</div>
|
||||
</Slide>
|
||||
)}
|
||||
|
||||
<Grid container className={classes.grid}>
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<Sidebar
|
||||
data={parent.children}
|
||||
isSelected={isSelected}
|
||||
displayName={it => it.label}
|
||||
onClick={onClick}
|
||||
/>
|
||||
)}
|
||||
<div className={contentClassName}>{!loading && <Routes />}</div>
|
||||
</Grid>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [wizardTested, setWizardTested] = useState(false)
|
||||
const [userData, setUserData] = useState(null)
|
||||
|
||||
const setRole = role => {
|
||||
if (userData && role && userData.role !== role) {
|
||||
setUserData({ ...userData, role })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{ wizardTested, setWizardTested, userData, setUserData, setRole }}>
|
||||
<Router>
|
||||
<ApolloProvider>
|
||||
<StylesProvider jss={jss}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Main />
|
||||
</MuiThemeProvider>
|
||||
</StylesProvider>
|
||||
</ApolloProvider>
|
||||
</Router>
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { memo } from 'react'
|
||||
import ReactCarousel from 'react-material-ui-carousel'
|
||||
import LeftArrow from 'src/styling/icons/arrow/carousel-left-arrow.svg?react'
|
||||
import RightArrow from 'src/styling/icons/arrow/carousel-right-arrow.svg?react'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
imgWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex'
|
||||
},
|
||||
imgInner: {
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
width: 500,
|
||||
height: 400,
|
||||
marginBottom: 40
|
||||
}
|
||||
})
|
||||
|
||||
export const Carousel = memo(({ photosData, slidePhoto }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactCarousel
|
||||
PrevIcon={<LeftArrow />}
|
||||
NextIcon={<RightArrow />}
|
||||
navButtonsProps={{
|
||||
style: {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
color: 'transparent',
|
||||
opacity: 1
|
||||
}
|
||||
}}
|
||||
navButtonsWrapperProps={{
|
||||
style: {
|
||||
marginLeft: -22,
|
||||
marginRight: -22
|
||||
}
|
||||
}}
|
||||
autoPlay={false}
|
||||
indicators={false}
|
||||
navButtonsAlwaysVisible={true}
|
||||
next={activeIndex => slidePhoto(activeIndex)}
|
||||
prev={activeIndex => slidePhoto(activeIndex)}>
|
||||
{photosData.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div className={classes.imgWrapper}>
|
||||
<img
|
||||
className={classes.imgInner}
|
||||
src={`/${item?.photoDir}/${item?.path}`}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ReactCarousel>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import Chip from '@material-ui/core/Chip'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import {
|
||||
fontColor,
|
||||
inputFontWeight,
|
||||
subheaderColor,
|
||||
smallestFontSize,
|
||||
inputFontFamily
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
backgroundColor: subheaderColor,
|
||||
borderRadius: 4,
|
||||
margin: theme.spacing(0.5, 0.25),
|
||||
height: 18
|
||||
},
|
||||
label: {
|
||||
fontSize: smallestFontSize,
|
||||
color: fontColor,
|
||||
fontWeight: inputFontWeight,
|
||||
fontFamily: inputFontFamily,
|
||||
paddingRight: 4,
|
||||
paddingLeft: 4
|
||||
}
|
||||
})
|
||||
|
||||
const LsChip = memo(({ classes, ...props }) => (
|
||||
<Chip size="small" classes={classes} {...props} />
|
||||
))
|
||||
|
||||
export default withStyles(styles)(LsChip)
|
||||
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