diff --git a/.gitignore b/.gitignore
index cedfd45175..0cc1d73179 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,9 @@ test/Wallet/.log
test/Report
test/.env
test/Dev
+test/selenium/Onboarding/.log/*/*
+test/selenium/.env
+
# production
build
diff --git a/Dockerfile b/Dockerfile
index 8bd3c97ea8..853d0546f4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
-FROM node:10.15.3-stretch-slim
+FROM node:10.24.1-buster-slim
RUN apt-get update && \
- apt-get install -y --no-install-recommends git python build-essential && \
+ apt-get install -y --no-install-recommends curl openssl ca-certificates git python build-essential && \
rm -rf /var/lib/apt/lists/* && \
npm config set unsafe-perm true && \
npm install pm2@3.2.7 sequelize-cli@5.4.0 mocha -g --loglevel=error
diff --git a/Dockerfile.webtest b/Dockerfile.webtest
index 47ca33ca4b..171328f62d 100644
--- a/Dockerfile.webtest
+++ b/Dockerfile.webtest
@@ -1,7 +1,7 @@
-FROM node:10.15.3-stretch-slim
+FROM node:10.24.1-buster-slim
RUN apt-get update && \
- apt-get install -y --no-install-recommends git python build-essential && \
+ apt-get install -y --no-install-recommends curl openssl ca-certificates git python build-essential && \
rm -rf /var/lib/apt/lists/* && \
npm config set unsafe-perm true && \
npm install mocha -g --loglevel=error
diff --git a/server/api/controllers/admin.js b/server/api/controllers/admin.js
index 0a68e791b7..c2ddcba057 100644
--- a/server/api/controllers/admin.js
+++ b/server/api/controllers/admin.js
@@ -1,7 +1,7 @@
'use strict';
const { loggerAdmin } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { cloneDeep, pick } = require('lodash');
const { all } = require('bluebird');
const { USER_NOT_FOUND } = require('../../messages');
diff --git a/server/api/controllers/deposit.js b/server/api/controllers/deposit.js
index c59f77f327..0ef867f937 100644
--- a/server/api/controllers/deposit.js
+++ b/server/api/controllers/deposit.js
@@ -1,7 +1,7 @@
'use strict';
const { loggerDeposits } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { errorMessageConverter } = require('../../utils/conversion');
const getAdminDeposits = (req, res) => {
diff --git a/server/api/controllers/notification.js b/server/api/controllers/notification.js
index 0d30f0b912..c63ce4f198 100644
--- a/server/api/controllers/notification.js
+++ b/server/api/controllers/notification.js
@@ -1,7 +1,7 @@
'use strict';
const { loggerNotification } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { sendEmail } = require('../../mail');
const { MAILTYPE } = require('../../mail/strings');
const { publisher } = require('../../db/pubsub');
diff --git a/server/api/controllers/order.js b/server/api/controllers/order.js
index e59bebe522..66dbc04930 100644
--- a/server/api/controllers/order.js
+++ b/server/api/controllers/order.js
@@ -1,7 +1,7 @@
'use strict';
const { loggerOrders, loggerUser } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { isPlainObject, isNumber } = require('lodash');
const { errorMessageConverter } = require('../../utils/conversion');
const { isUUID } = require('validator');
diff --git a/server/api/controllers/otp.js b/server/api/controllers/otp.js
index 013d45e701..ccaab4bb69 100644
--- a/server/api/controllers/otp.js
+++ b/server/api/controllers/otp.js
@@ -2,7 +2,7 @@
const { INVALID_OTP_CODE } = require('../../messages');
const { loggerOtp } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { errorMessageConverter } = require('../../utils/conversion');
const requestOtp = (req, res) => {
diff --git a/server/api/controllers/public.js b/server/api/controllers/public.js
index 023faeecf2..b0c53fc284 100644
--- a/server/api/controllers/public.js
+++ b/server/api/controllers/public.js
@@ -3,7 +3,7 @@
const packageJson = require('../../package.json');
const { API_HOST, HOLLAEX_NETWORK_ENDPOINT } = require('../../constants');
const { loggerPublic } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { errorMessageConverter } = require('../../utils/conversion');
const getHealth = (req, res) => {
diff --git a/server/api/controllers/tier.js b/server/api/controllers/tier.js
index f770232176..82d3d75fe5 100644
--- a/server/api/controllers/tier.js
+++ b/server/api/controllers/tier.js
@@ -1,6 +1,6 @@
'use strict';
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { loggerTier } = require('../../config/logger');
const { errorMessageConverter } = require('../../utils/conversion');
diff --git a/server/api/controllers/trade.js b/server/api/controllers/trade.js
index 56a8fdb505..a89942d066 100644
--- a/server/api/controllers/trade.js
+++ b/server/api/controllers/trade.js
@@ -1,7 +1,7 @@
'use strict';
const { loggerTrades } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { errorMessageConverter } = require('../../utils/conversion');
const getUserTrades = (req, res) => {
diff --git a/server/api/controllers/user.js b/server/api/controllers/user.js
index 1265692541..8698d0aa3e 100644
--- a/server/api/controllers/user.js
+++ b/server/api/controllers/user.js
@@ -1,7 +1,7 @@
'use strict';
const { isEmail, isUUID } = require('validator');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { sendEmail } = require('../../mail');
const { MAILTYPE } = require('../../mail/strings');
const { loggerUser } = require('../../config/logger');
diff --git a/server/api/controllers/withdrawal.js b/server/api/controllers/withdrawal.js
index e491edb8ba..4ac7c033da 100644
--- a/server/api/controllers/withdrawal.js
+++ b/server/api/controllers/withdrawal.js
@@ -1,7 +1,7 @@
'use strict';
const { loggerWithdrawals } = require('../../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../../utils/toolsLib');
const { all } = require('bluebird');
const { USER_NOT_FOUND } = require('../../messages');
const { errorMessageConverter } = require('../../utils/conversion');
diff --git a/server/api/swagger/swagger.yaml b/server/api/swagger/swagger.yaml
index c84c1eb0e9..82ecd4c6ec 100644
--- a/server/api/swagger/swagger.yaml
+++ b/server/api/swagger/swagger.yaml
@@ -1,6 +1,6 @@
swagger: "2.0"
info:
- version: "2.2.3"
+ version: "2.2.4"
title: HollaEx Kit
host: api.hollaex.com
basePath: /v2
diff --git a/server/app.js b/server/app.js
index 0a029aefa0..055cdaf798 100644
--- a/server/app.js
+++ b/server/app.js
@@ -8,7 +8,7 @@ var YAML = require('yamljs');
var swaggerDoc = YAML.load('./api/swagger/swagger.yaml');
const { logEntryRequest, stream, logger } = require('./config/logger');
const { domainMiddleware, helmetMiddleware } = require('./config/middleware');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('./utils/toolsLib');
const { checkStatus } = require('./init');
const { API_HOST, CUSTOM_CSS } = require('./constants');
diff --git a/server/config/middleware.js b/server/config/middleware.js
index 21633ea847..2b4362cbd6 100644
--- a/server/config/middleware.js
+++ b/server/config/middleware.js
@@ -6,7 +6,7 @@ const ALLOWED_DOMAINS = () => toolsLib.getKitSecrets().allowed_domains || (proce
const helmet = require('helmet');
const expectCt = require('expect-ct');
const { apm } = require('./logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../utils/toolsLib');
const domainMiddleware = (req, res, next) => {
logger.verbose(req.uuid, 'origin', req.headers['x-real-origin']);
diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml
index 7837fecedf..9628cef7d3 100644
--- a/server/docker-compose.yaml
+++ b/server/docker-compose.yaml
@@ -26,6 +26,7 @@ services:
ports:
- 10010:10010
- 10011:10011
+ - 10012:10012
build:
context: .
dockerfile: ./tools/Dockerfile.pm2
@@ -41,7 +42,7 @@ services:
- ./api:/app/api
- ./config:/app/config
- ./db:/app/db
- - ./plugins.js:/app/plugins.js
+ - ./plugins:/app/plugins
- ./mail:/app/mail
- ./ws:/app/ws
- ./app.js:/app/app.js
diff --git a/server/ecosystem.config.js b/server/ecosystem.config.js
index 43434af664..705254a7f8 100644
--- a/server/ecosystem.config.js
+++ b/server/ecosystem.config.js
@@ -42,7 +42,7 @@ const ws = {
const plugins = {
// plugins application
name : 'plugins',
- script : 'plugins.js',
+ script : 'plugins/index.js',
error_file: '/dev/null',
out_file: '/dev/null',
watch,
diff --git a/server/init.js b/server/init.js
index d86ce9280e..3e083c916b 100644
--- a/server/init.js
+++ b/server/init.js
@@ -1,6 +1,6 @@
'use strict';
-const { Network } = require('hollaex-node-lib');
+const Network = require('./utils/nodeLib');
const { all } = require('bluebird');
const rp = require('request-promise');
const { loggerInit } = require('./config/logger');
diff --git a/server/mail/templates/withdrawalRequest.js b/server/mail/templates/withdrawalRequest.js
index 3191f2eb91..0f4919229f 100644
--- a/server/mail/templates/withdrawalRequest.js
+++ b/server/mail/templates/withdrawalRequest.js
@@ -11,7 +11,7 @@ const fetchMessage = (email, data, language, domain) => {
const html = (email, data, language, domain) => {
const WITHDRAWALREQUEST = require('../strings').getStringObject(language, 'WITHDRAWALREQUEST');
- const link = `${domain}/confirm-withdraw/${data.transaction_id}`;
+ const link = data.confirmation_link || `${domain}/confirm-withdraw/${data.transaction_id}`;
return `
@@ -44,7 +44,7 @@ const html = (email, data, language, domain) => {
const text = (email, data, language, domain) => {
const WITHDRAWALREQUEST = require('../strings').getStringObject(language, 'WITHDRAWALREQUEST');
- const link = `${domain}/confirm-withdraw/${data.transaction_id}`;
+ const link = data.confirmation_link || `${domain}/confirm-withdraw/${data.transaction_id}`;
return `
${WITHDRAWALREQUEST.GREETING(email)}
${WITHDRAWALREQUEST.BODY[1](data.currency, data.amount, data.address)}
diff --git a/server/package.json b/server/package.json
index e562b831cc..0ad8395f8b 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,5 +1,5 @@
{
- "version": "2.2.3",
+ "version": "2.2.4",
"private": false,
"description": "HollaEx Kit",
"keywords": [
@@ -26,11 +26,10 @@
"expect-ct": "0.1.0",
"express": "4.16.2",
"express-validator": "6.7.0",
+ "file-type": "16.5.2",
"flat": "5.0.0",
"geoip-lite": "1.4.1",
"helmet": "3.12.0",
- "hollaex-node-lib": "github:bitholla/hollaex-node-lib#2.11",
- "hollaex-tools-lib": "github:bitholla/hollaex-tools-lib#2.15",
"http": "0.0.0",
"install": "0.10.4",
"json2csv": "4.5.4",
@@ -42,6 +41,7 @@
"moment-timezone": "0.5.28",
"morgan": "1.9.0",
"multer": "1.4.2",
+ "multicoin-address-validator": "0.4.4",
"node-cron": "2.0.3",
"nodemailer": "6.4.6",
"npm": "5.7.1",
@@ -66,6 +66,7 @@
"validator": "9.4.1",
"winston": "3.2.1",
"winston-elasticsearch-apm": "0.0.7",
+ "ws": "7.4.0",
"ws-heartbeat": "1.1.0",
"yamljs": "0.3.0"
},
diff --git a/server/plugins.js b/server/plugins.js
deleted file mode 100644
index 60339f3735..0000000000
--- a/server/plugins.js
+++ /dev/null
@@ -1,1444 +0,0 @@
-'use strict';
-
-const _eval = require('eval');
-const lodash = require('lodash');
-const PORT = process.env.PLUGIN_PORT || 10011;
-const toolsLib = require('hollaex-tools-lib');
-const bodyParser = require('body-parser');
-const expressValidator = require('express-validator');
-const { checkSchema } = expressValidator;
-const morgan = require('morgan');
-const { logEntryRequest, stream, loggerPlugin } = require('./config/logger');
-const { domainMiddleware, helmetMiddleware } = require('./config/middleware');
-const morganType = process.env.NODE_ENV === 'development' ? 'dev' : 'combined';
-const multer = require('multer');
-const moment = require('moment');
-const { checkStatus } = require('./init');
-const uglifyEs = require('uglify-es');
-const cors = require('cors');
-const mathjs = require('mathjs');
-const bluebird = require('bluebird');
-const rp = require('request-promise');
-const uuid = require('uuid/v4');
-const fs = require('fs');
-const path = require('path');
-const latestVersion = require('latest-version');
-const { resolve } = bluebird;
-const npm = require('npm-programmatic');
-const sequelize = require('sequelize');
-const umzug = require('umzug');
-const jwt = require('jsonwebtoken');
-const momentTz = require('moment-timezone');
-const json2csv = require('json2csv');
-const flat = require('flat');
-const ws = require('ws');
-const cron = require('node-cron');
-const randomString = require('random-string');
-const bcryptjs = require('bcryptjs');
-const expectCt = require('expect-ct');
-const validator = require('validator');
-const otp = require('otp');
-const geoipLite = require('geoip-lite');
-const nodemailer = require('nodemailer');
-const wsHeartbeatServer = require('ws-heartbeat/server');
-const wsHeartbeatClient = require('ws-heartbeat/client');
-const winston = require('winston');
-const elasticApmNode = require('elastic-apm-node');
-const winstonElasticsearchApm = require('winston-elasticsearch-apm');
-const tripleBeam = require('triple-beam');
-const { Plugin } = require('./db/models');
-
-const getInstalledLibrary = async (name, version) => {
- const jsonFilePath = path.resolve(__dirname, './node_modules', name, 'package.json');
-
- let fileData = fs.readFileSync(jsonFilePath);
- fileData = JSON.parse(fileData);
-
- loggerPlugin.verbose(`${name} library found`);
- if (version === 'latest') {
- const v = await latestVersion(name);
- if (fileData.version === v) {
- loggerPlugin.verbose(`${name} version ${version} found`);
- const lib = require(name);
- return resolve(lib);
- } else {
- throw new Error('Version does not match');
- }
- } else {
- if (fileData.version === version) {
- loggerPlugin.verbose(`${name} version ${version} found`);
- const lib = require(name);
- return resolve(lib);
- } else {
- throw new Error('Version does not match');
- }
- }
-};
-
-const installLibrary = (library) => {
- const [name, version = 'latest'] = library.split('@');
- return getInstalledLibrary(name, version)
- .then((data) => {
- return data;
- })
- .catch((err) => {
- loggerPlugin.verbose(`${name} version ${version} installing`);
- return npm.install([`${name}@${version}`], {
- cwd: path.resolve(__dirname, './'),
- save: true,
- output: true
- });
- })
- .then(() => {
- loggerPlugin.verbose(`${name} version ${version} installed`);
- const lib = require(name);
- return lib;
- });
-};
-
-checkStatus()
- .then(() => {
- loggerPlugin.info(
- 'plugins.js Initializing Plugin Server'
- );
-
- var app = require('express')();
-
- app.use(morgan(morganType, { stream }));
- app.listen(PORT);
- app.use(cors());
- app.use(bodyParser.urlencoded({ extended: true }));
- app.use(bodyParser.json());
- app.use(logEntryRequest);
- app.use(domainMiddleware);
- helmetMiddleware(app);
-
- app.get('/plugins', [
- checkSchema({
- name: {
- in: ['query'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: true
- },
- search: {
- in: ['query'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: true
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- const { name, search } = req.query;
-
- let promiseQuery = null;
-
- if (name) {
- promiseQuery = toolsLib.plugin.getPlugin(
- name,
- {
- raw: true,
- attributes: {
- exclude: [
- 'id',
- 'script',
- 'meta',
- 'prescript',
- 'postscript'
- ]
- }
- }
- )
- .then((data) => {
- if (!data) {
- throw new Error('Plugin not found');
- } else {
- data.enabled_admin_view = !!data.admin_view;
- return lodash.omit(data, [ 'admin_view' ]);
- }
- });
- } else {
- const options = {
- where: {},
- raw: true,
- attributes: {
- exclude: [
- 'id',
- 'script',
- 'meta',
- 'prescript',
- 'postscript'
- ]
- },
- order: [[ 'id', 'asc' ]]
- };
-
- if (search) {
- options.where = {
- name: { [sequelize.Op.like]: `%${search}%` }
- };
- }
-
- promiseQuery = Plugin.findAndCountAll(options)
- .then((data) => {
- return {
- count: data.count,
- data: data.rows.map((plugin) => {
- plugin.enabled_admin_view = !!plugin.admin_view;
- return lodash.omit(plugin, [ 'admin_view' ]);
- })
- };
- });
- }
-
- promiseQuery
- .then((data) => {
- return res.json(data);
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'GET /plugins err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.delete('/plugins', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['query'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'DELETE /plugins auth',
- req.auth.sub
- );
-
- const { name } = req.query;
-
- loggerPlugin.info(req.uuid, 'DELETE /plugins name', name);
-
- toolsLib.plugin.getPlugin(name)
- .then((plugin) => {
- if (!plugin) {
- throw new Error('Plugin not found');
- }
-
- return bluebird.all([
- plugin,
- plugin.destroy()
- ]);
- })
- .then(([ { enabled, script } ]) => {
- loggerPlugin.info(req.uuid, 'DELETE /plugins deleted plugin', name);
-
- res.json({ message: 'Success' });
-
- if (enabled && script) {
- process.exit();
- }
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'DELETE /plugins err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.put('/plugins', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['body'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- },
- script: {
- in: ['body'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 5',
- options: { min: 5 }
- },
- optional: true
- },
- version: {
- in: ['body'],
- errorMessage: 'must be a number',
- isNumeric: true,
- optional: false
- },
- description: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- author: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- url: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- bio: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- documentation: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- icon: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- logo: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- admin_view: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- web_view: {
- in: ['body'],
- errorMessage: 'must be an array or null',
- isArray: true,
- optional: { options: { nullable: true } }
- },
- prescript: {
- in: ['body'],
- custom: {
- options: (value) => {
- if (!lodash.isPlainObject(value)) {
- return false;
- }
- if (value.install && lodash.isArray(value.install)) {
- for (let lib of value.install) {
- if (!lodash.isString(lib)) {
- return false;
- }
- }
- }
- if (value.run && !lodash.isString(value.run)) {
- return false;
- }
- return true;
- },
- errorMessage: 'must be an object. install value must be an array of strings. run value must be a string'
- },
- optional: { options: { nullable: true } }
- },
- postscript: {
- in: ['body'],
- custom: {
- options: (value) => {
- if (!lodash.isPlainObject(value)) {
- return false;
- }
- if (value.run && lodash.isString(value.run)) {
- return false;
- }
- return true;
- },
- errorMessage: 'must be an object. run value must be a string'
- },
- optional: true
- },
- meta: {
- in: ['body'],
- custom: {
- options: (value) => {
- return lodash.isPlainObject(value);
- },
- errorMessage: 'must be an object'
- },
- optional: { options: { nullable: true } }
- },
- public_meta: {
- in: ['body'],
- custom: {
- options: (value) => {
- return lodash.isPlainObject(value);
- },
- errorMessage: 'must be an object'
- },
- optional: { options: { nullable: true } }
- },
- type: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'PUT /plugins auth',
- req.auth.sub
- );
-
- const {
- name,
- script,
- version,
- description,
- author,
- url,
- icon,
- documentation,
- bio,
- web_view,
- admin_view,
- logo,
- prescript,
- postscript,
- meta,
- public_meta,
- type
- } = req.body;
-
- loggerPlugin.info(req.uuid, 'PUT /plugins name', name, 'version', version);
-
- let sameTypePlugin = null;
-
- if (type) {
- sameTypePlugin = Plugin.findOne({
- where: {
- type,
- name: {
- [sequelize.Op.not]: name
- }
- },
- raw: true,
- attributes: ['id', 'name', 'type']
- });
- }
-
- bluebird.all([
- toolsLib.plugin.getPlugin(name),
- sameTypePlugin
- ])
- .then(([ plugin, sameTypePlugin ]) => {
- if (!plugin) {
- throw new Error('Plugin not installed');
- }
- if (plugin.version === version) {
- throw new Error('Version is already installed');
- }
- if (sameTypePlugin) {
- throw new Error(`${name} version ${version} cannot be ran in parallel with an installed plugin (${sameTypePlugin.name}). Uninstall the plugin ${sameTypePlugin.name} before updating this plugin.`);
- }
-
- const updatedPlugin = {
- version
- };
-
- if (script) {
- const minifiedScript = uglifyEs.minify(script);
-
- if (minifiedScript.error) {
- throw new Error(`Error while minifying script: ${minifiedScript.error.message}`);
- }
-
- updatedPlugin.script = minifiedScript.code;
- }
-
- if (description) {
- updatedPlugin.description = description;
- }
-
- if (bio) {
- updatedPlugin.bio = bio;
- }
-
- if (author) {
- updatedPlugin.author = author;
- }
-
- if (type) {
- updatedPlugin.type = type;
- }
-
- if (documentation) {
- updatedPlugin.documentation = documentation;
- }
-
- if (icon) {
- updatedPlugin.icon = icon;
- }
-
- if (url) {
- updatedPlugin.url = url;
- }
-
- if (logo) {
- updatedPlugin.logo = logo;
- }
-
- if (!lodash.isUndefined(web_view)) {
- updatedPlugin.web_view = web_view;
- }
-
- if (!lodash.isUndefined(admin_view)) {
- updatedPlugin.admin_view = admin_view;
- }
-
- if (lodash.isPlainObject(prescript)) {
- updatedPlugin.prescript = prescript;
- }
-
- if (lodash.isPlainObject(postscript)) {
- updatedPlugin.postscript = postscript;
- }
-
- if (lodash.isPlainObject(meta)) {
- for (let key in plugin.meta) {
- if (
- plugin.meta[key].overwrite === false
- && (!meta[key] || meta[key].overwrite === false)
- ) {
- meta[key] = plugin.meta[key];
- }
- }
-
- const existingMeta = lodash.pick(plugin.meta, Object.keys(meta));
-
- for (let key in meta) {
- if (existingMeta[key] !== undefined) {
- if (lodash.isPlainObject(meta[key]) && !lodash.isPlainObject(existingMeta[key])) {
- meta[key].value = existingMeta[key];
- } else if (!lodash.isPlainObject(meta[key]) && !lodash.isPlainObject(existingMeta[key])) {
- meta[key] = existingMeta[key];
- } else if (!lodash.isPlainObject(meta[key]) && lodash.isPlainObject(existingMeta[key])) {
- meta[key] = existingMeta[key].value;
- } else if (lodash.isPlainObject(meta[key]) && lodash.isPlainObject(existingMeta[key])) {
- meta[key].value = existingMeta[key].value;
- }
- }
- }
-
- updatedPlugin.meta = meta;
- }
-
- if (lodash.isPlainObject(public_meta)) {
- for (let key in plugin.public_meta) {
- if (
- plugin.public_meta[key].overwrite === false
- && (!public_meta[key] || public_meta[key].overwrite === false)
- ) {
- public_meta[key] = plugin.public_meta[key];
- }
- }
-
- const existingPublicMeta = lodash.pick(plugin.public_meta, Object.keys(public_meta));
-
- for (let key in public_meta) {
- if (existingPublicMeta[key] !== undefined) {
- if (lodash.isPlainObject(public_meta[key]) && !lodash.isPlainObject(existingPublicMeta[key])) {
- public_meta[key].value = existingPublicMeta[key];
- } else if (!lodash.isPlainObject(public_meta[key]) && !lodash.isPlainObject(existingPublicMeta[key])) {
- public_meta[key] = existingPublicMeta[key];
- } else if (!lodash.isPlainObject(public_meta[key]) && lodash.isPlainObject(existingPublicMeta[key])) {
- public_meta[key] = existingPublicMeta[key].value;
- } else if (lodash.isPlainObject(public_meta[key]) && lodash.isPlainObject(existingPublicMeta[key])) {
- public_meta[key].value = existingPublicMeta[key].value;
- }
- }
- }
-
- updatedPlugin.public_meta = public_meta;
- }
-
- return bluebird.all([
- plugin,
- plugin.update(updatedPlugin)
- ]);
- })
- .then(([ { enabled, script }, plugin ]) => {
- loggerPlugin.info(req.uuid, 'PUT /plugins updated', name);
-
- plugin = plugin.dataValues;
-
- let restartProcess = false;
- if (enabled && script) {
- restartProcess = true;
- }
-
- plugin.enabled_admin_view = !!plugin.admin_view;
-
- res.json(lodash.omit(plugin, [
- 'id',
- 'meta',
- 'admin_view',
- 'script',
- 'prescript',
- 'postscript'
- ]));
-
- if (restartProcess) {
- process.exit();
- }
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'POST /plugins err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.post('/plugins', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['body'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- },
- script: {
- in: ['body'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 5',
- options: { min: 5 }
- },
- optional: true
- },
- version: {
- in: ['body'],
- errorMessage: 'must be a number',
- isNumeric: true,
- optional: false
- },
- author: {
- in: ['body'],
- errorMessage: 'must be a string',
- isString: true,
- optional: false
- },
- enabled: {
- in: ['body'],
- errorMessage: 'must be a boolean',
- isBoolean: true,
- optional: false
- },
- description: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- bio: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- documentation: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- icon: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- url: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- logo: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- admin_view: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- },
- web_view: {
- in: ['body'],
- errorMessage: 'must be an array or null',
- isArray: true,
- optional: { options: { nullable: true } }
- },
- prescript: {
- in: ['body'],
- custom: {
- options: (value) => {
- if (!lodash.isPlainObject(value)) {
- return false;
- }
- if (value.install && lodash.isArray(value.install)) {
- for (let lib of value.install) {
- if (!lodash.isString(lib)) {
- return false;
- }
- }
- }
- if (value.run && !lodash.isString(value.run)) {
- return false;
- }
- return true;
- },
- errorMessage: 'must be an object. install value must be an array of strings. run value must be a string'
- },
- optional: { options: { nullable: true } }
- },
- postscript: {
- in: ['body'],
- custom: {
- options: (value) => {
- if (!lodash.isPlainObject(value)) {
- return false;
- }
- if (value.run && !lodash.isString(value.run)) {
- return false;
- }
- return true;
- },
- errorMessage: 'must be an object. run value must be a string'
- },
- optional: { options: { nullable: true } }
- },
- meta: {
- in: ['body'],
- custom: {
- options: (value) => {
- return lodash.isPlainObject(value);
- },
- errorMessage: 'must be an object'
- },
- optional: { options: { nullable: true } }
- },
- public_meta: {
- in: ['body'],
- custom: {
- options: (value) => {
- return lodash.isPlainObject(value);
- },
- errorMessage: 'must be an object'
- },
- optional: { options: { nullable: true } }
- },
- type: {
- in: ['body'],
- errorMessage: 'must be a string or null',
- isString: true,
- optional: { options: { nullable: true } }
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'POST /plugins auth',
- req.auth.sub
- );
-
- const {
- name,
- script,
- version,
- description,
- author,
- icon,
- bio,
- documentation,
- web_view,
- admin_view,
- url,
- logo,
- enabled,
- prescript,
- postscript,
- meta,
- public_meta,
- type
- } = req.body;
-
- loggerPlugin.info(req.uuid, 'POST /plugins name', name, 'version', version);
-
- let sameTypePlugin = null;
-
- if (type) {
- sameTypePlugin = Plugin.findOne({
- where: { type },
- raw: true,
- attributes: ['id', 'name', 'type']
- });
- }
-
- bluebird.all([
- Plugin.findOne({
- where: { name },
- raw: true,
- attributes: ['id', 'name']
- }),
- sameTypePlugin
- ])
- .then(([ sameNamePlugin, sameTypePlugin ]) => {
- if (sameNamePlugin) {
- throw new Error(`Plugin ${name} is already installed`);
- }
-
- if (sameTypePlugin) {
- throw new Error(`${name} cannot be ran in parallel with an installed plugin (${sameTypePlugin.name}). Uninstall the plugin ${sameTypePlugin.name} before installing this plugin.`);
- }
-
- const newPlugin = {
- name,
- version,
- author,
- enabled
- };
-
- if (script) {
- const minifiedScript = uglifyEs.minify(script);
-
- if (minifiedScript.error) {
- throw new Error(`Error while minifying script: ${minifiedScript.error.message}`);
- }
-
- newPlugin.script = minifiedScript.code;
- }
-
- if (description) {
- newPlugin.description = description;
- }
-
- if (bio) {
- newPlugin.bio = bio;
- }
-
- if (documentation) {
- newPlugin.documentation = documentation;
- }
-
- if (icon) {
- newPlugin.icon = icon;
- }
-
- if (url) {
- newPlugin.url = url;
- }
-
- if (logo) {
- newPlugin.logo = logo;
- }
-
- if (type) {
- newPlugin.type = type;
- }
-
- if (!lodash.isUndefined(web_view)) {
- newPlugin.web_view = web_view;
- }
-
- if (!lodash.isUndefined(admin_view)) {
- newPlugin.admin_view = admin_view;
- }
-
- if (lodash.isPlainObject(prescript)) {
- newPlugin.prescript = prescript;
- }
-
- if (lodash.isPlainObject(postscript)) {
- newPlugin.postscript = postscript;
- }
-
- if (lodash.isPlainObject(meta)) {
- newPlugin.meta = meta;
- }
-
- if (lodash.isPlainObject(public_meta)) {
- newPlugin.public_meta = public_meta;
- }
-
- return toolsLib.database.create('plugin', newPlugin);
- })
- .then((plugin) => {
- loggerPlugin.info(req.uuid, 'POST /plugins installed', name);
-
- plugin = plugin.dataValues;
-
- let restartProcess = false;
- if (plugin.enabled && plugin.script) {
- restartProcess = true;
- }
-
- plugin.enabled_admin_view = !!plugin.admin_view;
-
- res.json(lodash.omit(plugin, [
- 'id',
- 'meta',
- 'admin_view',
- 'script',
- 'prescript',
- 'postscript'
- ]));
-
- if (restartProcess) {
- process.exit();
- }
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'POST /plugins err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.put('/plugins/public-meta', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['body'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- },
- public_meta: {
- in: ['body'],
- custom: {
- options: (value) => {
- return lodash.isPlainObject(value);
- },
- errorMessage: 'must be an object'
- },
- optional: false
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'PUT /plugins/public-meta auth',
- req.auth.sub
- );
-
- const { name, public_meta } = req.body;
-
- loggerPlugin.info(req.uuid, 'PUT /plugins/public-meta name', name);
-
- toolsLib.plugin.getPlugin(name)
- .then((plugin) => {
- if (!plugin) {
- throw new Error('Plugin not found');
- }
-
- const newPublicMeta = plugin.public_meta;
-
- for (let key in newPublicMeta) {
- if (public_meta[key] !== undefined) {
- if (lodash.isPlainObject(newPublicMeta[key])) {
- newPublicMeta[key].value = public_meta[key];
- } else {
- newPublicMeta[key] = public_meta[key];
- }
- }
- }
-
- return plugin.update({ public_meta: newPublicMeta }, { fields: ['public_meta'] });
- })
- .then((plugin) => {
- loggerPlugin.info(req.uuid, 'PUT /plugins/public-meta updated', name);
-
- res.json({
- name: plugin.name,
- version: plugin.version,
- public_meta: plugin.public_meta
- });
-
- if (plugin.enabled && plugin.script) {
- process.exit();
- }
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'PUT /plugins/public-meta err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.put('/plugins/meta', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['body'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- },
- meta: {
- in: ['body'],
- custom: {
- options: (value) => {
- return lodash.isPlainObject(value);
- },
- errorMessage: 'must be an object'
- },
- optional: true
- },
- public_meta: {
- in: ['body'],
- custom: {
- options: (value) => {
- return lodash.isPlainObject(value);
- },
- errorMessage: 'must be an object'
- },
- optional: true
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'PUT /plugins/meta auth',
- req.auth.sub
- );
-
- const { name, meta, public_meta } = req.body;
-
- if (!meta && !public_meta) {
- loggerPlugin.error(req.uuid, 'PUT /plugins/meta err', 'Must provide meta or public_meta to update');
- return res.status(400).json({ errors: 'Must provide meta or public_meta to update' });
- }
-
- loggerPlugin.info(req.uuid, 'PUT /plugins/meta name', name, 'meta', meta, 'public_meta', public_meta);
-
- toolsLib.plugin.getPlugin(name)
- .then((plugin) => {
- if (!plugin) {
- throw new Error('Plugin not found');
- }
-
- const params = {};
-
- if (meta) {
- const newMeta = plugin.meta;
-
- for (let key in newMeta) {
- if (meta[key] !== undefined) {
- if (lodash.isPlainObject(newMeta[key])) {
- newMeta[key].value = meta[key];
- } else {
- newMeta[key] = meta[key];
- }
- }
- }
-
- params.meta = newMeta;
- }
-
- if (public_meta) {
- const newPublicMeta = plugin.public_meta;
-
- for (let key in newPublicMeta) {
- if (public_meta[key] !== undefined) {
- if (lodash.isPlainObject(newPublicMeta[key])) {
- newPublicMeta[key].value = public_meta[key];
- } else {
- newPublicMeta[key] = public_meta[key];
- }
- }
- }
-
- params.public_meta = newPublicMeta;
- }
-
- return plugin.update(params, { fields: Object.keys(params) });
- })
- .then((plugin) => {
- loggerPlugin.info(req.uuid, 'PUT /plugins/meta updated', name);
-
- res.json({
- name: plugin.name,
- version: plugin.version,
- public_meta: plugin.public_meta,
- meta: plugin.meta
- });
-
- if (plugin.enabled && plugin.script) {
- process.exit();
- }
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'PUT /plugins/meta err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.get('/plugins/meta', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['query'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'GET /plugins/meta auth',
- req.auth.sub
- );
-
- const { name } = req.query;
-
- loggerPlugin.info(req.uuid, 'GET /plugins/meta name', name);
-
- toolsLib.plugin.getPlugin(name, { raw: true, attributes: ['name', 'version', 'meta', 'public_meta'] })
- .then((plugin) => {
- if (!plugin) {
- throw new Error('Plugin not found');
- }
-
- return res.json(plugin);
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'GET /plugins/meta err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.get('/plugins/script', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['query'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'GET /plugins/script auth',
- req.auth.sub
- );
-
- const { name } = req.query;
-
- loggerPlugin.info(req.uuid, 'GET /plugins/script name', name);
-
- toolsLib.plugin.getPlugin(name, { raw: true, attributes: ['name', 'version', 'script', 'prescript', 'postscript', 'admin_view'] })
- .then((plugin) => {
- if (!plugin) {
- throw new Error('Plugin not found');
- }
-
- return res.json(plugin);
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'GET /plugins/script err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.get('/plugins/disable', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['query'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'GET /plugins/disable auth',
- req.auth.sub
- );
-
- const { name } = req.query;
-
- loggerPlugin.info(req.uuid, 'GET /plugins/disable name', name);
-
- toolsLib.plugin.getPlugin(name)
- .then((plugin) => {
- if (!plugin) {
- throw new Error('Plugin not found');
- }
-
- if (!plugin.enabled) {
- throw new Error('Plugin is already disabled');
- }
-
- return plugin.update({ enabled: false }, { fields: ['enabled']});
- })
- .then((plugin) => {
- loggerPlugin.info(req.uuid, 'GET /plugins/disable disabled plugin', name);
-
- res.json({ message: 'Success' });
-
- if (plugin.script) {
- process.exit();
- }
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'GET /plugins/disable err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- app.get('/plugins/enable', [
- toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
- checkSchema({
- name: {
- in: ['query'],
- errorMessage: 'must be a string',
- isString: true,
- isLength: {
- errorMessage: 'must be minimum length of 1',
- options: { min: 1 }
- },
- optional: false
- }
- })
- ], (req, res) => {
- const errors = expressValidator.validationResult(req);
- if (!errors.isEmpty()) {
- return res.status(400).json({ errors: errors.array() });
- }
-
- loggerPlugin.verbose(
- req.uuid,
- 'GET /plugins/enable auth',
- req.auth.sub
- );
-
- const { name } = req.query;
-
- loggerPlugin.info(req.uuid, 'GET /plugins/enable name', name);
-
- toolsLib.plugin.getPlugin(name)
- .then((plugin) => {
- if (!plugin) {
- throw new Error('Plugin not found');
- }
-
- if (plugin.enabled) {
- throw new Error('Plugin is already enabled');
- }
-
- return plugin.update({ enabled: true }, { fields: ['enabled']});
- })
- .then((plugin) => {
- loggerPlugin.info(req.uuid, 'GET /plugins/enable enabled plugin', name);
-
- res.json({ message: 'Success' });
-
- if (plugin.script) {
- process.exit();
- }
- })
- .catch((err) => {
- loggerPlugin.error(req.uuid, 'GET /plugins/enable err', err.message);
- return res.status(err.status || 400).json({ message: err.message });
- });
- });
-
- toolsLib.database.findAll('plugin', {
- where: {
- enabled: true,
- script: {
- [sequelize.Op.not]: null
- }
- },
- raw: true
- })
- .then(async (plugins) => {
- for (let plugin of plugins) {
- try {
- loggerPlugin.verbose('plugin', plugin.name, 'enabling');
- const context = {
- app,
- toolsLib,
- lodash,
- expressValidator,
- loggerPlugin,
- multer,
- moment,
- mathjs,
- bluebird,
- umzug,
- rp,
- sequelize,
- uuid,
- jwt,
- momentTz,
- json2csv,
- flat,
- ws,
- cron,
- randomString,
- bcryptjs,
- expectCt,
- validator,
- uglifyEs,
- otp,
- latestVersion,
- geoipLite,
- nodemailer,
- wsHeartbeatServer,
- wsHeartbeatClient,
- cors,
- winston,
- elasticApmNode,
- winstonElasticsearchApm,
- tripleBeam,
- bodyParser,
- morgan,
- meta: plugin.meta,
- publicMeta: plugin.public_meta,
- installedLibraries: {}
- };
- if (plugin.prescript && plugin.prescript.install) {
- loggerPlugin.verbose('plugin', plugin.name, 'installing packages');
- for (let library of plugin.prescript.install) {
- context.installedLibraries[library] = await installLibrary(library);
- }
- loggerPlugin.verbose('plugin', plugin.name, 'packages installed');
- }
-
- _eval(plugin.script, plugin.name, context, true);
- loggerPlugin.verbose('plugin', plugin.name, 'enabled');
- } catch (err) {
- loggerPlugin.error('plugin', plugin.name, 'error while installing prepackages', err.message);
- }
- }
-
- loggerPlugin.info(
- `Plugin server running on port: ${PORT}`
- );
- });
- })
- .catch((err) => {
- let message = 'Plugin Initialization failed';
- if (err.message) {
- message = err.message;
- }
- if (err.statusCode && err.statusCode === 402) {
- message = err.error.message;
- }
- loggerPlugin.error('plugins/checkStatus Error ', message);
- setTimeout(() => { process.exit(1); }, 5000);
- });
\ No newline at end of file
diff --git a/server/plugins/controllers.js b/server/plugins/controllers.js
new file mode 100644
index 0000000000..1e888b1363
--- /dev/null
+++ b/server/plugins/controllers.js
@@ -0,0 +1,790 @@
+'use strict';
+
+const { Plugin } = require('../db/models');
+const { validationResult } = require('express-validator');
+const lodash = require('lodash');
+const sequelize = require('sequelize');
+const { loggerPlugin } = require('../config/logger');
+const { omit, pick, isUndefined, isPlainObject, cloneDeep, isString, isEmpty, isBoolean } = require('lodash');
+const uglifyEs = require('uglify-es');
+
+const getPlugins = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ console.log(errors);
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { name, search } = req.query;
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/getPlugins',
+ 'name:',
+ name,
+ 'search:',
+ search
+ );
+
+ try {
+ const options = {
+ raw: true,
+ attributes: {
+ exclude: [
+ 'id',
+ 'script',
+ 'meta',
+ 'prescript',
+ 'postscript'
+ ]
+ },
+ order: [[ 'id', 'asc' ]]
+ };
+
+ if (name) {
+ options.where = { name };
+ } else if (search) {
+ options.where = {
+ name: { [sequelize.Op.like]: `%${search}%` }
+ };
+ }
+
+ const data = await Plugin.findAndCountAll(options);
+
+ if (name && data.count === 0) {
+ throw new Error('Plugin not found');
+ }
+
+ const formattedData = {
+ count: data.count,
+ data: data.rows.map((plugin) => {
+ plugin.enabled_admin_view = !!plugin.admin_view;
+ return lodash.omit(plugin, [ 'admin_view' ]);
+ })
+ };
+
+ return name ? res.json(formattedData.data[0]) : res.json(formattedData);
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/getPlugins err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const deletePlugin = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/deletePlugin auth',
+ req.auth.sub
+ );
+
+ const { name } = req.query;
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/deletePlugin name:',
+ name
+ );
+
+ try {
+ const plugin = await Plugin.findOne({
+ where: { name }
+ });
+
+ if (!plugin) {
+ throw new Error('Plugin not found');
+ }
+
+ const restartAfterDelete = plugin.enabled && plugin.script;
+
+ await plugin.destroy();
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/deletePlugin',
+ 'plugin deleted',
+ name
+ );
+
+ res.json({ message: 'Success' });
+
+ if (restartAfterDelete) {
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/deletePlugin',
+ 'restarting plugin process'
+ );
+
+ process.exit();
+ }
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/deletePlugin err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const postPlugin = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/postPlugin auth',
+ req.auth.sub
+ );
+
+ const { name, version, author } = req.body;
+ let { enabled } = req.body;
+
+ if (!isBoolean(enabled)) {
+ enabled = true;
+ }
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/postPlugin',
+ name,
+ 'version:',
+ version,
+ 'author:',
+ author
+ );
+
+ const configValues = pick(req.body, [
+ 'script',
+ 'description',
+ 'icon',
+ 'bio',
+ 'documentation',
+ 'web_view',
+ 'admin_view',
+ 'url',
+ 'logo',
+ 'prescript',
+ 'postscript',
+ 'meta',
+ 'public_meta',
+ 'type'
+ ]);
+
+ try {
+ const sameNamePlugin = await Plugin.findOne({
+ where: { name },
+ raw: true,
+ attributes: ['id', 'name']
+ });
+
+ if (sameNamePlugin) {
+ throw new Error(`Plugin ${name} is already installed`);
+ }
+
+ if (configValues.type) {
+ const sameTypePlugin = await Plugin.findOne({
+ where: { type: configValues.type },
+ raw: true,
+ attributes: ['id', 'name', 'type']
+ });
+
+ if (sameTypePlugin) {
+ throw new Error(`${name} cannot be ran in parallel with an installed plugin (${sameTypePlugin.name}). Uninstall the plugin ${sameTypePlugin.name} before installing this plugin.`);
+ }
+ }
+
+ const pluginConfig = {
+ name,
+ version,
+ author,
+ enabled
+ };
+
+ for (const field in configValues) {
+ const value = configValues[field];
+
+ switch (field) {
+ case 'script':
+ if (value) {
+ const minifiedScript = uglifyEs.minify(value);
+
+ if (minifiedScript.error) {
+ throw new Error(`Error while minifying script: ${minifiedScript.error.message}`);
+ }
+
+ pluginConfig[field] = minifiedScript.code;
+ }
+ break;
+ case 'description':
+ case 'bio':
+ case 'documentation':
+ case 'icon':
+ case 'url':
+ case 'logo':
+ case 'type':
+ if (isString(value)) {
+ pluginConfig[field] = value;
+ }
+ break;
+ case 'web_view':
+ case 'admin_view':
+ if (!isUndefined(value)) {
+ pluginConfig[field] = value;
+ }
+ break;
+ case 'prescript':
+ case 'postscript':
+ case 'meta':
+ case 'public_meta':
+ if (isPlainObject(value)) {
+ pluginConfig[field] = value;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ const plugin = await Plugin.create(pluginConfig);
+ const formattedPlugin = cloneDeep(plugin.dataValues);
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/postPlugin plugin installed',
+ name
+ );
+
+ formattedPlugin.enabled_admin_view = !!formattedPlugin.admin_view;
+
+ res.json(
+ omit(formattedPlugin, [
+ 'id',
+ 'meta',
+ 'admin_view',
+ 'script',
+ 'prescript',
+ 'postscript'
+ ])
+ );
+
+ if (plugin.enabled && plugin.script) {
+ process.exit();
+ }
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/postPlugin',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const putPlugin = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/putPlugin auth',
+ req.auth.sub
+ );
+
+ const { name, version } = req.body;
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/putPlugin',
+ name,
+ 'version:',
+ version
+ );
+
+ const configValues = pick(req.body, [
+ 'script',
+ 'description',
+ 'icon',
+ 'bio',
+ 'documentation',
+ 'web_view',
+ 'admin_view',
+ 'url',
+ 'logo',
+ 'prescript',
+ 'postscript',
+ 'meta',
+ 'public_meta',
+ 'type'
+ ]);
+
+ try {
+ const plugin = await Plugin.findOne({ where: { name }});
+
+ if (!plugin) {
+ throw new Error('Plugin not installed');
+ }
+
+ if (plugin.version === version) {
+ throw new Error('Version is already installed');
+ }
+
+ if (configValues.type) {
+ const sameTypePlugin = await Plugin.findOne({
+ where: {
+ type: configValues.type,
+ name: {
+ [sequelize.Op.not]: name
+ }
+ },
+ raw: true,
+ attributes: ['id', 'name', 'type']
+ });
+
+ if (sameTypePlugin) {
+ throw new Error(`${name} version ${version} cannot be ran in parallel with an installed plugin (${sameTypePlugin.name}). Uninstall the plugin ${sameTypePlugin.name} before updating this plugin.`);
+ }
+ }
+
+ const pluginConfig = {
+ version
+ };
+
+ for (const field in configValues) {
+ const value = configValues[field];
+
+ switch (field) {
+ case 'script':
+ if (value) {
+ const minifiedScript = uglifyEs.minify(value);
+
+ if (minifiedScript.error) {
+ throw new Error(`Error while minifying script: ${minifiedScript.error.message}`);
+ }
+
+ pluginConfig[field] = minifiedScript.code;
+ }
+ break;
+ case 'description':
+ case 'bio':
+ case 'author':
+ case 'type':
+ case 'documentation':
+ case 'icon':
+ case 'url':
+ case 'logo':
+ if (value) {
+ pluginConfig[field] = value;
+ }
+ break;
+ case 'web_view':
+ case 'admin_view':
+ if (!isUndefined(value)) {
+ pluginConfig[field] = value;
+ }
+ break;
+ case 'prescript':
+ case 'postscript':
+ if (isPlainObject(value)) {
+ pluginConfig[field] = value;
+ }
+ break;
+ case 'meta':
+ case 'public_meta':
+ if (isPlainObject(value)) {
+ for (const key in plugin[field]) {
+ if (
+ lodash.isPlainObject(plugin[field])
+ && plugin[field][key].overwrite === false
+ && (!value[key] || value[key].overwrite === false)
+ ) {
+ value[key] = plugin[field][key];
+ }
+ }
+
+ const existingConfig = pick(plugin[field], Object.keys(value));
+
+ for (const key in value) {
+ if (existingConfig[key] !== undefined) {
+ if (isPlainObject(value[key]) && !isPlainObject(existingConfig[key])) {
+ value[key].value = existingConfig[key];
+ } else if (!isPlainObject(value[key]) && !isPlainObject(existingConfig[key])) {
+ value[key] = existingConfig[key];
+ } else if (!isPlainObject(value[key]) && isPlainObject(existingConfig[key])) {
+ value[key] = existingConfig[key].value;
+ } else if (isPlainObject(value[key]) && isPlainObject(existingConfig[key])) {
+ value[key].value = existingConfig[key].value;
+ }
+ }
+ }
+
+ pluginConfig[field] = value;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ const updatedPlugin = await plugin.update(pluginConfig);
+ const formattedPlugin = cloneDeep(updatedPlugin.dataValues);
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/putPlugin plugin updated',
+ name
+ );
+
+ formattedPlugin.enabled_admin_view = !!formattedPlugin.admin_view;
+
+ res.json(
+ omit(formattedPlugin, [
+ 'id',
+ 'meta',
+ 'admin_view',
+ 'script',
+ 'prescript',
+ 'postscript'
+ ])
+ );
+
+ if (updatedPlugin.enabled && updatedPlugin.script) {
+ process.exit();
+ }
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/putPlugin err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const getPluginConfig = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/getPluginConfig auth',
+ req.auth.sub
+ );
+
+ const { name } = req.query;
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/getPluginConfig name',
+ name
+ );
+
+ try {
+ const plugin = await Plugin.findOne({
+ where: { name },
+ raw: true,
+ attributes: [
+ 'name',
+ 'version',
+ 'meta',
+ 'public_meta'
+ ]
+ });
+
+ if (!plugin) {
+ throw new Error('Plugin not found');
+ }
+
+ return res.json(plugin);
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/getPluginConfig err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const putPluginConfig = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/putPluginConfig auth',
+ req.auth.sub
+ );
+
+ const { name } = req.body;
+ const configValues = pick(req.body, ['meta', 'public_meta']);
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/putPluginConfig name:',
+ name
+ );
+
+ try {
+ if (isEmpty(configValues)) {
+ throw new Error('Must provide meta or public_meta to update');
+ }
+
+ const plugin = await Plugin.findOne({ where: { name }});
+
+ if (!plugin) {
+ throw new Error('Plugin not found');
+ }
+
+ const updatedConfig = {};
+
+ for (const field in configValues) {
+ const value = configValues[field];
+
+ switch (field) {
+ case 'meta':
+ case 'public_meta':
+ if (value) {
+ const newConfig = plugin[field];
+
+ for (const key in newConfig) {
+ if (value[key] !== undefined) {
+ if (isPlainObject(newConfig[key])) {
+ newConfig[key].value = value[key];
+ } else {
+ newConfig[key] = value[key];
+ }
+ }
+ }
+
+ updatedConfig[field] = newConfig;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ const updatedPlugin = await plugin.update(updatedConfig, { fields: Object.keys(updatedConfig) });
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/putPluginConfig plugin updated',
+ name
+ );
+
+ res.json(
+ pick(updatedPlugin.dataValues, [
+ 'name',
+ 'version',
+ 'public_meta',
+ 'meta'
+ ])
+ );
+
+ if (plugin.enabled && plugin.script) {
+ process.exit();
+ }
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/putPluginConfig err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const getPluginScript = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/getPluginScript auth',
+ req.auth.sub
+ );
+
+ const { name } = req.query;
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/getPluginScript name:',
+ name
+ );
+
+ try {
+ const plugin = await Plugin.findOne({
+ where: { name },
+ raw: true,
+ attributes: [
+ 'name',
+ 'version',
+ 'script',
+ 'prescript',
+ 'postscript',
+ 'admin_view'
+ ]
+ });
+
+ if (!plugin) {
+ throw new Error('Plugin not found');
+ }
+
+ return res.json(plugin);
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/getPluginScript err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const disablePlugin = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/disablePlugin auth',
+ req.auth.sub
+ );
+
+ const { name } = req.query;
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/disablePlugin name:',
+ name
+ );
+
+ try {
+ const plugin = await Plugin.findOne({ where: { name }});
+
+ if (!plugin) {
+ throw new Error('Plugin not found');
+ }
+
+ if (!plugin.enabled) {
+ throw new Error('Plugin is already disabled');
+ }
+
+ await plugin.update({ enabled: false }, { fields: ['enabled'] });
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/disablePlugin plugin disabled',
+ name
+ );
+
+ res.json({ message: 'Success' });
+
+ if (plugin.script) {
+ process.exit();
+ }
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/disablePlugin err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+const enablePlugin = async (req, res) => {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/enablePlugin auth',
+ req.auth.sub
+ );
+
+ const { name } = req.query;
+
+ loggerPlugin.info(
+ req.uuid,
+ 'plugins/controllers/enablePlugin name:',
+ name
+ );
+
+ try {
+ const plugin = await Plugin.findOne({ where: { name }});
+
+ if (!plugin) {
+ throw new Error('Plugin not found');
+ }
+
+ if (plugin.enabled) {
+ throw new Error('Plugin is already enabled');
+ }
+
+ await plugin.update({ enabled: true }, { fields: ['enabled'] });
+
+ loggerPlugin.verbose(
+ req.uuid,
+ 'plugins/controllers/enablePlugin plugin enabled',
+ name
+ );
+
+ res.json({ message: 'Success' });
+
+ if (plugin.script) {
+ process.exit();
+ }
+ } catch (err) {
+ loggerPlugin.error(
+ req.uuid,
+ 'plugins/controllers/enablePlugin err',
+ err.message
+ );
+
+ return res.status(err.status || 400).json({ message: err.message });
+ }
+};
+
+module.exports = {
+ getPlugins,
+ deletePlugin,
+ postPlugin,
+ putPlugin,
+ getPluginConfig,
+ putPluginConfig,
+ getPluginScript,
+ disablePlugin,
+ enablePlugin
+};
diff --git a/server/plugins/dev.js b/server/plugins/dev.js
new file mode 100644
index 0000000000..724e4ab67b
--- /dev/null
+++ b/server/plugins/dev.js
@@ -0,0 +1,152 @@
+
+/**
+ * Add your mock publicMeta and meta values in the object below
+ * In production, these values are stored in the configuration JSON file
+
+ * Example mock configurations are included below
+**/
+
+this.configValues = {
+ // // ------------ CONFIG VALUES EXAMPLE START ------------
+
+ // publicMeta: {
+ // public_value: {
+ // type: 'string',
+ // description: 'Public meta value',
+ // required: false,
+ // value: 'i am public'
+ // }
+ // },
+ // meta: {
+ // private_value: {
+ // type: 'string',
+ // description: 'Private meta value',
+ // required: true,
+ // value: 'i am private'
+ // }
+ // }
+
+ // // ------------ CONFIG VALUES EXAMPLE END ------------
+};
+
+const pluginScript = () => {
+ /**
+ * Add the plugin script here
+ * The script within this function should be in the script.js file for a plugin
+
+ * An example of a plugin script is included below
+ **/
+
+ // // ------------ PLUGIN EXAMPLE START ------------
+
+ // const { app, loggerPlugin, toolsLib } = this.pluginLibraries; // this.pluginLibraries holds app, loggerPlugin, and toolsLib in the plugin script
+ // const { publicMeta, meta } = this.configValues; // this.configValues holds publicMeta and meta in the plugin script
+
+ // const lodash = require('lodash');
+ // const moment = require('moment');
+ // const { public_value: { value: PUBLIC_VALUE } } = publicMeta;
+ // const { private_value: { value: PRIVATE_VALUE } } = meta;
+
+ // // All endpoints for a plugin should follow the format: '/plugins//...'. For this example, the plugin name is 'test'
+ // const HEALTH_ENDPOINT = '/plugins/test/health';
+ // const CONFIG_VALUES_ENDPOINT = '/plugins/test/config-values';
+
+ // // We recommend creating an init function that checks for all required configuration values and all other requirements for this plugin to run
+ // const init = async () => {
+ // loggerPlugin.verbose(
+ // 'DEV PLUGIN initializing...'
+ // );
+
+ // if (!lodash.isString(PRIVATE_VALUE)) {
+ // throw new Error('Private Value must be configured for this plugin to run');
+ // }
+
+ // loggerPlugin.verbose(
+ // 'DEV PLUGIN initialized'
+ // );
+ // };
+
+ // init()
+ // .then(() => {
+ // app.get(HEALTH_ENDPOINT, async (req, res) => {
+ // loggerPlugin.info(
+ // req.uuid,
+ // HEALTH_ENDPOINT
+ // );
+
+ // return res.json({
+ // status: 'running',
+ // current_time: moment().toISOString(),
+ // exchange_name: toolsLib.getKitConfig().info.name
+ // });
+ // });
+
+ // app.get(CONFIG_VALUES_ENDPOINT, async (req, res) => {
+ // loggerPlugin.info(
+ // req.uuid,
+ // CONFIG_VALUES_ENDPOINT
+ // );
+
+ // return res.json({
+ // public_value: PUBLIC_VALUE,
+ // private_value: PRIVATE_VALUE
+ // });
+ // });
+ // })
+ // .catch((err) => {
+ // // It's important to catch all errors in a script. If a thrown error is not caught, the plugin process will exit and continuously try to restart
+ // loggerPlugin.error(
+ // 'DEV PLUGIN initialization error',
+ // err.message
+ // );
+ // });
+
+ // // ------------ PLUGIN EXAMPLE END ------------
+};
+
+
+
+
+
+
+
+
+// BELOW IS THE SCRIPT FOR RUNNING THE PLUGIN DEV ENVIRONMENT THAT IS NOT NECESSARY IN THE PLUGIN ITSELF
+
+const { checkStatus } = require('../init');
+
+const initializeDevPlugin = async () => {
+ await checkStatus();
+
+ const morgan = require('morgan');
+ const { logEntryRequest, stream, loggerPlugin } = require('../config/logger');
+ const { domainMiddleware, helmetMiddleware } = require('../config/middleware');
+ const morganType = process.env.NODE_ENV === 'development' ? 'dev' : 'combined';
+ const PORT = 10012;
+ const cors = require('cors');
+ const toolsLib = require('../utils/toolsLib');
+ const express = require('express');
+
+ const app = express();
+ app.use(morgan(morganType, { stream }));
+ app.listen(PORT);
+ app.use(cors());
+ app.use(express.urlencoded({ extended: true }));
+ app.use(express.json());
+ app.use(logEntryRequest);
+ app.use(domainMiddleware);
+ helmetMiddleware(app);
+
+ const pluginLibraries = {
+ app,
+ loggerPlugin,
+ toolsLib
+ };
+
+ this.pluginLibraries = pluginLibraries;
+};
+
+(async () => {
+ await initializeDevPlugin();
+ pluginScript();
+})();
\ No newline at end of file
diff --git a/server/plugins/index.js b/server/plugins/index.js
new file mode 100644
index 0000000000..01d694c758
--- /dev/null
+++ b/server/plugins/index.js
@@ -0,0 +1,247 @@
+'use strict';
+
+const { checkStatus } = require('../init');
+const express = require('express');
+const PORT = process.env.PLUGIN_PORT || 10011;
+const morgan = require('morgan');
+const morganType = process.env.NODE_ENV === 'development' ? 'dev' : 'combined';
+const { logEntryRequest, stream, loggerPlugin } = require('../config/logger');
+const cors = require('cors');
+const { domainMiddleware, helmetMiddleware } = require('../config/middleware');
+const routes = require('./routes');
+const { Plugin } = require('../db/models');
+const path = require('path');
+const fs = require('fs');
+const latestVersion = require('latest-version');
+const npm = require('npm-programmatic');
+const sequelize = require('sequelize');
+const _eval = require('eval');
+const toolsLib = require('../utils/toolsLib');
+const lodash = require('lodash');
+const expressValidator = require('express-validator');
+const multer = require('multer');
+const moment = require('moment');
+const mathjs = require('mathjs');
+const bluebird = require('bluebird');
+const umzug = require('umzug');
+const rp = require('request-promise');
+const uuid = require('uuid/v4');
+const jwt = require('jsonwebtoken');
+const momentTz = require('moment-timezone');
+const json2csv = require('json2csv');
+const flat = require('flat');
+const ws = require('ws');
+const cron = require('node-cron');
+const randomString = require('random-string');
+const bcryptjs = require('bcryptjs');
+const expectCt = require('expect-ct');
+const validator = require('validator');
+const otp = require('otp');
+const geoipLite = require('geoip-lite');
+const nodemailer = require('nodemailer');
+const wsHeartbeatServer = require('ws-heartbeat/server');
+const wsHeartbeatClient = require('ws-heartbeat/client');
+const winston = require('winston');
+const elasticApmNode = require('elastic-apm-node');
+const winstonElasticsearchApm = require('winston-elasticsearch-apm');
+const tripleBeam = require('triple-beam');
+const uglifyEs = require('uglify-es');
+const bodyParser = require('body-parser');
+
+const getInstalledLibrary = async (name, version) => {
+ const jsonFilePath = path.resolve(__dirname, '../node_modules', name, 'package.json');
+
+ const fileData = fs.readFileSync(jsonFilePath);
+ const parsedFileData = JSON.parse(fileData);
+
+ loggerPlugin.verbose(
+ 'plugins/index/getInstalledLibrary',
+ `${name} library found`
+ );
+
+ const checkVersion = version === 'latest' ? await latestVersion(name) : version;
+
+ if (parsedFileData.version !== checkVersion) {
+ throw new Error('Version does not match');
+ }
+
+ loggerPlugin.verbose(
+ 'plugins/index/getInstalledLibrary',
+ `${name} version ${version} found`
+ );
+
+ const lib = require(name);
+ return lib;
+};
+
+const installLibrary = async (library) => {
+ const [name, version = 'latest'] = library.split('@');
+
+ try {
+ const data = await getInstalledLibrary(name, version);
+ return data;
+ } catch (err) {
+ loggerPlugin.verbose(
+ 'plugins/index/installLibrary',
+ `${name} version ${version} installing`
+ );
+
+ await npm.install([`${name}@${version}`], {
+ cwd: path.resolve(__dirname, '../'),
+ save: true,
+ output: true
+ });
+
+ loggerPlugin.verbose(
+ 'plugins/index/installLibrary',
+ `${name} version ${version} installed`
+ );
+
+ const lib = require(name);
+ return lib;
+ }
+};
+
+checkStatus()
+ .then(async () => {
+ loggerPlugin.info(
+ '/plugins/index/initialization',
+ 'Initializing Plugin Server...'
+ );
+
+ const app = express();
+
+ app.use(morgan(morganType, { stream }));
+ app.listen(PORT);
+ app.use(cors());
+ app.use(express.urlencoded({ extended: true }));
+ app.use(express.json());
+ app.use(logEntryRequest);
+ app.use(domainMiddleware);
+ helmetMiddleware(app);
+
+ app.use('/plugins', routes);
+
+ const plugins = await Plugin.findAll({
+ where: {
+ enabled: true,
+ script: {
+ [sequelize.Op.not]: null
+ }
+ },
+ raw: true
+ });
+
+ for (const plugin of plugins) {
+ try {
+ loggerPlugin.verbose(
+ 'plugins/index/initialization',
+ `starting plugin ${plugin.name}`
+ );
+
+ const context = {
+ configValues: {
+ publicMeta: plugin.public_meta,
+ meta: plugin.meta
+ },
+ pluginLibraries: {
+ app,
+ loggerPlugin,
+ toolsLib
+ },
+ app,
+ toolsLib,
+ lodash,
+ expressValidator,
+ loggerPlugin,
+ multer,
+ moment,
+ mathjs,
+ bluebird,
+ umzug,
+ rp,
+ sequelize,
+ uuid,
+ jwt,
+ momentTz,
+ json2csv,
+ flat,
+ ws,
+ cron,
+ randomString,
+ bcryptjs,
+ expectCt,
+ validator,
+ uglifyEs,
+ otp,
+ latestVersion,
+ geoipLite,
+ nodemailer,
+ wsHeartbeatServer,
+ wsHeartbeatClient,
+ cors,
+ winston,
+ elasticApmNode,
+ winstonElasticsearchApm,
+ tripleBeam,
+ bodyParser,
+ morgan,
+ meta: plugin.meta,
+ publicMeta: plugin.public_meta,
+ installedLibraries: {}
+ };
+
+ if (plugin.prescript && lodash.isArray(plugin.prescript.install) && !lodash.isEmpty(plugin.prescript.install)) {
+ loggerPlugin.verbose(
+ 'plugins/index/initialization',
+ `Installing packages for plugin ${plugin.name}`
+ );
+
+ for (const library of plugin.prescript.install) {
+ context.installedLibraries[library] = await installLibrary(library);
+ }
+
+ loggerPlugin.verbose(
+ 'plugins/index/initialization',
+ `Plugin ${plugin.name} packages installed`
+ );
+ }
+
+ _eval(plugin.script, plugin.name, context, true);
+
+ loggerPlugin.verbose(
+ 'plugins/index/initialization',
+ `Plugin ${plugin.name} running`
+ );
+ } catch (err) {
+ loggerPlugin.error(
+ 'plugins/index/initialization',
+ `error while starting plugin ${plugin.name}`,
+ err.message
+ );
+ }
+ }
+
+ loggerPlugin.info(
+ '/plugins/index/initialization',
+ `Plugin server running on port: ${PORT}`
+ );
+ })
+ .catch((err) => {
+ let message = 'Plugin Initialization failed';
+
+ if (err.message) {
+ message = err.message;
+ }
+
+ if (err.statusCode && err.statusCode === 402) {
+ message = err.error.message;
+ }
+
+ loggerPlugin.error(
+ '/plugins/index/initialization err',
+ message
+ );
+
+ setTimeout(() => { process.exit(1); }, 5000);
+ });
\ No newline at end of file
diff --git a/server/plugins/routes.js b/server/plugins/routes.js
new file mode 100644
index 0000000000..9a89755248
--- /dev/null
+++ b/server/plugins/routes.js
@@ -0,0 +1,270 @@
+'use strict';
+
+const express = require('express');
+const router = express.Router();
+const {
+ getPlugins,
+ deletePlugin,
+ postPlugin,
+ putPlugin,
+ getPluginConfig,
+ putPluginConfig,
+ getPluginScript,
+ disablePlugin,
+ enablePlugin
+} = require('./controllers');
+const { checkSchema, query, body } = require('express-validator');
+const toolsLib = require('../utils/toolsLib');
+const lodash = require('lodash');
+
+router.get(
+ '/',
+ [
+ query('name').isString().notEmpty().trim().toLowerCase().optional(),
+ query('search').isString().notEmpty().optional()
+ ],
+ getPlugins
+);
+
+router.delete(
+ '/',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ query('name').isString().notEmpty().trim().toLowerCase()
+ ],
+ deletePlugin
+);
+
+router.post(
+ '/',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ body('name').isString().notEmpty().trim().toLowerCase(),
+ body('version').isInt({ min: 1 }),
+ body('author').isString(),
+ body('enabled').isBoolean().optional(),
+ body('type').isString().notEmpty().trim().toLowerCase().optional({ nullable: true }),
+ body('script').isString().notEmpty().optional({ nullable: true }),
+ body('description').isString().optional({ nullable: true }),
+ body('bio').isString().optional({ nullable: true }),
+ body('documentation').isString().optional({ nullable: true }),
+ body('icon').isString().optional({ nullable: true }),
+ body('url').isString().optional({ nullable: true }),
+ body('logo').isString().optional({ nullable: true }),
+ body('admin_view').isString().optional({ nullable: true }),
+ body('web_view').isArray().optional({ nullable: true }),
+ checkSchema({
+ prescript: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ if (!lodash.isPlainObject(value)) {
+ return false;
+ }
+ if (value.install && lodash.isArray(value.install)) {
+ for (let lib of value.install) {
+ if (!lodash.isString(lib)) {
+ return false;
+ }
+ }
+ }
+ if (value.run && !lodash.isString(value.run)) {
+ return false;
+ }
+ return true;
+ },
+ errorMessage: 'must be an object. install value must be an array of strings. run value must be a string'
+ },
+ optional: { options: { nullable: true } }
+ },
+ postscript: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ if (!lodash.isPlainObject(value)) {
+ return false;
+ }
+ if (value.run && !lodash.isString(value.run)) {
+ return false;
+ }
+ return true;
+ },
+ errorMessage: 'must be an object. run value must be a string'
+ },
+ optional: { options: { nullable: true } }
+ },
+ meta: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ return lodash.isPlainObject(value);
+ },
+ errorMessage: 'must be an object'
+ },
+ optional: { options: { nullable: true } }
+ },
+ public_meta: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ return lodash.isPlainObject(value);
+ },
+ errorMessage: 'must be an object'
+ },
+ optional: { options: { nullable: true } }
+ }
+ })
+ ],
+ postPlugin
+);
+
+router.put(
+ '/',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ body('name').isString().notEmpty().trim().toLowerCase(),
+ body('version').isInt({ min: 1 }),
+ body('type').isString().notEmpty().trim().toLowerCase().optional({ nullable: true }),
+ body('author').isString().optional({ nullable: true }),
+ body('script').isString().notEmpty().optional({ nullable: true }),
+ body('description').isString().optional({ nullable: true }),
+ body('bio').isString().optional({ nullable: true }),
+ body('documentation').isString().optional({ nullable: true }),
+ body('icon').isString().optional({ nullable: true }),
+ body('url').isString().optional({ nullable: true }),
+ body('logo').isString().optional({ nullable: true }),
+ body('admin_view').isString().optional({ nullable: true }),
+ body('web_view').isArray().optional({ nullable: true }),
+ checkSchema({
+ prescript: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ if (!lodash.isPlainObject(value)) {
+ return false;
+ }
+ if (value.install && lodash.isArray(value.install)) {
+ for (let lib of value.install) {
+ if (!lodash.isString(lib)) {
+ return false;
+ }
+ }
+ }
+ if (value.run && !lodash.isString(value.run)) {
+ return false;
+ }
+ return true;
+ },
+ errorMessage: 'must be an object. install value must be an array of strings. run value must be a string'
+ },
+ optional: { options: { nullable: true } }
+ },
+ postscript: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ if (!lodash.isPlainObject(value)) {
+ return false;
+ }
+ if (value.run && !lodash.isString(value.run)) {
+ return false;
+ }
+ return true;
+ },
+ errorMessage: 'must be an object. run value must be a string'
+ },
+ optional: { options: { nullable: true } }
+ },
+ meta: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ return lodash.isPlainObject(value);
+ },
+ errorMessage: 'must be an object'
+ },
+ optional: { options: { nullable: true } }
+ },
+ public_meta: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ return lodash.isPlainObject(value);
+ },
+ errorMessage: 'must be an object'
+ },
+ optional: { options: { nullable: true } }
+ }
+ })
+ ],
+ putPlugin
+);
+
+router.get(
+ '/meta',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ query('name').isString().notEmpty().trim().toLowerCase()
+ ],
+ getPluginConfig
+);
+
+router.put(
+ '/meta',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ body('name').isString().notEmpty().trim().toLowerCase(),
+ checkSchema({
+ meta: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ return lodash.isPlainObject(value);
+ },
+ errorMessage: 'must be an object'
+ },
+ optional: true
+ },
+ public_meta: {
+ in: ['body'],
+ custom: {
+ options: (value) => {
+ return lodash.isPlainObject(value);
+ },
+ errorMessage: 'must be an object'
+ },
+ optional: true
+ }
+ })
+ ],
+ putPluginConfig
+);
+
+router.get(
+ '/script',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ query('name').isString().notEmpty().trim().toLowerCase()
+ ],
+ getPluginScript
+);
+
+router.get(
+ '/disable',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ query('name').isString().notEmpty().trim().toLowerCase()
+ ],
+ disablePlugin
+);
+
+router.get(
+ '/enable',
+ [
+ toolsLib.security.verifyBearerTokenExpressMiddleware(['admin']),
+ query('name').isString().notEmpty().trim().toLowerCase()
+ ],
+ enablePlugin
+);
+
+module.exports = router;
\ No newline at end of file
diff --git a/server/utils/nodeLib/index.js b/server/utils/nodeLib/index.js
new file mode 100644
index 0000000000..9c81e03b32
--- /dev/null
+++ b/server/utils/nodeLib/index.js
@@ -0,0 +1,3138 @@
+'use strict';
+
+const moment = require('moment');
+const {
+ isBoolean,
+ isPlainObject,
+ isNumber,
+ isString,
+ isArray,
+ isBuffer,
+ omit,
+ isNull,
+ isEmpty,
+ snakeCase
+} = require('lodash');
+const {
+ createRequest,
+ generateHeaders,
+ checkKit,
+ createSignature,
+ parameterError,
+ isDatetime,
+ sanitizeDate,
+ isUrl
+} = require('./utils');
+const WebSocket = require('ws');
+const { setWsHeartbeat } = require('ws-heartbeat/client');
+const { reject } = require('bluebird');
+const FileType = require('file-type');
+
+class HollaExNetwork {
+ constructor(
+ opts = {
+ apiUrl: 'https://api.hollaex.network',
+ baseUrl: '/v2',
+ apiKey: '',
+ apiSecret: '',
+ apiExpiresAfter: 60,
+ activation_code: undefined, // kit activation code used only for exchange operators to initialize the exchange
+ kit_version: null
+ }
+ ) {
+ this.apiUrl = opts.apiUrl || 'https://api.hollaex.network';
+ this.baseUrl = opts.baseUrl || '/v2';
+ this.apiKey = opts.apiKey;
+ this.apiSecret = opts.apiSecret;
+ this.apiExpiresAfter = opts.apiExpiresAfter || 60;
+ this.headers = {
+ 'content-type': 'application/json',
+ Accept: 'application/json',
+ 'api-key': opts.apiKey
+ };
+
+ if (opts.kit_version) {
+ this.headers['kit-version'] = opts.kit_version;
+ }
+
+ this.activation_code = opts.activation_code;
+ this.exchange_id = opts.exchange_id;
+ const [ protocol, endpoint ] = this.apiUrl.split('://');
+ this.wsUrl =
+ protocol === 'https'
+ ? `wss://${endpoint}/stream?exchange_id=${this.exchange_id}`
+ : `ws://${endpoint}/stream?exchange_id=${this.exchange_id}`;
+ this.ws = null;
+ this.wsEvents = [];
+ this.wsReconnect = true;
+ this.wsReconnectInterval = 5000;
+ this.wsEventListeners = null;
+ this.wsConnected = () => this.ws && this.ws.readyState === WebSocket.OPEN;
+ }
+
+ /* Kit Operator Network Endpoints*/
+
+ /**
+ * Initialize your Kit for HollaEx Network. Must have passed activation_code in constructor
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Your exchange values
+ */
+ async init(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.activation_code);
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/init/${
+ this.activation_code
+ }`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ let exchange = await createRequest(
+ verb,
+ `${this.apiUrl}${path}`,
+ headers
+ );
+ this.exchange_id = exchange.id;
+ return exchange;
+ }
+
+ /**
+ * Create a user for the exchange on the network
+ * @param {string} email - Email of new user
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Created user's values on network
+ */
+ createUser(email, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!email) {
+ return reject(parameterError('email', 'cannot be null'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/signup`;
+ const data = { email };
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Get all trades for the exchange on the network
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.symbol - Symbol of trades. Leave blank to get trades for all symbols
+ * @param {number} opts.limit - Amount of trades per page
+ * @param {number} opts.page - Page of trades data
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id. Default: id
+ * @param {string} opts.order - Ascending (asc) or descending (desc). Default: desc
+ * @param {string} opts.startDate - Start date of query in ISO8601 format
+ * @param {string} opts.endDate - End date of query in ISO8601 format
+ * @param {string} opts.format - Custom format of data set. Enum: ['all']
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Fields: Count, Data. Count is the number of trades on the page. Data is an array of trades
+ */
+ getTrades(
+ opts = {
+ symbol: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ format: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${this.exchange_id}/user/trades?`;
+
+ if (isString(opts.symbol)) {
+ path += `&symbol=${opts.symbol}`;
+ }
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (isString(opts.format)) {
+ path += `&format=${opts.format}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb, path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all trades for a user on the network
+ * @param {number} userId - User id on network
+ * @param {object} opts - Optional parameters
+ * @param {string} opts.symbol - Symbol of trades. Leave blank to get trades for all symbols
+ * @param {number} opts.limit - Amount of trades per page
+ * @param {number} opts.page - Page of trades data
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id. Default: id
+ * @param {string} opts.order - Ascending (asc) or descending (desc). Default: desc
+ * @param {string} opts.startDate - Start date of query in ISO8601 format
+ * @param {string} opts.endDate - End date of query in ISO8601 format
+ * @param {string} opts.format - Custom format of data set. Enum: ['all']
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Fields: Count, Data. Count is the number of trades on the page. Data is an array of trades
+ */
+ getUserTrades(
+ userId,
+ opts = {
+ symbol: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ format: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${this.exchange_id}/user/trades?user_id=${userId}`;
+
+ if (isString(opts.symbol)) {
+ path += `&symbol=${opts.symbol}`;
+ }
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (isString(opts.format)) {
+ path += `&format=${opts.format}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get user network data
+ * @param {number} userId - User's network id
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} User network data
+ */
+ getUser(userId, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/user?user_id=${userId}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all users for the exchange on the network
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Fields: Count, Data. Count is the number of users for the exchange on the network. Data is an array of users
+ */
+ getUsers(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${this.exchange_id}/users`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Create a crypto address for user
+ * @param {number} userId - User id on network.
+ * @param {string} crypto - Crypto to create address for.
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.network - Crypto's blockchain network
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with new address
+ */
+ createUserCryptoAddress(userId, crypto, opts = {
+ network: null,
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!crypto) {
+ return reject(parameterError('crypto', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${this.exchange_id}/create-address?user_id=${userId}&crypto=${crypto}`;
+
+ if (opts.network) {
+ path += `&network=${opts.network}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Create a withdrawal for an exchange's user on the network
+ * @param {number} userId - User id on network
+ * @param {string} address - Address to send withdrawal to
+ * @param {string} currency - Curreny to withdraw
+ * @param {number} amount - Amount to withdraw
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.network - Specify crypto currency network
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Withdrawal made on the network
+ */
+ performWithdrawal(userId, address, currency, amount, opts = {
+ network: null,
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!address) {
+ return reject(parameterError('address', 'cannot be null'));
+ } else if (!currency) {
+ return reject(parameterError('currency', 'cannot be null'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/withdraw?user_id=${userId}`;
+ const data = { address, currency, amount };
+ if (opts.network) {
+ data.network = opts.network;
+ }
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Cancel a withdrawal for an exchange's user on the network
+ * @param {number} userId - User id on network
+ * @param {string} withdrawalId - Withdrawal's id on network (not transaction id).
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Withdrawal canceled on the network
+ */
+ cancelWithdrawal(userId, withdrawalId, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!withdrawalId) {
+ return reject(parameterError('withdrawalId', 'cannot be null'));
+ }
+
+ const verb = 'DELETE';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/withdraw?user_id=${userId}&id=${withdrawalId}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all deposits for the exchange on the network
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.currency - Currency of deposits. Leave blank to get deposits for all currencies
+ * @param {boolean} opts.status - Confirmed status of the deposits to get. Leave blank to get all confirmed and unconfirmed deposits
+ * @param {boolean} opts.dismissed - Dismissed status of the deposits to get. Leave blank to get all dismissed and undismissed deposits
+ * @param {boolean} opts.rejected - Rejected status of the deposits to get. Leave blank to get all rejected and unrejected deposits
+ * @param {boolean} opts.processing - Processing status of the deposits to get. Leave blank to get all processing and unprocessing deposits
+ * @param {boolean} opts.waiting - Waiting status of the deposits to get. Leave blank to get all waiting and unwaiting deposits
+ * @param {number} opts.limit - Amount of trades per page. Maximum: 50. Default: 50
+ * @param {number} opts.page - Page of trades data. Default: 1
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id.
+ * @param {string} opts.order - Ascending (asc) or descending (desc).
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {string} opts.transactionId - Deposit with specific transaction ID.
+ * @param {string} opts.address - Deposits with specific address.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Fields: Count, Data. Count is the number of deposits on the page. Data is an array of deposits
+ */
+ getDeposits(
+ opts = {
+ currency: null,
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ transactionId: null,
+ address: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/deposits?`;
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isString(opts.address)) {
+ path += `&address=${opts.address}`;
+ }
+
+ if (isString(opts.transactionId)) {
+ path += `&transaction_id=${opts.transactionId}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (opts.currency) {
+ path += `¤cy=${opts.currency}`;
+ }
+
+ if (isBoolean(opts.status)) {
+ path += `&status=${opts.status}`;
+ }
+
+ if (isBoolean(opts.dismissed)) {
+ path += `&dismissed=${opts.dismissed}`;
+ }
+
+ if (isBoolean(opts.rejected)) {
+ path += `&rejected=${opts.rejected}`;
+ }
+
+ if (isBoolean(opts.processing)) {
+ path += `&processing=${opts.processing}`;
+ }
+
+ if (isBoolean(opts.waiting)) {
+ path += `&waiting=${opts.waiting}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all deposits for a user on the network
+ * @param {number} userId - User id on network. Leave blank to get all deposits for the exchange
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.currency - Currency of deposits. Leave blank to get deposits for all currencies
+ * @param {boolean} opts.status - Confirmed status of the deposits to get. Leave blank to get all confirmed and unconfirmed deposits
+ * @param {boolean} opts.dismissed - Dismissed status of the deposits to get. Leave blank to get all dismissed and undismissed deposits
+ * @param {boolean} opts.rejected - Rejected status of the deposits to get. Leave blank to get all rejected and unrejected deposits
+ * @param {boolean} opts.processing - Processing status of the deposits to get. Leave blank to get all processing and unprocessing deposits
+ * @param {boolean} opts.waiting - Waiting status of the deposits to get. Leave blank to get all waiting and unwaiting deposits
+ * @param {number} opts.limit - Amount of trades per page. Maximum: 50. Default: 50
+ * @param {number} opts.page - Page of trades data. Default: 1
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id.
+ * @param {string} opts.order - Ascending (asc) or descending (desc).
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {string} opts.transactionId - Deposit with specific transaction ID.
+ * @param {string} opts.address - Deposits with specific address.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Fields: Count, Data. Count is the number of deposits on the page. Data is an array of deposits
+ */
+ getUserDeposits(
+ userId,
+ opts = {
+ currency: null,
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ transactionId: null,
+ address: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/deposits?user_id=${userId}`;
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isString(opts.address)) {
+ path += `&address=${opts.address}`;
+ }
+
+ if (isString(opts.transactionId)) {
+ path += `&transaction_id=${opts.transactionId}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (opts.currency) {
+ path += `¤cy=${opts.currency}`;
+ }
+
+ if (isBoolean(opts.status)) {
+ path += `&status=${opts.status}`;
+ }
+
+ if (isBoolean(opts.dismissed)) {
+ path += `&dismissed=${opts.dismissed}`;
+ }
+
+ if (isBoolean(opts.rejected)) {
+ path += `&rejected=${opts.rejected}`;
+ }
+
+ if (isBoolean(opts.processing)) {
+ path += `&processing=${opts.processing}`;
+ }
+
+ if (isBoolean(opts.waiting)) {
+ path += `&waiting=${opts.waiting}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all withdrawals for the exchange on the network
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.currency - Currency of withdrawals. Leave blank to get withdrawals for all currencies
+ * @param {boolean} opts.status - Confirmed status of the withdrawals to get. Leave blank to get all confirmed and unconfirmed withdrawals
+ * @param {boolean} opts.dismissed - Dismissed status of the withdrawals to get. Leave blank to get all dismissed and undismissed withdrawals
+ * @param {boolean} opts.rejected - Rejected status of the withdrawals to get. Leave blank to get all rejected and unrejected withdrawals
+ * @param {boolean} opts.processing - Processing status of the withdrawals to get. Leave blank to get all processing and unprocessing withdrawals
+ * @param {boolean} opts.waiting - Waiting status of the withdrawals to get. Leave blank to get all waiting and unwaiting withdrawals
+ * @param {number} opts.limit - Amount of trades per page. Maximum: 50. Default: 50
+ * @param {number} opts.page - Page of trades data. Default: 1
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id.
+ * @param {string} opts.order - Ascending (asc) or descending (desc).
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {string} opts.transactionId - Withdrawals with specific transaction ID.
+ * @param {string} opts.address - Withdrawals with specific address.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Fields: Count, Data. Count is the number of withdrawals on the page. Data is an array of withdrawals
+ */
+ getWithdrawals(
+ opts = {
+ currency: null,
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ transactionId: null,
+ address: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/withdrawals?`;
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isString(opts.address)) {
+ path += `&address=${opts.address}`;
+ }
+
+ if (isString(opts.transactionId)) {
+ path += `&transaction_id=${opts.transactionId}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (opts.currency) {
+ path += `¤cy=${opts.currency}`;
+ }
+
+ if (isBoolean(opts.status)) {
+ path += `&status=${opts.status}`;
+ }
+
+ if (isBoolean(opts.dismissed)) {
+ path += `&dismissed=${opts.dismissed}`;
+ }
+
+ if (isBoolean(opts.rejected)) {
+ path += `&rejected=${opts.rejected}`;
+ }
+
+ if (isBoolean(opts.processing)) {
+ path += `&processing=${opts.processing}`;
+ }
+
+ if (isBoolean(opts.waiting)) {
+ path += `&waiting=${opts.waiting}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all withdrawals for a user on the network
+ * @param {number} userId - User id on network. Leave blank to get all withdrawals for the exchange
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.currency - Currency of withdrawals. Leave blank to get withdrawals for all currencies
+ * @param {boolean} opts.status - Confirmed status of the depowithdrawalssits to get. Leave blank to get all confirmed and unconfirmed withdrawals
+ * @param {boolean} opts.dismissed - Dismissed status of the withdrawals to get. Leave blank to get all dismissed and undismissed withdrawals
+ * @param {boolean} opts.rejected - Rejected status of the withdrawals to get. Leave blank to get all rejected and unrejected withdrawals
+ * @param {boolean} opts.processing - Processing status of the withdrawals to get. Leave blank to get all processing and unprocessing withdrawals
+ * @param {boolean} opts.waiting - Waiting status of the withdrawals to get. Leave blank to get all waiting and unwaiting withdrawals
+ * @param {number} opts.limit - Amount of trades per page. Maximum: 50. Default: 50
+ * @param {number} opts.page - Page of trades data. Default: 1
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id.
+ * @param {string} opts.order - Ascending (asc) or descending (desc).
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {string} opts.transactionId - Withdrawals with specific transaction ID.
+ * @param {string} opts.address - Withdrawals with specific address.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Fields: Count, Data. Count is the number of withdrawals on the page. Data is an array of withdrawals
+ */
+ getUserWithdrawals(
+ userId,
+ opts = {
+ currency: null,
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ transactionId: null,
+ address: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/withdrawals?user_id=${userId}`;
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isString(opts.address)) {
+ path += `&address=${opts.address}`;
+ }
+
+ if (isString(opts.transactionId)) {
+ path += `&transaction_id=${opts.transactionId}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (opts.currency) {
+ path += `¤cy=${opts.currency}`;
+ }
+
+ if (isBoolean(opts.status)) {
+ path += `&status=${opts.status}`;
+ }
+
+ if (isBoolean(opts.dismissed)) {
+ path += `&dismissed=${opts.dismissed}`;
+ }
+
+ if (isBoolean(opts.rejected)) {
+ path += `&rejected=${opts.rejected}`;
+ }
+
+ if (isBoolean(opts.processing)) {
+ path += `&processing=${opts.processing}`;
+ }
+
+ if (isBoolean(opts.waiting)) {
+ path += `&waiting=${opts.waiting}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get the balance for the exchange on the network
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Available, pending, and total balance for all currencies for your exchange on the network
+ */
+ getBalance(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${this.exchange_id}/balance`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get the balance for an exchange's user on the network
+ * @param {number} userId - User id on network
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Available, pending, and total balance for all currencies for your exchange on the network
+ */
+ getUserBalance(userId, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${this.exchange_id}/balance?user_id=${userId}`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get an order for the exchange on the network
+ * @param {number} userId - Id of order's user
+ * @param {number} orderId - Order id
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Order on the network with current data e.g. side, size, filled, etc.
+ */
+ getOrder(userId, orderId, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!orderId) {
+ return reject(parameterError('orderId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/order?user_id=${userId}&order_id=${orderId}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Create a new order for the exchange on the network
+ * @param {number} userId - User id on the network
+ * @param {string} symbol - The currency pair symbol e.g. 'hex-usdt'
+ * @param {string} side - The side of the order e.g. 'buy', 'sell'
+ * @param {number} size - The amount of currency to order
+ * @param {string} type - The type of order to create e.g. 'market', 'limit'
+ * @param {number} price - The price at which to order (only required if type is 'limit')
+ * @param {object} feeData - Object with fee data
+ * @param {object} feeData.fee_structure - Object with maker and taker fees
+ * @param {number} feeData.fee_structure.maker - Maker fee.
+ * @param {number} feeData.fee_structure.taker - Taker fee
+ * @param {object} opts - Optional parameters.
+ * @param {number} opts.stop - Stop price of order. This makes the order a stop loss order.
+ * @param {object} opts.meta - Meta values for order.
+ * @param {boolean} opts.meta.post_only - Whether or not the order should only be made if market maker.
+ * @param {string} opts.meta.note - Additional note to add to order data.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Newly created order values e.g. symbol, id, side, status, etc.
+ */
+ createOrder(
+ userId,
+ symbol,
+ side,
+ size,
+ type,
+ price = 0,
+ feeData = {
+ fee_structure: null,
+ fee_coin: null
+ },
+ opts = {
+ stop: null,
+ meta: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!symbol) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ } else if (side !== 'buy' && side !== 'sell') {
+ return reject(parameterError('side', 'must be buy or sell'));
+ } else if (!size) {
+ return reject(parameterError('size', 'cannot be null'));
+ } else if (type !== 'market' && type !== 'limit') {
+ return reject(parameterError('type', 'must be limit or market'));
+ } else if (!price && type !== 'market') {
+ return reject(parameterError('price', 'cannot be null for limit orders'));
+ } else if (!isPlainObject(feeData) || !isPlainObject(feeData.fee_structure)) {
+ return reject(parameterError('feeData', 'feeData must be an object and contain fee_structure'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/order?user_id=${userId}`;
+ const data = { symbol, side, size, type, price };
+
+ if (isPlainObject(feeData.fee_structure)) {
+ data.fee_structure = feeData.fee_structure;
+ }
+
+ if (feeData.fee_coin) {
+ data.fee_coin = feeData.fee_coin;
+ }
+
+ if (isPlainObject(opts.meta)) {
+ data.meta = opts.meta;
+ }
+
+ if (isNumber(opts.stop)) {
+ data.stop = opts.stop;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Cancel an order for the exchange on the network
+ * @param {number} userId - Id of order's user
+ * @param {number} orderId - Order id
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Value of canceled order on the network with values side, size, filled, etc.
+ */
+ cancelOrder(userId, orderId, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!orderId) {
+ return reject(parameterError('orderId', 'cannot be null'));
+ }
+
+ const verb = 'DELETE';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/order?user_id=${userId}&order_id=${orderId}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all orders for the exchange on the network
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.symbol - Symbol of orders. Leave blank to get orders for all symbols
+ * @param {string} opts.side - Side of orders to query e.g. buy, sell
+ * @param {string} opts.type - Type of orders to query e.g. active, stop
+ * @param {number} opts.limit - Amount of trades per page. Maximum: 50. Default: 50
+ * @param {number} opts.page - Page of trades data. Default: 1
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id.
+ * @param {string} opts.order - Ascending (asc) or descending (desc).
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {array} Array of queried orders
+ */
+ getOrders(
+ opts = {
+ symbol: null,
+ side: null,
+ status: null,
+ open: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${this.exchange_id}/orders?`;
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (opts.symbol) {
+ path += `&symbol=${opts.symbol}`;
+ }
+
+ if (opts.side) {
+ path += `&side=${opts.side}`;
+ }
+
+ if (opts.status) {
+ path += `&status=${opts.status}`;
+ }
+
+ if (isBoolean(opts.open)) {
+ path += `&open=${opts.open}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get all orders for a user on the network
+ * @param {number} userId - User id on network. Leave blank to get all orders for the exchange
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.symbol - Symbol of orders. Leave blank to get orders for all symbols
+ * @param {string} opts.side - Side of orders to query e.g. buy, sell
+ * @param {string} opts.type - Type of orders to query e.g. active, stop
+ * @param {number} opts.limit - Amount of trades per page. Maximum: 50. Default: 50
+ * @param {number} opts.page - Page of trades data. Default: 1
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id.
+ * @param {string} opts.order - Ascending (asc) or descending (desc).
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {array} Array of queried orders
+ */
+ getUserOrders(
+ userId,
+ opts = {
+ symbol: null,
+ side: null,
+ status: null,
+ open: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${this.exchange_id}/orders?user_id=${userId}`;
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (opts.symbol) {
+ path += `&symbol=${opts.symbol}`;
+ }
+
+ if (opts.side) {
+ path += `&side=${opts.side}`;
+ }
+
+ if (opts.status) {
+ path += `&status=${opts.status}`;
+ }
+
+ if (isBoolean(opts.open)) {
+ path += `&open=${opts.open}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Cancel all orders for an exchange's user on the network
+ * @param {number} userId - User id on network
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.symbol - Symbol of orders to cancel. Leave blank to cancel user's orders for all symbols
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {array} Array of canceled orders
+ */
+ cancelAllOrders(userId, opts = {
+ symbol: null,
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'DELETE';
+
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/order/all?user_id=${userId}`;
+ if (opts.symbol) {
+ path += `&symbol=${opts.symbol}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get sum of user trades and its stats
+ * @param {number} userId - User id on network
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with field data that contains stats info
+ */
+ getUserStats(userId, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/user/stats?user_id=${userId}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Check transaction in network. Will update transaction status on Kit accordingly
+ * @param {string} currency; - Currency of transaction
+ * @param {string} transactionId - Transaction id
+ * @param {string} address - Transaction receiving address
+ * @param {string} network - Crypto's blockchain network
+ * @param {object} opts - Optional parameters.
+ * @param {boolean} opts.isTestnet - Specify transaction was made on testnet blockchain.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Success or failed message
+ */
+ checkTransaction(
+ currency,
+ transactionId,
+ address,
+ network,
+ opts = {
+ isTestnet: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!currency) {
+ return reject(parameterError('currency', 'cannot be null'));
+ } else if (!transactionId) {
+ return reject(parameterError('transactionId', 'cannot be null'));
+ } else if (!address) {
+ return reject(parameterError('address', 'cannot be null'));
+ } else if (!network) {
+ return reject(parameterError('network', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ let path = `${this.baseUrl}/check-transaction?currency=${currency}&transaction_id=${transactionId}&address=${address}&network=${network}`;
+
+ if (isBoolean(opts.isTestnet)) {
+ path += `&is_testnet=${opts.isTestnet}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Transfer funds between two users
+ * @param {number} senderId; - Network id of user that is sending funds
+ * @param {number} receiverId - Network id of user that is receiving funds
+ * @param {string} currency - Currency to transfer
+ * @param {number} amount - Amount to transfer
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.description - Description of transfer.
+ * @param {boolean} opts.email - Send email to users after transfer. Default: true.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with field transaction_id
+ */
+ transferAsset(
+ senderId,
+ receiverId,
+ currency,
+ amount,
+ opts = {
+ description: null,
+ email: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!senderId) {
+ return reject(parameterError('senderId', 'cannot be null'));
+ } else if (!receiverId) {
+ return reject(parameterError('receiverId', 'cannot be null'));
+ } else if (!currency) {
+ return reject(parameterError('currency', 'cannot be null'));
+ } else if (!amount) {
+ return reject(parameterError('amount', 'cannot be null'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/transfer`;
+ const data = {
+ sender_id: senderId,
+ receiver_id: receiverId,
+ currency,
+ amount
+ };
+
+ if (opts.description) {
+ data.description = opts.description;
+ }
+
+ if (isBoolean(opts.email)) {
+ data.email = opts.email;
+ } else {
+ data.email = true;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Get trade history for exchange on network
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.symbol - Symbol of trades.
+ * @param {string} opts.side - Side of trades.
+ * @param {number} opts.limit - Amount of trades per page. Maximum: 50. Default: 50
+ * @param {number} opts.page - Page of trades data. Default: 1
+ * @param {string} opts.orderBy - The field to order data by e.g. amount, id.
+ * @param {string} opts.order - Ascending (asc) or descending (desc).
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Count and data format.
+ */
+ getTradesHistory(
+ opts = {
+ symbol: null,
+ side: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/trades/history?`;
+
+ if (isNumber(opts.limit)) {
+ path += `&limit=${opts.limit}`;
+ }
+
+ if (isNumber(opts.page)) {
+ path += `&page=${opts.page}`;
+ }
+
+ if (isString(opts.orderBy)) {
+ path += `&order_by=${opts.orderBy}`;
+ }
+
+ if (isString(opts.order)) {
+ path += `&order=${opts.order}`;
+ }
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ if (opts.symbol) {
+ path += `&symbol=${opts.symbol}`;
+ }
+
+ if (opts.side) {
+ path += `&side=${opts.side}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /* Network Engine Endpoints*/
+
+ /**
+ * Get Public trades on network
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.symbol - Symbol to get trades for. Leave blank to get trades of all symbols
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with trades
+ */
+ getPublicTrades(opts = {
+ symbol: null,
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${this.exchange_id}/trades`;
+
+ if (opts.symbol) {
+ path += `?symbol=${opts.symbol}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get top orderbook for specific symbol
+ * @param {string} symbol - Symbol to get orderbook for. Leave blank to get orderbook of all symbols
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with orderbook
+ */
+ getOrderbook(symbol, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!symbol) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/orderbook`;
+
+ if (symbol) {
+ path += `?symbol=${symbol}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get top orderbooks
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with orderbook
+ */
+ getOrderbooks(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/orderbooks`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get TradingView trade history HOLCV
+ * @param {string} from - Starting date of trade history in UNIX timestamp format
+ * @param {string} to - Ending date of trade history in UNIX timestamp format
+ * @param {string} symbol - Symbol to get trade history for
+ * @param {string} resolution - Resolution of trade history. 1d, 1W, etc
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with trade history info
+ */
+ getChart(from, to, symbol, resolution, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!from) {
+ return reject(parameterError('from', 'cannot be null'));
+ } else if (!to) {
+ return reject(parameterError('to', 'cannot be null'));
+ } else if (!symbol) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ } else if (!resolution) {
+ return reject(parameterError('resolution', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/chart?from=${from}&to=${to}&symbol=${symbol}&resolution=${resolution}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get TradingView trade history HOLCV for all pairs
+ * @param {string} from - Starting date of trade history in UNIX timestamp format
+ * @param {string} to - Ending date of trade history in UNIX timestamp format
+ * @param {string} resolution - Resolution of trade history. 1d, 1W, etc
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {array} Array of objects with trade history info
+ */
+ getCharts(from, to, resolution, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!from) {
+ return reject(parameterError('from', 'cannot be null'));
+ } else if (!to) {
+ return reject(parameterError('to', 'cannot be null'));
+ } else if (!resolution) {
+ return reject(parameterError('resolution', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/charts?from=${from}&to=${to}&resolution=${resolution}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get TradingView udf config
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with TradingView udf config
+ */
+ getUdfConfig(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/udf/config`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get TradingView udf history HOLCV
+ * @param {string} from - Starting date in UNIX timestamp format
+ * @param {string} to - Ending date in UNIX timestamp format
+ * @param {string} symbol - Symbol to get
+ * @param {string} resolution - Resolution of query. 1d, 1W, etc
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with TradingView udf history HOLCV
+ */
+ getUdfHistory(from, to, symbol, resolution, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!from) {
+ return reject(parameterError('from', 'cannot be null'));
+ } else if (!to) {
+ return reject(parameterError('to', 'cannot be null'));
+ } else if (!symbol) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ } else if (!resolution) {
+ return reject(parameterError('resolution', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/udf/history?from=${from}&to=${to}&symbol=${symbol}&resolution=${resolution}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get TradingView udf symbols
+ * @param {string} symbol - Symbol to get
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with TradingView udf symbols
+ */
+ getUdfSymbols(symbol, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!symbol) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/udf/symbols?symbol=${symbol}`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get historical data, time interval is 5 minutes
+ * @param {string} symbol - Symbol to get
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with historical data
+ */
+ getTicker(symbol, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!symbol) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ }
+
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${this.exchange_id}/ticker`;
+
+ if (symbol) {
+ path += `?symbol=${symbol}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Get historical data for all symbols, time interval is 5 minutes
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with historical data for all symbols
+ */
+ getTickers(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/tickers`;
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Mint an asset you own to a user
+ * @param {number} userId; - Network id of user.
+ * @param {string} currency - Currency to mint.
+ * @param {number} amount - Amount to mint.
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.description - Description of transfer.
+ * @param {string} opts.transactionId - Custom transaction ID for mint.
+ * @param {boolean} opts.status - Status of mint created. Default: true.
+ * @param {boolean} opts.email - Send email notification to user. Default: true.
+ * @param {number} opts.fee - Optional fee to display in data.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with created mint's data.
+ */
+ mintAsset(userId, currency, amount, opts = {
+ description: null,
+ transactionId: null,
+ status: true,
+ email: true,
+ fee: null,
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!currency) {
+ return reject(parameterError('currency', 'cannot be null'));
+ } else if (!amount) {
+ return reject(parameterError('amount', 'cannot be null'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${this.exchange_id}/mint`;
+ const data = {
+ user_id: userId,
+ currency,
+ amount
+ };
+
+ if (opts.description) {
+ data.description = opts.description;
+ }
+
+ if (opts.transactionId) {
+ data.transaction_id = opts.transactionId;
+ }
+
+ if (isBoolean(opts.status)) {
+ data.status = opts.status;
+ } else {
+ data.status = true;
+ }
+
+ if (isBoolean(opts.email)) {
+ data.email = opts.email;
+ } else {
+ data.email = true;
+ }
+
+ if (isNumber(opts.fee)) {
+ data.fee = opts.fee;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Update a pending mint
+ * @param {string} transactionId; - Transaction ID of pending mint.
+ * @param {object} opts - Optional parameters.
+ * @param {boolean} opts.status - Set to true to confirm pending mint.
+ * @param {boolean} opts.dismissed - Set to true to dismiss pending mint.
+ * @param {boolean} opts.rejected - Set to true to reject pending mint.
+ * @param {boolean} opts.processing - Set to true to set state to processing.
+ * @param {boolean} opts.waiting - Set to true to set state to waiting.
+ * @param {string} opts.updatedTransactionId - Value to update transaction ID of pending mint to.
+ * @param {boolean} opts.email - Send email notification to user. Default: true.
+ * @param {string} opts.updatedDescription - Value to update transaction description to.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with updated mint's data.
+ */
+ updatePendingMint(
+ transactionId,
+ opts = {
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ updatedTransactionId: null,
+ email: true,
+ updatedDescription: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!transactionId) {
+ return reject(parameterError('transactionId', 'cannot be null'));
+ }
+
+ const status = isBoolean(opts.status) ? opts.status : false;
+ const rejected = isBoolean(opts.rejected) ? opts.rejected : false;
+ const dismissed = isBoolean(opts.dismissed) ? opts.dismissed : false;
+ const processing = isBoolean(opts.processing) ? opts.processing : false;
+ const waiting = isBoolean(opts.waiting) ? opts.waiting : false;
+
+ if (!status && !rejected && !dismissed && !processing && !waiting) {
+ return reject(new Error('Must give one parameter to update'));
+ } else if (
+ status && (rejected || dismissed || processing || waiting)
+ || rejected && (status || dismissed || processing || waiting)
+ || dismissed && (status || rejected || processing || waiting)
+ || processing && (status || dismissed || rejected || waiting)
+ || waiting && (status || rejected || dismissed || processing)
+ ) {
+ return reject(new Error('Can only update one parmaeter'));
+ }
+
+ const verb = 'PUT';
+ const path = `${this.baseUrl}/network/${this.exchange_id}/mint`;
+ const data = {
+ transaction_id: transactionId,
+ status,
+ rejected,
+ dismissed,
+ processing,
+ waiting
+ };
+
+ if (opts.updatedTransactionId) {
+ data.updated_transaction_id = opts.updatedTransactionId;
+ }
+
+ if (opts.updatedDescription) {
+ data.updated_description = opts.updatedDescription;
+ }
+
+ if (isBoolean(opts.email)) {
+ data.email = opts.email;
+ } else {
+ data.email = true;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Burn an asset you own to a user
+ * @param {number} userId; - Network id of user.
+ * @param {string} currency - Currency to burn.
+ * @param {number} amount - Amount to burn.
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.description - Description of transfer.
+ * @param {string} opts.transactionId - Custom transaction ID for burn.
+ * @param {boolean} opts.status - Status of burn created. Default: true.
+ * @param {boolean} opts.email - Send email notification to user. Default: true.
+ * @param {number} opts.fee - Optional fee to display in data.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with created burn's data.
+ */
+ burnAsset(userId, currency, amount, opts = {
+ description: null,
+ transactionId: null,
+ status: true,
+ email: true,
+ fee: null,
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!userId) {
+ return reject(parameterError('userId', 'cannot be null'));
+ } else if (!currency) {
+ return reject(parameterError('currency', 'cannot be null'));
+ } else if (!amount) {
+ return reject(parameterError('amount', 'cannot be null'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${this.exchange_id}/burn`;
+ const data = {
+ user_id: userId,
+ currency,
+ amount
+ };
+
+ if (opts.description) {
+ data.description = opts.description;
+ }
+
+ if (opts.transactionId) {
+ data.transaction_id = opts.transactionId;
+ }
+
+ if (isBoolean(opts.status)) {
+ data.status = opts.status;
+ } else {
+ data.status = true;
+ }
+
+ if (isBoolean(opts.email)) {
+ data.email = opts.email;
+ } else {
+ data.email = true;
+ }
+
+ if (isNumber(opts.fee)) {
+ data.fee = opts.fee;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Update a pending burn
+ * @param {string} transactionId; - Transaction ID of pending burn.
+ * @param {object} opts - Optional parameters.
+ * @param {boolean} opts.status - Set to true to confirm pending burn.
+ * @param {boolean} opts.dismissed - Set to true to dismiss pending burn.
+ * @param {boolean} opts.rejected - Set to true to reject pending burn.
+ * @param {boolean} opts.processing - Set to true to set state to processing.
+ * @param {boolean} opts.waiting - Set to true to set state to waiting.
+ * @param {string} opts.updatedTransactionId - Value to update transaction ID of pending burn to.
+ * @param {boolean} opts.email - Send email notification to user. Default: true.
+ * @param {string} opts.updatedDescription - Value to update transaction description to.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with updated burn's data.
+ */
+ updatePendingBurn(
+ transactionId,
+ opts = {
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ updatedTransactionId: null,
+ email: true,
+ updatedDescription: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!transactionId) {
+ return reject(parameterError('transactionId', 'cannot be null'));
+ }
+
+ const status = isBoolean(opts.status) ? opts.status : false;
+ const rejected = isBoolean(opts.rejected) ? opts.rejected : false;
+ const dismissed = isBoolean(opts.dismissed) ? opts.dismissed : false;
+ const processing = isBoolean(opts.processing) ? opts.processing : false;
+ const waiting = isBoolean(opts.waiting) ? opts.waiting : false;
+
+ if (!status && !rejected && !dismissed && !processing && !waiting) {
+ return reject(new Error('Must give one parameter to update'));
+ } else if (
+ status && (rejected || dismissed || processing || waiting)
+ || rejected && (status || dismissed || processing || waiting)
+ || dismissed && (status || rejected || processing || waiting)
+ || processing && (status || dismissed || rejected || waiting)
+ || waiting && (status || rejected || dismissed || processing)
+ ) {
+ return reject(new Error('Can only update one parmaeter'));
+ }
+
+ const verb = 'PUT';
+ const path = `${this.baseUrl}/network/${this.exchange_id}/burn`;
+ const data = {
+ transaction_id: transactionId,
+ status,
+ rejected,
+ dismissed,
+ processing,
+ waiting
+ };
+
+ if (opts.updatedTransactionId) {
+ data.updated_transaction_id = opts.updatedTransactionId;
+ }
+
+ if (opts.updatedDescription) {
+ data.updated_description = opts.updatedDescription;
+ }
+
+ if (isBoolean(opts.email)) {
+ data.email = opts.email;
+ } else {
+ data.email = true;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ /**
+ * Get generated fees for exchange
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.startDate - Start date of query in ISO8601 format.
+ * @param {string} opts.endDate - End date of query in ISO8601 format.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with generated fees
+ */
+ getGeneratedFees(
+ opts = {
+ startDate: null,
+ endDate: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ let path = `${this.baseUrl}/network/${this.exchange_id}/fees?`;
+
+ if (isDatetime(opts.startDate)) {
+ path += `&start_date=${sanitizeDate(opts.startDate)}`;
+ }
+
+ if (isDatetime(opts.endDate)) {
+ path += `&end_date=${sanitizeDate(opts.endDate)}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Settle exchange fees
+ * @param {object} opts - Optional parameters.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with settled fees.
+ */
+ settleFees(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+
+ const path = `${this.baseUrl}/network/${this.exchange_id}/fees/settle`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ /**
+ * Convert assets to a quote asset
+ * @param {array} assets - Array of assets to convert as strings
+ * @param {object} opts - Optional parameters.
+ * @param {string} opts.quote - Quote asset to convert to. Default: usdt.
+ * @param {number} opts.amount - Amount of quote asset to convert to. Default: 1.
+ * @param {object} opts.additionalHeaders - Object storing addtional headers to send with request.
+ * @return {object} Object with converted assets.
+ */
+ getOraclePrices(assets = [], opts = {
+ quote: 'usdt',
+ amount: 1,
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!assets || !isArray(assets) || assets.length === 0) {
+ return reject(parameterError('assets', 'must be an array with length greater than one'));
+ }
+
+ assets = assets.join(',');
+
+ const verb = 'GET';
+ let path = `${this.baseUrl}/oracle/prices?exchange_id=${
+ this.exchange_id
+ }&assets=${assets}`;
+
+ if (isString(opts.quote)) {
+ path += `"e=${opts.quote}`;
+ } else {
+ path += '"e=usdt';
+ }
+
+ if (isNumber(opts.amount)) {
+ path += `&amount=${opts.amount}`;
+ } else {
+ path += '&amount=1';
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ getConstants(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${this.exchange_id}/constants`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ getExchange(opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${this.exchange_id}/exchange`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ updateExchange(
+ fields = {
+ info: null,
+ isPublic: null,
+ type: null,
+ name: null,
+ displayName: null,
+ url: null,
+ businessInfo: null,
+ pairs: null,
+ coins: null
+ },
+ opts = {
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ const verb = 'PUT';
+ const path = `${this.baseUrl}/network/${this.exchange_id}/exchange`;
+ const data = {
+ id: this.exchange_id
+ };
+
+ if (isPlainObject(fields.info)) {
+ data.info = fields.info;
+ }
+
+ if (isBoolean(fields.isPublic)) {
+ data.is_public = fields.isPublic;
+ }
+
+ if (isString(fields.type) && ['DIY', 'Cloud', 'Enterprise'].includes(fields.type)) {
+ data.type = fields.type;
+ }
+
+ if (isString(fields.name)) {
+ data.name = fields.name;
+ }
+
+ if (isString(fields.displayName)) {
+ data.display_name = fields.displayName;
+ }
+
+ if (isString(fields.url)) {
+ data.url = fields.url;
+ }
+
+ if (isPlainObject(fields.businessInfo)) {
+ data.business_info = fields.businessInfo;
+ }
+
+ if (isArray(fields.pairs) && !fields.pairs.some((pair) => !isString(pair))) {
+ data.pairs = fields.pairs;
+ }
+
+ if (isArray(fields.coins) && !fields.coins.some((coin) => !isString(coin))) {
+ data.coins = fields.coins;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ getAllCoins(
+ opts = {
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/coin/all`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ createCoin(
+ symbol,
+ fullname,
+ opts = {
+ code: null,
+ withdrawalFee: null,
+ min: null,
+ max: null,
+ incrementUnit: null,
+ logo: null,
+ meta: null,
+ estimatedPrice: null,
+ type: null,
+ network: null,
+ standard: null,
+ allowDeposit: null,
+ allowWithdrawal: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!isString(symbol)) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ } else if (!isString(fullname)) {
+ return reject(parameterError('fullname', 'cannot be null'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/coin`;
+ const data = {
+ symbol,
+ fullname
+ };
+
+ if (isString(opts.code)) {
+ data.code = opts.code;
+ }
+
+ if (isNumber(opts.withdrawalFee) && opts.withdrawalFee >= 0) {
+ data.withdrawal_fee = opts.withdrawalFee;
+ }
+
+ if (isNumber(opts.min)) {
+ data.min = opts.min;
+ }
+
+ if (isNumber(opts.max)) {
+ data.max = opts.max;
+ }
+
+ if (isNumber(opts.incrementUnit) && opts.incrementUnit >= 0) {
+ data.increment_unit = opts.incrementUnit;
+ }
+
+ if (isUrl(opts.logo)) {
+ data.logo = opts.logo;
+ }
+
+ if (isPlainObject(opts.meta)) {
+ data.meta = opts.meta;
+ }
+
+ if (isNumber(opts.estimatedPrice) && opts.estimatedPrice >= 0) {
+ data.estimated_price = opts.estimatedPrice;
+ }
+
+ if (isString(opts.type) && ['blockchain', 'fiat', 'other'].includes(opts.type)) {
+ data.type = opts.type;
+ }
+
+ if (isString(opts.network)) {
+ data.network = opts.network;
+ }
+
+ if (isString(opts.standard)) {
+ data.standard = opts.standard;
+ }
+
+ if (isBoolean(opts.allowDeposit)) {
+ data.allow_deposit = opts.allowDeposit;
+ }
+
+ if (isBoolean(opts.allowWithdrawal)) {
+ data.allow_withdrawal = opts.allowWithdrawal;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ getCoins (
+ opts = {
+ search: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/coins?`;
+
+ if (isString(opts.search)) {
+ path += `&search=${opts.search}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ updateCoin(
+ code,
+ fields = {
+ fullname: null,
+ withdrawalFee: null,
+ description: null,
+ withdrawalFees: null,
+ min: null,
+ max: null,
+ isPublic: null,
+ incrementUnit: null,
+ logo: null,
+ meta: null,
+ estimatedPrice: null,
+ type: null,
+ network: null,
+ standard: null,
+ allowDeposit: null,
+ allowWithdrawal: null
+ },
+ opts = {
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!isString(code)) {
+ return reject(parameterError('code', 'cannot be null'));
+ }
+
+ if (isEmpty(fields)) {
+ return reject(parameterError('fields', 'cannot be empty'));
+ }
+
+ const verb = 'PUT';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/coin`;
+ const data = {};
+
+ for (const field in fields) {
+ const value = fields[field];
+ const formattedField = snakeCase(field);
+
+ switch (field) {
+ case 'type':
+ if (['blockchain', 'fiat', 'other'].includes(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ case 'fullname':
+ case 'description':
+ case 'network':
+ case 'standard':
+ if (isString(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ case 'withdrawalFee':
+ case 'min':
+ case 'max':
+ case 'incrementUnit':
+ case 'estimatedPrice':
+ if (isNumber(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ case 'isPublic':
+ case 'allowDeposit':
+ case 'allowWithdrawal':
+ if (isBoolean(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ case 'logo':
+ if (isUrl(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ case 'meta':
+ case 'withdrawalFees':
+ if (isPlainObject(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (isEmpty(data)) {
+ return reject(new Error('No updatable fields given'));
+ }
+
+ data.code = code;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ getAllPairs(
+ opts = {
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ const verb = 'GET';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/pair/all`;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ createPair(
+ name,
+ baseCoin,
+ quoteCoin,
+ opts = {
+ code: null,
+ active: null,
+ minSize: null,
+ maxSize: null,
+ minPrice: null,
+ maxPrice: null,
+ incrementSize: null,
+ incrementPrice: null,
+ estimatedPrice: null,
+ isPublic: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!isString(name)) {
+ return reject(parameterError('symbol', 'cannot be null'));
+ } else if (!isString(baseCoin)) {
+ return reject(parameterError('baseCoin', 'cannot be null'));
+ } else if (!isString(quoteCoin)) {
+ return reject(parameterError('quoteCoin', 'cannot be null'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/pair`;
+ const data = {
+ name,
+ pair_base: baseCoin,
+ pair_2: quoteCoin
+ };
+
+ if (isString(opts.code)) {
+ data.code = opts.code;
+ }
+
+ if (isBoolean(opts.active)) {
+ data.active = opts.active;
+ }
+
+ if (isNumber(opts.minSize)) {
+ data.min_size = opts.minSize;
+ }
+
+ if (isNumber(opts.maxSize)) {
+ data.max_size = opts.maxSize;
+ }
+
+ if (isNumber(opts.minPrice)) {
+ data.min_price = opts.minPrice;
+ }
+
+ if (isNumber(opts.maxPrice)) {
+ data.max_price = opts.maxPrice;
+ }
+
+ if (isNumber(opts.incrementSize) && opts.incrementSize >= 0) {
+ data.increment_size = opts.incrementSize;
+ }
+
+ if (isNumber(opts.incrementPrice) && opts.incrementPrice >= 0) {
+ data.increment_price = opts.incrementPrice;
+ }
+
+ if (isNumber(opts.estimatedPrice) && opts.estimatedPrice >= 0) {
+ data.estimated_price = opts.estimatedPrice;
+ }
+
+ if (isNumber(opts.incrementUnit) && opts.incrementUnit >= 0) {
+ data.increment_unit = opts.incrementUnit;
+ }
+
+ if (isBoolean(opts.isPublic)) {
+ data.is_public = opts.isPublic;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ getPairs (
+ opts = {
+ search: null,
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ const verb = 'GET';
+ let path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/pairs?`;
+
+ if (isString(opts.search)) {
+ path += `&search=${opts.search}`;
+ }
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers);
+ }
+
+ updatePair(
+ code,
+ fields = {
+ minSize: null,
+ maxSize: null,
+ minPrice: null,
+ maxPrice: null,
+ incrementSize: null,
+ incrementPrice: null,
+ estimatedPrice: null,
+ isPublic: null,
+ circuitBreaker: null
+ },
+ opts = {
+ additionalHeaders: null
+ }
+ ) {
+ checkKit(this.exchange_id);
+
+ if (!isString(code)) {
+ return reject(parameterError('code', 'cannot be null'));
+ }
+
+ if (isEmpty(fields)) {
+ return reject(parameterError('fields', 'cannot be empty'));
+ }
+
+ const verb = 'PUT';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/pair`;
+ const data = {};
+
+ for (const field in fields) {
+ const value = fields[field];
+ const formattedField = snakeCase(field);
+
+ switch (field) {
+ case 'minSize':
+ case 'maxSize':
+ case 'minPrice':
+ case 'maxPrice':
+ case 'incrementSize':
+ case 'incrementPrice':
+ case 'estimatedPrice':
+ if (isNumber(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ case 'isPublic':
+ case 'circuitBreaker':
+ if (isBoolean(value)) {
+ data[formattedField] = value;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (isEmpty(data)) {
+ return reject(new Error('No updatable fields given'));
+ }
+
+ data.code = code;
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders) ? { ...this.headers, ...opts.additionalHeaders } : this.headers,
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ data
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { data });
+ }
+
+ async uploadIcon(image, name, opts = {
+ additionalHeaders: null
+ }) {
+ checkKit(this.exchange_id);
+
+ if (!isBuffer(image)) {
+ return reject(parameterError('image', 'must be a buffer'));
+ } else if (!isString(name)) {
+ return reject(parameterError('name', 'cannot be null'));
+ }
+
+ const { ext, mime } = await FileType.fromBuffer(image);
+
+ if (mime.indexOf('image/') !== 0) {
+ return reject(parameterError('image', 'must be an image'));
+ }
+
+ const verb = 'POST';
+ const path = `${this.baseUrl}/network/${
+ this.exchange_id
+ }/icon`;
+
+ const formData = {
+ file: {
+ value: image,
+ options: {
+ filename: `${name}.${ext}`,
+ contentType: mime
+ }
+ },
+ file_name: name
+ };
+
+ const headers = generateHeaders(
+ isPlainObject(opts.additionalHeaders)
+ ? { ...this.headers, ...opts.additionalHeaders, 'content-type': 'multipart/form-data' }
+ : { ...this.headers, 'content-type': 'multipart/form-data' },
+ this.apiSecret,
+ verb,
+ path,
+ this.apiExpiresAfter,
+ omit(formData, [ 'file' ])
+ );
+
+ return createRequest(verb, `${this.apiUrl}${path}`, headers, { formData });
+ }
+
+ /**
+ * Connect to websocket
+ * @param {array} events - Array of events to connect to
+ */
+ connect(events = []) {
+ checkKit(this.exchange_id);
+ this.wsReconnect = true;
+ this.wsEvents = events;
+ const apiExpires = moment().unix() + this.apiExpiresAfter;
+ const signature = createSignature(
+ this.apiSecret,
+ 'CONNECT',
+ '/stream',
+ apiExpires
+ );
+
+ this.ws = new WebSocket(this.wsUrl, {
+ headers: {
+ 'api-key': this.apiKey,
+ 'api-signature': signature,
+ 'api-expires': apiExpires
+ }
+ });
+
+ if (this.wsEventListeners) {
+ this.ws._events = this.wsEventListeners;
+ } else {
+ this.ws.on('unexpected-response', () => {
+ if (this.ws.readyState !== WebSocket.CLOSING) {
+ if (this.ws.readyState === WebSocket.OPEN) {
+ this.ws.close();
+ } else if (this.wsReconnect) {
+ this.wsEventListeners = this.ws._events;
+ this.ws = null;
+ setTimeout(() => {
+ this.connect(this.wsEvents);
+ }, this.wsReconnectInterval);
+ } else {
+ this.wsEventListeners = null;
+ this.ws = null;
+ }
+ }
+ });
+
+ this.ws.on('error', () => {
+ if (this.ws.readyState !== WebSocket.CLOSING) {
+ if (this.ws.readyState === WebSocket.OPEN) {
+ this.ws.close();
+ } else if (this.wsReconnect) {
+ this.wsEventListeners = this.ws._events;
+ this.ws = null;
+ setTimeout(() => {
+ this.connect(this.wsEvents);
+ }, this.wsReconnectInterval);
+ } else {
+ this.wsEventListeners = null;
+ this.ws = null;
+ }
+ }
+ });
+
+ this.ws.on('close', () => {
+ if (this.wsReconnect) {
+ this.wsEventListeners = this.ws._events;
+ this.ws = null;
+ setTimeout(() => {
+ this.connect(this.wsEvents);
+ }, this.wsReconnectInterval);
+ } else {
+ this.wsEventListeners = null;
+ this.ws = null;
+ }
+ });
+
+ this.ws.on('open', () => {
+ if (this.wsEvents.length > 0) {
+ this.subscribe(this.wsEvents);
+ }
+
+ setWsHeartbeat(this.ws, 'ping', {
+ pingTimeout: 60000,
+ pingInterval: 25000
+ });
+ });
+ }
+ }
+
+ /**
+ * Disconnect from Network websocket
+ */
+ disconnect() {
+ checkKit(this.exchange_id);
+ if (this.wsConnected()) {
+ this.wsReconnect = false;
+ this.ws.close();
+ } else {
+ throw new Error('Websocket not connected');
+ }
+ }
+
+ /**
+ * Subscribe to Network websocket events
+ * @param {array} events - The events to listen to
+ */
+ subscribe(events = []) {
+ checkKit(this.exchange_id);
+ if (this.wsConnected()) {
+ this.ws.send(
+ JSON.stringify({
+ op: 'subscribe',
+ args: events
+ })
+ );
+ } else {
+ throw new Error('Websocket not connected');
+ }
+ }
+
+ /**
+ * Unsubscribe to Network websocket events
+ * @param {array} events - The events to unsub from
+ */
+ unsubscribe(events = []) {
+ checkKit(this.exchange_id);
+ if (this.wsConnected()) {
+ this.ws.send(
+ JSON.stringify({
+ op: 'unsubscribe',
+ args: events
+ })
+ );
+ } else {
+ throw new Error('Websocket not connected');
+ }
+ }
+}
+
+module.exports = HollaExNetwork;
diff --git a/server/utils/nodeLib/utils.js b/server/utils/nodeLib/utils.js
new file mode 100644
index 0000000000..0624f5002f
--- /dev/null
+++ b/server/utils/nodeLib/utils.js
@@ -0,0 +1,85 @@
+const rp = require('request-promise');
+const crypto = require('crypto');
+const moment = require('moment');
+const { isDate } = require('lodash');
+
+const createRequest = (verb, url, headers, opts = { data: null, formData: null }) => {
+ const requestObj = {
+ headers,
+ url,
+ json: true
+ };
+
+ if (opts.data) {
+ requestObj.body = opts.data;
+ }
+
+ if (opts.formData) {
+ requestObj.formData = opts.formData;
+ }
+
+ return rp[verb.toLowerCase()](requestObj);
+};
+
+const createSignature = (secret = '', verb, path, expires, data = '') => {
+ const stringData = typeof data === 'string' ? data : JSON.stringify(data);
+
+ const signature = crypto
+ .createHmac('sha256', secret)
+ .update(verb + path + expires + stringData)
+ .digest('hex');
+ return signature;
+};
+
+const generateHeaders = (headers, secret, verb, path, expiresAfter, data) => {
+ const expires = moment().unix() + expiresAfter;
+ const signature = createSignature(secret, verb, path, expires, data);
+ const header = {
+ ...headers,
+ 'api-signature': signature,
+ 'api-expires': expires
+ };
+ return header;
+};
+
+const checkKit = (kit) => {
+ if (!kit) {
+ throw new Error(
+ 'Missing Kit ID. ID of the exchange Kit should be initialized in HollaEx constructor'
+ );
+ }
+ return true;
+};
+
+const parameterError = (parameter, msg) => {
+ return new Error(`Parameter ${parameter} error: ${msg}`);
+};
+
+const isDatetime = (date, formats = [ moment.ISO_8601 ]) => {
+ return moment(date, formats, true).isValid();
+};
+
+const sanitizeDate = (date) => {
+ let result = date;
+ if (isDate(result)) {
+ result = moment(result).toISOString();
+ }
+
+ return result;
+};
+
+const isUrl = (url) => {
+ const pattern = /^(^|\s)((http(s)?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/;
+ return pattern.test(url);
+};
+
+module.exports = {
+ createRequest,
+ createSignature,
+ generateHeaders,
+ checkKit,
+ parameterError,
+ isDatetime,
+ sanitizeDate,
+ isUrl
+};
diff --git a/server/utils/strings.js b/server/utils/strings.js
index 05254091e4..6826e90539 100644
--- a/server/utils/strings.js
+++ b/server/utils/strings.js
@@ -1,5 +1,5 @@
'use strict';
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../utils/toolsLib');
const DEFAULT_LANGUAGE = () => toolsLib.getKitConfig().defaults.language;
const VALID_LANGUAGES = () => toolsLib.getKitConfig().valid_languages;
diff --git a/server/utils/toolsLib/constants.js b/server/utils/toolsLib/constants.js
new file mode 100644
index 0000000000..6b5fbfb0a9
--- /dev/null
+++ b/server/utils/toolsLib/constants.js
@@ -0,0 +1,5 @@
+'use strict';
+
+const path = require('path');
+
+exports.SERVER_PATH = path.resolve(__dirname, '../../');
\ No newline at end of file
diff --git a/server/utils/toolsLib/index.js b/server/utils/toolsLib/index.js
new file mode 100644
index 0000000000..15c7933e87
--- /dev/null
+++ b/server/utils/toolsLib/index.js
@@ -0,0 +1 @@
+module.exports = require('./tools');
\ No newline at end of file
diff --git a/server/utils/toolsLib/tools/coin.js b/server/utils/toolsLib/tools/coin.js
new file mode 100644
index 0000000000..b5daa48fe7
--- /dev/null
+++ b/server/utils/toolsLib/tools/coin.js
@@ -0,0 +1,81 @@
+'use strict';
+
+const { SERVER_PATH } = require('../constants');
+const { getNodeLib } = require(`${SERVER_PATH}/init`);
+const {
+ subscribedToCoin,
+ getKitCoin,
+ getKitCoins,
+ getKitCoinsConfig
+} = require('./common');
+
+const getNetworkCoins = (
+ opts = {
+ search: null,
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().getCoins(opts);
+};
+
+const createCoin = async (
+ symbol,
+ fullname,
+ opts = {
+ code: null,
+ withdrawalFee: null,
+ min: null,
+ max: null,
+ incrementUnit: null,
+ logo: null,
+ meta: null,
+ estimatedPrice: null,
+ type: null,
+ network: null,
+ standard: null,
+ allowDeposit: null,
+ allowWithdrawal: null,
+ additionalHeaders: null
+ }
+) => {
+ const formattedSymbol = symbol.trim().toLowerCase();
+
+ return getNodeLib().createCoin(formattedSymbol, fullname, opts);
+};
+
+const updateCoin = async (
+ code,
+ fields = {
+ fullname: null,
+ withdrawalFee: null,
+ description: null,
+ withdrawalFees: null,
+ min: null,
+ max: null,
+ isPublic: null,
+ incrementUnit: null,
+ logo: null,
+ meta: null,
+ estimatedPrice: null,
+ type: null,
+ network: null,
+ standard: null,
+ allowDeposit: null,
+ allowWithdrawal: null
+ },
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().updateCoin(code, fields, opts);
+};
+
+module.exports = {
+ subscribedToCoin,
+ getKitCoin,
+ getKitCoins,
+ getKitCoinsConfig,
+ createCoin,
+ updateCoin,
+ getNetworkCoins
+};
diff --git a/server/utils/toolsLib/tools/common.js b/server/utils/toolsLib/tools/common.js
new file mode 100644
index 0000000000..fec5c10386
--- /dev/null
+++ b/server/utils/toolsLib/tools/common.js
@@ -0,0 +1,798 @@
+'use strict';
+
+const { SERVER_PATH } = require('../constants');
+const dbQuery = require('./database/query');
+const {
+ SECRET_MASK,
+ KIT_CONFIG_KEYS,
+ KIT_SECRETS_KEYS,
+ COMMUNICATOR_AUTHORIZED_KIT_CONFIG,
+ ROLES,
+ CONFIGURATION_CHANNEL,
+ INIT_CHANNEL,
+ SEND_CONTACT_US_EMAIL,
+ GET_COINS,
+ GET_PAIRS,
+ GET_TIERS,
+ GET_KIT_CONFIG,
+ GET_KIT_SECRETS,
+ GET_FROZEN_USERS,
+ HOLLAEX_NETWORK_ENDPOINT,
+ HOLLAEX_NETWORK_BASE_URL,
+ USER_META_KEYS,
+ VALID_USER_META_TYPES,
+ DOMAIN,
+ DEFAULT_FEES
+} = require(`${SERVER_PATH}/constants`);
+const {
+ COMMUNICATOR_CANNOT_UPDATE,
+ MASK_VALUE_GIVEN,
+ SUPPORT_DISABLED,
+ NO_NEW_DATA
+} = require(`${SERVER_PATH}/messages`);
+const { each, difference, isPlainObject, isString, pick, isNil, omit } = require('lodash');
+const { publisher } = require('./database/redis');
+const { sendEmail: sendSmtpEmail } = require(`${SERVER_PATH}/mail`);
+const { sendSMTPEmail: nodemailerEmail } = require(`${SERVER_PATH}/mail/utils`);
+const { errorMessageConverter: handleCatchError } = require(`${SERVER_PATH}/utils/conversion`);
+const { TemplateEmail } = require(`${SERVER_PATH}/mail/templates/helpers/common`);
+const { MAILTYPE } = require(`${SERVER_PATH}/mail/strings`);
+const { reject, resolve } = require('bluebird');
+const flatten = require('flat');
+const { getNodeLib } = require(`${SERVER_PATH}/init`);
+const rp = require('request-promise');
+const { isEmail: isValidEmail } = require('validator');
+const moment = require('moment');
+// const { Transform } = require('json2csv');
+
+const getKitVersion = () => {
+ return dbQuery.findOne('status', {
+ raw: true,
+ attributes: ['id', 'kit_version']
+ })
+ .then(({ kit_version }) => kit_version);
+};
+
+/**
+ * Checks if url given is a valid url.
+ * @param {string} url - Ids of frozen users.
+ * @returns {boolean} True if url is valid. False if not.
+ */
+const isUrl = (url) => {
+ const pattern = /^(^|\s)((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)$/;
+ return pattern.test(url);
+};
+
+const subscribedToCoin = (coin) => {
+ return getKitCoins().includes(coin);
+};
+
+const subscribedToPair = (pair) => {
+ return getKitPairs().includes(pair);
+};
+
+const getKitTiers = () => {
+ return GET_TIERS();
+};
+
+const getKitTier = (tier) => {
+ return GET_TIERS()[tier];
+};
+
+const isValidTierLevel = (level) => {
+ const levels = Object.keys(getKitTiers()).map((tier) => parseInt(tier));
+ if (!levels.includes(level)) {
+ return false;
+ } else {
+ return true;
+ }
+};
+
+const getTierLevels = () => {
+ return Object.keys(getKitTiers());
+};
+
+const getKitConfig = () => {
+ return GET_KIT_CONFIG();
+};
+
+const getKitSecrets = () => {
+ return GET_KIT_SECRETS();
+};
+
+const getKitCoin = (coin) => {
+ return getKitCoinsConfig()[coin];
+};
+
+const getKitCoinsConfig = () => {
+ return GET_COINS();
+};
+
+const getKitCoins = () => {
+ return Object.keys(getKitCoinsConfig());
+};
+
+const getKitPair = (pair) => {
+ return getKitPairsConfig()[pair];
+};
+
+const getKitPairsConfig = () => {
+ return GET_PAIRS();
+};
+
+const getKitPairs = () => {
+ return Object.keys(getKitPairsConfig());
+};
+
+const getFrozenUsers = () => {
+ return GET_FROZEN_USERS();
+};
+
+const maskSecrets = (secrets) => {
+ each(secrets, (secret, secretKey) => {
+ if (secretKey === 'captcha') {
+ secret.secret_key = SECRET_MASK;
+ } else if (secretKey === 'smtp') {
+ secret.password = SECRET_MASK;
+ }
+ });
+ return secrets;
+};
+
+const updateKitConfigSecrets = (data = {}, scopes) => {
+ let role = 'admin';
+
+ if (!data.kit && !data.secrets) {
+ return reject(new Error(NO_NEW_DATA));
+ }
+
+ if (scopes.indexOf(ROLES.COMMUNICATOR) > -1) {
+ role = 'communicator';
+
+ if (data.secrets) {
+ return reject(new Error('Communicator operators cannot update secrets values'));
+ }
+
+ let unauthorizedKeys = [];
+ if (data.kit) {
+ unauthorizedKeys = unauthorizedKeys.concat(difference(Object.keys(data.kit), COMMUNICATOR_AUTHORIZED_KIT_CONFIG));
+ }
+ if (unauthorizedKeys.length > 0) {
+ return reject(new Error(COMMUNICATOR_CANNOT_UPDATE(unauthorizedKeys)));
+ }
+ }
+
+ return dbQuery.findOne('status', {
+ attributes: ['id', 'kit', 'secrets']
+ })
+ .then((status) => {
+ const updatedKitConfig = {};
+ if (data.kit && Object.keys(data.kit).length > 0) {
+ updatedKitConfig.kit = joinKitConfig(status.dataValues.kit, data.kit, role);
+ }
+ if (data.secrets && Object.keys(data.secrets).length > 0) {
+ updatedKitConfig.secrets = joinKitSecrets(status.dataValues.secrets, data.secrets, role);
+ }
+ return status.update(updatedKitConfig, {
+ fields: [
+ 'kit',
+ 'secrets'
+ ],
+ returning: true
+ });
+ })
+ .then((status) => {
+ const info = getKitConfig().info;
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update', data: { kit: status.dataValues.kit, secrets: status.dataValues.secrets }
+ })
+ );
+ return {
+ kit: { ...status.dataValues.kit, info },
+ secrets: maskSecrets(status.dataValues.secrets)
+ };
+ });
+};
+
+const updateKitConfig = (kit, scopes) => {
+ return updateKitConfigSecrets({ kit }, scopes);
+};
+
+const updateKitSecrets = (secrets, scopes) => {
+ return updateKitConfigSecrets({ secrets }, scopes);
+};
+
+const joinKitConfig = (existingKitConfig = {}, newKitConfig = {}) => {
+ const newKeys = difference(Object.keys(newKitConfig), KIT_CONFIG_KEYS);
+ if (newKeys.length > 0) {
+ throw new Error(`Invalid kit keys given: ${newKeys}`);
+ }
+
+ if (newKitConfig.user_meta) {
+ for (let metaKey in newKitConfig.user_meta) {
+ const isValid = kitUserMetaFieldIsValid(metaKey, newKitConfig.user_meta[metaKey]);
+
+ if (!isValid.success) {
+ throw new Error(isValid.message);
+ }
+
+ newKitConfig.user_meta[metaKey] = pick(
+ newKitConfig.user_meta[metaKey],
+ ...USER_META_KEYS
+ );
+ }
+ }
+
+ const joinedKitConfig = {};
+
+ KIT_CONFIG_KEYS.forEach((key) => {
+ if (newKitConfig[key] === undefined) {
+ joinedKitConfig[key] = existingKitConfig[key];
+ } else {
+ if (
+ key === 'strings'
+ || key === 'icons'
+ || key === 'meta'
+ || key === 'color'
+ || key === 'injected_values'
+ || key === 'injected_html'
+ ) {
+ joinedKitConfig[key] = newKitConfig[key];
+ } else if (isPlainObject(existingKitConfig[key])) {
+ joinedKitConfig[key] = { ...existingKitConfig[key], ...newKitConfig[key] };
+ } else {
+ joinedKitConfig[key] = newKitConfig[key];
+ }
+ }
+ });
+
+ return joinedKitConfig;
+};
+
+const joinKitSecrets = (existingKitSecrets = {}, newKitSecrets = {}) => {
+ const newKeys = difference(Object.keys(newKitSecrets), KIT_SECRETS_KEYS);
+ if (newKeys.length > 0) {
+ throw new Error(`Invalid secret keys given: ${newKeys}`);
+ }
+
+ const flattenedNewKitSecrets = flatten(newKitSecrets);
+ if (Object.values(flattenedNewKitSecrets).includes(SECRET_MASK)) {
+ throw new Error(MASK_VALUE_GIVEN);
+ }
+
+ const joinedKitSecrets = {};
+
+ KIT_SECRETS_KEYS.forEach((key) => {
+ if (newKitSecrets[key]) {
+ if (isPlainObject(existingKitSecrets[key])) {
+ joinedKitSecrets[key] = { ...existingKitSecrets[key], ...newKitSecrets[key] };
+ } else {
+ joinedKitSecrets[key] = newKitSecrets[key];
+ }
+ } else {
+ joinedKitSecrets[key] = existingKitSecrets[key];
+ }
+ });
+ return joinedKitSecrets;
+};
+
+const sendEmailToSupport = (email, category, subject, description) => {
+ if (!SEND_CONTACT_US_EMAIL) {
+ return reject(new Error(SUPPORT_DISABLED));
+ }
+
+ const emailData = {
+ email,
+ category,
+ subject,
+ description
+ };
+ sendSmtpEmail(MAILTYPE.CONTACT_FORM, email, emailData, {});
+ return resolve();
+};
+
+const getNetworkKeySecret = () => {
+ return dbQuery.findOne('status', {
+ raw: true,
+ attributes: ['id', 'api_key', 'api_secret']
+ })
+ .then((status) => {
+ return {
+ apiKey: status.api_key,
+ apiSecret: status.api_secret
+ };
+ });
+};
+
+const setExchangeInitialized = () => {
+ return dbQuery.findOne('status')
+ .then((status) => {
+ if (status.dataValues.initialized === true) {
+ throw new Error('Exchange already initialized');
+ }
+ return status.update({ initialized: true }, { returning: true, fields: ['initialized'] });
+ })
+ .then((status) => {
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update', data: { info: { initialized: status.initialized } }
+ })
+ );
+ return;
+ });
+};
+
+const setExchangeSetupCompleted = () => {
+ return dbQuery.findOne('status')
+ .then((status) => {
+ if (status.dataValues.kit.setup_completed) {
+ throw new Error('Exchange setup is already flagged as completed');
+ }
+ const kit = {
+ ...status.dataValues.kit,
+ setup_completed: true
+ };
+ return status.update({
+ kit
+ }, { returning: true, fields: ['kit'] });
+ })
+ .then((status) => {
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update', data: { kit: status.kit }
+ })
+ );
+ return;
+ });
+};
+
+const updateNetworkKeySecret = (apiKey, apiSecret) => {
+ if (!apiKey || !apiSecret) {
+ return reject(new Error('Must provide both key and secret'));
+ }
+
+ return dbQuery.findOne('status')
+ .then((status) => {
+ return status.update({
+ api_key: apiKey,
+ api_secret: apiSecret
+ }, { fields: ['api_key', 'api_secret'] });
+ })
+ .then(() => {
+ publisher.publish(
+ INIT_CHANNEL,
+ JSON.stringify({ type: 'refreshInit' })
+ );
+ return;
+ });
+};
+
+const getAssetsPrices = (assets = [], quote, amount, opts = {
+ additionalHeaders: null
+}) => {
+ for (let asset of assets) {
+ if (!subscribedToCoin(asset)) {
+ return reject(new Error('Invalid asset'));
+ }
+ }
+
+ if (amount <= 0) {
+ return reject(new Error('Amount must be greater than 0'));
+ }
+
+ return getNodeLib().getOraclePrices(assets, { quote, amount, ...opts });
+};
+
+const storeImageOnNetwork = async (image, name, opts = {
+ additionalHeaders: null
+}) => {
+
+ return getNodeLib().uploadIcon(image, name, opts);
+};
+
+const getPublicTrades = (symbol, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getPublicTrades({ symbol, ...opts });
+};
+
+const getOrderbook = (symbol, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getOrderbook(symbol, opts);
+};
+
+const getOrderbooks = (opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getOrderbooks(opts);
+};
+
+const getChart = (from, to, symbol, resolution, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getChart(from, to, symbol, resolution, opts);
+};
+
+const getCharts = (from, to, resolution, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getCharts(from, to, resolution, opts);
+};
+
+const getUdfConfig = (opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getUdfConfig(opts);
+};
+
+const getUdfHistory = (from, to, symbol, resolution, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getUdfHistory(from, to, symbol, resolution, opts);
+};
+
+const getUdfSymbols = (symbol, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getUdfSymbols(symbol, opts);
+};
+
+const getTicker = (symbol, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getTicker(symbol, opts);
+};
+
+const getTickers = (opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getTickers(opts);
+};
+
+const getTradesHistory = (
+ symbol,
+ side,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().getTradesHistory({
+ symbol,
+ side,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ });
+};
+
+const sendEmail = (
+ type,
+ receiver,
+ data,
+ userSettings = {},
+ domain
+) => {
+ return sendSmtpEmail(MAILTYPE[type], receiver, data, userSettings, domain);
+};
+
+const isEmail = (email) => {
+ return isValidEmail(email);
+};
+
+const sleep = (ms) => {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+};
+
+const sendCustomEmail = (to, subject, html, opts = { from: null, cc: null, text: null, bcc: null }) => {
+ const { emails } = getKitSecrets();
+
+ const params = {
+ from: opts.from ? opts.from : `${getKitConfig().api_name} Support <${emails.sender}>`,
+ to: to.split(','),
+ subject,
+ html
+ };
+
+ if (opts.bcc === 'default') {
+ params.bcc = emails.send_email_to_support ? [emails.audit] : [];
+ } else if (isString(opts.bcc)) {
+ params.bcc = opts.bcc.split(',');
+ }
+
+ if (opts.cc) {
+ params.cc = opts.cc.split(',');
+ }
+
+ if (opts.text) {
+ params.text = opts.text;
+ }
+
+ return nodemailerEmail(params);
+};
+
+const emailHtmlBoilerplate = (html) => TemplateEmail({}, html);
+
+const kitUserMetaFieldIsValid = (field, data) => {
+ const missingUserMetaKeys = difference(USER_META_KEYS, Object.keys(data));
+ if (missingUserMetaKeys.length > 0) {
+ return {
+ success: false,
+ message: `Missing user_meta keys for field ${field}: ${missingUserMetaKeys}`
+ };
+ }
+
+ if (typeof data.type !== 'string' || !VALID_USER_META_TYPES.includes(data.type)) {
+ return {
+ success: false,
+ message: `Invalid type value given for field ${field}`
+ };
+ }
+
+ if (typeof data.description !== 'string') {
+ return {
+ success: false,
+ message: `Invalid description value given for field ${field}`
+ };
+ }
+
+ if (typeof data.required !== 'boolean') {
+ return {
+ success: false,
+ message: `Invalid required value given for field ${field}`
+ };
+ }
+
+ return { success: true };
+};
+
+const addKitUserMeta = async (name, type, description, required = false) => {
+ const existingUserMeta = getKitConfig().user_meta;
+
+ if (existingUserMeta[name]) {
+ throw new Error(`User meta field ${name} already exists`);
+ }
+
+ const data = {
+ type,
+ required,
+ description
+ };
+
+ const validCheck = kitUserMetaFieldIsValid(name, data);
+
+ if (!validCheck.success) {
+ throw new Error(validCheck.message);
+ }
+
+ const status = await dbQuery.findOne('status', {
+ attributes: ['id', 'kit']
+ });
+
+ const updatedUserMeta = {
+ ...existingUserMeta,
+ [name]: data
+ };
+
+ const updatedStatus = await status.update({
+ kit: {
+ ...status.kit,
+ user_meta: updatedUserMeta
+ }
+ });
+
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update', data: { kit: updatedStatus.kit }
+ })
+ );
+
+ return updatedStatus.kit.user_meta;
+};
+
+const updateKitUserMeta = async (name, data = {
+ type: null,
+ description: null,
+ required: null
+}) => {
+ const existingUserMeta = getKitConfig().user_meta;
+
+ if (!existingUserMeta[name]) {
+ throw new Error(`User meta field ${name} does not exist`);
+ }
+
+ if (isNil(data.type) && isNil(data.description) && isNil(data.required)) {
+ throw new Error('Must give a value to update');
+ }
+
+ const updatedField = {
+ type: isNil(data.type) ? existingUserMeta[name].type : data.type,
+ description: isNil(data.description) ? existingUserMeta[name].description : data.description,
+ required: isNil(data.required) ? existingUserMeta[name].required : data.required
+ };
+
+ const validCheck = kitUserMetaFieldIsValid(name, updatedField);
+
+ if (!validCheck.success) {
+ throw new Error(validCheck.message);
+ }
+
+ const status = await dbQuery.findOne('status', {
+ attributes: ['id', 'kit']
+ });
+
+ const updatedUserMeta = {
+ ...existingUserMeta,
+ [name]: updatedField
+ };
+
+ const updatedStatus = await status.update({
+ kit: {
+ ...status.kit,
+ user_meta: updatedUserMeta
+ }
+ });
+
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update', data: { kit: updatedStatus.kit }
+ })
+ );
+
+ return updatedStatus.kit.user_meta;
+};
+
+const deleteKitUserMeta = async (name) => {
+ const existingUserMeta = getKitConfig().user_meta;
+
+ if (!existingUserMeta[name]) {
+ throw new Error(`User meta field ${name} does not exist`);
+ }
+
+ const status = await dbQuery.findOne('status', {
+ attributes: ['id', 'kit']
+ });
+
+ const updatedUserMeta = omit(existingUserMeta, name);
+
+ const updatedStatus = await status.update({
+ kit: {
+ ...status.kit,
+ user_meta: updatedUserMeta
+ }
+ });
+
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update', data: { kit: updatedStatus.kit }
+ })
+ );
+
+ return updatedStatus.kit.user_meta;
+};
+
+const stringIsDate = (date) => {
+ return (typeof date === 'string' && new Date(date) !== 'Invalid Date') && !isNaN(new Date(date));
+};
+
+const isDatetime = (date, formats = [ moment.ISO_8601 ]) => {
+ return moment(date, formats, true).isValid();
+};
+
+const errorMessageConverter = (err) => {
+ return handleCatchError(err);
+};
+
+const getDomain = () => {
+ return DOMAIN;
+};
+
+// const getCsvParser = (opts = {
+// model: null,
+// exclude: null,
+// objectMode: null
+// }) => {
+// return new Transform(
+// {},
+// {
+// encoding: 'utf-8',
+// objectMode: opts.objectMode ? true : false
+// }
+// );
+// };
+
+const getNetworkConstants = (opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getConstants(opts);
+};
+
+const getNetworkEndpoint = () => HOLLAEX_NETWORK_ENDPOINT;
+
+const getDefaultFees = () => {
+ const { info: { type, collateral_level } } = getKitConfig();
+ if (type === 'Enterprise') {
+ return {
+ maker: 0,
+ taker: 0
+ };
+ } else {
+ return DEFAULT_FEES[collateral_level];
+ }
+};
+
+module.exports = {
+ getKitVersion,
+ isUrl,
+ getKitConfig,
+ getKitSecrets,
+ subscribedToCoin,
+ getKitTier,
+ getKitTiers,
+ getKitCoin,
+ getKitCoins,
+ getKitCoinsConfig,
+ subscribedToPair,
+ getKitPair,
+ getFrozenUsers,
+ getKitPairs,
+ getKitPairsConfig,
+ maskSecrets,
+ updateKitConfig,
+ updateKitSecrets,
+ updateKitConfigSecrets,
+ sendEmailToSupport,
+ getNetworkKeySecret,
+ setExchangeInitialized,
+ setExchangeSetupCompleted,
+ updateNetworkKeySecret,
+ isValidTierLevel,
+ getTierLevels,
+ getAssetsPrices,
+ storeImageOnNetwork,
+ getPublicTrades,
+ getOrderbook,
+ getOrderbooks,
+ getChart,
+ getCharts,
+ getUdfConfig,
+ getUdfHistory,
+ getUdfSymbols,
+ getTicker,
+ getTickers,
+ getTradesHistory,
+ sendEmail,
+ isEmail,
+ sleep,
+ sendCustomEmail,
+ addKitUserMeta,
+ updateKitUserMeta,
+ deleteKitUserMeta,
+ kitUserMetaFieldIsValid,
+ stringIsDate,
+ errorMessageConverter,
+ getDomain,
+ isDatetime,
+ // getCsvParser,
+ emailHtmlBoilerplate,
+ getNetworkConstants,
+ getNetworkEndpoint,
+ getDefaultFees
+};
diff --git a/server/utils/toolsLib/tools/database/helpers.js b/server/utils/toolsLib/tools/database/helpers.js
new file mode 100644
index 0000000000..cb469c058d
--- /dev/null
+++ b/server/utils/toolsLib/tools/database/helpers.js
@@ -0,0 +1,78 @@
+'use strict';
+
+const moment = require('moment');
+
+/**
+ * Returns object for sequelize pagination query. Default is { limit: 50, offset: 1 }
+ * @param {number} limit - Limit of values in page. Max: 50.
+ * @param {number} page - Page to retrieve. Default: 1.
+ * @returns {object} Sequelize pagination object with keys limit, offset.
+ */
+const paginationQuery = (limit = 50, page = 1) => {
+ let _limit = 50;
+ let _page = 1;
+ if (limit) {
+ if (limit > 50) {
+ _limit = 50;
+ } else if (limit <= 0) {
+ _limit = 1;
+ } else {
+ _limit = limit;
+ }
+ }
+
+ if (page && page >= 0) {
+ _page = page;
+ }
+ return {
+ limit: _limit,
+ offset: _limit * (_page - 1)
+ };
+};
+
+/**
+ * Returns object for sequelize timeframe query. Default is {} (no filter for timeframe).
+ * @param {string} startDate - Start date to filter by in timestamp format (ISO 8601).
+ * @param {string} endDate - End date to filter by in timestamp format (ISO 8601).
+ * @returns {object} Sequelize timeframe object.
+ */
+const timeframeQuery = (startDate = 0, endDate = moment().valueOf()) => {
+ let timestamp = {
+ $gte: startDate,
+ $lte: endDate
+ };
+ return timestamp;
+};
+
+/**
+ * Returns array for sequelize ordering query. Default is [id, desc] (By desceding id values).
+ * @param {string} orderBy - Table column (value) to order query by.
+ * @param {string} order - Order to put query. Can be desc (descending) or asc (ascending).
+ * @returns {array} Sequelize ordering array.
+ */
+const orderingQuery = (orderBy = 'id', order = 'desc') => {
+ return [orderBy, order === 'asc' || order === 'desc' ? order : 'desc'];
+};
+
+/**
+ * Format sequelize findAndCountAll query data to count/data format.
+ * @param {object} data - Original data from sequelize findAndCountAll query.
+ * @returns {object} Formatted query result with count and data.
+ */
+const convertSequelizeCountAndRows = (data) => {
+ return {
+ count: data.count,
+ data: data.rows.map((row) => {
+ const item = Object.assign({}, row.dataValues);
+ // delete item.id;
+ return item;
+ })
+ };
+};
+
+module.exports = {
+ paginationQuery,
+ timeframeQuery,
+ orderingQuery,
+ convertSequelizeCountAndRows
+};
diff --git a/server/utils/toolsLib/tools/database/index.js b/server/utils/toolsLib/tools/database/index.js
new file mode 100644
index 0000000000..ab3a44b922
--- /dev/null
+++ b/server/utils/toolsLib/tools/database/index.js
@@ -0,0 +1,13 @@
+'use strict';
+
+const helpers = require('./helpers');
+const model = require('./model');
+const query = require('./query');
+const redis = require('./redis');
+
+module.exports = {
+ ...helpers,
+ ...model,
+ ...query,
+ ...redis
+};
diff --git a/server/utils/toolsLib/tools/database/model.js b/server/utils/toolsLib/tools/database/model.js
new file mode 100644
index 0000000000..eee7854b4a
--- /dev/null
+++ b/server/utils/toolsLib/tools/database/model.js
@@ -0,0 +1,95 @@
+'use strict';
+
+const { SERVER_PATH } = require('../../constants');
+const models = require(`${SERVER_PATH}/db/models`);
+const { PROVIDE_TABLE_NAME } = require(`${SERVER_PATH}/messages`);
+const { capitalize, camelCase } = require('lodash');
+
+/**
+ * Get sequelize model of table.
+ * @param {string} table - Table name of model.
+ * @returns {object} Sequelize model.
+ */
+const getModel = (table = '') => {
+ if (table.length === 0) {
+ throw new Error(PROVIDE_TABLE_NAME);
+ }
+
+ if (table !== 'sequelize') {
+ table = table
+ .split(' ')
+ .map((word) => `${word[0].toUpperCase()}${word.slice(1)}`)
+ .join('');
+ }
+
+ return models[table];
+};
+
+const create = (table, query = {}, options = {}) => {
+ return getModel(table).create(query, options);
+};
+
+const destroy = (table, query = {}, options = {}) => {
+ return getModel(table).destroy(query, options);
+};
+
+const update = (table, query = {}, options = {}) => {
+ return getModel(table).update(query, options);
+};
+
+const createModel = (
+ name,
+ properties = {},
+ options = {
+ timestamps: true,
+ underscored: true
+ }
+) => {
+ const result = models.sequelize.import(name, (sequelize, DataTypes) => {{
+ const modelProperties = {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: DataTypes.INTEGER
+ }
+ };
+
+ for (let prop in properties) {
+ if (!properties[prop].type) {
+ throw new Error('Type not given for property ' + prop);
+ }
+ properties[prop].type = DataTypes[properties[prop].type.toUpperCase()];
+ modelProperties[prop] = properties[prop];
+ }
+ const model = models.sequelize.define(
+ name.split(' ').map((word) => `${capitalize(word)}`).join(''),
+ modelProperties,
+ {
+ timestamps: true,
+ underscored: true,
+ ...options
+ }
+ );
+ return model;
+ }});
+
+ return result;
+};
+
+const associateModel = (model, association, associatedModel, options = {}) => {
+ model.associate = (models) => {
+ model[camelCase(association)](models[associatedModel.split(' ').map((word) => `${capitalize(word)}`).join('')], options);
+ };
+
+ model.associate(models);
+};
+
+module.exports = {
+ createModel,
+ associateModel,
+ getModel,
+ create,
+ destroy,
+ update
+};
diff --git a/server/utils/toolsLib/tools/database/query.js b/server/utils/toolsLib/tools/database/query.js
new file mode 100644
index 0000000000..a24e3517d2
--- /dev/null
+++ b/server/utils/toolsLib/tools/database/query.js
@@ -0,0 +1,67 @@
+'use strict';
+
+const Model = (table) => require('./model').getModel(table);
+const { convertSequelizeCountAndRows } = require('./helpers');
+
+/**
+ * Returns Promise with Sequelize findOne query result.
+ * @param {string} table - Table name of model.
+ * @param {object} query - Sequelize query object.
+ * @returns {Promise} Promise with result of query.
+ */
+const findOne = (table, query = {}, model) => {
+ if (model) {
+ return model.findOne(query);
+ } else {
+ return Model(table).findOne(query);
+ }
+};
+
+/**
+ * Returns Promise with Sequelize findAll query result.
+ * @param {string} table - Table name of model.
+ * @param {object} query - Sequelize query object.
+ * @returns {Promise} Promise with result of query.
+ */
+const findAll = (table, query = {}, model) => {
+ if (model) {
+ return model.findAll(query);
+ } else {
+ return Model(table).findAll(query);
+ }
+};
+
+/**
+ * Returns Promise with Sequelize findAndCountAll query result.
+ * @param {string} table - Table name of model.
+ * @param {object} query - Sequelize query object.
+ * @returns {Promise} Promise with result of query.
+ */
+const findAndCountAll = (table, query = {}, model) => {
+ if (model) {
+ return model.findAndCountAll(query);
+ } else {
+ return Model(table).findAndCountAll(query);
+ }
+};
+
+/**
+ * Returns Promise with Sequelize findAndCountAll query result in count/data format.
+ * @param {string} table - Table name of model.
+ * @param {object} query - Sequelize query object.
+ * @returns {Promise} Promise with result of query in count/data format.
+ */
+const findAndCountAllWithRows = (table, query = {}, model) => {
+ if (model) {
+ return model.findAndCountAll(query).then(convertSequelizeCountAndRows);
+ } else {
+ return Model(table).findAndCountAll(query).then(convertSequelizeCountAndRows);
+ }
+};
+
+module.exports = {
+ findOne,
+ findAll,
+ findAndCountAll,
+ findAndCountAllWithRows
+};
diff --git a/server/utils/toolsLib/tools/database/redis.js b/server/utils/toolsLib/tools/database/redis.js
new file mode 100644
index 0000000000..fbb5a68014
--- /dev/null
+++ b/server/utils/toolsLib/tools/database/redis.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const { SERVER_PATH } = require('../../constants');
+const publisher = require(`${SERVER_PATH}/db/pubsub`).publisher;
+const subscriber = require(`${SERVER_PATH}/db/pubsub`).subscriber;
+const client = require(`${SERVER_PATH}/db/redis`).duplicate();
+
+module.exports = {
+ publisher,
+ subscriber,
+ client
+};
\ No newline at end of file
diff --git a/server/utils/toolsLib/tools/exchange.js b/server/utils/toolsLib/tools/exchange.js
new file mode 100644
index 0000000000..0a76b18a42
--- /dev/null
+++ b/server/utils/toolsLib/tools/exchange.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const { SERVER_PATH } = require('../constants');
+const { getNodeLib } = require(`${SERVER_PATH}/init`);
+const { publisher } = require('./database/redis');
+const { INIT_CHANNEL } = require(`${SERVER_PATH}/constants`);
+
+const getExchangeConfig = async (
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().getExchange(opts);
+};
+
+const updateExchangeConfig = async (
+ fields = {
+ info: null,
+ isPublic: null,
+ type: null,
+ name: null,
+ displayName: null,
+ url: null,
+ businessInfo: null,
+ pairs: null,
+ coins: null
+ },
+ opts = {
+ additionalHeaders: null,
+ skip_refresh: null
+ }
+) => {
+ const { additionalHeaders, skip_refresh } = opts;
+ const result = await getNodeLib().updateExchange(fields, { additionalHeaders });
+
+ if (!skip_refresh) {
+ publisher.publish(
+ INIT_CHANNEL,
+ JSON.stringify({ type: 'refreshInit' })
+ );
+ }
+
+ return result;
+};
+
+module.exports = {
+ getExchangeConfig,
+ updateExchangeConfig
+};
diff --git a/server/utils/toolsLib/tools/index.js b/server/utils/toolsLib/tools/index.js
new file mode 100644
index 0000000000..1adc973625
--- /dev/null
+++ b/server/utils/toolsLib/tools/index.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const common = require('./common');
+const database = require('./database');
+const order = require('./order');
+const plugin = require('./plugin');
+const user = require('./user');
+const wallet = require('./wallet');
+const tier = require('./tier');
+const security = require('./security');
+const coin = require('./coin');
+const pair = require('./pair');
+const exchange = require('./exchange');
+
+module.exports = {
+ ...common,
+ database,
+ order,
+ plugin,
+ user,
+ wallet,
+ tier,
+ security,
+ coin,
+ pair,
+ exchange
+};
diff --git a/server/utils/toolsLib/tools/order.js b/server/utils/toolsLib/tools/order.js
new file mode 100644
index 0000000000..73d4c0728f
--- /dev/null
+++ b/server/utils/toolsLib/tools/order.js
@@ -0,0 +1,731 @@
+'use strict';
+
+const { getUserByKitId, getUserByEmail, getUserByNetworkId, mapNetworkIdToKitId } = require('./user');
+const { SERVER_PATH } = require('../constants');
+const { getNodeLib } = require(`${SERVER_PATH}/init`);
+const { INVALID_SYMBOL, NO_DATA_FOR_CSV, USER_NOT_FOUND, USER_NOT_REGISTERED_ON_NETWORK } = require(`${SERVER_PATH}/messages`);
+const { parse } = require('json2csv');
+const { subscribedToPair, getKitTier, getDefaultFees } = require('./common');
+const { reject } = require('bluebird');
+const { loggerOrders } = require(`${SERVER_PATH}/config/logger`);
+const math = require('mathjs');
+
+const createUserOrderByKitId = (userKitId, symbol, side, size, type, price = 0, opts = { stop: null, meta: null, additionalHeaders: null }) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByKitId(userKitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+
+ const feeData = generateOrderFeeData(
+ user.verification_level,
+ symbol,
+ {
+ discount: user.discount
+ }
+ );
+
+ return getNodeLib().createOrder(user.network_id, symbol, side, size, type, price, feeData, opts);
+ });
+};
+
+const createUserOrderByEmail = (email, symbol, side, size, type, price = 0, opts = { stop: null, meta: null, additionalHeaders: null }) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByEmail(email)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+
+ const feeData = generateOrderFeeData(
+ user.verification_level,
+ symbol,
+ {
+ discount: user.discount
+ }
+ );
+
+ return getNodeLib().createOrder(user.network_id, symbol, side, size, type, price, feeData, opts);
+ });
+};
+
+const createUserOrderByNetworkId = (networkId, symbol, side, size, type, price = 0, opts = { stop: null, meta: null, additionalHeaders: null }) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByNetworkId(networkId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+
+ const feeData = generateOrderFeeData(
+ user.verification_level,
+ symbol,
+ {
+ discount: user.discount
+ }
+ );
+
+ return getNodeLib().createOrder(user.network_id, symbol, side, size, type, price, feeData, opts);
+ });
+};
+
+const createOrderNetwork = (networkId, symbol, side, size, type, price, feeData = {}, opts = { stop: null, meta: null, additionalHeaders: null }) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ return getNodeLib().createOrder(networkId, symbol, side, size, type, price, feeData, opts);
+};
+
+const getUserOrderByKitId = (userKitId, orderId, opts = {
+ additionalHeaders: null
+}) => {
+ return getUserByKitId(userKitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getOrder(user.network_id, orderId, opts);
+ });
+};
+
+const getUserOrderByEmail = (email, orderId, opts = {
+ additionalHeaders: null
+}) => {
+ return getUserByEmail(email)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getOrder(user.network_id, orderId, opts);
+ });
+};
+
+const getUserOrderByNetworkId = (networkId, orderId, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ return getNodeLib().getOrder(networkId, orderId, opts);
+};
+
+const cancelUserOrderByKitId = (userKitId, orderId, opts = {
+ additionalHeaders: null
+}) => {
+ return getUserByKitId(userKitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().cancelOrder(user.network_id, orderId, opts);
+ });
+};
+
+const cancelUserOrderByEmail = (email, orderId, opts = {
+ additionalHeaders: null
+}) => {
+ return getUserByEmail(email)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().cancelOrder(user.network_id, orderId, opts);
+ });
+};
+
+const cancelUserOrderByNetworkId = (networkId, orderId, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ return getNodeLib().cancelOrder(networkId, orderId, opts);
+};
+
+const getAllExchangeOrders = (symbol, side, status, open, limit, page, orderBy, order, startDate, endDate, opts = {
+ additionalHeaders: null
+}) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getNodeLib().getOrders({
+ symbol,
+ side,
+ status,
+ open,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ });
+};
+
+const getAllUserOrdersByKitId = (userKitId, symbol, side, status, open, limit, page, orderBy, order, startDate, endDate, opts = {
+ additionalHeaders: null
+}) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByKitId(userKitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getUserOrders(user.network_id, {
+ symbol,
+ side,
+ status,
+ open,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ });
+ });
+};
+
+const getAllUserOrdersByEmail = (email, symbol, side, status, open, limit, page, orderBy, order, startDate, endDate, opts = {
+ additionalHeaders: null
+}) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByEmail(email)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getUserOrders(user.network_id, {
+ symbol,
+ side,
+ status,
+ open,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ });
+ });
+};
+
+const getAllUserOrdersByNetworkId = (networkId, symbol, side, status, open, limit, page, orderBy, order, startDate, endDate, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getNodeLib().getUserOrders(networkId, {
+ symbol,
+ side,
+ status,
+ open,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ });
+};
+
+const cancelAllUserOrdersByKitId = (userKitId, symbol, opts = {
+ additionalHeaders: null
+}) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByKitId(userKitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().cancelAllOrders(user.network_id, { symbol, ...opts });
+ });
+};
+
+const cancelAllUserOrdersByEmail = (email, symbol, opts = {
+ additionalHeaders: null
+}) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByEmail(email)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().cancelAllOrders(user.network_id, { symbol, ...opts });
+ });
+};
+
+const cancelAllUserOrdersByNetworkId = (networkId, symbol, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getNodeLib().cancelAllOrders(networkId, { symbol, ...opts });
+};
+
+const getAllTradesNetwork = (symbol, limit, page, orderBy, order, startDate, endDate, format, opts = { additionalHeaders: null }) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+
+ const params = {
+ symbol,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ };
+
+ if (format) {
+ params.format = 'all';
+ }
+
+ return getNodeLib().getTrades(params)
+ .then(async (trades) => {
+ if (trades.data.length > 0) {
+ const networkIds = [];
+
+ for (const trade of trades.data) {
+ networkIds.push(trade.maker_id, trade.taker_id);
+ }
+
+ const idDictionary = await mapNetworkIdToKitId(networkIds);
+
+ for (let trade of trades.data) {
+ const maker_kit_id = idDictionary[trade.maker_id] || 0;
+ const taker_kit_id = idDictionary[trade.taker_id] || 0;
+
+ trade.maker_network_id = trade.maker_id;
+ trade.maker_id = maker_kit_id;
+
+ trade.taker_network_id = trade.taker_id;
+ trade.taker_id = taker_kit_id;
+ }
+ }
+
+ if (format === 'csv') {
+ if (trades.data.length === 0) {
+ throw new Error(NO_DATA_FOR_CSV);
+ }
+ const csv = parse(trades.data, Object.keys(trades.data[0]));
+ return csv;
+ } else {
+ return trades;
+ }
+ });
+};
+
+const getAllUserTradesByKitId = (userKitId, symbol, limit, page, orderBy, order, startDate, endDate, format, opts = {
+ additionalHeaders: null
+}) => {
+ if (symbol && !subscribedToPair(symbol)) {
+ return reject(new Error(INVALID_SYMBOL(symbol)));
+ }
+ return getUserByKitId(userKitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+
+ const params = {
+ symbol,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ };
+
+ if (format) {
+ params.format = 'all';
+ }
+
+ return getNodeLib().getUserTrades(user.network_id, params);
+ })
+ .then((trades) => {
+ if (format === 'csv') {
+ if (trades.data.length === 0) {
+ throw new Error(NO_DATA_FOR_CSV);
+ }
+ const csv = parse(trades.data, Object.keys(trades.data[0]));
+ return csv;
+ } else {
+ return trades;
+ }
+ });
+};
+
+// const getAllTradesNetworkStream = (opts = {
+// symbol: null,
+// limit: null,
+// page: null,
+// orderBy: null,
+// order: null,
+// startDate: null,
+// endDate: null
+// }) => {
+// if (opts.symbol && !subscribedToPair(opts.symbol)) {
+// return reject(new Error(INVALID_SYMBOL(opts.symbol)));
+// }
+// return getNodeLib().getTrades({ ...opts, format: 'all' });
+// };
+
+// const getAllTradesNetworkCsv = (opts = {
+// symbol: null,
+// limit: null,
+// page: null,
+// orderBy: null,
+// order: null,
+// startDate: null,
+// endDate: null
+// }) => {
+// return getAllTradesNetworkStream(opts)
+// .then((data) => {
+// const parser = getCsvParser();
+
+// parser.on('error', (error) => {
+// throw error;
+// });
+
+// parser.on('error', (error) => {
+// parser.destroy();
+// throw error;
+// });
+
+// parser.on('end', () => {
+// parser.destroy();
+// });
+
+// return data.pipe(parser);
+// });
+// };
+
+// const getUserTradesByKitIdStream = (userKitId, opts = {
+// symbol: null,
+// limit: null,
+// page: null,
+// orderBy: null,
+// order: null,
+// startDate: null,
+// endDate: null
+// }) => {
+// if (opts.symbol && !subscribedToPair(opts.symbol)) {
+// return reject(new Error(INVALID_SYMBOL(opts.symbol)));
+// }
+// return getUserByKitId(userKitId)
+// .then((user) => {
+// if (!user) {
+// throw new Error(USER_NOT_FOUND);
+// } else if (!user.network_id) {
+// throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+// }
+// return getNodeLib().getUserTrades(user.network_id, { ...opts, format: 'all' });
+// });
+// };
+
+// const getUserTradesByKitIdCsv = (userKitId, opts = {
+// symbol: null,
+// limit: null,
+// page: null,
+// orderBy: null,
+// order: null,
+// startDate: null,
+// endDate: null
+// }) => {
+// return getUserTradesByKitIdStream(userKitId, opts)
+// .then((data) => {
+// const parser = getCsvParser();
+
+// parser.on('error', (error) => {
+// parser.destroy();
+// throw error;
+// });
+
+// parser.on('end', () => {
+// parser.destroy();
+// });
+
+// return data.pipe(parser);
+// });
+// };
+
+// const getUserTradesByNetworkIdStream = (userNetworkId, opts = {
+// symbol: null,
+// limit: null,
+// page: null,
+// orderBy: null,
+// order: null,
+// startDate: null,
+// endDate: null
+// }) => {
+// if (opts.symbol && !subscribedToPair(opts.symbol)) {
+// return reject(new Error(INVALID_SYMBOL(opts.symbol)));
+// }
+// return getNodeLib().getUserTrades(userNetworkId, { ...opts, format: 'all' });
+// };
+
+// const getUserTradesByNetworkIdCsv = (userNetworkId, opts = {
+// symbol: null,
+// limit: null,
+// page: null,
+// orderBy: null,
+// order: null,
+// startDate: null,
+// endDate: null
+// }) => {
+// return getUserTradesByNetworkIdStream(userNetworkId, opts)
+// .then((data) => {
+// const parser = getCsvParser();
+
+// parser.on('error', (error) => {
+// parser.destroy();
+// throw error;
+// });
+
+// parser.on('end', () => {
+// parser.destroy();
+// });
+
+// return data.pipe(parser);
+// });
+// };
+
+const getAllUserTradesByNetworkId = (networkId, symbol, limit, page, orderBy, order, startDate, endDate, format, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+
+ const params = {
+ symbol,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ ...opts
+ };
+
+ if (format) {
+ params.format = 'all';
+ }
+
+ return getNodeLib().getUserTrades(networkId, opts)
+ .then((trades) => {
+ if (format === 'csv') {
+ if (trades.data.length === 0) {
+ throw new Error(NO_DATA_FOR_CSV);
+ }
+ const csv = parse(trades.data, Object.keys(trades.data[0]));
+ return csv;
+ } else {
+ return trades;
+ }
+ });
+};
+
+const getGeneratedFees = (startDate, endDate, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getGeneratedFees({
+ startDate,
+ endDate,
+ ...opts
+ });
+};
+
+const settleFees = (opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().settleFees(opts);
+};
+
+const generateOrderFeeData = (userTier, symbol, opts = { discount: 0 }) => {
+ loggerOrders.debug(
+ 'generateOrderFeeData',
+ 'symbol',
+ symbol,
+ 'userTier',
+ userTier
+ );
+
+ const tier = getKitTier(userTier);
+
+ if (!tier) {
+ throw new Error(`User tier ${userTier} not found`);
+ }
+
+ let makerFee = tier.fees.maker[symbol];
+ let takerFee = tier.fees.taker[symbol];
+
+ loggerOrders.debug(
+ 'generateOrderFeeData',
+ 'current makerFee',
+ makerFee,
+ 'current takerFee',
+ takerFee
+ );
+
+ if (opts.discount) {
+ loggerOrders.debug(
+ 'generateOrderFeeData',
+ 'discount percentage',
+ opts.discount
+ );
+
+ const discountToBigNum = math.divide(
+ math.bignumber(opts.discount),
+ math.bignumber(100)
+ );
+
+ const discountedMakerFee = math.number(
+ math.subtract(
+ math.bignumber(makerFee),
+ math.multiply(
+ math.bignumber(makerFee),
+ discountToBigNum
+ )
+ )
+ );
+
+ const discountedTakerFee = math.number(
+ math.subtract(
+ math.bignumber(takerFee),
+ math.multiply(
+ math.bignumber(takerFee),
+ discountToBigNum
+ )
+ )
+ );
+
+ const exchangeMinFee = getDefaultFees();
+
+ loggerOrders.verbose(
+ 'generateOrderFeeData',
+ 'discounted makerFee',
+ discountedMakerFee,
+ 'discounted takerFee',
+ discountedTakerFee,
+ 'exchange minimum fees',
+ exchangeMinFee
+ );
+
+ if (discountedMakerFee > exchangeMinFee.maker) {
+ makerFee = discountedMakerFee;
+ } else {
+ makerFee = exchangeMinFee.maker;
+ }
+
+ if (discountedTakerFee > exchangeMinFee.taker) {
+ takerFee = discountedTakerFee;
+ } else {
+ takerFee = exchangeMinFee.taker;
+ }
+ }
+
+ const feeData = {
+ fee_structure: {
+ maker: makerFee,
+ taker: takerFee
+ }
+ };
+
+ loggerOrders.verbose(
+ 'generateOrderFeeData',
+ 'generated fee data',
+ feeData
+ );
+
+ return feeData;
+};
+
+module.exports = {
+ getAllExchangeOrders,
+ createUserOrderByKitId,
+ createUserOrderByEmail,
+ getUserOrderByKitId,
+ getUserOrderByEmail,
+ cancelUserOrderByKitId,
+ cancelUserOrderByEmail,
+ getAllUserOrdersByKitId,
+ getAllUserOrdersByEmail,
+ cancelAllUserOrdersByKitId,
+ cancelAllUserOrdersByEmail,
+ getAllTradesNetwork,
+ getAllUserTradesByKitId,
+ getAllUserTradesByNetworkId,
+ getUserOrderByNetworkId,
+ createUserOrderByNetworkId,
+ createOrderNetwork,
+ cancelUserOrderByNetworkId,
+ getAllUserOrdersByNetworkId,
+ cancelAllUserOrdersByNetworkId,
+ getGeneratedFees,
+ settleFees,
+ generateOrderFeeData,
+ // getUserTradesByKitIdStream,
+ // getUserTradesByNetworkIdStream,
+ // getAllTradesNetworkStream,
+ // getAllTradesNetworkCsv,
+ // getUserTradesByKitIdCsv,
+ // getUserTradesByNetworkIdCsv
+};
diff --git a/server/utils/toolsLib/tools/pair.js b/server/utils/toolsLib/tools/pair.js
new file mode 100644
index 0000000000..31ef292016
--- /dev/null
+++ b/server/utils/toolsLib/tools/pair.js
@@ -0,0 +1,87 @@
+'use strict';
+
+const { SERVER_PATH } = require('../constants');
+const { getNodeLib } = require(`${SERVER_PATH}/init`);
+const {
+ subscribedToCoin,
+ getKitCoin,
+ getKitCoins,
+ getKitCoinsConfig,
+ subscribedToPair,
+ getKitPair,
+ getKitPairs,
+ getKitPairsConfig
+} = require('./common');
+
+const getNetworkPairs = (
+ opts = {
+ search: null,
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().getPairs(opts);
+};
+
+const createPair = async (
+ name,
+ baseCoin,
+ quoteCoin,
+ opts = {
+ code: null,
+ active: null,
+ minSize: null,
+ maxSize: null,
+ minPrice: null,
+ maxPrice: null,
+ incrementSize: null,
+ incrementPrice: null,
+ estimatedPrice: null,
+ isPublic: null,
+ additionalHeaders: null
+ }
+) => {
+ const formattedName = name.trim().toLowerCase();
+ const formattedBaseCoin = baseCoin.trim().toLowerCase();
+ const formattedQuoteCoin = quoteCoin.trim().toLowerCase();
+
+ return getNodeLib().createPair(
+ formattedName,
+ formattedBaseCoin,
+ formattedQuoteCoin,
+ opts
+ );
+};
+
+const updatePair = async (
+ code,
+ fields = {
+ minSize: null,
+ maxSize: null,
+ minPrice: null,
+ maxPrice: null,
+ incrementSize: null,
+ incrementPrice: null,
+ estimatedPrice: null,
+ isPublic: null,
+ circuitBreaker: null
+ },
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().updatePair(
+ code,
+ fields,
+ opts
+ );
+};
+
+module.exports = {
+ subscribedToPair,
+ getKitPair,
+ getKitPairs,
+ getKitPairsConfig,
+ createPair,
+ updatePair,
+ getNetworkPairs
+};
diff --git a/server/utils/toolsLib/tools/plugin.js b/server/utils/toolsLib/tools/plugin.js
new file mode 100644
index 0000000000..56301edda7
--- /dev/null
+++ b/server/utils/toolsLib/tools/plugin.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const dbQuery = require('./database/query');
+const { paginationQuery } = require('./database/helpers');
+const { Op } = require('sequelize');
+
+const getPaginatedPlugins = (limit, page, search) => {
+ const options = {
+ where: {},
+ raw: true,
+ attributes: [
+ 'name',
+ 'version',
+ 'enabled',
+ 'author',
+ 'description',
+ 'bio',
+ 'url',
+ 'type',
+ 'web_view',
+ 'logo',
+ 'icon',
+ 'documentation',
+ 'created_at',
+ 'updated_at',
+ 'public_meta',
+ 'admin_view'
+ ],
+ ...paginationQuery(limit, page)
+ };
+
+ if (search) {
+ options.where = {
+ name: { [Op.like]: `%${search}%` }
+ };
+ }
+
+ return dbQuery.findAndCountAll('plugin', options)
+ .then((data) => {
+ return {
+ count: data.count,
+ data: data.rows.map((plugin) => {
+ plugin.enabled_admin_view = !!plugin.admin_view;
+ delete plugin.admin_view;
+ return plugin;
+ })
+ };
+ });
+};
+
+const getPlugin = (name, opts = {}) => {
+ return dbQuery.findOne('plugin', {
+ where: { name },
+ ...opts
+ });
+};
+
+module.exports = {
+ getPaginatedPlugins,
+ getPlugin
+};
diff --git a/server/utils/toolsLib/tools/security.js b/server/utils/toolsLib/tools/security.js
new file mode 100644
index 0000000000..690c2db191
--- /dev/null
+++ b/server/utils/toolsLib/tools/security.js
@@ -0,0 +1,1113 @@
+'use strict';
+
+const rp = require('request-promise');
+const crypto = require('crypto');
+const jwt = require('jsonwebtoken');
+const { intersection, has } = require('lodash');
+const { isEmail } = require('validator');
+const { SERVER_PATH } = require('../constants');
+const {
+ INVALID_CAPTCHA,
+ USER_NOT_FOUND,
+ TOKEN_OTP_MUST_BE_ENABLED,
+ INVALID_OTP_CODE,
+ ACCESS_DENIED,
+ NOT_AUTHORIZED,
+ TOKEN_EXPIRED,
+ INVALID_TOKEN,
+ MISSING_HEADER,
+ DEACTIVATED_USER,
+ TOKEN_NOT_FOUND,
+ TOKEN_REVOKED,
+ MULTIPLE_API_KEY,
+ API_KEY_NULL,
+ API_REQUEST_EXPIRED,
+ API_SIGNATURE_NULL,
+ API_KEY_INACTIVE,
+ API_KEY_INVALID,
+ API_KEY_EXPIRED,
+ API_KEY_OUT_OF_SCOPE,
+ API_SIGNATURE_INVALID,
+ INVALID_PASSWORD,
+ SAME_PASSWORD,
+ CODE_NOT_FOUND
+} = require(`${SERVER_PATH}/messages`);
+const {
+ NODE_ENV,
+ CAPTCHA_ENDPOINT,
+ BASE_SCOPES,
+ ROLES,
+ ISSUER,
+ SECRET,
+ TOKEN_TYPES,
+ HMAC_TOKEN_EXPIRY,
+ HMAC_TOKEN_KEY
+} = require(`${SERVER_PATH}/constants`);
+const { resolve, reject, promisify } = require('bluebird');
+const { getKitSecrets, getKitConfig, getFrozenUsers, getNetworkKeySecret } = require('./common');
+const bcrypt = require('bcryptjs');
+const { all } = require('bluebird');
+const { sendEmail } = require(`${SERVER_PATH}/mail`);
+const { MAILTYPE } = require(`${SERVER_PATH}/mail/strings`);
+const { getModel } = require('./database/model');
+const dbQuery = require('./database/query');
+const otp = require('otp');
+const { client } = require('./database/redis');
+const { loggerAuth } = require(`${SERVER_PATH}/config/logger`);
+const moment = require('moment');
+const { generateHash } = require(`${SERVER_PATH}/utils/security`);
+const geoip = require('geoip-lite');
+
+const getCountryFromIp = (ip) => {
+ const geo = geoip.lookup(ip);
+ if (!geo) {
+ return '';
+ }
+ return geo.country;
+};
+
+const checkIp = async (remoteip = '') => {
+ const dataGeofence = getKitConfig().black_list_countries;
+ if (dataGeofence && dataGeofence.length > 0 && remoteip) {
+ if (dataGeofence.includes(getCountryFromIp(remoteip))) {
+ throw new Error('ERROR IP LOCATION');
+ }
+ }
+ return;
+};
+
+const checkCaptcha = (captcha = '', remoteip = '') => {
+ if (!captcha) {
+ if (NODE_ENV === 'development') {
+ return resolve();
+ } else {
+ return reject(new Error(INVALID_CAPTCHA));
+ }
+ } else if (!getKitSecrets().captcha || !getKitSecrets().captcha.secret_key) {
+ return resolve();
+ }
+
+ const options = {
+ method: 'POST',
+ form: {
+ secret: getKitSecrets().captcha.secret_key,
+ response: captcha,
+ remoteip
+ },
+ uri: CAPTCHA_ENDPOINT
+ };
+
+ return rp(options)
+ .then((response) => JSON.parse(response))
+ .then((response) => {
+ if (!response.success) {
+ throw new Error(INVALID_CAPTCHA);
+ }
+ return;
+ });
+};
+
+const validatePassword = (userPassword, inputPassword) => {
+ return bcrypt.compare(inputPassword, userPassword);
+};
+
+const isValidPassword = (value) => {
+ return /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/.test(value);
+};
+
+const resetUserPassword = (resetPasswordCode, newPassword) => {
+ if (!isValidPassword(newPassword)) {
+ return reject(new Error(INVALID_PASSWORD));
+ }
+ return getResetPasswordCode(resetPasswordCode)
+ .then((user_id) => {
+ return all([
+ dbQuery.findOne('user', { where: { id: user_id } }),
+ client.delAsync(`ResetPasswordCode:${resetPasswordCode}`)
+ ]);
+ })
+ .then(([user]) => {
+ return user.update({ password: newPassword }, { fields: ['password'] });
+ });
+};
+
+const confirmChangeUserPassword = (code, domain) => {
+ return getChangePasswordCode(code)
+ .then((data) => {
+ const dataValues = JSON.parse(data);
+ return all([
+ dbQuery.findOne('user', { where: { id: dataValues.id } }),
+ dataValues,
+ client.delAsync(`ChangePasswordCode:${code}`)
+ ]);
+ })
+ .then(([user, dataValues]) => {
+ return user.update({ password: dataValues.password }, { fields: ['password'], hooks: false });
+ })
+ .then((user) => {
+ sendEmail(
+ MAILTYPE.PASSWORD_CHANGED,
+ user.email,
+ { code },
+ user.settings,
+ domain
+ );
+ return;
+ });
+};
+
+const changeUserPassword = (email, oldPassword, newPassword, ip, domain) => {
+ if (oldPassword === newPassword) {
+ return reject(new Error(SAME_PASSWORD));
+ }
+ if (!isValidPassword(newPassword)) {
+ return reject(new Error(INVALID_PASSWORD));
+ }
+ return dbQuery.findOne('user', { where: { email } })
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ return all([createChangePasswordCode(user.id, newPassword), user]);
+ })
+ .then(([code, user]) => {
+ sendEmail(
+ MAILTYPE.CHANGE_PASSWORD,
+ email,
+ { code, ip },
+ user.settings,
+ domain
+ );
+ return;
+ });
+};
+
+const getChangePasswordCode = (code) => {
+ return client.getAsync(`ChangePasswordCode:${code}`)
+ .then((data) => {
+ if (!data) {
+ const error = new Error(CODE_NOT_FOUND);
+ error.status = 404;
+ throw error;
+ }
+ return data;
+ });
+};
+
+const createChangePasswordCode = (userId, newPassword) => {
+ //Generate new random code
+ const code = crypto.randomBytes(20).toString('hex');
+ //Code is expire in 5 mins
+ return generateHash(newPassword)
+ .then((hashedPassword) => {
+ return client.setexAsync(`ChangePasswordCode:${code}`, 60 * 5, JSON.stringify({
+ id: userId,
+ password: hashedPassword
+ }));
+ })
+ .then(() => {
+ return code;
+ });
+};
+
+const getResetPasswordCode = (code) => {
+ return client.getAsync(`ResetPasswordCode:${code}`)
+ .then((user_id) => {
+ if (!user_id) {
+ const error = new Error(CODE_NOT_FOUND);
+ error.status = 404;
+ throw error;
+ }
+ return user_id;
+ });
+};
+
+const createResetPasswordCode = (userId) => {
+ //Generate new random code
+ const code = crypto.randomBytes(20).toString('hex');
+
+ //Code is expire in 5 mins
+ return client.setexAsync(`ResetPasswordCode:${code}`, 60 * 5, userId)
+ .then(() => {
+ return code;
+ });
+};
+
+const sendResetPasswordCode = (email, captcha, ip, domain) => {
+ if (typeof email !== 'string' || !isEmail(email)) {
+ return reject(new Error(USER_NOT_FOUND));
+ }
+
+ return dbQuery.findOne('user', { where: { email } })
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ return all([createResetPasswordCode(user.id), user, checkCaptcha(captcha, ip)]);
+ })
+ .then(([code, user]) => {
+ sendEmail(
+ MAILTYPE.RESET_PASSWORD,
+ email,
+ { code, ip },
+ user.settings,
+ domain
+ );
+ return;
+ });
+};
+
+const generateOtp = (secret, epoch = 0) => {
+ const options = {
+ name: getKitConfig().api_name,
+ secret,
+ epoch
+ };
+ const totp = otp(options).totp();
+ return totp;
+};
+
+const verifyOtp = (userSecret, userDigits) => {
+ const serverDigits = [generateOtp(userSecret, 30), generateOtp(userSecret), generateOtp(userSecret, -30)];
+ return serverDigits.includes(userDigits);
+};
+
+const hasUserOtpEnabled = (id) => {
+ return dbQuery.findOne('user', {
+ where: { id },
+ attributes: ['otp_enabled']
+ }).then((user) => {
+ return user.otp_enabled;
+ });
+};
+
+const verifyUserOtpCode = (user_id, otp_code) => {
+ return dbQuery.findOne('otp code', {
+ where: {
+ used: true,
+ user_id
+ },
+ attributes: ['id', 'secret'],
+ order: [['updated_at', 'DESC']]
+ })
+ .then((otpCode) => {
+ return verifyOtp(otpCode.secret, otp_code);
+ })
+ .then((validOtp) => {
+ if (!validOtp) {
+ throw new Error(INVALID_OTP_CODE);
+ }
+ return true;
+ });
+};
+
+const verifyOtpBeforeAction = (user_id, otp_code) => {
+ return hasUserOtpEnabled(user_id).then((otp_enabled) => {
+ if (otp_enabled) {
+ return verifyUserOtpCode(user_id, otp_code);
+ } else {
+ return true;
+ }
+ });
+};
+
+const checkOtp = (userId) => {
+ return hasUserOtpEnabled(userId).then((otp_enabled) => {
+ if (otp_enabled) {
+ throw new Error('OTP is already enabled');
+ }
+ return findUserOtp(userId);
+ });
+};
+
+/*
+ Function generate the otp secret.
+ Return the otp secret.
+ */
+const generateOtpSecret = () => {
+ const seed = otp({
+ name: getKitConfig().api_name
+ });
+ return seed.secret;
+};
+
+/*
+ Function to find the user otp code, should take one parameter:
+ Param 1(integer): user id
+ Return a promise with the otp code row from the db.
+ */
+const findUserOtp = (user_id) => {
+ return dbQuery.findOne('otp code', {
+ where: {
+ used: false,
+ user_id
+ },
+ attributes: ['id', 'secret']
+ });
+};
+
+/*
+ Function to create a user otp code, should take one parameter:
+ Param 1(integer): user id
+ Return a promise with the otp secret created.
+ */
+const createOtp = (user_id) => {
+ const secret = generateOtpSecret();
+ return getModel('otp code').create({
+ user_id,
+ secret
+ })
+ .then((otpCode) => otpCode.secret);
+};
+
+/*
+ Function to find update the uset otp_enabled field,
+ should take two parameter:
+
+ Param 1(integer): user id
+ Param 2(boolean): otp_enabled
+
+ Return a promise with the updated user.
+ */
+const updateUserOtpEnabled = (id, otp_enabled = false, transaction) => {
+ return dbQuery.findOne('user', {
+ where: { id },
+ attributes: ['id', 'otp_enabled']
+ }).then((user) => {
+ return user.update(
+ { otp_enabled },
+ { fields: ['otp_enabled'], transaction }
+ );
+ });
+};
+
+/*
+ Function to set used to true in the user otp code and update the user and set otp_enabled to true, should take one parameter:
+ Param 1(integer): user id
+ Return a promise with the user updated.
+ */
+const setActiveUserOtp = (user_id) => {
+ return getModel('sequelize').transaction((transaction) => {
+ return findUserOtp(user_id)
+ .then((otp) => {
+ return otp.update(
+ { used: true },
+ { fields: ['used'], transaction }
+ );
+ })
+ .then(() => {
+ return updateUserOtpEnabled(user_id, true, transaction);
+ });
+ });
+};
+
+const userHasOtpEnabled = (userId) => {
+ return dbQuery.findOne('user', {
+ where: { id: userId },
+ raw: true,
+ attributes: ['otp_enabled']
+ })
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ return user.otp_enabled;
+ });
+};
+
+const checkUserOtpActive = (userId, otpCode) => {
+ return all([
+ dbQuery.findOne('user', {
+ where: { id: userId },
+ raw: true,
+ attributes: ['otp_enabled']
+ }),
+ verifyOtpBeforeAction(userId, otpCode)
+ ]).then(([user, validOtp]) => {
+ if (!user.otp_enabled) {
+ throw new Error(TOKEN_OTP_MUST_BE_ENABLED);
+ } else if (!validOtp) {
+ throw new Error(INVALID_OTP_CODE);
+ }
+ return;
+ });
+};
+
+//Here we setup the security checks for the endpoints
+//that need it (in our case, only /protected). This
+//function will be called every time a request to a protected
+//endpoint is received
+const verifyBearerTokenMiddleware = (req, authOrSecDef, token, cb, isSocket = false) => {
+ const sendError = (msg) => {
+ if (isSocket) {
+ return cb(new Error(ACCESS_DENIED(msg)));
+ } else {
+ return req.res.status(403).json({ message: ACCESS_DENIED(msg) });
+ }
+ };
+
+ if (!has(req.headers, 'api-key') && !has(req.headers, 'authorization')) {
+ return sendError(MISSING_HEADER);
+ }
+
+ if (has(req.headers, 'api-key') && has(req.headers, 'authorization')) {
+ return sendError(MULTIPLE_API_KEY);
+ } else if (!has(req.headers, 'api-key') && has(req.headers, 'authorization')) {
+
+ // Swagger endpoint scopes
+ const endpointScopes = req.swagger
+ ? req.swagger.operation['x-security-scopes']
+ : BASE_SCOPES;
+
+ let ip;
+ if (isSocket) {
+ ip = req.socket ? req.socket.remoteAddress : undefined;
+ } else {
+ ip = req.headers ? req.headers['x-real-ip'] : undefined;
+ }
+
+ //validate the 'Authorization' header. it should have the following format:
+ //'Bearer tokenString'
+ if (token && token.indexOf('Bearer ') === 0) {
+ const tokenString = token.split(' ')[1];
+
+ jwt.verify(tokenString, SECRET, (verificationError, decodedToken) => {
+ //check if the JWT was verified correctly
+ if (!verificationError && decodedToken) {
+ loggerAuth.verbose(
+ 'helpers/auth/verifyToken verified_token',
+ ip,
+ decodedToken.ip,
+ decodedToken.sub
+ );
+
+ // Check set of permissions that are available with the token and set of acceptable permissions set on swagger endpoint
+ if (intersection(decodedToken.scopes, endpointScopes).length === 0) {
+ loggerAuth.error(
+ 'verifyToken',
+ 'not permission',
+ decodedToken.sub.email,
+ decodedToken.scopes,
+ endpointScopes
+ );
+
+ return sendError(NOT_AUTHORIZED);
+ }
+
+ if (decodedToken.iss !== ISSUER) {
+ loggerAuth.error(
+ 'helpers/auth/verifyToken unverified_token',
+ ip
+ );
+ //return the error in the callback if there is one
+ return sendError(TOKEN_EXPIRED);
+ }
+
+ if (getFrozenUsers()[decodedToken.sub.id]) {
+ loggerAuth.error(
+ 'helpers/auth/verifyToken deactivated account',
+ decodedToken.sub.email
+ );
+ //return the error in the callback if there is one
+ return sendError(DEACTIVATED_USER);
+ }
+
+ req.auth = decodedToken;
+ return cb(null);
+ } else {
+ //return the error in the callback if the JWT was not verified
+ return sendError(INVALID_TOKEN);
+ }
+ });
+ } else {
+ //return the error in the callback if the Authorization header doesn't have the correct format
+ return sendError(MISSING_HEADER);
+ }
+ }
+};
+
+const verifyHmacTokenMiddleware = (req, definition, apiKey, cb, isSocket = false) => {
+ const sendError = (msg) => {
+ if (isSocket) {
+ return cb(new Error(ACCESS_DENIED(msg)));
+ } else {
+ return req.res.status(403).json({ message: ACCESS_DENIED(msg) });
+ }
+ };
+
+ // Swagger endpoint scopes
+ const endpointScopes = req.swagger ? req.swagger.operation['x-security-scopes'] : BASE_SCOPES;
+
+ const apiSignature = req.headers ? req.headers['api-signature'] : undefined;
+ const apiExpires = req.headers ? req.headers['api-expires'] : undefined;
+
+ let ip;
+ if (isSocket) {
+ ip = req.socket ? req.socket.remoteAddress : undefined;
+ } else {
+ ip = req.headers ? req.headers['x-real-ip'] : undefined;
+ }
+
+ loggerAuth.verbose('helpers/auth/checkHmacKey ip', ip);
+
+ if (has(req.headers, 'api-key') && has(req.headers, 'authorization')) {
+ return sendError(MULTIPLE_API_KEY);
+ } else if (has(req.headers, 'api-key') && !has(req.headers, 'authorization')) {
+ if (!apiKey) {
+ loggerAuth.error('helpers/auth/checkHmacKey null key', apiKey);
+ return sendError(API_KEY_NULL);
+ } else if (moment().unix() > apiExpires) {
+ loggerAuth.error('helpers/auth/checkHmacKey expired', apiExpires);
+ return sendError(API_REQUEST_EXPIRED);
+ } else if (!apiSignature) {
+ loggerAuth.error('helpers/auth/checkHmacKey null secret', apiKey);
+ return sendError(API_SIGNATURE_NULL);
+ } else {
+ findTokenByApiKey(apiKey)
+ .then((token) => {
+ if (!endpointScopes.includes(token.type)) {
+ loggerAuth.error(
+ 'helpers/auth/checkApiKey/findTokenByApiKey out of scope',
+ apiKey,
+ token.type
+ );
+ return sendError(API_KEY_OUT_OF_SCOPE);
+ } else if (new Date(token.expiry) < new Date()) {
+ loggerAuth.error(
+ 'helpers/auth/checkApiKey/findTokenByApiKey expired key',
+ apiKey
+ );
+ return sendError(API_KEY_EXPIRED);
+ } else if (!token.active) {
+ loggerAuth.error(
+ 'helpers/auth/checkApiKey/findTokenByApiKey inactive',
+ apiKey
+ );
+ return sendError(API_KEY_INACTIVE);
+ } else {
+ const isSignatureValid = checkHmacSignature(
+ token.secret,
+ req
+ );
+ if (!isSignatureValid) {
+ return sendError(API_SIGNATURE_INVALID);
+ } else {
+ req.auth = {
+ sub: { id: token.user.id, email: token.user.email, networkId: token.user.network_id }
+ };
+ cb();
+ }
+ }
+ })
+ .catch((err) => {
+ loggerAuth.error('helpers/auth/checkApiKey catch', err);
+ return sendError(err.message);
+ });
+ }
+ }
+};
+
+const verifyNetworkHmacToken = (req) => {
+ const givenApiKey = req.headers ? req.headers['api-key'] : undefined;
+ const apiSignature = req.headers ? req.headers['api-signature'] : undefined;
+ const apiExpires = req.headers ? req.headers['api-expires'] : undefined;
+
+ if (!givenApiKey) {
+ return reject(new Error(API_KEY_NULL));
+ } else if (!apiSignature) {
+ return reject(new Error(API_SIGNATURE_NULL));
+ } else if (moment().unix() > apiExpires) {
+ return reject(new Error(API_REQUEST_EXPIRED));
+ }
+
+ return getNetworkKeySecret()
+ .then(({ apiKey, apiSecret }) => {
+ if (givenApiKey !== apiKey) {
+ throw new Error(API_KEY_INVALID);
+ }
+ const isSignatureValid = checkHmacSignature(
+ apiSecret,
+ req
+ );
+ if (!isSignatureValid) {
+ throw new Error(API_SIGNATURE_INVALID);
+ } else {
+ return;
+ }
+ });
+};
+
+
+const verifyBearerTokenExpressMiddleware = (scopes = BASE_SCOPES) => (req, res, next) => {
+ const sendError = (msg) => {
+ return req.res.status(403).json({ message: ACCESS_DENIED(msg) });
+ };
+
+ const token = req.headers['authorization'];
+
+ if (token && token.indexOf('Bearer ') === 0) {
+ let tokenString = token.split(' ')[1];
+
+ jwt.verify(tokenString, SECRET, (verificationError, decodedToken) => {
+ if (!verificationError && decodedToken) {
+
+ const issuerMatch = decodedToken.iss == ISSUER;
+
+ if (!issuerMatch) {
+ return sendError(TOKEN_EXPIRED);
+ }
+
+ if (intersection(decodedToken.scopes, scopes).length === 0) {
+ loggerAuth.error(
+ 'verifyToken',
+ 'not permission',
+ decodedToken.sub.email,
+ decodedToken.scopes,
+ scopes
+ );
+
+ return sendError(NOT_AUTHORIZED);
+ }
+
+ if (getFrozenUsers()[decodedToken.sub.id]) {
+ loggerAuth.error(
+ 'helpers/auth/verifyToken deactivated account',
+ decodedToken.sub.email
+ );
+ //return the error in the callback if there is one
+ return sendError(DEACTIVATED_USER);
+ }
+
+ req.auth = decodedToken;
+ return next();
+ } else {
+ return sendError(INVALID_TOKEN);
+ }
+ });
+ } else {
+ return sendError(MISSING_HEADER);
+ }
+};
+
+const verifyBearerTokenPromise = (token, ip, scopes = BASE_SCOPES) => {
+ if (token && token.indexOf('Bearer ') === 0) {
+ const tokenString = token.split(' ')[1];
+ const jwtVerifyAsync = promisify(jwt.verify, jwt);
+
+ return jwtVerifyAsync(tokenString, SECRET)
+ .then((decodedToken) => {
+ loggerAuth.verbose(
+ 'helpers/auth/verifyToken verified_token',
+ ip,
+ decodedToken.ip,
+ decodedToken.sub
+ );
+
+ // Check set of permissions that are available with the token and set of acceptable permissions set on swagger endpoint
+ if (intersection(decodedToken.scopes, scopes).length === 0) {
+ loggerAuth.error(
+ 'verifyToken',
+ 'not permission',
+ decodedToken.sub.email,
+ decodedToken.scopes,
+ scopes
+ );
+
+ throw new Error(NOT_AUTHORIZED);
+ }
+
+ if (decodedToken.iss !== ISSUER) {
+ loggerAuth.error(
+ 'helpers/auth/verifyToken unverified_token',
+ ip
+ );
+ //return the error in the callback if there is one
+ throw new Error(TOKEN_EXPIRED);
+ }
+
+ if (getFrozenUsers()[decodedToken.sub.id]) {
+ loggerAuth.error(
+ 'helpers/auth/verifyToken deactivated account',
+ decodedToken.sub.email
+ );
+ throw new Error(DEACTIVATED_USER);
+ }
+ return decodedToken;
+ });
+ } else {
+ //return the error in the callback if the Authorization header doesn't have the correct format
+ return reject(new Error(MISSING_HEADER));
+ }
+};
+
+const verifyHmacTokenPromise = (apiKey, apiSignature, apiExpires, method, originalUrl, body, scopes = BASE_SCOPES) => {
+ if (!apiKey) {
+ return reject(new Error(API_KEY_NULL));
+ } else if (!apiSignature) {
+ return reject(new Error(API_SIGNATURE_NULL));
+ } else if (moment().unix() > apiExpires) {
+ return reject(new Error(API_REQUEST_EXPIRED));
+ } else {
+ return findTokenByApiKey(apiKey)
+ .then((token) => {
+ if (!scopes.includes(token.type)) {
+ loggerAuth.error(
+ 'helpers/auth/checkApiKey/findTokenByApiKey out of scope',
+ apiKey,
+ token.type
+ );
+ throw new Error(API_KEY_OUT_OF_SCOPE);
+ } else if (new Date(token.expiry) < new Date()) {
+ loggerAuth.error(
+ 'helpers/auth/checkApiKey/findTokenByApiKey expired key',
+ apiKey
+ );
+ throw new Error(API_KEY_EXPIRED);
+ } else if (!token.active) {
+ loggerAuth.error(
+ 'helpers/auth/checkApiKey/findTokenByApiKey inactive',
+ apiKey
+ );
+ throw new Error(API_KEY_INACTIVE);
+ } else {
+ const isSignatureValid = checkHmacSignature(
+ token.secret,
+ { body, headers: { 'api-signature': apiSignature, 'api-expires': apiExpires }, method, originalUrl }
+ );
+ if (!isSignatureValid) {
+ throw new Error(API_SIGNATURE_INVALID);
+ } else {
+ return {
+ sub: { id: token.user.id, email: token.user.email, networkId: token.user.network_id }
+ };
+ }
+ }
+ });
+ }
+};
+
+/**
+ * Function that checks to see if user's scope is valid for the endpoint.
+ * @param {array} endpointScopes - Authorized scopes for the endpoint.
+ * @param {array} userScopes - Scopes of the user.
+ * @returns {boolean} True if user scope is authorized for endpoint. False if not.
+ */
+const userScopeIsValid = (endpointScopes, userScopes) => {
+ if (intersection(endpointScopes, userScopes).length === 0) {
+ return false;
+ } else {
+ return true;
+ }
+};
+
+/**
+ * Function that checks to see if user's account is deactivated.
+ * @param {array} deactivatedUsers - Ids of deactivated users.
+ * @param {array} userId - Id of user.
+ * @returns {boolean} True if user account is deactivated. False if not.
+ */
+const userIsDeactivated = (deactivatedUsers, userId) => {
+ if (deactivatedUsers[userId]) {
+ return true;
+ } else {
+ return false;
+ }
+};
+
+const checkAdminIp = (whiteListedIps = [], ip = '') => {
+ if (whiteListedIps.length === 0) {
+ return true; // no ip restriction for admin
+ } else {
+ return whiteListedIps.includes(ip);
+ }
+};
+
+const issueToken = (
+ id,
+ networkId,
+ email,
+ ip,
+ isAdmin = false,
+ isSupport = false,
+ isSupervisor = false,
+ isKYC = false,
+ isCommunicator = false
+) => {
+ // Default scope is ['user']
+ let scopes = [].concat(BASE_SCOPES);
+
+ if (checkAdminIp(getKitSecrets().admin_whitelist, ip)) {
+ if (isAdmin) {
+ scopes = scopes.concat(ROLES.ADMIN);
+ }
+ if (isSupport) {
+ scopes = scopes.concat(ROLES.SUPPORT);
+ }
+ if (isSupervisor) {
+ scopes = scopes.concat(ROLES.SUPERVISOR);
+ }
+ if (isKYC) {
+ scopes = scopes.concat(ROLES.KYC);
+ }
+ if (isCommunicator) {
+ scopes = scopes.concat(ROLES.COMMUNICATOR);
+ }
+ }
+
+ const token = jwt.sign(
+ {
+ sub: {
+ id,
+ email,
+ networkId
+ },
+ scopes,
+ ip,
+ iss: ISSUER
+ },
+ SECRET,
+ {
+ expiresIn: getKitSecrets().security.token_time
+ }
+ );
+ return token;
+};
+
+const createHmacSignature = (secret, verb, path, expires, data = '') => {
+ const stringData = typeof data === 'string' ? data : JSON.stringify(data);
+
+ const signature = crypto
+ .createHmac('sha256', secret)
+ .update(verb + path + expires + stringData)
+ .digest('hex');
+ return signature;
+};
+
+const maskToken = (token = '') => {
+ return token.substr(0, 3) + '********';
+};
+/*
+ Function that transform the token object from the db to a formated object
+ Takes one parameter:
+
+ Parameter 1(object): token object from the db
+
+ Retuns a json objecet
+*/
+const formatTokenObject = (tokenData) => ({
+ id: tokenData.id,
+ name: tokenData.name,
+ apiKey: tokenData.key,
+ secret: maskToken(tokenData.secret),
+ active: tokenData.active,
+ revoked: tokenData.revoked,
+ expiry: tokenData.expiry,
+ created: tokenData.created_at
+});
+
+const getUserKitHmacTokens = (userId) => {
+ return dbQuery.findAndCountAllWithRows('token', {
+ where: {
+ user_id: userId,
+ type: TOKEN_TYPES.HMAC
+ },
+ attributes: {
+ exclude: ['user_id', 'updated_at']
+ },
+ order: [['created_at', 'DESC'], ['id', 'ASC']]
+ })
+ .then(({ count, data }) => {
+ const result = {
+ count: count,
+ data: data.map(formatTokenObject)
+ };
+ return result;
+ });
+};
+
+const createUserKitHmacToken = (userId, otpCode, ip, name) => {
+ const key = crypto.randomBytes(20).toString('hex');
+ const secret = crypto.randomBytes(25).toString('hex');
+ const expiry = Date.now() + HMAC_TOKEN_EXPIRY;
+
+ return checkUserOtpActive(userId, otpCode)
+ .then(() => {
+ return getModel('token').create({
+ user_id: userId,
+ ip,
+ key,
+ secret,
+ expiry,
+ role: ROLES.USER,
+ type: TOKEN_TYPES.HMAC,
+ name,
+ active: true
+ });
+ })
+ .then(() => {
+ return {
+ apiKey: key,
+ secret
+ };
+ });
+};
+
+const deleteUserKitHmacToken = (userId, otpCode, tokenId) => {
+ return checkUserOtpActive(userId, otpCode)
+ .then(() => {
+ return dbQuery.findOne('token', {
+ where: {
+ id: tokenId,
+ user_id: userId
+ }
+ });
+ })
+ .then((token) => {
+ if (!token) {
+ throw new Error(TOKEN_NOT_FOUND);
+ } else if (token.revoked) {
+ throw new Error(TOKEN_REVOKED);
+ }
+ return token.update(
+ {
+ active: false,
+ revoked: true
+ },
+ { fields: ['active', 'revoked'], returning: true }
+ );
+ })
+ .then((token) => {
+ client.hdelAsync(HMAC_TOKEN_KEY, token.key);
+ return formatTokenObject(token);
+ });
+};
+
+const findToken = (query) => {
+ return dbQuery.findOne('token', query);
+};
+
+const findTokenByApiKey = (apiKey) => {
+ return client.hgetAsync(HMAC_TOKEN_KEY, apiKey)
+ .then(async (token) => {
+ if (!token) {
+ loggerAuth.debug(
+ 'security/findTokenByApiKey apiKey not found in redis',
+ apiKey
+ );
+
+ token = await dbQuery.findOne('token', {
+ where: {
+ key: apiKey,
+ active: true
+ },
+ raw: true,
+ nest: true,
+ include: [
+ {
+ model: getModel('user'),
+ as: 'user',
+ attributes: ['id', 'email', 'network_id']
+ }
+ ]
+ });
+
+ if (!token) {
+ loggerAuth.error(
+ 'security/findTokenByApiKey invalid key',
+ apiKey
+ );
+ throw new Error(API_KEY_INVALID);
+ }
+
+ client.hsetAsync(HMAC_TOKEN_KEY, apiKey, JSON.stringify(token));
+
+ loggerAuth.debug(
+ 'security/findTokenByApiKey apiKey stored in redis',
+ apiKey
+ );
+
+ return token;
+ } else {
+ loggerAuth.debug(
+ 'security/findTokenByApiKey apiKey found in redis',
+ apiKey
+ );
+ return JSON.parse(token);
+ }
+ });
+};
+
+const calculateSignature = (secret = '', verb, path, nonce, data = '') => {
+ const stringData = typeof data === 'string' ? data : JSON.stringify(data);
+
+ const signature = crypto
+ .createHmac('sha256', secret)
+ .update(verb + path + nonce + stringData)
+ .digest('hex');
+ return signature;
+};
+
+const checkHmacSignature = (
+ secret,
+ { body, headers, method, originalUrl }
+) => {
+ const signature = headers['api-signature'];
+ const expires = headers['api-expires'];
+
+ const calculatedSignature = calculateSignature(
+ secret,
+ method,
+ originalUrl,
+ expires,
+ body
+ );
+ return calculatedSignature === signature;
+};
+
+const isValidScope = (endpointScopes, userScopes) => {
+ if (intersection(endpointScopes, userScopes).length === 0) {
+ return false;
+ } else {
+ return true;
+ }
+};
+
+module.exports = {
+ checkCaptcha,
+ resetUserPassword,
+ isValidPassword,
+ validatePassword,
+ sendResetPasswordCode,
+ changeUserPassword,
+ confirmChangeUserPassword,
+ hasUserOtpEnabled,
+ verifyOtpBeforeAction,
+ verifyOtp,
+ checkOtp,
+ generateOtp,
+ generateOtpSecret,
+ findUserOtp,
+ setActiveUserOtp,
+ updateUserOtpEnabled,
+ createOtp,
+ userHasOtpEnabled,
+ checkUserOtpActive,
+ verifyBearerTokenPromise,
+ verifyHmacTokenPromise,
+ verifyBearerTokenMiddleware,
+ verifyHmacTokenMiddleware,
+ verifyNetworkHmacToken,
+ userScopeIsValid,
+ userIsDeactivated,
+ findToken,
+ issueToken,
+ getUserKitHmacTokens,
+ createUserKitHmacToken,
+ deleteUserKitHmacToken,
+ checkHmacSignature,
+ createHmacSignature,
+ isValidScope,
+ verifyBearerTokenExpressMiddleware,
+ getCountryFromIp,
+ checkIp
+};
diff --git a/server/utils/toolsLib/tools/tier.js b/server/utils/toolsLib/tools/tier.js
new file mode 100644
index 0000000000..48960a2f36
--- /dev/null
+++ b/server/utils/toolsLib/tools/tier.js
@@ -0,0 +1,263 @@
+'use strict';
+
+const { SERVER_PATH } = require('../constants');
+const dbQuery = require('./database/query');
+const { getModel } = require('./database');
+const { getKitTiers, getKitPairs, subscribedToPair, getTierLevels, getDefaultFees } = require('./common');
+const { reject, all } = require('bluebird');
+const { difference, omit, isNumber, each, isString } = require('lodash');
+const { publisher } = require('./database/redis');
+const { CONFIGURATION_CHANNEL } = require(`${SERVER_PATH}/constants`);
+const flatten = require('flat');
+
+const findTier = (level) => {
+ return dbQuery.findOne('tier', {
+ where: {
+ id: level
+ }
+ })
+ .then((tier) => {
+ if (!tier) {
+ throw new Error('Tier does not exist');
+ }
+ return tier;
+ });
+};
+
+const createTier = (level, name, icon, description, deposit_limit, withdrawal_limit, fees = {}, note = '') => {
+ const existingTiers = getKitTiers();
+
+ if (existingTiers[level]) {
+ return reject(new Error('Tier already exists'));
+ } else if (
+ withdrawal_limit < 0
+ && withdrawal_limit !== -1
+ ) {
+ return reject(new Error('Withdrawal limit cannot be a negative number other than -1'));
+ } else if (
+ deposit_limit < 0
+ && deposit_limit !== -1
+ ) {
+ return reject(new Error('Withdrawal limit cannot be a negative number other than -1'));
+ }
+
+ const givenMakerSymbols = Object.keys(omit(fees.maker, 'default'));
+ const givenTakerSymbols = Object.keys(omit(fees.taker, 'default'));
+
+ if (
+ givenMakerSymbols.length > 0
+ && difference(givenMakerSymbols, getKitPairs()).length > 0
+ ) {
+ return reject(new Error('Maker fees includes a symbol that you are not subscribed to'));
+ } else if (
+ givenTakerSymbols.length > 0
+ && difference(givenTakerSymbols, getKitPairs()).length > 0
+ ) {
+ return reject(new Error('Taker fees includes a symbol that you are not subscribed to'));
+ }
+
+ const minFees = getDefaultFees();
+
+ const invalidMakerFees = Object.values(flatten(fees.maker)).some(fee => fee < minFees.maker);
+ const invalidTakerFees = Object.values(flatten(fees.taker)).some(fee => fee < minFees.taker);
+
+ if (invalidMakerFees || invalidTakerFees) {
+ return reject(new Error(`Invalid fee given. Minimum maker fee: ${minFees.maker}. Minimum taker fee: ${minFees.taker}`));
+ }
+
+ const tierFees = {
+ maker: {},
+ taker: {}
+ };
+
+ each(getKitPairs(), (pair) => {
+ tierFees.maker[pair] = fees.maker[pair] || fees.maker.default;
+ tierFees.taker[pair] = fees.taker[pair] || fees.taker.default;
+ });
+
+ return getModel('tier').create({
+ id: level,
+ name,
+ icon,
+ description,
+ deposit_limit,
+ withdrawal_limit,
+ fees: tierFees,
+ note
+ })
+ .then((tier) => {
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update',
+ data: {
+ tiers: {
+ [tier.id]: tier
+ }
+ }
+ })
+ );
+ return tier;
+ });
+};
+
+const updateTier = (level, updateData) => {
+ const existingTiers = getKitTiers();
+
+ if (!existingTiers[level]) {
+ return reject(new Error('Tier does not exist'));
+ } else if (updateData.deposit_limit !== undefined || updateData.withdrawal_limit !== undefined) {
+ return reject(new Error('Cannot update limits through this endpoint'));
+ } else if (updateData.fees !== undefined) {
+ return reject(new Error('Cannot update fees through this endpoint'));
+ }
+
+ return findTier(level)
+ .then((tier) => {
+ const newData = {};
+
+ if (isString(updateData.name)) {
+ newData.name = updateData.name;
+ }
+
+ if (isString(updateData.icon)) {
+ newData.icon = updateData.icon;
+ }
+
+ if (isString(updateData.note)) {
+ newData.note = updateData.note;
+ }
+
+ if (isString(updateData.description)) {
+ newData.description = updateData.description;
+ }
+
+ return tier.update(newData);
+ })
+ .then((tier) => {
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update',
+ data: {
+ tiers: {
+ [tier.id]: tier
+ }
+ }
+ })
+ );
+ return tier;
+ });
+};
+
+const updatePairFees = (pair, fees) => {
+ if (!subscribedToPair(pair)) {
+ return reject(new Error('Invalid pair'));
+ }
+
+ const tiersToUpdate = Object.keys(fees);
+
+ if (difference(tiersToUpdate, getTierLevels()).length > 0) {
+ return reject(new Error('Invalid tier level given'));
+ }
+
+ return getModel('sequelize').transaction((transaction) => {
+ return all(tiersToUpdate.map(async (level) => {
+
+ const minFees = getDefaultFees();
+
+ if (fees[level].maker < minFees.maker || fees[level].taker < minFees.taker) {
+ throw new Error(`Invalid fee given. Minimum maker fee: ${minFees.maker}. Minimum taker fee: ${minFees.taker}`);
+ }
+
+ const tier = await dbQuery.findOne('tier', { where: { id: level } });
+
+ const updatedFees = {
+ maker: { ...tier.fees.maker },
+ taker: { ...tier.fees.taker }
+ };
+ updatedFees.maker[pair] = fees[level].maker;
+ updatedFees.taker[pair] = fees[level].taker;
+
+ return tier.update(
+ { fees: updatedFees },
+ { fields: ['fees'], transaction }
+ );
+ }));
+ })
+ .then((data) => {
+ const updatedTiers = {};
+ each(data, (tier) => {
+ updatedTiers[tier.id] = {
+ ...tier.dataValues
+ };
+ });
+
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update',
+ data: {
+ tiers: updatedTiers
+ }
+ })
+ );
+ });
+};
+
+const updateTiersLimits = (limits) => {
+ if (!Object.keys(limits).length === 0) {
+ return reject(new Error('No new limits given'));
+ }
+
+ const tiersToUpdate = Object.keys(limits);
+
+ if (difference(tiersToUpdate, getTierLevels()).length > 0) {
+ return reject(new Error('Invalid tier level given'));
+ }
+
+ if (Object.values(flatten(limits)).some(limit => limit < 0 && limit !== -1)) {
+ return reject(new Error('Limits can be either -1 or GTE 0'));
+ }
+
+ return getModel('sequelize').transaction((transaction) => {
+ return all(tiersToUpdate.map(async (level) => {
+
+ const tier = await dbQuery.findOne('tier', { where: { id: level } });
+
+ const deposit_limit = isNumber(limits[level].deposit_limit) ? limits[level].deposit_limit : tier.deposit_limit;
+ const withdrawal_limit = isNumber(limits[level].withdrawal_limit) ? limits[level].withdrawal_limit : tier.withdrawal_limit;
+
+ return tier.update(
+ { deposit_limit, withdrawal_limit },
+ { fields: [ 'deposit_limit', 'withdrawal_limit' ], transaction }
+ );
+ }));
+ })
+ .then((data) => {
+ const updatedTiers = {};
+ each(data, (tier) => {
+ updatedTiers[tier.id] = {
+ ...tier.dataValues
+ };
+ });
+
+ publisher.publish(
+ CONFIGURATION_CHANNEL,
+ JSON.stringify({
+ type: 'update',
+ data: {
+ tiers: updatedTiers
+ }
+ })
+ );
+ });
+};
+
+module.exports = {
+ findTier,
+ createTier,
+ updateTier,
+ updatePairFees,
+ updateTiersLimits
+};
diff --git a/server/utils/toolsLib/tools/user.js b/server/utils/toolsLib/tools/user.js
new file mode 100644
index 0000000000..65a21f4b02
--- /dev/null
+++ b/server/utils/toolsLib/tools/user.js
@@ -0,0 +1,1664 @@
+'use strict';
+
+const { getModel } = require('./database/model');
+const dbQuery = require('./database/query');
+const {
+ has,
+ omit,
+ pick,
+ each,
+ differenceWith,
+ isEqual,
+ isString,
+ isNumber,
+ isBoolean,
+ isPlainObject,
+ isNil,
+ isArray,
+ isInteger,
+ keyBy,
+ isEmpty,
+ uniq
+} = require('lodash');
+const { isEmail } = require('validator');
+const randomString = require('random-string');
+const { SERVER_PATH } = require('../constants');
+const {
+ SIGNUP_NOT_AVAILABLE,
+ PROVIDE_VALID_EMAIL,
+ USER_EXISTS,
+ INVALID_PASSWORD,
+ INVALID_VERIFICATION_CODE,
+ USER_NOT_FOUND,
+ USER_NOT_VERIFIED,
+ USER_NOT_ACTIVATED,
+ INVALID_CREDENTIALS,
+ INVALID_OTP_CODE,
+ USERNAME_CANNOT_BE_CHANGED,
+ USERNAME_IS_TAKEN,
+ INVALID_USERNAME,
+ ACCOUNT_NOT_VERIFIED,
+ INVALID_VERIFICATION_LEVEL,
+ USER_EMAIL_NOT_VERIFIED,
+ USER_EMAIL_IS_VERIFIED,
+ NO_DATA_FOR_CSV,
+ PROVIDE_USER_CREDENTIALS,
+ PROVIDE_KIT_ID,
+ PROVIDE_NETWORK_ID,
+ CANNOT_DEACTIVATE_ADMIN,
+ USER_ALREADY_DEACTIVATED,
+ USER_NOT_DEACTIVATED,
+ CANNOT_CHANGE_ADMIN_ROLE,
+ VERIFICATION_CODE_USED,
+ USER_NOT_REGISTERED_ON_NETWORK
+} = require(`${SERVER_PATH}/messages`);
+const { publisher } = require('./database/redis');
+const {
+ CONFIGURATION_CHANNEL,
+ AUDIT_KEYS,
+ USER_FIELD_ADMIN_LOG,
+ ADDRESS_FIELDS,
+ ID_FIELDS,
+ SETTING_KEYS,
+ OMITTED_USER_FIELDS,
+ DEFAULT_ORDER_RISK_PERCENTAGE,
+ AFFILIATION_CODE_LENGTH
+} = require(`${SERVER_PATH}/constants`);
+const { sendEmail } = require(`${SERVER_PATH}/mail`);
+const { MAILTYPE } = require(`${SERVER_PATH}/mail/strings`);
+const { getKitConfig, isValidTierLevel, getKitTier, isDatetime } = require('./common');
+const { isValidPassword } = require('./security');
+const { getNodeLib } = require(`${SERVER_PATH}/init`);
+const { all, reject } = require('bluebird');
+const { Op } = require('sequelize');
+const { paginationQuery, timeframeQuery, orderingQuery } = require('./database/helpers');
+const { parse } = require('json2csv');
+const flatten = require('flat');
+const uuid = require('uuid/v4');
+const { checkCaptcha, validatePassword, verifyOtpBeforeAction } = require('./security');
+
+ /* Onboarding*/
+
+const signUpUser = (email, password, opts = { referral: null }) => {
+ if (!getKitConfig().new_user_is_activated) {
+ return reject(new Error(SIGNUP_NOT_AVAILABLE));
+ }
+
+ if (!email || !isEmail(email)) {
+ return reject(new Error(PROVIDE_VALID_EMAIL));
+ }
+
+ if (!isValidPassword(password)) {
+ return reject(new Error(INVALID_PASSWORD));
+ }
+
+ email = email.toLowerCase();
+
+ return dbQuery.findOne('user', {
+ where: { email },
+ attributes: ['email']
+ })
+ .then((user) => {
+ if (user) {
+ throw new Error(USER_EXISTS);
+ }
+ return getModel('sequelize').transaction((transaction) => {
+ return getModel('user').create({
+ email,
+ password,
+ verification_level: 1,
+ settings: INITIAL_SETTINGS()
+ }, { transaction })
+ .then((user) => {
+ return all([
+ createUserOnNetwork(email),
+ user
+ ]);
+ })
+ .then(([ networkUser, user ]) => {
+ return user.update(
+ { network_id: networkUser.id },
+ { fields: ['network_id'], returning: true, transaction }
+ );
+ });
+ });
+ })
+ .then((user) => {
+ return all([
+ getVerificationCodeByUserId(user.id),
+ user
+ ]);
+ })
+ .then(([ verificationCode, user ]) => {
+ sendEmail(
+ MAILTYPE.SIGNUP,
+ email,
+ verificationCode.code,
+ {}
+ );
+ if (opts.referral && isString(opts.referral)) {
+ checkAffiliation(opts.referral, user.id);
+ }
+ return user;
+ });
+};
+
+const verifyUser = (email, code) => {
+ email = email.toLowerCase();
+ return dbQuery.findOne('user',
+ { where: { email }, attributes: ['id', 'email', 'settings', 'network_id'] }
+ )
+ .then((user) => {
+ return all([
+ dbQuery.findOne('verification code',
+ {
+ where: { user_id: user.id },
+ attributes: ['id', 'code', 'verified', 'user_id']
+ }
+ ),
+ user
+ ]);
+ })
+ .then(([ verificationCode, user ]) => {
+ if (verificationCode.verified) {
+ throw new Error(USER_EMAIL_IS_VERIFIED);
+ }
+ if (code !== verificationCode.code) {
+ throw new Error(INVALID_VERIFICATION_CODE);
+ }
+ return all([
+ user,
+ verificationCode.update({ verified: true }, { fields: ['verified'], returning: true })
+ ]);
+ })
+ .then(([ user ]) => {
+ return user;
+ });
+};
+
+const createUser = (
+ email,
+ password,
+ opts = {
+ role: 'user',
+ id: null,
+ additionalHeaders: null
+ }
+) => {
+ email = email.toLowerCase();
+ return getModel('sequelize').transaction((transaction) => {
+ return dbQuery.findOne('user', {
+ where: { email }
+ })
+ .then((user) => {
+ if (user) {
+ throw new Error(USER_EXISTS);
+ }
+ const roles = {
+ is_admin: false,
+ is_supervisor: false,
+ is_support: false,
+ is_kyc: false,
+ is_communicator: false
+ };
+
+ if (opts.role !== 'user') {
+ const userRole = 'is_' + opts.role.toLowerCase();
+ if (roles[userRole] === undefined) {
+ throw new Error('Role does not exist');
+ }
+ each(roles, (value, key) => {
+ if (key === userRole) {
+ roles[key] = true;
+ }
+ });
+ }
+
+ const options = {
+ email,
+ password,
+ settings: INITIAL_SETTINGS(),
+ ...roles
+ };
+
+ if (isNumber(opts.id)) {
+ options.id = opts.id;
+ }
+
+ return getModel('user').create(options, { transaction });
+ })
+ .then((user) => {
+ return all([
+ user,
+ getNodeLib().createUser(email, { additionalHeaders: opts.additionalHeaders })
+ ]);
+ })
+ .then(([ kitUser, networkUser ]) => {
+ return kitUser.update({
+ network_id: networkUser.id
+ }, { returning: true, fields: ['network_id'], transaction });
+ });
+ })
+ .then((user) => {
+ return all([
+ user,
+ getModel('verification code').update(
+ { verified: true },
+ { where: { user_id: user.id }, fields: [ 'verified' ]}
+ )
+ ]);
+ })
+ .then(([ user ]) => {
+ sendEmail(
+ MAILTYPE.WELCOME,
+ user.email,
+ {},
+ user.settings
+ );
+ return;
+ });
+};
+
+const createUserOnNetwork = (email, opts = {
+ additionalHeaders: null
+}) => {
+ if (!isEmail(email)) {
+ return reject(new Error(PROVIDE_VALID_EMAIL));
+ }
+
+ return getNodeLib().createUser(email, opts);
+};
+
+const loginUser = (email, password, otp_code, captcha, ip, device, domain, origin, referer) => {
+ return getUserByEmail(email.toLowerCase())
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.verification_level === 0) {
+ throw new Error(USER_NOT_VERIFIED);
+ } else if (getKitConfig().email_verification_required && !user.email_verified) {
+ throw new Error(USER_EMAIL_NOT_VERIFIED);
+ } else if (!user.activated) {
+ throw new Error(USER_NOT_ACTIVATED);
+ }
+ return all([
+ user,
+ validatePassword(user.password, password)
+ ]);
+ })
+ .then(([ user, passwordIsValid ]) => {
+ if (!passwordIsValid) {
+ throw new Error(INVALID_CREDENTIALS);
+ }
+
+ if (!user.otp_enabled) {
+ return all([ user, checkCaptcha(captcha, ip) ]);
+ } else {
+ return all([
+ user,
+ verifyOtpBeforeAction(user.id, otp_code).then((validOtp) => {
+ if (!validOtp) {
+ throw new Error(INVALID_OTP_CODE);
+ } else {
+ return checkCaptcha(captcha, ip);
+ }
+ })
+ ]);
+ }
+ })
+ .then(([ user ]) => {
+ if (ip) {
+ registerUserLogin(user.id, ip, device, domain, origin, referer);
+ }
+ return user;
+ });
+};
+
+const registerUserLogin = (
+ userId,
+ ip,
+ opts = {
+ device: null,
+ domain: null,
+ origin: null,
+ referer: null
+ }
+) => {
+ const login = {
+ user_id: userId,
+ ip
+ };
+
+ if (isString(opts.device)) {
+ login.device = opts.device;
+ }
+
+ if (isString(opts.domain)) {
+ login.domain = opts.domain;
+ }
+
+ if (isString(opts.origin)) {
+ login.origin = opts.origin;
+ }
+
+ if (isString(opts.referer)) {
+ login.referer = opts.referer;
+ }
+
+ return getModel('login').create(login);
+};
+
+ /* Public Endpoints*/
+
+
+const getVerificationCodeByUserEmail = (email) => {
+ return getUserByEmail(email)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ return getVerificationCodeByUserId(user.id);
+ });
+};
+
+const generateAffiliationCode = () => {
+ return randomString({
+ length: AFFILIATION_CODE_LENGTH,
+ numeric: true,
+ letters: true
+ }).toUpperCase();
+};
+
+const getVerificationCodeByUserId = (user_id) => {
+ return dbQuery.findOne('verification code', {
+ where: { user_id },
+ attributes: ['id', 'code', 'verified', 'user_id']
+ });
+};
+
+const getUserByAffiliationCode = (affiliationCode) => {
+ const code = affiliationCode.toUpperCase().trim();
+ return dbQuery.findOne('user', {
+ where: { affiliation_code: code },
+ attributes: ['id', 'email', 'affiliation_code']
+ });
+};
+
+const checkAffiliation = (affiliationCode, user_id) => {
+ // let discount = 0; // default discount rate in percentage
+ return getUserByAffiliationCode(affiliationCode)
+ .then((referrer) => {
+ if (referrer) {
+ return getModel('affiliation').create({
+ user_id,
+ referer_id: referrer.id
+ });
+ } else {
+ return;
+ }
+ });
+ // .then((affiliation) => {
+ // return getModel('user').update(
+ // {
+ // discount
+ // },
+ // {
+ // where: {
+ // id: affiliation.user_id
+ // },
+ // fields: ['discount']
+ // }
+ // );
+ // });
+};
+
+const getAffiliationCount = (userId) => {
+ return getModel('affiliation').count({
+ where: {
+ referer_id: userId
+ }
+ });
+};
+
+const isValidUsername = (username) => {
+ return /^[a-z0-9_]{3,15}$/.test(username);
+};
+
+/**
+ *
+ * @param {object} user - User object
+ * @return {object}
+ */
+const omitUserFields = (user) => {
+ return omit(user, OMITTED_USER_FIELDS);
+};
+
+const getAllUsers = () => {
+ return dbQuery.findAll('user', {
+ attributes: {
+ exclude: OMITTED_USER_FIELDS
+ }
+ });
+};
+
+const getAllUsersAdmin = (opts = {
+ id: null,
+ search: null,
+ pending: null,
+ limit: null,
+ page: null,
+ order_by: null,
+ order: null,
+ start_date: null,
+ end_date: null,
+ format: null,
+ additionalHeaders: null
+}) => {
+ const pagination = paginationQuery(opts.limit, opts.page);
+ const timeframe = timeframeQuery(opts.start_date, opts.end_date);
+ const ordering = orderingQuery(opts.order_by, opts.order);
+ let query = {
+ where: {
+ created_at: timeframe
+ }
+ };
+ if (opts.id || opts.search) {
+ query.attributes = {
+ exclude: ['balance', 'password', 'updated_at']
+ };
+ if (opts.id) {
+ query.where.id = opts.id;
+ } else {
+ query.where = {
+ $or: [
+ {
+ email: {
+ [Op.like]: `%${opts.search}%`
+ }
+ },
+ {
+ username: {
+ [Op.like]: `%${opts.search}%`
+ }
+ },
+ {
+ full_name: {
+ [Op.like]: `%${opts.search}%`
+ }
+ },
+ {
+ phone_number: {
+ [Op.like]: `%${opts.search}%`
+ }
+ },
+ getModel('sequelize').literal(`id_data ->> 'number'='${opts.search}'`)
+ ]
+ };
+ }
+ } else if (isBoolean(opts.pending) && opts.pending) {
+ query = {
+ where: {
+ $or: [
+ getModel('sequelize').literal('bank_account @> \'[{"status":1}]\''),
+ {
+ id_data: {
+ status: 1
+ }
+ },
+ {
+ activated: false
+ }
+ ]
+ },
+ attributes: [
+ 'id',
+ 'email',
+ 'verification_level',
+ 'id_data',
+ 'bank_account',
+ 'activated'
+ ],
+ order: [ordering]
+ };
+ } else {
+ query = {
+ where: {},
+ attributes: {
+ exclude: ['password', 'is_admin', 'is_support', 'is_supervisor', 'is_kyc', 'is_communicator']
+ },
+ order: [ordering]
+ };
+ }
+
+ if (!opts.format) {
+ query = {...query, ...pagination};
+ } else if (isBoolean(opts.pending) && !opts.pending) {
+ query.attributes.exclude.push('settings');
+ }
+
+ return dbQuery.findAndCountAllWithRows('user', query)
+ .then(async ({ count, data }) => {
+ if (opts.id || opts.search) {
+ if (count === 0) {
+ // Need to throw error if query was for one user and the user is not found
+ const error = new Error(USER_NOT_FOUND);
+ error.status = 404;
+ throw error;
+ } else if (data[0].verification_level > 0 && data[0].network_id) {
+ const userNetworkData = await getNodeLib().getUser(data[0].network_id, { additionalHeaders: opts.additionalHeaders });
+ data[0].balance = userNetworkData.balance;
+ data[0].wallet = userNetworkData.wallet;
+ return { count, data };
+ }
+ }
+ return { count, data };
+ })
+ .then(async (users) => {
+ if (opts.format) {
+ if (users.data.length === 0) {
+ throw new Error(NO_DATA_FOR_CSV);
+ }
+ const flatData = users.data.map((user) => {
+ let id_data;
+ if (user.id_data) {
+ id_data = user.id_data;
+ user.id_data = {};
+ }
+ const result = flatten(user, { safe: true });
+ if (id_data) result.id_data = id_data;
+ return result;
+ });
+ const csv = parse(flatData, Object.keys(flatData[0]));
+ return csv;
+ } else {
+ return users;
+ }
+ });
+};
+
+const getUser = (identifier = {}, rawData = true, networkData = false, opts = {
+ additionalHeaders: null
+}) => {
+ if (!identifier.email && !identifier.kit_id && !identifier.network_id) {
+ return reject(new Error(PROVIDE_USER_CREDENTIALS));
+ }
+
+ const where = {};
+ if (identifier.email) {
+ where.email = identifier.email;
+ } else if (identifier.kit_id) {
+ where.id = identifier.kit_id;
+ } else {
+ where.network_id = identifier.network_id;
+ }
+
+ return dbQuery.findOne('user', {
+ where,
+ raw: rawData
+ })
+ .then(async (user) => {
+ if (user && networkData) {
+ const networkData = await getNodeLib().getUser(user.network_id, opts);
+ user.balance = networkData.balance;
+ user.wallet = networkData.wallet;
+ if (!rawData) {
+ user.dataValues.balance = networkData.balance;
+ user.dataValues.wallet = networkData.wallet;
+ }
+ }
+ return user;
+ });
+};
+
+const getUserNetwork = (networkId, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getUser(networkId, opts);
+};
+
+const getUsersNetwork = (opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getUsers(opts);
+};
+
+const getUserByEmail = (email, rawData = true, networkData = false, opts = {
+ additionalHeaders: null
+}) => {
+ if (!email || !isEmail(email)) {
+ return reject(new Error(PROVIDE_VALID_EMAIL));
+ }
+ return getUser({ email }, rawData, networkData, opts);
+};
+
+const getUserByKitId = (kit_id, rawData = true, networkData = false, opts = {
+ additionalHeaders: null
+}) => {
+ if (!kit_id) {
+ return reject(new Error(PROVIDE_KIT_ID));
+ }
+ return getUser({ kit_id }, rawData, networkData, opts);
+};
+
+const getUserTier = (user_id) => {
+ return getUser({ user_id }, true)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.verification_level < 1) {
+ throw new Error('User is not verified');
+ }
+ return dbQuery.findOne('tier', {
+ where: {
+ id: user.verification_level
+ },
+ raw: true
+ });
+ });
+};
+
+const getUserByNetworkId = (network_id, rawData = true, networkData = false, opts = {
+ additionalHeaders: null
+}) => {
+ if (!network_id) {
+ return reject(new Error(PROVIDE_NETWORK_ID));
+ }
+ return getUser({ network_id }, rawData, networkData, opts);
+};
+
+const freezeUserById = (userId) => {
+ if (userId === 1) {
+ return reject(new Error(CANNOT_DEACTIVATE_ADMIN));
+ }
+ return getUserByKitId(userId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (!user.activated) {
+ throw new Error(USER_ALREADY_DEACTIVATED);
+ }
+ return user.update({ activated: false }, { fields: ['activated'], returning: true });
+ })
+ .then((user) => {
+ publisher.publish(CONFIGURATION_CHANNEL, JSON.stringify({type: 'freezeUser', data: user.id }));
+ sendEmail(
+ MAILTYPE.USER_DEACTIVATED,
+ user.email,
+ {
+ type: 'deactivated'
+ },
+ user.settings
+ );
+ return user;
+ });
+};
+
+const freezeUserByEmail = (email) => {
+ return getUserByEmail(email, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.id === 1) {
+ throw new Error(CANNOT_DEACTIVATE_ADMIN);
+ }
+ if (!user.activated) {
+ throw new Error(USER_ALREADY_DEACTIVATED);
+ }
+ return user.update({ activated: false }, { fields: ['activated'], returning: true });
+ })
+ .then((user) => {
+ publisher.publish(CONFIGURATION_CHANNEL, JSON.stringify({type: 'freezeUser', data: user.id }));
+ sendEmail(
+ MAILTYPE.USER_DEACTIVATED,
+ user.email,
+ {
+ type: 'deactivated'
+ },
+ user.settings
+ );
+ return user;
+ });
+};
+
+const unfreezeUserById = (userId) => {
+ return getUserByKitId(userId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.activated) {
+ throw new Error(USER_NOT_DEACTIVATED);
+ }
+ return user.update({ activated: true }, { fields: ['activated'], returning: true });
+ })
+ .then((user) => {
+ publisher.publish(CONFIGURATION_CHANNEL, JSON.stringify({type: 'unfreezeUser', data: user.id }));
+ sendEmail(
+ MAILTYPE.USER_DEACTIVATED,
+ user.email,
+ {
+ type: 'activated'
+ },
+ user.settings
+ );
+ return user;
+ });
+};
+
+const unfreezeUserByEmail = (email) => {
+ return getUserByEmail(email, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.activated) {
+ throw new Error(USER_NOT_DEACTIVATED);
+ }
+ return user.update({ activated: true }, { fields: ['activated'], returning: true });
+ })
+ .then((user) => {
+ publisher.publish(CONFIGURATION_CHANNEL, JSON.stringify({type: 'unfreezeUser', data: user.id }));
+ sendEmail(
+ MAILTYPE.USER_DEACTIVATED,
+ user.email,
+ {
+ type: 'activated'
+ },
+ user.settings
+ );
+ return user;
+ });
+};
+
+const getUserRole = (opts = {}) => {
+ return getUser(opts, true)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.is_admin) {
+ return 'admin';
+ } else if (user.is_supervisor) {
+ return 'supervisor';
+ } else if (user.is_support) {
+ return 'support';
+ } else if (user.is_kyc) {
+ return 'kyc';
+ } else if (user.is_communicator) {
+ return 'communicator';
+ } else {
+ return 'user';
+ }
+ });
+};
+
+const updateUserRole = (user_id, role) => {
+ if (user_id === 1) {
+ return reject(new Error(CANNOT_CHANGE_ADMIN_ROLE));
+ }
+ return dbQuery.findOne('user', {
+ where: {
+ id: user_id
+ },
+ attributes: [
+ 'id',
+ 'email',
+ 'is_admin',
+ 'is_support',
+ 'is_supervisor',
+ 'is_kyc',
+ 'is_communicator'
+ ]
+ })
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ const roles = pick(
+ user.dataValues,
+ 'is_admin',
+ 'is_supervisor',
+ 'is_support',
+ 'is_kyc',
+ 'is_communicator'
+ );
+
+ const roleChange = 'is_' + role.toLowerCase();
+
+ if (roles[roleChange]) {
+ throw new Error (`User already has role ${role}`);
+ }
+
+ each(roles, (value, key) => {
+ if (key === roleChange) {
+ roles[key] = true;
+ } else {
+ roles[key] = false;
+ }
+ });
+
+ return all([user, roles]);
+ })
+ .then(([user, roles]) => {
+ return user.update(
+ roles,
+ { fields: ['is_admin', 'is_support', 'is_supervisor', 'is_kyc', 'is_communicator'], returning: true }
+ );
+ })
+ .then((user) => {
+ const result = pick(
+ user,
+ 'id',
+ 'email',
+ 'is_admin',
+ 'is_support',
+ 'is_supervisor',
+ 'is_kyc',
+ 'is_communicator'
+ );
+ return result;
+ });
+};
+
+const DEFAULT_SETTINGS = {
+ language: getKitConfig().defaults.language,
+ orderConfirmationPopup: true
+};
+
+const joinSettings = (userSettings = {}, newSettings = {}) => {
+ const joinedSettings = {};
+ SETTING_KEYS.forEach((key) => {
+ if (has(newSettings, key)) {
+ if (
+ key === 'chat' &&
+ (!isPlainObject(newSettings[key]) || !isBoolean(newSettings[key].set_username))
+ ) {
+ throw new Error('set-username must be a boolean value');
+ } else if (
+ key === 'language' &&
+ (!isString(newSettings[key]) || getKitConfig().valid_languages.indexOf(newSettings[key]) === -1)
+ ) {
+ throw new Error('Invalid language given');
+ }
+ joinedSettings[key] = newSettings[key];
+ } else if (has(userSettings, key)) {
+ joinedSettings[key] = userSettings[key];
+ } else {
+ joinedSettings[key] = DEFAULT_SETTINGS[key];
+ }
+ });
+ return joinedSettings;
+};
+
+const updateUserSettings = (userOpts = {}, settings = {}) => {
+ return getUser(userOpts, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (Object.keys(settings).length > 0) {
+ settings = joinSettings(user.dataValues.settings, settings);
+ }
+ return user.update({ settings }, {
+ fields: [ 'settings' ],
+ returning: true
+ });
+ })
+ .then((user) => {
+ return omitUserFields(user.dataValues);
+ });
+};
+
+const INITIAL_SETTINGS = () => {
+ return {
+ notification: {
+ popup_order_confirmation: true,
+ popup_order_completed: true,
+ popup_order_partially_filled: true
+ },
+ interface: {
+ order_book_levels: 10,
+ theme: getKitConfig().defaults.theme
+ },
+ language: getKitConfig().defaults.language,
+ audio: {
+ order_completed: true,
+ order_partially_completed: true,
+ public_trade: false
+ },
+ risk: {
+ order_portfolio_percentage: DEFAULT_ORDER_RISK_PERCENTAGE
+ },
+ chat: {
+ set_username: false
+ }
+ };
+};
+
+const getUserEmailByVerificationCode = (code) => {
+ return dbQuery.findOne('verification code', {
+ where: { code },
+ attributes: ['id', 'code', 'verified', 'user_id']
+ })
+ .then((verificationCode) => {
+ if (!verificationCode) {
+ throw new Error(INVALID_VERIFICATION_CODE);
+ } else if (verificationCode.verified) {
+ throw new Error(VERIFICATION_CODE_USED);
+ }
+ return dbQuery.findOne('user', {
+ where: { id: verificationCode.user_id },
+ attributes: ['email']
+ });
+ })
+ .then((user) => {
+ return user.email;
+ });
+};
+
+const verifyUserEmailByKitId = (kitId) => {
+ return getUserByKitId(kitId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.email_verified) {
+ throw new Error('User email already verified');
+ }
+ return user.update(
+ { email_verified: true },
+ { fields: ['email_verified'], returning: true }
+ );
+ });
+};
+
+const updateUserNote = (userId, note) => {
+ return getUserByKitId(userId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ return user.update({ note }, { fields: ['note'] });
+ });
+};
+
+const updateUserDiscount = (userId, discount) => {
+ if (discount < 0 || discount > 100) {
+ return reject(new Error(`Invalid discount rate ${discount}. Min: 0. Max: 1`));
+ }
+
+ return getUserByKitId(userId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (user.discount === discount) {
+ throw new Error(`User discount is already ${discount}`);
+ }
+ return all([
+ user.discount,
+ user.update({ discount }, { fields: ['discount'] })
+ ]);
+ })
+ .then(([ previousDiscountRate, user ]) => {
+ if (user.discount > previousDiscountRate) {
+ sendEmail(
+ MAILTYPE.DISCOUNT_UPDATE,
+ user.email,
+ {
+ rate: user.discount
+ },
+ user.settings
+ );
+ }
+ return pick(user.dataValues, ['id', 'discount']);
+ });
+};
+
+const changeUserVerificationLevelById = (userId, newLevel, domain) => {
+ if (!isValidTierLevel(newLevel)) {
+ return reject(new Error(INVALID_VERIFICATION_LEVEL(newLevel)));
+ }
+
+ let currentVerificationLevel = 0;
+ return getUserByKitId(userId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.verification_level === 0) {
+ throw new Error(ACCOUNT_NOT_VERIFIED);
+ }
+ currentVerificationLevel = user.verification_level;
+ return user.update(
+ { verification_level: newLevel },
+ { fields: ['verification_level'], returning: true }
+ );
+ })
+ .then((user) => {
+ if (currentVerificationLevel < user.verification_level) {
+ sendEmail(
+ MAILTYPE.ACCOUNT_UPGRADE,
+ user.email,
+ getKitTier(user.verification_level).name,
+ user.settings,
+ domain
+ );
+ }
+ return;
+ });
+};
+
+const deactivateUserOtpById = (userId) => {
+ return getUserByKitId(userId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ return user.update(
+ { otp_enabled: false },
+ { fields: [ 'otp_enabled' ]}
+ );
+ });
+};
+
+const toggleFlaggedUserById = (userId) => {
+ return getUserByKitId(userId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ return user.update(
+ { flagged: !user.flagged },
+ { fields: ['flagged'] }
+ );
+ });
+};
+
+const getUserLogins = (opts = {
+ userId: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ format: null
+}) => {
+ const pagination = paginationQuery(opts.limit, opts.page);
+ const timeframe = timeframeQuery(opts.startDate, opts.endDate);
+ const ordering = orderingQuery(opts.orderBy, opts.order);
+ let options = {
+ where: {
+ timestamp: timeframe
+ },
+ attributes: {
+ exclude: ['id', 'origin', 'referer']
+ },
+ order: [ordering]
+ };
+ if (!opts.format) {
+ options = { ...options, ...pagination};
+ }
+
+ if (opts.userId) options.where.user_id = opts.userId;
+
+ return dbQuery.findAndCountAllWithRows('login', options)
+ .then((logins) => {
+ if (opts.format) {
+ if (logins.data.length === 0) {
+ throw new Error(NO_DATA_FOR_CSV);
+ }
+ const csv = parse(logins.data, Object.keys(logins.data[0]));
+ return csv;
+ } else {
+ return logins;
+ }
+ });
+};
+
+const bankComparison = (bank1, bank2, description) => {
+ let difference = [];
+ let note = '';
+ if (bank1.length === bank2.length) {
+ note = 'bank info updated';
+ difference = differenceWith(bank1, bank2, isEqual);
+ } else if (bank1.length > bank2.length) {
+ note = 'bank removed';
+ difference = differenceWith(bank1, bank2, isEqual);
+ } else if (bank1.length < bank2.length) {
+ note = 'bank added';
+ difference = differenceWith(bank2, bank1, isEqual);
+ }
+
+ // bank data is changed
+ if (difference.length > 0) {
+ description.note = note;
+ description.new.bank_account = bank2;
+ description.old.bank_account = bank1;
+ }
+ return description;
+};
+
+const createAuditDescription = (userId, prevData = {}, newData = {}) => {
+ let description = {
+ userId,
+ note: `Change in user ${userId} information`,
+ old: {},
+ new: {}
+ };
+ for (const key in newData) {
+ if (USER_FIELD_ADMIN_LOG.includes(key)) {
+ let prevRecord = prevData[key] || 'empty';
+ let newRecord = newData[key] || 'empty';
+ if (key === 'bank_account') {
+ description = bankComparison(
+ prevData.bank_account,
+ newData.bank_account,
+ description
+ );
+ } else if (key === 'id_data') {
+ ID_FIELDS.forEach((field) => {
+ if (newRecord[field] != prevRecord[field]) {
+ description.old[field] = prevRecord[field];
+ description.new[field] = newRecord[field];
+ }
+ });
+ } else if (key === 'address') {
+ ADDRESS_FIELDS.forEach((field) => {
+ if (prevRecord[field] != newRecord[field]) {
+ description.old[field] = prevRecord[field];
+ description.new[field] = newRecord[field];
+ }
+ });
+ } else {
+ if (prevRecord.toString() != newRecord.toString()) {
+ description.old[key] = prevRecord;
+ description.new[key] = newRecord;
+ }
+ }
+ }
+ }
+ return description;
+};
+
+const createAudit = (adminId, event, ip, opts = {
+ userId: null,
+ prevUserData: null,
+ newUserData: null,
+ domain: null
+}) => {
+ const options = {
+ admin_id: adminId,
+ event,
+ description: createAuditDescription(opts.userId, opts.prevUserData, opts.newUserData),
+ ip,
+ };
+ if (opts.domain) {
+ options.domain = opts.domain;
+ }
+ return getModel('audit').create({
+ admin_id: adminId,
+ event,
+ description: createAuditDescription(opts.userId, opts.prevUserData, opts.newUserData),
+ ip
+ });
+};
+
+const getUserAudits = (opts = {
+ userId: null,
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null,
+ startDate: null,
+ endDate: null,
+ format: null
+}) => {
+ const pagination = paginationQuery(opts.limit, opts.page);
+ const timeframe = timeframeQuery(opts.startDate, opts.endDate);
+ const ordering = orderingQuery(opts.orderBy, opts.order);
+ let options = {
+ where: {
+ timestamp: timeframe
+ },
+ order: [ordering]
+ };
+
+ if (!opts.format) {
+ options = { ...options, ...pagination };
+ }
+
+ if (isNumber(opts.userId)) options.where.description = getModel('sequelize').literal(`description ->> 'user_id' = '${opts.userId}'`);
+
+ return dbQuery.findAndCountAllWithRows('audit', options)
+ .then((audits) => {
+ if (opts.format) {
+ if (audits.data.length === 0) {
+ throw new Error(NO_DATA_FOR_CSV);
+ }
+ const flatData = audits.data.map((audit) => flatten(audit, { maxDepth: 2 }));
+ const csv = parse(flatData, AUDIT_KEYS);
+ return csv;
+ } else {
+ return audits;
+ }
+ });
+};
+
+const checkUsernameIsTaken = (username) => {
+ return getModel('user').count({ where: { username }})
+ .then((count) => {
+ if (count > 0) {
+ throw new Error(USERNAME_IS_TAKEN);
+ } else {
+ return true;
+ }
+ });
+};
+
+const setUsernameById = (userId, username) => {
+ if (!isValidUsername(username)) {
+ return reject(new Error(INVALID_USERNAME));
+ }
+ return getUserByKitId(userId, false)
+ .then((user) =>{
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+ if (user.settings.chat.set_username) {
+ throw new Error(USERNAME_CANNOT_BE_CHANGED);
+ }
+ return all([ user, checkUsernameIsTaken(username) ]);
+ })
+ .then(([ user ]) => {
+ return user.update(
+ {
+ username,
+ settings: {
+ ...user.settings,
+ chat: {
+ set_username: true
+ }
+ }
+ },
+ { fields: ['username', 'settings'] }
+ );
+ });
+};
+
+const createUserCryptoAddressByNetworkId = (networkId, crypto, opts = {
+ network: null,
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ return getNodeLib().createUserCryptoAddress(networkId, crypto, opts);
+};
+
+const createUserCryptoAddressByKitId = (kitId, crypto, opts = {
+ network: null,
+ additionalHeaders: null
+}) => {
+ return getUserByKitId(kitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().createUserCryptoAddress(user.network_id, crypto, opts);
+ });
+};
+
+const getUserStatsByKitId = (userId, opts = {
+ additionalHeaders: null
+}) => {
+ return getUserByKitId(userId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getUserStats(user.network_id, opts);
+ });
+};
+
+const getUserStatsByNetworkId = (networkId, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ return getNodeLib().getUserStats(networkId, opts);
+};
+
+const getExchangeOperators = (opts = {
+ limit: null,
+ page: null,
+ orderBy: null,
+ order: null
+}) => {
+ const pagination = paginationQuery(opts.limit, opts.page);
+ const ordering = orderingQuery(opts.orderBy, opts.order);
+
+ const options = {
+ where: {
+ [Op.or]: [
+ { is_admin: true },
+ { is_supervisor: true },
+ { is_support: true },
+ { is_kyc: true },
+ { is_communicator: true }
+ ]
+ },
+ attributes: ['id', 'email', 'is_admin', 'is_supervisor', 'is_support', 'is_kyc', 'is_communicator'],
+ order: [ordering],
+ ...pagination
+ };
+
+ return dbQuery.findAndCountAllWithRows('user', options);
+};
+
+const inviteExchangeOperator = (invitingEmail, email, role, opts = {
+ additionalHeaders: null
+}) => {
+ const roles = {
+ is_admin: false,
+ is_supervisor: false,
+ is_support: false,
+ is_kyc: false,
+ is_communicator: false
+ };
+
+ if (!email || !isEmail(email)) {
+ return reject(new Error(PROVIDE_VALID_EMAIL));
+ }
+
+ role = role.toLowerCase();
+ const roleToUpdate = `is_${role}`;
+
+ if (role === 'user') {
+ return reject(new Error('Must invite user as an operator role'));
+ } else {
+ if (roles[roleToUpdate] === undefined) {
+ return reject(new Error('Invalid role'));
+ } else {
+ roles[roleToUpdate] = true;
+ }
+ }
+
+ const tempPassword = uuid();
+
+ return getModel('sequelize').transaction((transaction) => {
+ return getModel('user').findOrCreate({
+ defaults: {
+ email,
+ password: tempPassword,
+ ...roles,
+ settings: INITIAL_SETTINGS()
+ },
+ where: { email },
+ transaction
+ })
+ .then(async ([ user, created ]) => {
+ if (created) {
+ const networkUser = await getNodeLib().createUser(email, opts);
+ return all([
+ user.update(
+ { network_id: networkUser.id },
+ { returning: true, fields: ['network_id'], transaction }
+ ),
+ created
+ ]);
+ } else {
+ if (user.is_admin || user.is_supervisor || user.is_support || user.is_kyc || user.is_communicator) {
+ throw new Error('User is already an operator');
+ }
+ return all([
+ user.update({ ...roles }, { returning: true, fields: Object.keys(roles), transaction }),
+ created
+ ]);
+ }
+ });
+ })
+ .then(async ([ user, created ]) => {
+ if (created) {
+ await getModel('verification code').update(
+ { verified: true },
+ { where: { user_id: user.id }, fields: [ 'verified' ]}
+ );
+ }
+ sendEmail(
+ MAILTYPE.INVITED_OPERATOR,
+ user.email,
+ {
+ invitingEmail,
+ created,
+ password: created ? tempPassword : undefined,
+ role
+ },
+ user.settings
+ );
+ return;
+ });
+};
+
+const updateUserMeta = async (id, givenMeta = {}, opts = { overwrite: null }) => {
+ const { user_meta: referenceMeta } = getKitConfig();
+
+ const user = await getUserByKitId(id, false);
+
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ }
+
+ const deletedKeys = [];
+
+ for (let key in user.meta) {
+ if (!referenceMeta[key] && isNil(user.meta[key])) {
+ delete user.meta[key];
+ }
+ }
+
+ for (let key in givenMeta) {
+ if (!referenceMeta[key]) {
+ if (!has(user.meta, key)) {
+ throw new Error(`Field ${key} does not exist in the user meta reference`);
+ } else {
+ if (isNil(givenMeta[key])) {
+ deletedKeys.push(key);
+ } else {
+ const storedDataType = isDatetime(user.meta[key]) ? 'date-time' : typeof user.meta[key];
+ const givenDataType = isDatetime(givenMeta[key]) ? 'date-time' : typeof givenMeta[key];
+
+ if (storedDataType !== givenDataType) {
+ throw new Error(`Wrong data type given for field ${key}: ${givenDataType}. Expected data type: ${storedDataType}`);
+ }
+ }
+ }
+ } else {
+ if (isNil(givenMeta[key]) && referenceMeta[key].required) {
+ throw new Error(`Field ${key} is a required value`);
+ } else if (!isNil(givenMeta[key])) {
+ const givenDataType = isDatetime(givenMeta[key]) ? 'date-time' : typeof givenMeta[key];
+
+ if (referenceMeta[key].type !== givenDataType) {
+ throw new Error(`Wrong data type given for field ${key}: ${givenDataType}. Expected data type: ${referenceMeta[key].type}`);
+ }
+ }
+ }
+ }
+
+ const updatedUserMeta = opts.overwrite ? omit(givenMeta, ...deletedKeys) : omit({ ...user.meta, ...givenMeta }, ...deletedKeys);
+
+ const updatedUser = await user.update({
+ meta: updatedUserMeta
+ });
+
+ return pick(updatedUser, 'id', 'email', 'meta');
+};
+
+const mapNetworkIdToKitId = async (
+ networkIds = []
+) => {
+ if (!isArray(networkIds)) {
+ throw new Error('networkIds must be an array');
+ }
+
+ const opts = {
+ attributes: ['id', 'network_id'],
+ raw: true
+ };
+
+ if (networkIds.length > 0) {
+ if (networkIds.some((id) => !isInteger(id) || id <= 0)) {
+ throw new Error('networkIds can only contain integers greater than 0');
+ } else {
+ opts.where = {
+ network_id: uniq(networkIds)
+ };
+ }
+ }
+
+ const users = await dbQuery.findAll('user', opts);
+
+ if (users.length === 0) {
+ throw new Error('No users found with given networkIds');
+ }
+
+ const result = users.reduce((data, user) => {
+ if (user.network_id) {
+ return {
+ ...data,
+ [user.network_id]: user.id
+ };
+ } else {
+ return data;
+ }
+ }, {});
+
+ return result;
+};
+
+const updateUserInfo = async (userId, data = {}) => {
+ if (!isInteger(userId) || userId <= 0) {
+ throw new Error('UserId must be a positive integer');
+ }
+ if (!isPlainObject(data)) {
+ throw new Error('Update data must be an object');
+ }
+
+ if (isEmpty(data)) {
+ throw new Error('No fields to update');
+ }
+
+ const user = await getUserByKitId(userId, false);
+
+ if (!user) {
+ throw new Error('User not found');
+ }
+
+ const updateData = {};
+
+ for (const field in data) {
+ const value = data[field];
+
+ switch (field) {
+ case 'full_name':
+ case 'nationality':
+ case 'phone_number':
+ if (isString(value)) {
+ updateData[field] = value;
+ }
+ break;
+ case 'gender':
+ if (isBoolean(value)) {
+ updateData[field] = value;
+ }
+ break;
+ case 'dob':
+ if (isDatetime(value)) {
+ updateData[field] = value;
+ }
+ break;
+ case 'address':
+ if (isPlainObject(value)) {
+ updateData[field] = {
+ ...user.address,
+ ...pick(value, ['address', 'city', 'country', 'postal_code'])
+ };
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (isEmpty(updateData)) {
+ throw new Error('No fields to update');
+ }
+
+ await user.update(
+ updateData,
+ { fields: Object.keys(updateData) }
+ );
+
+ return omitUserFields(user.dataValues);
+};
+
+module.exports = {
+ loginUser,
+ getUserTier,
+ createUser,
+ getUserByEmail,
+ getUserByKitId,
+ getUserByNetworkId,
+ freezeUserById,
+ freezeUserByEmail,
+ unfreezeUserById,
+ unfreezeUserByEmail,
+ getAllUsers,
+ getUserRole,
+ updateUserSettings,
+ omitUserFields,
+ signUpUser,
+ registerUserLogin,
+ verifyUser,
+ getVerificationCodeByUserEmail,
+ getUserEmailByVerificationCode,
+ getAllUsersAdmin,
+ updateUserRole,
+ updateUserNote,
+ updateUserDiscount,
+ changeUserVerificationLevelById,
+ deactivateUserOtpById,
+ toggleFlaggedUserById,
+ getUserLogins,
+ getUserAudits,
+ setUsernameById,
+ getAffiliationCount,
+ isValidUsername,
+ createUserCryptoAddressByKitId,
+ createAudit,
+ getUserStatsByKitId,
+ getExchangeOperators,
+ inviteExchangeOperator,
+ createUserOnNetwork,
+ getUserNetwork,
+ getUsersNetwork,
+ createUserCryptoAddressByNetworkId,
+ getUserStatsByNetworkId,
+ getVerificationCodeByUserId,
+ checkAffiliation,
+ verifyUserEmailByKitId,
+ generateAffiliationCode,
+ updateUserMeta,
+ mapNetworkIdToKitId,
+ updateUserInfo
+};
\ No newline at end of file
diff --git a/server/utils/toolsLib/tools/wallet.js b/server/utils/toolsLib/tools/wallet.js
new file mode 100644
index 0000000000..d15dd8c6a0
--- /dev/null
+++ b/server/utils/toolsLib/tools/wallet.js
@@ -0,0 +1,976 @@
+'use strict';
+
+const { SERVER_PATH } = require('../constants');
+const { sendEmail } = require(`${SERVER_PATH}/mail`);
+const { MAILTYPE } = require(`${SERVER_PATH}/mail/strings`);
+const { WITHDRAWALS_REQUEST_KEY } = require(`${SERVER_PATH}/constants`);
+const { verifyOtpBeforeAction } = require('./security');
+const { subscribedToCoin, getKitCoin, getKitSecrets, getKitConfig, sleep } = require('./common');
+const {
+ INVALID_OTP_CODE,
+ INVALID_WITHDRAWAL_TOKEN,
+ EXPIRED_WITHDRAWAL_TOKEN,
+ INVALID_COIN,
+ INVALID_AMOUNT,
+ WITHDRAWAL_DISABLED_FOR_COIN,
+ UPGRADE_VERIFICATION_LEVEL,
+ NO_DATA_FOR_CSV,
+ USER_NOT_FOUND,
+ USER_NOT_REGISTERED_ON_NETWORK,
+ INVALID_NETWORK,
+ NETWORK_REQUIRED
+} = require(`${SERVER_PATH}/messages`);
+const { getUserByKitId, mapNetworkIdToKitId } = require('./user');
+const { findTier } = require('./tier');
+const { client } = require('./database/redis');
+const crypto = require('crypto');
+const uuid = require('uuid/v4');
+const { all, reject } = require('bluebird');
+const { getNodeLib } = require(`${SERVER_PATH}/init`);
+const moment = require('moment');
+const math = require('mathjs');
+const { parse } = require('json2csv');
+const { loggerWithdrawals } = require(`${SERVER_PATH}/config/logger`);
+const WAValidator = require('multicoin-address-validator');
+
+const isValidAddress = (currency, address, network) => {
+ if (network === 'eth' || network === 'ethereum') {
+ return WAValidator.validate(address, 'eth');
+ } else if (network === 'stellar' || network === 'xlm') {
+ return WAValidator.validate(address.split(':')[0], 'xlm');
+ } else if (network === 'tron' || network === 'trx') {
+ return WAValidator.validate(address, 'trx');
+ } else if (network === 'bsc' || currency === 'bnb' || network === 'bnb') {
+ return WAValidator.validate(address, 'eth');
+ } else if (currency === 'btc' || currency === 'bch' || currency === 'xmr') {
+ return WAValidator.validate(address, currency);
+ } else if (currency === 'xrp') {
+ return WAValidator.validate(address.split(':')[0], currency);
+ } else if (currency === 'etn') {
+ // skip the validation
+ return true;
+ } else {
+ return WAValidator.validate(address, currency);
+ }
+};
+
+const getWithdrawalFee = (currency, network) => {
+ if (!subscribedToCoin(currency)) {
+ return reject(new Error(INVALID_COIN(currency)));
+ }
+
+ const coinConfiguration = getKitCoin(currency);
+
+ let fee = coinConfiguration.withdrawal_fee;
+ let fee_coin = currency;
+
+ if (network && coinConfiguration.withdrawal_fees && coinConfiguration.withdrawal_fees[network]) {
+ fee = coinConfiguration.withdrawal_fees[network].value;
+ fee_coin = coinConfiguration.withdrawal_fees[network].symbol;
+ }
+
+ return { fee, fee_coin };
+};
+
+const sendRequestWithdrawalEmail = (id, address, amount, currency, opts = {
+ network: null,
+ otpCode: null,
+ ip: null,
+ domain: null
+}) => {
+
+ const coinConfiguration = getKitCoin(currency);
+
+ if (!subscribedToCoin(currency)) {
+ return reject(new Error(INVALID_COIN(currency)));
+ }
+
+ if (amount <= 0) {
+ return reject(new Error(INVALID_AMOUNT(amount)));
+ }
+
+ if (!coinConfiguration.allow_withdrawal) {
+ return reject(new Error(WITHDRAWAL_DISABLED_FOR_COIN(currency)));
+ }
+
+ if (coinConfiguration.network) {
+ if (!opts.network) {
+ return reject(new Error(NETWORK_REQUIRED(currency, coinConfiguration.network)));
+ } else if (!coinConfiguration.network.split(',').includes(opts.network)) {
+ return reject(new Error(INVALID_NETWORK(opts.network, coinConfiguration.network)));
+ }
+ } else if (opts.network) {
+ return reject(new Error(`Invalid ${currency} network given: ${opts.network}`));
+ }
+
+ if (!isValidAddress(currency, address, opts.network)) {
+ return reject(new Error(`Invalid ${currency} address: ${address}`));
+ }
+
+ return verifyOtpBeforeAction(id, opts.otpCode)
+ .then((validOtp) => {
+ if (!validOtp) {
+ throw new Error(INVALID_OTP_CODE);
+ }
+ return getUserByKitId(id);
+ })
+ .then(async (user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ if (user.verification_level < 1) {
+ throw new Error(UPGRADE_VERIFICATION_LEVEL(1));
+ }
+
+ const { fee, fee_coin } = getWithdrawalFee(currency, opts.network);
+
+ const balance = await getNodeLib().getUserBalance(user.network_id);
+
+ if (fee_coin === currency) {
+ const totalAmount =
+ fee > 0
+ ? math.number(math.add(math.bignumber(fee), math.bignumber(amount)))
+ : amount;
+
+ if (math.compare(totalAmount, balance[`${currency}_available`]) === 1) {
+ throw new Error(
+ `User ${currency} balance is lower than amount "${amount}" + fee "${fee}"`
+ );
+ }
+ } else {
+ if (math.compare(amount, balance[`${currency}_available`]) === 1) {
+ throw new Error(
+ `User ${currency} balance is lower than withdrawal amount "${amount}"`
+ );
+ }
+
+ if (math.compare(fee, balance[`${fee_coin}_available`]) === 1) {
+ throw new Error(
+ `User ${fee_coin} balance is lower than fee amount "${fee}"`
+ );
+ }
+ }
+
+ return all([
+ user,
+ fee,
+ fee_coin,
+ findTier(user.verification_level)
+ ]);
+ })
+ .then(async ([ user, fee, fee_coin, tier ]) => {
+ const limit = tier.withdrawal_limit;
+ if (limit === -1) {
+ throw new Error(WITHDRAWAL_DISABLED_FOR_COIN(currency));
+ } else if (limit > 0) {
+ await withdrawalBelowLimit(user.network_id, currency, limit, amount);
+ }
+
+ return withdrawalRequestEmail(
+ user,
+ {
+ user_id: id,
+ email: user.email,
+ amount,
+ fee,
+ fee_coin,
+ transaction_id: uuid(),
+ address,
+ currency,
+ network: opts.network
+ },
+ opts.domain,
+ opts.ip
+ );
+ });
+};
+
+const withdrawalRequestEmail = (user, data, domain, ip) => {
+ data.timestamp = Date.now();
+ let stringData = JSON.stringify(data);
+ const token = crypto.randomBytes(60).toString('hex');
+
+ return client.hsetAsync(WITHDRAWALS_REQUEST_KEY, token, stringData)
+ .then(() => {
+ const { email, amount, fee, fee_coin, currency, address, network } = data;
+ sendEmail(
+ MAILTYPE.WITHDRAWAL_REQUEST,
+ email,
+ {
+ amount,
+ fee,
+ fee_coin,
+ currency,
+ transaction_id: token,
+ address,
+ ip,
+ network
+ },
+ user.settings,
+ domain
+ );
+ return data;
+ });
+};
+
+const validateWithdrawalToken = (token) => {
+ return client.hgetAsync(WITHDRAWALS_REQUEST_KEY, token)
+ .then((withdrawal) => {
+ if (!withdrawal) {
+ throw new Error(INVALID_WITHDRAWAL_TOKEN);
+ } else {
+ withdrawal = JSON.parse(withdrawal);
+
+ client.hdelAsync(WITHDRAWALS_REQUEST_KEY, token);
+
+ if (Date.now() - withdrawal.timestamp > getKitSecrets().security.withdrawal_token_expiry) {
+ throw new Error(EXPIRED_WITHDRAWAL_TOKEN);
+ } else {
+ return withdrawal;
+ }
+ }
+ });
+};
+
+const cancelUserWithdrawalByKitId = (userId, withdrawalId, opts = {
+ additionalHeaders: null
+}) => {
+ return getUserByKitId(userId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().cancelWithdrawal(user.network_id, withdrawalId, opts);
+ });
+};
+
+const cancelUserWithdrawalByNetworkId = (networkId, withdrawalId, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ return getNodeLib().cancelWithdrawal(networkId, withdrawalId, opts);
+};
+
+const checkTransaction = (currency, transactionId, address, network, isTestnet = false, opts = {
+ additionalHeaders: null
+}) => {
+ if (!subscribedToCoin(currency)) {
+ return reject(new Error(INVALID_COIN(currency)));
+ }
+
+ return getNodeLib().checkTransaction(currency, transactionId, address, network, { isTestnet, ...opts });
+};
+
+const performWithdrawal = (userId, address, currency, amount, opts = {
+ network: null,
+ additionalHeaders: null
+}) => {
+ if (!subscribedToCoin(currency)) {
+ return reject(new Error(INVALID_COIN(currency)));
+ }
+ return getUserByKitId(userId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return all([
+ user,
+ findTier(user.verification_level)
+ ]);
+ })
+ .then(async ([ user, tier ]) => {
+ const limit = tier.withdrawal_limit;
+ if (limit === -1) {
+ throw new Error('Withdrawals are disabled for this coin');
+ } else if (limit > 0) {
+ await withdrawalBelowLimit(user.network_id, currency, limit, amount);
+ }
+ return getNodeLib().performWithdrawal(user.network_id, address, currency, amount, opts);
+ });
+};
+
+const performWithdrawalNetwork = (networkId, address, currency, amount, opts = {
+ network: null,
+ additionalHeaders: null
+}) => {
+ return getNodeLib().performWithdrawal(networkId, address, currency, amount, opts);
+};
+
+const get24HourAccumulatedWithdrawals = async (userId) => {
+ const withdrawals = await getNodeLib().getUserWithdrawals(userId, {
+ dismissed: false,
+ rejected: false,
+ startDate: moment().subtract(24, 'hours').toISOString()
+ });
+
+ const withdrawalData = withdrawals.data;
+
+ if (withdrawals.count > 50) {
+ const numofPages = Math.ceil(withdrawals.count / 50);
+ for (let i = 2; i <= numofPages; i++) {
+ await sleep(500);
+
+ const withdrawals = await getNodeLib().getUserWithdrawals(userId, {
+ dismissed: false,
+ rejected: false,
+ page: i,
+ startDate: moment().subtract(24, 'hours').toISOString()
+ });
+
+ withdrawalData.push(...withdrawals.data);
+ }
+ }
+
+ loggerWithdrawals.debug(
+ 'toolsLib/wallet/get24HourAccumulatedWithdrawals',
+ 'withdrawals made within last 24 hours',
+ withdrawals.count
+ );
+
+ const withdrawalAmount = {};
+
+ for (let withdrawal of withdrawalData) {
+ if (withdrawalAmount[withdrawal.currency] !== undefined) {
+ withdrawalAmount[withdrawal.currency] = math.number(math.add(math.bignumber(withdrawalAmount[withdrawal.currency]), math.bignumber(withdrawal.amount)));
+ } else {
+ withdrawalAmount[withdrawal.currency] = withdrawal.amount;
+ }
+ }
+
+ let totalWithdrawalAmount = 0;
+
+ for (let withdrawalCurrency in withdrawalAmount) {
+ loggerWithdrawals.debug(
+ 'toolsLib/wallet/get24HourAccumulatedWithdrawals',
+ `accumulated ${withdrawalCurrency} withdrawal amount`,
+ withdrawalAmount[withdrawalCurrency]
+ );
+
+ await sleep(500);
+
+ const convertedAmount = await getNodeLib().getOraclePrices([withdrawalCurrency], {
+ quote: getKitConfig().native_currency,
+ amount: withdrawalAmount[withdrawalCurrency]
+ });
+
+ if (convertedAmount[withdrawalCurrency] !== -1) {
+ loggerWithdrawals.debug(
+ 'toolsLib/wallet/get24HourAccumulatedWithdrawals',
+ `${withdrawalCurrency} withdrawal amount converted to ${getKitConfig().native_currency}`,
+ convertedAmount[withdrawalCurrency]
+ );
+
+ totalWithdrawalAmount = math.number(math.add(math.bignumber(totalWithdrawalAmount), math.bignumber(convertedAmount[withdrawalCurrency])));
+ } else {
+ loggerWithdrawals.debug(
+ 'toolsLib/wallet/get24HourAccumulatedWithdrawals',
+ `No conversion found between ${withdrawalCurrency} and ${getKitConfig().native_currency}`
+ );
+ }
+ }
+
+ return totalWithdrawalAmount;
+};
+
+const withdrawalBelowLimit = async (userId, currency, limit, amount = 0) => {
+ loggerWithdrawals.verbose(
+ 'toolsLib/wallet/withdrawalBelowLimit',
+ 'amount being withdrawn',
+ amount,
+ 'currency',
+ currency,
+ 'limit',
+ limit,
+ 'userId',
+ userId,
+ );
+
+ let totalWithdrawalAmount = 0;
+
+ const convertedWithdrawalAmount = await getNodeLib().getOraclePrices([currency], {
+ quote: getKitConfig().native_currency,
+ amount
+ });
+
+
+ if (convertedWithdrawalAmount[currency] !== -1) {
+ loggerWithdrawals.debug(
+ 'toolsLib/wallet/withdrawalBelowLimit',
+ `${currency} withdrawal request amount converted to ${getKitConfig().native_currency}`,
+ convertedWithdrawalAmount[currency]
+ );
+
+ totalWithdrawalAmount = math.number(
+ math.add(
+ math.bignumber(totalWithdrawalAmount),
+ math.bignumber(convertedWithdrawalAmount[currency])
+ )
+ );
+ } else {
+ loggerWithdrawals.debug(
+ 'toolsLib/wallet/withdrawalBelowLimit',
+ `No conversion found between ${currency} and ${getKitConfig().native_currency}`
+ );
+ return;
+ }
+
+ const last24HourWithdrawalAmount = await get24HourAccumulatedWithdrawals(userId);
+
+ loggerWithdrawals.verbose(
+ 'toolsLib/wallet/withdrawalBelowLimit',
+ `total 24 hour withdrawn amount converted to ${getKitConfig().native_currency}`,
+ last24HourWithdrawalAmount
+ );
+
+ totalWithdrawalAmount = math.number(
+ math.add(
+ math.bignumber(totalWithdrawalAmount),
+ math.bignumber(last24HourWithdrawalAmount)
+ )
+ );
+
+ loggerWithdrawals.verbose(
+ 'toolsLib/wallet/withdrawalBelowLimit',
+ 'total 24 hour withdrawn amount after performing current withdrawal',
+ totalWithdrawalAmount,
+ '24 hour withdrawal limit',
+ limit
+ );
+
+ if (totalWithdrawalAmount > limit) {
+ throw new Error(
+ `Total withdrawn amount would exceed withdrawal limit of ${limit} ${getKitConfig().native_currency}. Withdrawn amount: ${last24HourWithdrawalAmount} ${getKitConfig().native_currency}. Request amount: ${convertedWithdrawalAmount[currency]} ${getKitConfig().native_currency}`
+ );
+ }
+
+ return;
+};
+
+const transferAssetByKitIds = (senderId, receiverId, currency, amount, description = 'Admin Transfer', email = true, opts = {
+ additionalHeaders: null
+}) => {
+ if (!subscribedToCoin(currency)) {
+ return reject(new Error(INVALID_COIN(currency)));
+ }
+
+ if (amount <= 0) {
+ return reject(new Error(INVALID_AMOUNT(amount)));
+ }
+
+ return all([
+ getUserByKitId(senderId),
+ getUserByKitId(receiverId)
+ ])
+ .then(([ sender, receiver ]) => {
+ if (!sender || !receiver) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!sender.network_id || !receiver.network_id) {
+ throw new Error('User not registered on network');
+ }
+ return getNodeLib().transferAsset(sender.network_id, receiver.network_id, currency, amount, { description, email, ...opts });
+ });
+};
+
+const transferAssetByNetworkIds = (senderId, receiverId, currency, amount, description = 'Admin Transfer', email = true, opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().transferAsset(senderId, receiverId, currency, amount, { description, email, ...opts });
+};
+
+const getUserBalanceByKitId = (userKitId, opts = {
+ additionalHeaders: null
+}) => {
+ return getUserByKitId(userKitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getUserBalance(user.network_id, opts);
+ })
+ .then((data) => {
+ return {
+ user_id: userKitId,
+ ...data
+ };
+ });
+};
+
+const getUserBalanceByNetworkId = (networkId, opts = {
+ additionalHeaders: null
+}) => {
+ if (!networkId) {
+ return reject(new Error(USER_NOT_REGISTERED_ON_NETWORK));
+ }
+ return getNodeLib().getUserBalance(networkId, opts);
+};
+
+const getKitBalance = (opts = {
+ additionalHeaders: null
+}) => {
+ return getNodeLib().getBalance(opts);
+};
+
+const getUserTransactionsByKitId = (
+ type,
+ kitId,
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ format,
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ let promiseQuery;
+ if (kitId) {
+ if (type === 'deposit') {
+ promiseQuery = getUserByKitId(kitId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getUserDeposits(user.network_id, {
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ ...opts
+ });
+ });
+ } else if (type === 'withdrawal') {
+ promiseQuery = getUserByKitId(kitId, false)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().getUserWithdrawals(user.network_id, {
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ ...opts
+ });
+ });
+ }
+ } else {
+ if (type === 'deposit') {
+ promiseQuery = getExchangeDeposits(
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ opts
+ );
+ } else if (type === 'withdrawal') {
+ promiseQuery = getExchangeWithdrawals(
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ opts
+ );
+ }
+ }
+ return promiseQuery
+ .then((transactions) => {
+ if (format) {
+ if (transactions.data.length === 0) {
+ throw new Error(NO_DATA_FOR_CSV);
+ }
+ const csv = parse(transactions.data, Object.keys(transactions.data[0]));
+ return csv;
+ } else {
+ return transactions;
+ }
+ });
+};
+
+const getUserDepositsByKitId = (
+ kitId,
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ format,
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ return getUserTransactionsByKitId(
+ 'deposit',
+ kitId,
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ format,
+ opts
+ );
+};
+
+const getUserWithdrawalsByKitId = (
+ kitId,
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ format,
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ return getUserTransactionsByKitId(
+ 'withdrawal',
+ kitId,
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ format,
+ opts
+ );
+};
+
+const getExchangeDeposits = (
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+
+ return getNodeLib().getDeposits({
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ ...opts
+ })
+ .then(async (deposits) => {
+ if (deposits.data.length > 0) {
+ const networkIds = deposits.data.map((deposit) => deposit.user_id);
+ const idDictionary = await mapNetworkIdToKitId(networkIds);
+ for (let deposit of deposits.data) {
+ const user_kit_id = idDictionary[deposit.user_id];
+ deposit.network_id = deposit.user_id;
+ deposit.user_id = user_kit_id;
+ deposit.User.id = user_kit_id;
+ }
+ }
+ return deposits;
+ });
+};
+
+const getExchangeWithdrawals = (
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ opts = {
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().getWithdrawals({
+ currency,
+ status,
+ dismissed,
+ rejected,
+ processing,
+ waiting,
+ limit,
+ page,
+ orderBy,
+ order,
+ startDate,
+ endDate,
+ transactionId,
+ address,
+ ...opts
+ })
+ .then(async (withdrawals) => {
+ if (withdrawals.data.length > 0) {
+ const networkIds = withdrawals.data.map((withdrawal) => withdrawal.user_id);
+ const idDictionary = await mapNetworkIdToKitId(networkIds);
+ for (let withdrawal of withdrawals.data) {
+ const user_kit_id = idDictionary[withdrawal.user_id];
+ withdrawal.network_id = withdrawal.user_id;
+ withdrawal.user_id = user_kit_id;
+ withdrawal.User.id = user_kit_id;
+ }
+ }
+ return withdrawals;
+ });
+};
+
+const mintAssetByKitId = (
+ kitId,
+ currency,
+ amount,
+ opts = {
+ description: null,
+ transactionId: null,
+ status: null,
+ email: null,
+ fee: null,
+ additionalHeaders: null
+ }) => {
+ return getUserByKitId(kitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().mintAsset(user.network_id, currency, amount, opts);
+ });
+};
+
+const mintAssetByNetworkId = (
+ networkId,
+ currency,
+ amount,
+ opts = {
+ description: null,
+ transactionId: null,
+ status: null,
+ email: null,
+ fee: null,
+ additionalHeaders: null
+ }) => {
+ return getNodeLib().mintAsset(networkId, currency, amount, opts);
+};
+
+const updatePendingMint = (
+ transactionId,
+ opts = {
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ updatedTransactionId: null,
+ email: null,
+ updatedDescription: null,
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().updatePendingMint(transactionId, opts);
+};
+
+const burnAssetByKitId = (
+ kitId,
+ currency,
+ amount,
+ opts = {
+ description: null,
+ transactionId: null,
+ status: null,
+ email: null,
+ fee: null,
+ additionalHeaders: null
+ }) => {
+ return getUserByKitId(kitId)
+ .then((user) => {
+ if (!user) {
+ throw new Error(USER_NOT_FOUND);
+ } else if (!user.network_id) {
+ throw new Error(USER_NOT_REGISTERED_ON_NETWORK);
+ }
+ return getNodeLib().burnAsset(user.network_id, currency, amount, opts);
+ });
+};
+
+const burnAssetByNetworkId = (
+ networkId,
+ currency,
+ amount,
+ opts = {
+ description: null,
+ transactionId: null,
+ status: null,
+ email: null,
+ fee: null,
+ additionalHeaders: null
+ }) => {
+ return getNodeLib().burnAsset(networkId, currency, amount, opts);
+};
+
+const updatePendingBurn = (
+ transactionId,
+ opts = {
+ status: null,
+ dismissed: null,
+ rejected: null,
+ processing: null,
+ waiting: null,
+ updatedTransactionId: null,
+ email: null,
+ updatedDescription: null,
+ additionalHeaders: null
+ }
+) => {
+ return getNodeLib().updatePendingBurn(transactionId, opts);
+};
+
+module.exports = {
+ sendRequestWithdrawalEmail,
+ validateWithdrawalToken,
+ cancelUserWithdrawalByKitId,
+ checkTransaction,
+ performWithdrawal,
+ transferAssetByKitIds,
+ getUserBalanceByKitId,
+ getUserDepositsByKitId,
+ getUserWithdrawalsByKitId,
+ performWithdrawalNetwork,
+ cancelUserWithdrawalByNetworkId,
+ getExchangeDeposits,
+ getExchangeWithdrawals,
+ getUserBalanceByNetworkId,
+ transferAssetByNetworkIds,
+ mintAssetByKitId,
+ mintAssetByNetworkId,
+ burnAssetByKitId,
+ burnAssetByNetworkId,
+ getKitBalance,
+ updatePendingMint,
+ updatePendingBurn,
+ isValidAddress
+};
diff --git a/server/ws/server.js b/server/ws/server.js
index 1262ca8595..6ed37a8ee2 100644
--- a/server/ws/server.js
+++ b/server/ws/server.js
@@ -2,7 +2,7 @@
const WebSocket = require('ws');
const { loggerWebsocket } = require('../config/logger');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../utils/toolsLib');
const { MULTIPLE_API_KEY } = require('../messages');
const url = require('url');
const { hubConnected } = require('./hub');
diff --git a/server/ws/sub.js b/server/ws/sub.js
index e789c5dde8..1ba295ebe0 100644
--- a/server/ws/sub.js
+++ b/server/ws/sub.js
@@ -4,7 +4,7 @@ const { getPublicData } = require('./publicData');
const { addSubscriber, removeSubscriber, getChannels } = require('./channel');
const { WEBSOCKET_CHANNEL, WS_PUBSUB_DEPOSIT_CHANNEL, ROLES } = require('../constants');
const { each } = require('lodash');
-const toolsLib = require('hollaex-tools-lib');
+const toolsLib = require('../utils/toolsLib');
const { loggerWebsocket } = require('../config/logger');
const {
WS_AUTHENTICATION_REQUIRED,
diff --git a/test/selenium/Onboarding/AccountLevel.js b/test/selenium/Onboarding/AccountLevel.js
index ef5af74b09..e2d5ca334e 100644
--- a/test/selenium/Onboarding/AccountLevel.js
+++ b/test/selenium/Onboarding/AccountLevel.js
@@ -47,6 +47,7 @@ async function AccountLevel () {
console.log(step++,' | open | /login | ');
await driver.get(logInPage);
+ await util.takeHollashot(driver,reportPath,step);
await sleep(5000);
console.log(step++,' | echo | \'Supervisor can access all deposit, withdrawals and approval settings\' |');
@@ -60,18 +61,22 @@ async function AccountLevel () {
console.log(step++,' | click | css=.holla-button | ');
await driver.findElement(By.css('.holla-button')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(10000);
console.log(step++,' | click | css=a > .pl-1 | ');
await driver.findElement(By.css('a > .pl-1')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(5000);
console.log(step++,' | click | linkText=Users | ');
await driver.findElement(By.linkText('Users')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(3000);
console.log(step++,' | click | name=input | ');
await driver.findElement(By.name('input')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(3000);
console.log(step++,' | type | name=input | leveltest');
@@ -79,31 +84,35 @@ async function AccountLevel () {
console.log(step++,' | click | css=.ant-btn | ');
await driver.findElement(By.css('.ant-btn')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(5000);
console.log(step++,' | click | css=.ml-4 > .ant-btn > span | ');
await driver.findElement(By.css('.ml-4 > .ant-btn > span')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(3000);
console.log(step++,' | click | css=.ant-select-selector | ');
await driver.findElement(By.css('.ant-select-selector')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(3000);
- console.log(step++,' | click | xpath=//div[5]/div/div/div/div[2]/div[1]/div/div/div[4]/div/div/div[2] | ');
+ console.log(step++,' | click | xpath=//div[2]/div/div/div/div[2]/div/div/div[2] | ');
// /div[4]={1,..9}
- let level= Math.floor(Math.random() * 9)+1;
+ let level= Math.floor(Math.random() * 10)+1;
console.log('level : '+String(level))
await sleep(3000);
-
- if (level > 4){
- console.log('driver.executeScript("window.scrollBy(0," +10+ ")');
+ level = 9;//5//6789
+ if (level > 4 & level < 10){
+ level = level-4
+ console.log('driver.executeScript("window.scrollBy(0," +300+ ")');
{
- const element = await driver.findElement(By.xpath('//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2]'))
+ const element = await driver.findElement(By.xpath('//div[2]/div/div/div/div[5]/div/div/div[2]'));//'//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2]'))
await driver.executeScript('arguments[0].scrollIntoView(true);', element)
}
-
- console.log(step++,' | click |xpath=//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2] |')
- await driver.findElement(By.xpath('//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2]')).click();
+ await sleep(3000);
+ console.log(step++,' | click |xpath=//div[2]/div/div/div/div['+level+']/div/div/div[2] |')
+ await driver.findElement(By.xpath('//div[2]/div/div/div/div['+level+']/div/div/div[2]')).click();//('//div[2]/div/div/div/div[3]/div/div/div[2]'));//('//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2]')).click();
await sleep(3000);
console.log(step++,' | click | css=.w-100 > span |');
@@ -111,8 +120,8 @@ async function AccountLevel () {
await sleep(3000);
}else{
- console.log(step++,' | click | xpath=//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2] |');
- await driver.findElement(By.xpath('//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2]')).click();
+ console.log(step++,' | click | xpath=//div[2]/div/div/div/div['+level+']/div/div/div[2] |');
+ await driver.findElement(By.xpath('//div[2]/div/div/div/div['+level+']/div/div/div[2]')).click();//'//div[5]/div/div/div/div[2]/div[1]/div/div/div['+level+']/div/div/div[2]')).click();
await sleep(3000);
console.log(step++,' | click | css=.w-100 > span |');
@@ -126,10 +135,12 @@ async function AccountLevel () {
console.log(step++,' | click | css=.app-bar-account-content > div:nth-child(2) | ');
await driver.findElement(By.css('.app-bar-account-content > div:nth-child(2)')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(3000);
console.log(step++,' | click | css=.app-bar-account-menu-list:nth-child(10) > .edit-wrapper__container:nth-child(3) | ');
await driver.findElement(By.css('.app-bar-account-menu-list:nth-child(10) > .edit-wrapper__container:nth-child(3)')).click();
+ await util.takeHollashot(driver,reportPath,step);
await sleep(3000);
console.log(' entering into user account to assert Level of Account');
@@ -166,6 +177,7 @@ async function AccountLevel () {
console.log('This is the EndOfTest');
+
});
});
}
diff --git a/test/selenium/Onboarding/ChatBox.js b/test/selenium/Onboarding/ChatBox.js
index b1d97fd291..c9e90ea8c7 100644
--- a/test/selenium/Onboarding/ChatBox.js
+++ b/test/selenium/Onboarding/ChatBox.js
@@ -14,7 +14,7 @@ let passWord = process.env.ADMIN_PASS;
let logInPage = process.env.LOGIN_PAGE;
let Remot = process.env.SELENIUM_REMOTE_URL;
describe('Orders', function() {
- this.timeout(300000);
+ this.timeout(30000);
let driver;
let vars;
function sleep(ms) {
@@ -23,7 +23,7 @@ describe('Orders', function() {
});
}
beforeEach(async function() {
- driver.manage().window().maximize();
+ //driver.manage().window().maximize();
});
afterEach(async function() {
@@ -31,7 +31,7 @@ describe('Orders', function() {
});
it('firefox', async function() {
- // driver = await new Builder().forBrowser('chrome').build();
+ // driver = await new Builder().forBrowser('chrome').build();
driver = await new Builder().forBrowser('firefox').usingServer(Remot).build();
@@ -77,7 +77,7 @@ describe('Orders', function() {
// Test name: Untitled
// Step # | name | target | value
- // 1 | open | /account |
+ console.log(" 1 | open | /account |")
await driver.get(logInPage);
await sleep(10000);
// 2 | type | name=email | USER@bitholla.com
diff --git a/test/selenium/Onboarding/LogIn.js b/test/selenium/Onboarding/LogIn.js
index 744e00804b..c2b829003e 100644
--- a/test/selenium/Onboarding/LogIn.js
+++ b/test/selenium/Onboarding/LogIn.js
@@ -93,8 +93,41 @@ async function LogIn () {
shot();
console.log('This is the EndOfTest');
-
+ });
+ it('Email Confirmation', async function() {
+ console.log('Test name: Confirmation');
+ console.log('Step # | name | target | value');
+
+ await util.emailLogIn(step,driver,emailAdmin,emailPass);
+ await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'))), 50000);
+ await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)')).click();
+
+ console.log(step++,' | doubleClick | css=.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1) | ');
+ {
+ const element = await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'));
+ await driver.actions({ bridge: true}).doubleClick(element).perform();
+ }
+ await sleep(5000);
+
+ console.log(step++,' | selectFrame | index=1 | ');
+ await driver.switchTo().frame(1);
+ await sleep(10000);
+
+ console.log(step++,' | storeText | xpath=/html/body/pre/a[16] | content');
+ vars['content'] = await driver.findElement(By.xpath('/html/body/pre/a[16]')).getText();
+ const emailCont = await driver.findElement(By.css('pre')).getText();
+
+ console.log(step++,' | echo | ${content} | ');
+ console.log(vars['content']);
+ console.log(step++,' | assertText | xpath=/html/body/pre/a[16] | ${content}');
+ expect(vars['content']).to.equal(userName.toLowerCase());
+
+ console.log(step++,' | assertText | email body contains] | We have recorded a login to your account with the following details');
+ expect(util.chunkCleaner(emailCont).includes("We have recorded a login to your account with the following details")).to.be.true
+
+ console.log('This is the EndOfTest');
+
});
});
diff --git a/test/selenium/Onboarding/LogOut.js b/test/selenium/Onboarding/LogOut.js
index 0d59177cbb..139eeaa15c 100644
--- a/test/selenium/Onboarding/LogOut.js
+++ b/test/selenium/Onboarding/LogOut.js
@@ -2,17 +2,19 @@
//Using Selenium webderiver and Mocha/Chai
//given, when and then
async function LogOut(){
- const { Builder, By, Key, until } = require('selenium-webdriver');
- const assert = require('assert');
+ const { Builder, By, until } = require('selenium-webdriver');
const { expect } = require('chai');
const { Console } = require('console');
- const path = require('path')
+ const path = require('path');
+ const fs = require('fs');
+ const logPath = path.join(__dirname, './.log',path.basename(__filename,'.js'));
const reportPath = path.join(__dirname, './../Report',path.dirname(__filename).replace(path.dirname(__dirname),''),path.basename(__filename,'.js'));
- const util = require('../Utils/Utils.js');
+ const util = require ('./../Utils/Utils.js');
const { addConsoleHandler } = require('selenium-webdriver/lib/logging');
util.makeReportDir(reportPath);
+ util.makeReportDir(logPath);
require('console-stamp')(console, {
- format: ':date(yyyy/mm/dd HH:MM:ss.l)|'
+ format: ':date(yyyy/mm/dd HH:MM:ss.l)|' ,
} );
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
let userName = process.env.BOB;
@@ -20,6 +22,8 @@ async function LogOut(){
let logInPage = process.env.LOGIN_PAGE;
let signUpPage = process.env.SIGN_UP_PAGE;
let emailPage = process.env.EMAIL_PAGE;
+ let emailPass =process.env.EMAIL_PASS ;
+ let emailAdmin = process.env.EMAIL_ADMIN_USERNAME ;
let step = util.getStep();
util.logHolla(logPath)
@@ -28,7 +32,7 @@ async function LogOut(){
console.log('Variables are defined');
}
describe('BobLogOut', function() {
- this.timeout(30000);
+ this.timeout(300000);
let driver;
let vars;
function sleep(ms) {
@@ -36,6 +40,7 @@ async function LogOut(){
setTimeout(resolve, ms);
});
}
+ function shot(){util.takeHollashot(driver,reportPath,step);}
beforeEach(async function() {
driver = await new Builder().forBrowser('chrome').build();
vars = {};
@@ -44,8 +49,8 @@ async function LogOut(){
});
afterEach(async function() {
- util.setStep(step);
- await driver.quit();
+ await util.setStep(step);
+ // await driver.quit();
});
it('Simple log in', async function() {
//Given The user logged in
@@ -88,16 +93,58 @@ async function LogOut(){
await sleep(5000);
//Then Log out should happen
- await console.log(step++,' | click | xpath =//*[@id="root"]/div/div[2]/div/div/div[3]/div[1]/div/div[8]/div[2]/div |');
- await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/div/div[3]/div[1]/div/div[8]/div[2]/div')).click();
+ console.log(step++,' | click | css=.app-bar-account-content > div:nth-child(2) | ');
+ await driver.findElement(By.css(".app-bar-account-content > div:nth-child(2)")).click();
+ await sleep(5000);
+
+ console.log(step++,'| click | css=.app-bar-account-menu-list:nth-child(11) > .edit-wrapper__container:nth-child(3) | ');
+ await driver.findElement(By.css(".app-bar-account-menu-list:nth-child(11) > .edit-wrapper__container:nth-child(3)")).click()
await sleep(5000);
-
+
console.log(step++,' | assertText | css=.icon_title-text | Login');
expect(await driver.findElement(By.css('.icon_title-text')).getText()).to.equal( 'Login');
-
+ await sleep(2000);
+ shot();
+ await sleep(2000);
+
console.log('This is the EndOfTest');
});
+ it('Email Confirmation', async function() {
+ console.log('Test name: Confirmation');
+ console.log('Step # | name | target | value');
+
+ await util.emailLogIn(step,driver,emailAdmin,emailPass);
+ await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'))), 50000);
+ await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)')).click();
+
+ console.log(step++,' | doubleClick | css=.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1) | ');
+ {
+ const element = await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'));
+ await driver.actions({ bridge: true}).doubleClick(element).perform();
+ }
+ await sleep(5000);
+
+ console.log(step++,' | selectFrame | index=1 | ');
+ await driver.switchTo().frame(1);
+ await sleep(10000);
+
+ console.log(step++,' | storeText | xpath=/html/body/pre/a[16] | content');
+ vars['content'] = await driver.findElement(By.xpath('/html/body/pre/a[16]')).getText();
+ const emailCont = await driver.findElement(By.css('pre')).getText();
+
+ console.log(step++,' | echo | ${content} | ');
+ console.log(vars['content']);
+
+ console.log(step++,' | assertText | xpath=/html/body/pre/a[16] | ${content}');
+ expect(vars['content']).to.equal(userName.toLowerCase());
+
+ console.log(step++,' | assertText | email body contains] | We have recorded a login to your account with the following details');
+ expect(util.chunkCleaner(emailCont).includes("We have recorded a login to your account with the following details")).to.be.true
+
+ console.log('This is the EndOfTest');
+
+ });
});
}
describe('Main Test', function () {
diff --git a/test/selenium/Onboarding/ResendVerificationEmail.js b/test/selenium/Onboarding/ResendVerificationEmail.js
index 58bab20a91..3196413aae 100644
--- a/test/selenium/Onboarding/ResendVerificationEmail.js
+++ b/test/selenium/Onboarding/ResendVerificationEmail.js
@@ -24,7 +24,8 @@ async function ResendVerificationEmail(){
let passWord = process.env.PASSWORD;
let webSite = process.env.WEBSITE;
let signUpPage = process.env.SIGN_UP_PAGE;
- let emailAdmin =process.env.Email_ADMIN_USERNAME;
+ let emailAdmin =process.env.EMAIl_ADMIN_USERNAME;
+ let emailPass = process.env.EMAIL_PASS;
let step = util.getStep();
util.logHolla(logPath)
if (process.env.NODE_ENV == 'test') {
@@ -123,7 +124,7 @@ async function ResendVerificationEmail(){
let reuserName = util.getNewUser();
console.log('Step # | name | target | value');
- await util.emailLogIn(step,driver,emailAdmin,passWord);
+ await util.emailLogIn(step,driver,emailAdmin,emailPass);
await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'))), 50000);
await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)')).click();
diff --git a/test/selenium/Onboarding/ResetPassword.js b/test/selenium/Onboarding/ResetPassword.js
index 916afbec9a..a83bdf39d6 100644
--- a/test/selenium/Onboarding/ResetPassword.js
+++ b/test/selenium/Onboarding/ResetPassword.js
@@ -23,7 +23,8 @@ async function ResetPassword(){
let passWord = process.env.PASSWORD;
let newPassWord = process.env.NEWPASS
let webSite = process.env.WEBSITE;
- let emailAdmin =process.env.Email_ADMIN_USERNAME;
+ let emailAdmin =process.env.EMAIl_ADMIN_USERNAME;
+ let emailPass = process.env.EMAIL_PASS;
let step = util.getStep();
util.logHolla(logPath)
@@ -85,7 +86,7 @@ async function ResetPassword(){
console.log('Test name: Confirmation');
console.log('Step # | name | target | value');
- await util.emailLogIn(step,driver,emailAdmin,passWord);
+ await util.emailLogIn(step,driver,emailAdmin,emailPass);
await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'))), 50000);
await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)')).click();
diff --git a/test/selenium/Onboarding/SignUp.js b/test/selenium/Onboarding/SignUp.js
index 7b89fd028b..a81bb2f289 100644
--- a/test/selenium/Onboarding/SignUp.js
+++ b/test/selenium/Onboarding/SignUp.js
@@ -20,7 +20,8 @@ async function SignUp(){
let User = process.env.NEW_USER;
let passWord = process.env.PASSWORD;
let signUpPage = process.env.SIGN_UP_PAGE;
- let emailAdmin =process.env.Email_ADMIN_USERNAME;
+ let emailAdmin =process.env.EMAIl_ADMIN_USERNAME;
+ let emailPass = process.env.EMAIL_PASS;
let step = util.getStep();
util.logHolla(logPath)
const newUser = util.defineNewUser(User,4) ;
@@ -89,7 +90,7 @@ async function SignUp(){
console.log('Test name: Confirmation');
console.log('Step # | name | target | value');
- await util.emailLogIn(step,driver,emailAdmin,passWord);
+ await util.emailLogIn(step,driver,emailAdmin,emailPass);
await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'))), 50000);
diff --git a/test/selenium/Onboarding/reCAPTCHA.js b/test/selenium/Onboarding/reCAPTCHA.js
index 3e1b38c159..8890ec0253 100644
--- a/test/selenium/Onboarding/reCAPTCHA.js
+++ b/test/selenium/Onboarding/reCAPTCHA.js
@@ -44,7 +44,7 @@ async function ReCAPTCHA(){
afterEach(async function() {
util.setStep(step);
- await driver.quit();
+ //await driver.quit();
});
it('ReCHAPTCHA log in', async function() {
@@ -83,19 +83,22 @@ async function ReCAPTCHA(){
console.log(step++,' | switch | defaultContent | ');
await driver.switchTo().defaultContent();
-
- console.log(step++,' | type | name=email |', userName);
+ for (let i = 0; i < 100; i++) {
+ console.log(step++,' | type | name=email | iam@not.com', );
await driver.findElement(By.name('email')).click();
- await driver.findElement(By.name('email')).sendKeys(userName);
+ await driver.findElement(By.name('email')).clear();
+ await driver.findElement(By.name('email')).sendKeys("iam@not.com");
console.log(step++,' | type | name=password | PASSWORD');
await driver.wait(until.elementLocated(By.name('password')), 5000);
+ await driver.findElement(By.name('password')).click();
+ await driver.findElement(By.name('password')).clear();
await driver.findElement(By.name('password')).sendKeys(passWord);
console.log(step++,' | click | css=.auth_wrapper | ');
await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.auth_wrapper'))), 5000);
await driver.findElement(By.css('.auth_wrapper')).click();
-
+ }
console.log(step++,' | verifyElementPresent | css=.holla-button |');
{
const elements = await driver.findElements(By.css('.holla-button'));
@@ -113,9 +116,9 @@ async function ReCAPTCHA(){
await console.log(await driver.findElement(By.css('.app-bar-account-content > div:nth-child(2)')).getText());
expect(await driver.findElement(By.css('.app-bar-account-content > div:nth-child(2)')).getText()).to.equal(userName);
- console.log(step++,' | ');
-
+
console.log('This is the EndOfTest');
+ util.takeHollashot(driver,reportPath,step);
});
});
}
diff --git a/test/selenium/Onboarding/referral.js b/test/selenium/Onboarding/referral.js
index 60db4837c1..2624f98532 100644
--- a/test/selenium/Onboarding/referral.js
+++ b/test/selenium/Onboarding/referral.js
@@ -65,7 +65,7 @@ async function Referral(){
await sleep(4000);
console.log(step++,' | click | name=email | ')
- await driver.findElement(By.name('email')).click();
+ await driver.findElement(By.name('email')).click();
console.log(step++,' | click | css=.holla-button | ')
await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.holla-button'))), 50000);
@@ -91,9 +91,13 @@ async function Referral(){
console.log(step++,' | click | css=.app-bar-account-content > div:nth-child(2) | ')
await driver.findElement(By.css('.app-bar-account-content > div:nth-child(2)')).click();
- console.log(step++,' | click | xpath=//*[@id="tab-account-menu"]/div[11]/div[3] | ')
- await driver.findElement(By.xpath('//*[@id="tab-account-menu"]/div[10]')).click();
+ // console.log(step++,' | click | xpath=//*[@id="tab-account-menu"]/div[11]/div[3] | ')
+ // await driver.findElement(By.xpath('//*[@id="tab-account-menu"]/div[10]')).click();
+ console.log(step++,'| click | css=.app-bar-account-menu-list:nth-child(11) > .edit-wrapper__container:nth-child(3) | ');
+ await driver.findElement(By.css(".app-bar-account-menu-list:nth-child(11) > .edit-wrapper__container:nth-child(3)")).click()
+ await sleep(5000);
+
console.log(step++,' | open | ',signUpPage);
await driver.get(signUpPage);
await sleep(5000);
@@ -129,7 +133,10 @@ async function Referral(){
// there is no need for verification
// util.adminVerifiesNewUser(driver,userName,apassWord,newUser)
-
+ console.log(step++,' | open | ',website);
+ await driver.get(website+"login");
+ await sleep(5000);
+
console.log(step++,' | type | name=email | USER@bitholla.com ')
await driver.findElement(By.name('email')).sendKeys(userName);
@@ -138,7 +145,7 @@ async function Referral(){
await sleep(4000);
console.log(step++,' | click | name=email | ')
- await driver.findElement(By.name('email')).click();
+ await driver.findElement(By.name('email')).click();
console.log(step++,' | click | css=.holla-button | ')
await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.holla-button'))), 50000);
diff --git a/test/selenium/Onboarding/setting.js b/test/selenium/Onboarding/setting.js
index 08ffed9d6e..d27fba1bb5 100644
--- a/test/selenium/Onboarding/setting.js
+++ b/test/selenium/Onboarding/setting.js
@@ -43,7 +43,7 @@ async function Setting(){
afterEach(async function() {
util.setStep(step);
- await driver.quit();
+ //await driver.quit();
});
it('Setting', async function() {
console.log(' Test name: Setting');
@@ -69,8 +69,8 @@ async function Setting(){
await driver.findElement(By.css('.holla-button')).click();
await sleep(4000);
- console.log(step++,' | click | css=.d-flex:nth-child(6) > .side-bar-txt > .edit-wrapper__container | ');
- await driver.findElement(By.css('.d-flex:nth-child(6) > .side-bar-txt > .edit-wrapper__container')).click();
+ console.log(step++,' | click | css=.d-flex:nth-child(7) > .side-bar-txt > .edit-wrapper__container | ');
+ await driver.findElement(By.css('.d-flex:nth-child(7) > .side-bar-txt > .edit-wrapper__container')).click();
await sleep(3000);
console.log(step++,' | click | css=.tab_item:nth-child(3) > div | ');
@@ -282,9 +282,8 @@ async function Setting(){
await driver.findElement(By.css('.settings-form')).click();
await sleep(3000);
- console.log('should be fixed')
- console.log(step++,' | assertText | css=.settings-form | Language\nLanguage preferences (Includes Emails)\nEnglish');
- assert(await driver.findElement(By.css('.settings-form')).getText() == 'Language preferences (Includes Emails)');
+ console.log(step++,' | assertText | css=.settings-form | Language preferences (Includes Emails)');
+ assert(await driver.findElement(By.css('.d-flex > .field-label')).getText() == 'Language preferences (Includes Emails)');
await sleep(3000);
console.log('This is the EndOfTest');
diff --git a/test/selenium/Onboarding/shot.js b/test/selenium/Onboarding/shot.js
new file mode 100644
index 0000000000..1207da37c5
--- /dev/null
+++ b/test/selenium/Onboarding/shot.js
@@ -0,0 +1,158 @@
+
+const { Builder, By, Key, until } = require('selenium-webdriver');
+ const assert = require('assert');
+ const { expect } = require('chai');
+ const { Console } = require('console');
+ const path = require('path');
+ const logPath = path.join(__dirname, './.log',path.basename(__filename,'.js'));
+ const reportPath = path.join(__dirname, './../Report',path.dirname(__filename).replace(path.dirname(__dirname),''),path.basename(__filename,'.js'));
+ const util = require ('./../Utils/Utils.js');
+ const { addConsoleHandler } = require('selenium-webdriver/lib/logging');
+ util.makeReportDir(reportPath);
+ util.makeReportDir(logPath);
+ require('console-stamp')(console, {
+ format: ':date(yyyy/mm/dd HH:MM:ss.l)|'
+ } );
+ require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
+
+ //let step = util.getStep();
+ util.logHolla(logPath)
+let i=0;
+let userName= "mahdi@testsae.com";
+let passWord = "Holla2021!";
+
+
+describe('shot', function() {
+ this.timeout(300000)
+ let driver
+ let vars
+ function sleep(ms) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+ }
+ beforeEach(async function() {
+ driver = await new Builder().forBrowser('chrome').build()
+ vars = {}
+ let reportPath = path.join(__dirname, './../Report',path.dirname(__filename).replace(path.dirname(__dirname),''),path.basename(__filename,'.js'));
+ console.log(reportPath)
+ })
+ afterEach(async function() {
+ await driver.quit();
+ })
+ it('Untitled', async function() {
+ console.log(" Test name: Untitled");
+ console.log(" Step # | name | target | value");
+ console.log(" 1 | open | /login | ");
+ await driver.get("https://pro.hollaex.com/login")
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++)
+ await sleep(3000);
+
+ console.log(" 2 | setWindowSize | maximize | ");
+ await driver.manage().window().maximize();;
+ await sleep(3000);
+ util.takeHollashot(driver,reportPath,i++)
+
+ await sleep(3000);
+
+ console.log(" 3 | type | name=email | username");
+ await driver.findElement(By.name("email")).sendKeys(userName);
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 4 | type | name=password | password");
+ await driver.findElement(By.name("password")).sendKeys(passWord);
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++)
+ await sleep(3000);
+
+ console.log(" 5 | click | css=.holla-button | ");
+ await driver.findElement(By.css(".holla-button")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++)
+ await sleep(3000);
+
+ console.log(" 6 | click | css=.app-menu-bar-content:nth-child(2) .edit-wrapper__container | ");
+ await driver.findElement(By.css(".app-menu-bar-content:nth-child(2) .edit-wrapper__container")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 7 | click | css=.app-menu-bar-content:nth-child(3) .edit-wrapper__container | ");
+ await driver.findElement(By.css(".app-menu-bar-content:nth-child(3) .edit-wrapper__container")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 8 | click | css=.app-menu-bar-content:nth-child(4) .edit-wrapper__container | ");
+ await driver.findElement(By.css(".app-menu-bar-content:nth-child(4) .edit-wrapper__container")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 9 | click | css=.d-flex:nth-child(3) > .side-bar-txt > .edit-wrapper__container | ");
+ await driver.findElement(By.css(".d-flex:nth-child(3) > .side-bar-txt > .edit-wrapper__container")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 10 | click | css=.tab_item:nth-child(2) > div | ");
+ await driver.findElement(By.css(".tab_item:nth-child(2) > div")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 11 | click | css=.tab_item:nth-child(3) > div | ");
+ await driver.findElement(By.css(".tab_item:nth-child(3) > div")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 12 | click | css=.tab_item:nth-child(4) > div | ");
+ await driver.findElement(By.css(".tab_item:nth-child(4) > div")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ console.log(" 13 | click | css=.app_container-main | ");
+ await driver.findElement(By.css(".app_container-main")).click();
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+ await driver.executeScript("window.scrollBy(0,350)", "");
+ await sleep(3000);
+ await util.takeHollashot(driver,reportPath,i++);
+ await sleep(3000);
+
+ })
+})
diff --git a/test/selenium/Roles/Communicator.js b/test/selenium/Roles/Communicator.js
index 46bb9e70cb..ebcd3b608a 100644
--- a/test/selenium/Roles/Communicator.js
+++ b/test/selenium/Roles/Communicator.js
@@ -34,7 +34,7 @@ async function Communicator(){
});
afterEach(async function() {
- //await driver.quit();
+ await driver.quit();
});
it('communicator', async function() {
console.log('Communicator can access to website direct editing for content management and communications');
@@ -58,11 +58,16 @@ async function Communicator(){
console.log(step++,' | click | css=.holla-button | ');
await driver.findElement(By.css('.holla-button')).click();
await sleep(5000);
-
+
console.log(step++,' | click | css=a > .pl-1 | ');
await driver.findElement(By.css('a > .pl-1')).click();
await sleep(5000);
+
+ console.log(step++,' | assertText | css=.sub-label | Communicator');
+ assert(await driver.findElement(By.css('.sub-label')).getText() == 'Communicator');
+ await sleep(5000);
+
console.log(step++,' | click | linkText=Users | ');
await driver.findElement(By.linkText('Users')).click();
await sleep(5000);
@@ -83,19 +88,24 @@ async function Communicator(){
console.log(step++,' | assertText | xpath=//*[@id=\'rc-tabs-0-panel-users\']/div/div/div[2]/div/div/div/div/div/table/tbody/tr/td/div/p | No Data');
assert(await driver.findElement(By.xpath('//*[@id="rc-tabs-0-panel-users"]/div/div/div[2]/div/div/div/div/div/table/tbody/tr/td/div/p')).getText() == 'No Data');
- util.takeHollashot(driver,reportPath,11);
+ util.takeHollashot(driver,reportPath,step);
- console.log(step++,' | click | linkText=Financials | ');
- await driver.findElement(By.linkText('Financials')).click();
+ console.log(step++,' | click | linkText=Assets | ');
+ await driver.findElement(By.linkText('Assets')).click();
console.log(step++,' | click | css=.content-wrapper | ');
await driver.findElement(By.css('.content-wrapper')).click();
- await sleep(5000);
+ await sleep(3000);
- console.log(step++,' | assertText | css=p | -Access denied: User is not authorized to access this endpoint-');
- assert(await driver.findElement(By.css('p')).getText() == '-Access denied: User is not authorized to access this endpoint-');
+ console.log(step++,' | assertText | css=.ant-message-custom-content| -Access denied: User is not authorized to access this endpoint-');
+ assert(await driver.findElement(By.css('.ant-message-custom-content')).getText() == 'Access denied: User is not authorized to access this endpoint');
util.takeHollashot(driver,reportPath,14);
+ console.log(step++,' | click | id=rc-tabs-1-tab-1 | ');
+ await driver.findElement(By.id('rc-tabs-1-tab-1')).click();
+ await sleep(5000);
+
+
console.log(step++,' | click | css=.ant-card-body > .ant-alert | ');
await driver.findElement(By.css('.ant-card-body > .ant-alert')).click();
await sleep(5000);
@@ -104,41 +114,58 @@ async function Communicator(){
assert(await driver.findElement(By.css('.ant-card-body .ant-alert-description')).getText() == 'Access denied: User is not authorized to access this endpoint');
util.takeHollashot(driver,reportPath,16);
- console.log(step++,' | click | id=rc-tabs-1-tab-1 | ');
- await driver.findElement(By.id('rc-tabs-1-tab-1')).click();
- await sleep(5000);
-
- console.log(step++,' | click | xpath=//*[@id="rc-tabs-1-panel-1"]/div/div[1]/button | ');
- await driver.findElement(By.xpath('//*[@id="rc-tabs-1-panel-1"]/div/div[1]/button')).click();
- await sleep(5000);
- {
- console.log(step++,' | assertText | css=.sub-title | Asset:');
- assert(await driver.findElement(By.css('.sub-title')).getText() == 'Asset:')
-
- console.log(step++,' | click | css=.btn-wrapper > .ant-btn:nth-child(1) |');
- await driver.findElement(By.css('.btn-wrapper > .ant-btn:nth-child(1)')).click();
- await sleep(3000);
- }
+
console.log(step++,' | click | id=rc-tabs-1-tab-2 | ');
await driver.findElement(By.id('rc-tabs-1-tab-2')).click();
await sleep(5000);
console.log(step++,' | click | css=.ant-alert-closable | ');
await driver.findElement(By.css('.ant-alert-closable')).click();
-
+ await sleep(5000);
+
console.log(step++,' | assertText | css=.ant-alert-closable > .ant-alert-message | Access denied: User is not authorized to access this endpoint');
assert(await driver.findElement(By.css('.ant-alert-closable > .ant-alert-message')).getText() == 'Access denied: User is not authorized to access this endpoint');
- util.takeHollashot(driver,reportPath,22);
+ util.takeHollashot(driver,reportPath,step);
+ await sleep(5000);
console.log(step++,' | click | id=rc-tabs-1-tab-3 | ');
await driver.findElement(By.id('rc-tabs-1-tab-3')).click();
await sleep(5000);
+
+ let y=await driver.findElement(By.xpath('//div[4]/div/div/div/div[2]')).getText()
+ console.log(y)
+
+ console.log(step++,' | assertText | css=.ant-alert-error | Access denied: User is not authorized to access this endpoint');
+ //assert(await driver.findElement(By.css(".ant-alert-error")).getText() == "Access denied: User is not authorized to access this endpoint\\\\nClose")
+ //util.takeHollashot(driver,reportPath,22);
+ await sleep(5000);
console.log('should be fixed');
- console.log(step++,' | assertText | xpath = //*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2] | Access denied: User is not authorized to access this endpoint');
- console.log(await driver.findElement(By.xpath('//*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2]')).getText() )
- assert(await driver.findElement(By.xpath('//*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2]')).getText() == 'Access denied: User is not authorized to access this endpoint');
- util.takeHollashot(driver,reportPath,1);
+ // console.log(step++,' | assertText | xpath = //*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2] | Access denied: User is not authorized to access this endpoint');
+ // console.log(await driver.findElement(By.xpath('//*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2]')).getText() )
+ // assert(await driver.findElement(By.xpath('//*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2]')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ // util.takeHollashot(driver,reportPath,1);
+
+ console.log(step++,' | click | id=rc-tabs-1-tab-4 | ');
+ await driver.findElement(By.id('rc-tabs-1-tab-4')).click();
+ await sleep(5000);
+
+ console.log(step++,' | click | css=.button:nth-child(1) > span | ');
+ await driver.findElement(By.css('.button:nth-child(1) > span')).click();
+ await sleep(5000);
+
+ console.log(step++,' | click | css=.modal-button:nth-child(2) > span| ');
+ await driver.findElement(By.css('.modal-button:nth-child(2) > span')).click();
+ await sleep(1000);
+
+ console.log(step++,' | assertText | css=.ant-message-custom-content > span:nth-child(2) | Access denied: User is not authorized to access this endpoint');
+ assert(await driver.findElement(By.css('.ant-message-custom-content > span:nth-child(2)')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ util.takeHollashot(driver,reportPath,22);
+ await sleep(5000);
+
+ console.log(step++,' | click | id=rc-tabs-1-tab-5 | ');
+ await driver.findElement(By.id('rc-tabs-1-tab-5')).click();
+ await sleep(5000);
console.log('This is the EndOfTest');
});
diff --git a/test/selenium/Roles/Kyc.js b/test/selenium/Roles/Kyc.js
index e00476e301..b8d0fb76db 100644
--- a/test/selenium/Roles/Kyc.js
+++ b/test/selenium/Roles/Kyc.js
@@ -34,7 +34,7 @@ async function Kyc(){
vars = {};
});
afterEach(async function() {
- await driver.quit();
+ // await driver.quit();
});
it('KYC', async function() {
console.log(' KYC role can access some user data to review KYC requirements');
@@ -55,10 +55,15 @@ async function Kyc(){
await driver.findElement(By.css('.holla-button')).click();
await sleep(5000);
+
console.log(step++,' | click | css=a > .pl-1 | ');
await driver.findElement(By.css('a > .pl-1')).click();
await sleep(5000);
-
+
+ console.log(step++,' | assertText | css=.sub-label | KYC');
+ assert(await driver.findElement(By.css('.sub-label')).getText() == 'KYC');
+ await sleep(5000);
+
console.log(step++,' | click | css=.role-section > div:nth-child(2) | ');
await driver.findElement(By.css('.role-section > div:nth-child(2)')).click();
@@ -95,63 +100,74 @@ async function Kyc(){
assert(elements.length);
}
- console.log(step++,' | click|linkText = Financials | ');
- await driver.findElement(By.linkText('Financials')).click();
+ console.log(step++,' | click|linkText = Assets | ');
+ await driver.findElement(By.linkText('Assets')).click();
await sleep(5000);
- console.log(step++,' | runScript | window.scrollTo(0,0) | ');
- await driver.executeScript('window.scrollTo(0,0)');
-
- console.log(step++,' | click | css=p | ');
- await sleep(5000);
- await driver.findElement(By.css('p')).click();
-
- console.log(step++,' | assertElementPresent | css=p | ');
- {
- const elements = await driver.findElements(By.css('p'));
- assert(elements.length);
- }
-
- console.log(step++,' | click | css=.ant-card-body > .ant-alert | ');
- await driver.findElement(By.css('.ant-card-body > .ant-alert')).click();
-
- console.log(step++,' | assertElementPresent | css=.ant-card-body .ant-alert-description | ');
- {
- const elements = await driver.findElements(By.css('.ant-card-body .ant-alert-description'));
- assert(elements.length);
- }
-
+ console.log(step++,' | assertText | css=.ant-empty-description | No Data');
+ assert(await driver.findElement(By.css('.ant-empty-description')).getText() == 'No Data');
+ await sleep(5000);
+
console.log(step++,' | click | id=rc-tabs-2-tab-1 | ');
await driver.findElement(By.id('rc-tabs-2-tab-1')).click();
await sleep(5000);
- console.log(step++,' | click | xpath=//*[@id="rc-tabs-2-panel-1"]/div/div[1]/button | ');
- await driver.findElement(By.xpath('//*[@id="rc-tabs-2-panel-1"]/div/div[1]/button')).click();
+ console.log(step++,' | assertText | xpath= //span[contains(.,"Access denied: User is not authorized to access this endpoint")]');
+ assert(await driver.findElement(By.xpath("//span[contains(.,'Access denied: User is not authorized to access this endpoint')]")).getText() == 'Access denied: User is not authorized to access this endpoint');
await sleep(5000);
-
- console.log(step++,' | assertText | css=.sub-title | Asset:');
- assert(await driver.findElement(By.css('.sub-title')).getText() == 'Asset:')
-
- console.log(step++,' | click | css=.btn-wrapper > .ant-btn:nth-child(1) |');
- await driver.findElement(By.css('.btn-wrapper > .ant-btn:nth-child(1)')).click();
- await sleep(3000);
console.log(step++,' | click | id =rc-tabs-2-tab-2 | ');
await driver.findElement(By.id('rc-tabs-2-tab-2')).click();
await sleep(5000);
+ console.log(step++,' | click | css=.ant-alert-closable | ');
+ await driver.findElement(By.css('.ant-alert-closable')).click();
+ await sleep(5000);
+
console.log(step++,' | assertText | css=.ant-alert-closable > .ant-alert-message | Access denied: User is not authorized to access this endpoint');
assert(await driver.findElement(By.css('.ant-alert-closable > .ant-alert-message')).getText() == 'Access denied: User is not authorized to access this endpoint');
-
+ util.takeHollashot(driver,reportPath,step);
+ await sleep(5000);
+
console.log(step++,' | click | id=rc-tabs-2-tab-3 | ');
await driver.findElement(By.id('rc-tabs-2-tab-3')).click();
await sleep(4000);
console.log('should be fixed');
- console.log(step++,' | assertText | css=#rc-tabs-2-panel-withdrawals .app-wrapper > .ant-alert > .ant-alert-message | Access denied: User is not authorized to access this endpoint');
- console.log(await driver.findElement(By.xpath('//*[@id="rc-tabs-2-panel-3"]/div/div/div/div[2]')).getText());
- assert(await driver.findElement(By.xpath('//*[@id="rc-tabs-2-panel-3"]/div/div/div/div[2]')).getText() == 'Access denied: User is not authorized to access this endpoint');
-
+ let y=await driver.findElement(By.xpath('//div[4]/div/div/div/div[2]')).getText()
+ console.log(y)
+
+ console.log(step++,' | assertText | css=.ant-alert-error | Access denied: User is not authorized to access this endpoint');
+ //assert(await driver.findElement(By.css(".ant-alert-error")).getText() == "Access denied: User is not authorized to access this endpoint\\\\nClose")
+ //util.takeHollashot(driver,reportPath,22);
+ await sleep(5000);
+
+ console.log('should be fixed');
+ // console.log(step++,' | assertText | xpath = //*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2] | Access denied: User is not authorized to access this endpoint');
+ // console.log(await driver.findElement(By.xpath('//*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2]')).getText() )
+ // assert(await driver.findElement(By.xpath('//*[@id="rc-tabs-1-panel-3"]/div/div/div/div[2]')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ // util.takeHollashot(driver,reportPath,1);
+
+ console.log(step++,' | click | id=rc-tabs-2-tab-4 | ');
+ await driver.findElement(By.id('rc-tabs-2-tab-4')).click();
+ await sleep(4000);
+
+ console.log(step++,' | click | css=.button:nth-child(1) > span | ');
+ await driver.findElement(By.css('.button:nth-child(1) > span')).click();
+ await sleep(5000);
+
+ console.log(step++,' | click | css=.modal-button:nth-child(2) > span| ');
+ await driver.findElement(By.css('.modal-button:nth-child(2) > span')).click();
+ await sleep(1000);
+
+ console.log(step++,' | assertText | css=.ant-message-custom-content > span:nth-child(2) | Access denied: User is not authorized to access this endpoint');
+ assert(await driver.findElement(By.css('.ant-message-custom-content > span:nth-child(2)')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ util.takeHollashot(driver,reportPath,22);
+ await sleep(5000);
+
+ console.log(step++,' | click | id=rc-tabs-2-tab-5 | ');
+ await driver.findElement(By.id('rc-tabs-2-tab-5')).click();
+ await sleep(5000);
console.log('This is the EndOfTest');
});
});
diff --git a/test/selenium/Roles/Supervisor.js b/test/selenium/Roles/Supervisor.js
index 62d0a7fff5..ec2a2039d7 100644
--- a/test/selenium/Roles/Supervisor.js
+++ b/test/selenium/Roles/Supervisor.js
@@ -7,7 +7,6 @@ async function Supervisor(){
const reportPath = path.join(__dirname, './../Report',path.dirname(__filename).replace(path.dirname(__dirname),''),path.basename(__filename,'.js'));
const util = require ('../Utils/Utils.js');
const { addConsoleHandler } = require('selenium-webdriver/lib/logging');
- const { Supervisor } = require('../Dev/Modules.js');
util.makeReportDir(reportPath);
util.makeReportDir(logPath);
require('console-stamp')(console, {
@@ -57,8 +56,12 @@ async function Supervisor(){
await sleep(5000);
console.log(step++,' | click | css=a > .pl-1 | ');
- await sleep(5000);
await driver.findElement(By.css('a > .pl-1')).click();
+ await sleep(5000);
+
+ console.log(step++,' | assertText | css=.sub-label |Supervisor');
+ assert(await driver.findElement(By.css('.sub-label')).getText() == 'SuperVisor');
+ await sleep(5000);
console.log(step++,' | click | linkText=Users | ');
await driver.findElement(By.linkText('Users')).click();
@@ -146,36 +149,40 @@ async function Supervisor(){
assert(elements.length);
}
- console.log(' 28 | click | linkText=Financials | ');
- await driver.findElement(By.linkText('Financials')).click();
- await sleep(5000);
+ console.log(' 28 | click | linkText=Assets | ');
+ await driver.findElement(By.linkText('Assets')).click();
+ await sleep(1000);
- console.log(step++,' | click | css=.app_container-content > .ant-alert | ');
- await driver.findElement(By.css('.app_container-content > .ant-alert')).click();
- await sleep(5000);
+ // console.log(step++,' | click | css=.app_container-content > .ant-alert | ');
+ // await driver.findElement(By.css('.app_container-content > .ant-alert')).click();
+ // await sleep(1000);
- console.log(step++,' | assertText | css=.app_container-content > .ant-alert > .ant-alert-description | Access denied: User is not authorized to access this endpoint');
- assert(await driver.findElement(By.css('.app_container-content > .ant-alert > .ant-alert-description')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ // console.log(step++,' | assertText | css=.app_container-content > .ant-alert > .ant-alert-description | Access denied: User is not authorized to access this endpoint');
+ // assert(await driver.findElement(By.css('.ant-message-custom-content > span:nth-child(2)')).getText() == 'Access denied: User is not authorized to access this endpoint');
- console.log(step++,' | click | css=.ant-card-body > .ant-alert | ');
- await driver.findElement(By.css('.ant-card-body > .ant-alert')).click();
+ // console.log(step++,' | click | css=.ant-card-body > .ant-alert | ');
+ // await driver.findElement(By.css('.ant-card-body > .ant-alert')).click();
- console.log(step++,' | assertText | css=.ant-card-body .ant-alert-description | Access denied: User is not authorized to access this endpoint');
- assert(await driver.findElement(By.css('.ant-card-body .ant-alert-description')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ // console.log(step++,' | assertText | css=.ant-card-body .ant-alert-description | Access denied: User is not authorized to access this endpoint');
+ // assert(await driver.findElement(By.css('.ant-card-body .ant-alert-description')).getText() == 'Access denied: User is not authorized to access this endpoint');
console.log(step++,' | click | id=rc-tabs-4-tab-1 | ');
await driver.findElement(By.id('rc-tabs-4-tab-1')).click();
-
- console.log(step++,' | click | xpath=//*[@id="rc-tabs-4-panel-1"]/div/div[1]/button | ');
- await driver.findElement(By.xpath('//*[@id="rc-tabs-4-panel-1"]/div/div[1]/button')).click();
await sleep(5000);
+
+ console.log(step++,' | assertText | css=.app_container-content > .ant-alert > .ant-alert-description | Access denied: User is not authorized to access this endpoint');
+ assert(await driver.findElement(By.css('.app_container-content > .ant-alert > .ant-alert-description')).getText() == 'Access denied: User is not authorized to access this endpoint');
+
+ // console.log(step++,' | click | xpath=//*[@id="rc-tabs-4-panel-1"]/div/div[1]/button | ');
+ // await driver.findElement(By.xpath('//*[@id="rc-tabs-4-panel-1"]/div/div[1]/button')).click();
+ // await sleep(5000);
- console.log(step++,' | assertText | css=.sub-title | Asset:');
- assert(await driver.findElement(By.css('.sub-title')).getText() == 'Asset:')
+ // console.log(step++,' | assertText | css=.sub-title | Asset:');
+ // assert(await driver.findElement(By.css('.sub-title')).getText() == 'Asset:')
- console.log(step++,' | click | css=.btn-wrapper > .ant-btn:nth-child(1) |');
- await driver.findElement(By.css('.btn-wrapper > .ant-btn:nth-child(1)')).click();
- await sleep(3000);
+ // console.log(step++,' | click | css=.btn-wrapper > .ant-btn:nth-child(1) |');
+ // await driver.findElement(By.css('.btn-wrapper > .ant-btn:nth-child(1)')).click();
+ // await sleep(3000);
console.log(step++,' | click | id=rc-tabs-4-tab-2 | ');
await driver.findElement(By.id('rc-tabs-4-tab-2')).click();
@@ -189,9 +196,60 @@ async function Supervisor(){
assert(await driver.findElement(By.css('.ant-table-row:nth-child(1) .d-flex')).getText() == 'Validated');
await sleep(5000);
+ console.log("should be fixed")
console.log(step++,' | click | id=rc-tabs-4-tab-3 | ');
await driver.findElement(By.id('rc-tabs-4-tab-3')).click();
await sleep(5000);
+
+ // console.log(step++,"2 | click | css=.filter-input-wrapper:nth-child(3) .ant-input | ");
+ // await driver.findElement(By.css(".filter-input-wrapper:nth-child(3) .ant-input")).click()
+ // await sleep(5000);
+
+ // console.log(step++," | type | css=.filter-input-wrapper:nth-child(3) .ant-input | 172");
+ // await driver.findElement(By.css(".filter-input-wrapper:nth-child(3) .ant-input")).sendKeys("172")
+ // await sleep(5000);
+
+ // console.log(step++," | click | css=.ant-btn > span:nth-child(2) | ")
+ // await driver.findElement(By.css(".ant-btn > span:nth-child(2)")).click()
+ // await sleep(5000);
+
+ // console.log(step++," | click | css=.ant-table-cell > .d-flex | ")
+ // await driver.findElement(By.css(".ant-table-cell > .d-flex")).click()
+ // await sleep(5000);
+ // console.log(step++,' | click | css=.ant-table-row:nth-child(1) .d-flex | ');
+ // await driver.findElement(By.css('.ant-table-cell > .d-flex')).click();
+ // await sleep(5000);
+
+ // console.log(step++," | assertText | css=.ant-table-row:nth-child(1) .d-flex | Validated")
+ // assert(await driver.findElement(By.css('.ant-table-row:nth-child(1) .d-flex')).getText() == "Validated")
+ // await sleep(5000);
+
+ console.log(step++,' | click | id=rc-tabs-4-tab-4 | ');
+ await driver.findElement(By.id('rc-tabs-4-tab-4')).click();
+ await sleep(5000);
+
+ console.log(step++,' | click | css=.button:nth-child(1) > span | ');
+ await driver.findElement(By.css('.button:nth-child(1) > span')).click();
+ await sleep(5000);
+
+ console.log(step++,' | click | css=.modal-button:nth-child(2) > span| ');
+ await driver.findElement(By.css('.modal-button:nth-child(2) > span')).click();
+ await sleep(1000);
+
+ console.log(step++,' | assertText | css=.ant-message-custom-content > span:nth-child(2) | Access denied: User is not authorized to access this endpoint');
+ assert(await driver.findElement(By.css('.ant-message-custom-content > span:nth-child(2)')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ util.takeHollashot(driver,reportPath,22);
+ await sleep(5000);
+
+ console.log(step++,' | assertText | css=.ant-message-custom-content > span:nth-child(2) | Access denied: User is not authorized to access this endpoint');
+ assert(await driver.findElement(By.css('.ant-message-custom-content > span:nth-child(2)')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ util.takeHollashot(driver,reportPath,22);
+ await sleep(5000);
+
+
+ console.log(step++,' | click | id=rc-tabs-4-tab-5 | ');
+ await driver.findElement(By.id('rc-tabs-4-tab-5')).click();
+ await sleep(5000);
console.log('This is the EndOfTest');
});
diff --git a/test/selenium/Roles/Support.js b/test/selenium/Roles/Support.js
index b147ecb7b8..ec34bfe004 100644
--- a/test/selenium/Roles/Support.js
+++ b/test/selenium/Roles/Support.js
@@ -16,10 +16,11 @@ async function Support(){
let support = process.env.SUPPORT;
let password = process.env.PASSWORD;
let logInPage = process.env.LOGIN_PAGE;
+ let webSite = process.env.WEBSITE;
let step = util.getStep();
util.logHolla(logPath)
- describe('support', function() {
+ describe('support ', function() {
this.timeout(300000);
let driver;
let vars;
@@ -34,7 +35,7 @@ async function Support(){
vars = {};
});
afterEach(async function() {
- await driver.quit();
+ // await driver.quit();
});
it('support', async function() {
console.log('Support can access some user information for user verification');
@@ -60,92 +61,142 @@ async function Support(){
console.log(step++,' | click | css=a > .pl-1 | ');
await driver.findElement(By.css('a > .pl-1')).click();
-
+ await sleep(2000);
+
console.log(step++,' | click | css=.role-section > div:nth-child(2) | ');
await driver.findElement(By.css('.role-section > div:nth-child(2)')).click();
console.log(step++,' | assertText | css=.sub-label | Support');
assert(await driver.findElement(By.css('.sub-label')).getText() == 'Support');
+
console.log(step++,' | click | linkText=Users | ');
await driver.findElement(By.linkText('Users')).click();
-
+ await sleep(2000);
+
console.log(step++,' | click | name=id | ');
await driver.findElement(By.name('id')).click();
-
+ await sleep(2000);
+
console.log(step++,' | type | name=id | 1');
await driver.findElement(By.name('id')).sendKeys('1');
-
+ await sleep(2000);
+
console.log(step++,' | sendKeys | name=id | ${KEY_ENTER}');
await driver.findElement(By.name('id')).sendKeys(Key.ENTER);
-
+ await sleep(2000);
+
console.log(step++,' | click | css=.ant-btn | ');
await driver.findElement(By.css('.ant-btn')).click();
-
+ await sleep(2000);
+
console.log(step++,' | click | css=div:nth-child(2) > .ant-btn-sm > span | ');
- await sleep(5000);
await driver.findElement(By.css('div:nth-child(2) > .ant-btn-sm > span')).click();
-
- console.log(step++,' | assertNotEditable | name=email | ');
- {
- const element = await driver.findElement(By.name('email'));
- assert(!await element.isEnabled());
- }
+ await sleep(2000);
+
+ await driver.findElement(By.name("phone_number")).click()
+ await sleep(2000);
+
+ console.log(step," | type | name=phone_number | 123456789")
+ await driver.findElement(By.name("phone_number")).sendKeys("123456789")
+ await sleep(2000);
+
+ console.log(step,"| click | css=.w-100 | ")
+ await driver.findElement(By.css(".w-100")).click()
+ await sleep(2000);
+
+ console.log(step," | click | css=div:nth-child(11) | ")
+ await driver.findElement(By.css("div:nth-child(11)")).click()
+ await sleep(2000);
+
+ console.log(step," | assertText | css=div:nth-child(11) > strong | Access denied: User is not authorized to access this endpoint")
+ assert(await driver.findElement(By.css("div:nth-child(11) > strong")).getText() == "Access denied: User is not authorized to access this endpoint")
+ await sleep(2000);
+
+ // console.log(step++,' | assertNotEditable | name=email | ');
+ // {
+ // const element = await driver.findElement(By.name('email'));
+ // assert(!await element.isEnabled());
+ // }
console.log(step++,' | click | closing| ');
- await driver.findElement(By.xpath('/html/body/div[4]/div/div[2]/div/div[2]/button')).click();
- await sleep(5000);
+ // await driver.findElement(By.css('.anticon-close > svgn')).click();
+ await driver.get(webSite+"admin/financials");
+ // https://sandbox.hollaex.com/admin/financials
+ await sleep(5000);
- console.log(step++,' | click | linkText=Financials | ');
- await driver.findElement(By.linkText('Financials')).click();
- await sleep(5000);
+ // console.log(step++,' | click | linkText=Assets | ');
+ // await driver.findElement(By.linkText('Assets')).click();
+ // await sleep(5000);
- console.log(step++,' | click | css=.app_container-content > .ant-alert | ');
- await driver.findElement(By.css('.app_container-content > .ant-alert')).click();
- await sleep(5000);
-
- console.log(step++,' | assertText | css=.app_container-content > .ant-alert > .ant-alert-description | Access denied: User is not authorized to access this endpoint');
- assert(await driver.findElement(By.css('.app_container-content > .ant-alert > .ant-alert-description')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ // console.log(step++,' | click | css=.app_container-content > .ant-alert | ');
+ // await driver.findElement(By.css('.app_container-content > .ant-alert')).click();
+ // await sleep(5000);
+ console.log(step++,' | assertText | css=.ant-empty-description | No Data');
+ assert(await driver.findElement(By.css('.ant-empty-description')).getText() == 'No Data');
+ await sleep(5000);
+
+ console.log(step++,' | click | id=rc-tabs-0-tab-1 | ');
+ await driver.findElement(By.id('rc-tabs-0-tab-1')).click();
+ await sleep(5000);
+
console.log(step++,' | click | css=.ant-card-body > .ant-alert | ');
await driver.findElement(By.css('.ant-card-body > .ant-alert')).click();
+ await sleep(5000);
console.log(step++,' | assertText | css=.ant-card-body .ant-alert-description | Access denied: User is not authorized to access this endpoint');
assert(await driver.findElement(By.css('.ant-card-body .ant-alert-description')).getText() == 'Access denied: User is not authorized to access this endpoint');
- await sleep(5000);
-
- console.log(step++,' | click | id=rc-tabs-2-tab-1 | ');
- await driver.findElement(By.id('rc-tabs-2-tab-1')).click();
- await sleep(5000);
+ util.takeHollashot(driver,reportPath,16);
- console.log(step++,' | click | xpath=//*[@id="rc-tabs-2-panel-1"]/div/div[1]/button | ');
- await driver.findElement(By.xpath('//*[@id="rc-tabs-2-panel-1"]/div/div[1]/button')).click();
- await sleep(5000);
+ // console.log(step++,' | click | xpath=//*[@id="rc-tabs-2-panel-1"]/div/div[1]/button | ');
+ // await driver.findElement(By.xpath('//*[@id="rc-tabs-2-panel-1"]/div/div[1]/button')).click();
+ // await sleep(5000);
- console.log(step++,' | assertText | css=.sub-title | Asset:');
- assert(await driver.findElement(By.css('.sub-title')).getText() == 'Asset:')
+ // console.log(step++,' | assertText | css=.sub-title | Asset:');
+ // assert(await driver.findElement(By.css('.sub-title')).getText() == 'Asset:')
- console.log(step++,' | click | css=.btn-wrapper > .ant-btn:nth-child(1) |');
- await driver.findElement(By.css('.btn-wrapper > .ant-btn:nth-child(1)')).click();
- await sleep(3000);
+ // console.log(step++,' | click | css=.btn-wrapper > .ant-btn:nth-child(1) |');
+ // await driver.findElement(By.css('.btn-wrapper > .ant-btn:nth-child(1)')).click();
+ // await sleep(3000);
- console.log(step++,' | click | id=rc-tabs-2-tab-2 | ');
- await driver.findElement(By.id('rc-tabs-2-tab-2')).click();
-
- console.log(step++,' | click | css=.ant-alert-closable | ');
- // await driver.findElement(By.css(".ant-alert-closable")).click()
-
- console.log(step++,' | assertText | css=.ant-alert-closable > .ant-alert-message | Access denied: User is not authorized to access this endpoint');
+ console.log(step++,' | click | id=rc-tabs-0-tab-2 | ');
+ await driver.findElement(By.id('rc-tabs-0-tab-2')).click();
await sleep(5000);
+
+ console.log(step++,' | assertText | css=.ant-alert-closable > .ant-alert-message | Access denied: User is not authorized to access this endpoint');
assert(await driver.findElement(By.css('.ant-alert-closable > .ant-alert-message')).getText() == 'Access denied: User is not authorized to access this endpoint');
- console.log(step++,' | click | id=rc-tabs-2-tab-3 | ');
- await driver.findElement(By.id('rc-tabs-2-tab-3')).click();
+ console.log(step++,' | click | id=rc-tabs-0-tab-3 | ');
+ await driver.findElement(By.id('rc-tabs-0-tab-3')).click();
await sleep(5000);
-
- console.log(step++,' | assertText | css=.ant-alert-closable > .ant-alert-message | Access denied: User is not authorized to access this endpoint');
- assert(await driver.findElement(By.xpath('//*[@id=\'rc-tabs-2-panel-3\']/div/div/div/div[2]/span[2]')).getText() == 'Access denied: User is not authorized to access this endpoint');
-
+
+ // console.log(step++,' | assertText | css=.ant-alert-closable > .ant-alert-message | Access denied: User is not authorized to access this endpoint');
+ // assert(await driver.findElement(By.css('.ant-alert-closable > .ant-alert-message')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ // await sleep(5000);
+
+ console.log(step++,' | click | id=rc-tabs-0-tab-4 | ');
+ await driver.findElement(By.id('rc-tabs-0-tab-4')).click();
+ await sleep(5000);
+
+ console.log(step++,' | click | css=.button:nth-child(1) > span | ');
+ await driver.findElement(By.css('.button:nth-child(1) > span')).click();
+ await sleep(5000);
+
+ console.log(step++,' | click | css=.modal-button:nth-child(2) > span| ');
+ await driver.findElement(By.css('.modal-button:nth-child(2) > span')).click();
+ await sleep(1000);
+
+ console.log(step++,' | assertText | css=.ant-message-custom-content > span:nth-child(2) | Access denied: User is not authorized to access this endpoint');
+ assert(await driver.findElement(By.css('.ant-message-custom-content > span:nth-child(2)')).getText() == 'Access denied: User is not authorized to access this endpoint');
+ util.takeHollashot(driver,reportPath,step);
+ await sleep(5000);
+
+
+ console.log(step++,' | click | id=rc-tabs-0-tab-5 | ');
+ await driver.findElement(By.id('rc-tabs-0-tab-5')).click();
+ await sleep(5000);
+
console.log('This is the EndOfTest');
});
});
diff --git a/test/selenium/Scenario/newUser.js b/test/selenium/Scenario/newUser.js
index fd1c6c1194..2056d0f391 100644
--- a/test/selenium/Scenario/newUser.js
+++ b/test/selenium/Scenario/newUser.js
@@ -53,7 +53,6 @@ describe('Main Test', function () {
describe('Roles', function () {
it('ŮŽGiven communicator can', async function() {
Communicator.Communicator()
-
})
it('Given KYC can', async function() {
Kyc.Kyc();
diff --git a/test/selenium/Scenario/test.js b/test/selenium/Scenario/test.js
index 5ab1dc93de..501333d1d0 100644
--- a/test/selenium/Scenario/test.js
+++ b/test/selenium/Scenario/test.js
@@ -1,5 +1,11 @@
//This scenario check for a new user to reset the password and check changing passwords
-const { LogIn, LogOut, SignUp, ResetPassword, Security, Utils, ResendVerificationEmail,ReCAPTCHA} = require('./Modules')
+const AccountLevel = require('../Onboarding/AccountLevel');
+const { Kyc } = require('../Roles/Kyc');
+const { Supervisor } = require('../Roles/Supervisor');
+const { CancelOrders } = require('../Trade/CancelOrders');
+const { QuickTrade } = require('../Trade/QuickTrade');
+const { TransactionFlow } = require('../Wallet/TransactionFlow');
+const { LogIn, LogOut, SignUp, ResetPassword, Security, Utils, ResendVerificationEmail,ReCAPTCHA, Referral, Setting, Verification, CancelOrder, Promotion, Communicator, Trade, Support, Wallet} = require('./Modules')
const { Builder, By, Key, until } = require('selenium-webdriver')-
Utils.setStep(1)
describe('Main Test', function () {
@@ -13,10 +19,35 @@ describe('Main Test', function () {
await sleep(5000);
//await driver.quit();
})
- describe('ResetPassword', function () {
- it('and the user change pasword securely', async function() {
- Security.Security();
-
+ describe('test', function () {
+ it('test is..', async function() {
+ // LogIn.LogIn();
+ // LogOut.LogOut();
+ // Promotion()
+ // ReCAPTCHA.ReCAPTCHA()
+ // Referral.Referral()
+ // ResendVerificationEmail.ResendVerificationEmail()
+ // ResetPassword.ResetPassword()
+ // Security.Security()
+ // Setting.Setting()
+ // SignUp.SignUp()
+ // Verification.Verification();failed
+ /*Roles*/
+ // Communicator.Communicator()
+ // Kyc()
+ //Supervisor()
+ // Support.Support()
+ // /*Trade*/
+ // CancelOrder.CancelOrder()
+ // CancelOrders()
+ //QuickTrade()
+ // Trade.Trade()
+ // TradeWithStop.TradeWithStop()
+ // /*Wallet*/
+ // TransactionFlow()
+ Wallet.Wallet();
+
+
})
})
diff --git a/test/selenium/Trade/CancelOrders.js b/test/selenium/Trade/CancelOrders.js
index 444bc87e4f..42619c23b8 100644
--- a/test/selenium/Trade/CancelOrders.js
+++ b/test/selenium/Trade/CancelOrders.js
@@ -60,16 +60,19 @@ async function CancelOrders(){
console.log(step++,' | click | css=.active-menu .edit-wrapper__container | | ');
await driver.findElement(By.css('.active-menu .edit-wrapper__container')).click();
-
+ await sleep(5000);
+
console.log(step++,' | click | css=.tabs-pair-details:nth-child(1) > .market-card__sparkline-wrapper | ');
await driver.findElement(By.css('.tabs-pair-details:nth-child(1) > .market-card__sparkline-wrapper')).click();
-
+ await sleep(5000);
+
console.log(step++,' | click | css=.table_body-row:nth-child(1) .action_notification-text | ');
await driver.findElement(By.css('.table_body-row:nth-child(1) .action_notification-text')).click();
console.log(step++,' | click | css=.table_body-row:nth-child(1) .action_notification-text | ');
await driver.findElement(By.css('.table_body-row:nth-child(1) .action_notification-text')).click();
-
+ await sleep(5000);
+
console.log(step++,' | click | css=.table_body-row:nth-child(1) .action_notification-text | ');
await driver.findElement(By.css('.table_body-row:nth-child(1) .action_notification-text')).click();
diff --git a/test/selenium/Trade/QuickTrade.js b/test/selenium/Trade/QuickTrade.js
index f3206a1120..ceb861ec2e 100644
--- a/test/selenium/Trade/QuickTrade.js
+++ b/test/selenium/Trade/QuickTrade.js
@@ -38,7 +38,7 @@ async function QuickTrade(){
driver = await new Builder().forBrowser('chrome').build();
vars = {};
driver.manage().window().maximize();
- util.kitLogIn(step,driver, userName,passWord);
+ await util.kitLogIn(step,driver, userName,passWord);
});
afterEach(async function() {
@@ -49,36 +49,88 @@ async function QuickTrade(){
console.log(step++,' | open | /summary | ');
- await driver.get(website +'summary')
+ //await driver.get("https://sandbox.hollaex.com/quick-trade/xht-usdt")//website +'summary')
await sleep(5000);
-
- console.log(step++,' | click | css=.app-menu-bar-content:nth-child(3) .edit-wrapper__container | ');
- await driver.findElement(By.css(".app-menu-bar-content:nth-child(3) .edit-wrapper__container")).click()
+ // Test name: g
+ // Step # | name | target | value
+ // 1 | open | /login |
- console.log(step++,' | click | xpath=//div[@id=root]/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/span/div/div/span[2]/div | ');
- await driver.findElement(By.xpath("//div[@id=\'root\']/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/span/div/div/span[2]/div")).click()
- await sleep(5000)
+ // 6 | click | css=.app-menu-bar-content:nth-child(3) .edit-wrapper__container |
+ await driver.findElement(By.css(".app-menu-bar-content:nth-child(3) .edit-wrapper__container")).click()
+ // 7 | click | xpath=//div[@id='root']/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/div[2]/div/div[3]/div/span/div/div |
+ await driver.findElement(By.xpath("//div[@id=\'root\']/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/div[2]/div/div[3]/div/span/div/div")).click()
+ // 8 | click | xpath=(//div[@name='selectedPairBase'])[7] |
+ await driver.findElement(By.xpath("(//div[@name=\'selectedPairBase\'])[7]")).click()
+ // 9 | click | xpath=//div[@id='root']/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/div[2]/div/div[4]/div/span/div/div |
+ await driver.findElement(By.xpath("//div[@id=\'root\']/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/div[2]/div/div[4]/div/span/div/div")).click()
+ // 10 | click | xpath=//span[contains(.,'USDT')] |
+ await driver.findElement(By.xpath("//span[contains(.,\'USDT\')]")).click()
+ // 11 | click | css=.py-2:nth-child(3) .ant-input |
+ await driver.findElement(By.css(".py-2:nth-child(3) .ant-input")).click()
+ // 12 | type | css=.py-2:nth-child(3) .ant-input | 4
+ await driver.findElement(By.css(".py-2:nth-child(3) .ant-input")).sendKeys("4")
+ // 13 | verifyEditable | css=.holla-button |
+ {
+ const element = await driver.findElement(By.css(".holla-button"))
+ assert(await element.isEnabled())
+ }
+ // console.log(step++,' | click | css=.app-menu-bar-content:nth-child(3) .edit-wrapper__container | ');
+ // await driver.findElement(By.css(".app-menu-bar-content:nth-child(3) .edit-wrapper__container")).click()
+ // await sleep(5000);
+
+ // // console.log(step++,' | click | xpath=//div[@id=root]/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/span/div/div/span[2]/div | ');
+ // // await driver.findElement(By.xpath("//div[@id=\'root\']/div/div[2]/div/div/div[3]/div/div/div/div/div[2]/span/div/div/span[2]/div")).click()
+ // // await sleep(5000)
- console.log(step++,' | click | xpath=//div[2]/div/div/div/div/div/div/span | ');
- await driver.findElement(By.xpath("//div[2]/div/div/div/div/div/div/span")).click()
- await sleep(5000)
+ // // console.log(step++,' | click | xpath=//div[2]/div/div/div/div/div/div/span | ');
+ // // await driver.findElement(By.xpath("//div[2]/div/div/div/div/div/div/span")).click()
+ // // await sleep(5000)
- console.log(step++,' | click | css=.py-2:nth-child(2) .ant-input | ');
- await driver.findElement(By.css(".py-2:nth-child(2) .ant-input")).click()
+ // // console.log(step++,' | click | css=.py-2:nth-child(2) .ant-input | ');
+ // // await driver.findElement(By.css(".py-2:nth-child(2) .ant-input")).click()
- console.log(step++,' | type | css=.py-2:nth-child(2) .ant-input | 1');
- await driver.findElement(By.css(".py-2:nth-child(2) .ant-input")).sendKeys("1")
- await sleep(5000)
+ // // console.log(step++,' | type | css=.py-2:nth-child(2) .ant-input | 1');
+ // // await driver.findElement(By.css(".py-2:nth-child(2) .ant-input")).sendKeys("1")
+ // // await sleep(5000)
- console.log(step++,' | click | css=.holla-button | ');
- await driver.findElement(By.css(".holla-button")).click()
+ // // console.log(step++,' | click | css=.holla-button | ');
+ // // await driver.findElement(By.css(".holla-button")).click()
- console.log(step++,' | click | css=.ReactModal__Content | ');
- await driver.findElement(By.css(".ReactModal__Content")).click()
+ // // console.log(step++,' | click | css=.ReactModal__Content | ');
+ // // await driver.findElement(By.css(".ReactModal__Content")).click()
- console.log(step++,' | assertText | css=.review-block-wrapper:nth-child(1) .with_price-block_amount-value | 1');
- assert(await driver.findElement(By.css(".review-block-wrapper:nth-child(1) .with_price-block_amount-value")).getText() == "1")
+ // // console.log(step++,' | assertText | css=.review-block-wrapper:nth-child(1) .with_price-block_amount-value | 1');
+ // // assert(await driver.findElement(By.css(".review-block-wrapper:nth-child(1) .with_price-block_amount-value")).getText() == "1")
+ // await driver.findElement(By.xpath("//div[2]/div/div/div/div[7]/div/div")).click()
+ // await sleep(5000);
+ // // 3 | click | css=.ant-select-item-option-active .d-flex |
+ // await driver.findElement(By.css(".ant-select-item-option-active .d-flex")).click()
+ // await sleep(5000);
+ // // 4 | click | css=.py-2:nth-child(4) .ant-select-arrow svg |
+ // await driver.findElement(By.css(".py-2:nth-child(4) .ant-select-arrow svg")).click()
+ // await sleep(5000);
+ // // 5 | click | css=.ant-select-item-option-active .pl-1 |
+ // await driver.findElement(By.css(".ant-select-item-option-active .pl-1")).click()
+ // await sleep(5000);
+ // // 6 | click | css=.py-2:nth-child(3) .ant-input |
+ await driver.findElement(By.css(".py-2:nth-child(3) .ant-input")).click()
+ await sleep(5000);
+ // 7 | type | css=.py-2:nth-child(3) .ant-input | 1
+ await driver.findElement(By.css(".py-2:nth-child(3) .ant-input")).sendKeys("1")
+ await sleep(5000);
+ // 8 | click | css=.holla-button |
+ await driver.findElement(By.css(".holla-button")).click()
+ await sleep(5000);
+ // 9 | click | css=.ml-2:nth-child(2) |
+ await driver.findElement(By.css(".ml-2:nth-child(2)")).click()
+ await sleep(5000);
+ // 10 | click | css=.holla-button:nth-child(4) |
+ await driver.findElement(By.css(".holla-button:nth-child(4)")).click()
+ await sleep(5000);
+ // 11 | click | css=.ml-2:nth-child(2) |
+ await driver.findElement(By.css(".ml-2:nth-child(2)")).click()
+ await sleep(5000);
console.log(step++,' | click | css=.ml-2 | ');
await driver.findElement(By.css(".ml-2")).click()
hollaTime.Hollatimestampe();
diff --git a/test/selenium/Wallet/TransactionFlow.js b/test/selenium/Wallet/TransactionFlow.js
index a34a29bf3d..39a3a535fd 100644
--- a/test/selenium/Wallet/TransactionFlow.js
+++ b/test/selenium/Wallet/TransactionFlow.js
@@ -22,11 +22,12 @@ async function TransactionFlow(){
let alice = process.env.ALICE;
let logInPage = process.env.LOGIN_PAGE;
let admin = process.env.Email_ADMIN_USERNAME;
+ let emailPass = process.env.EMAIL_PASS;
let step = util.getStep()
describe('Internal D/W Flow', function() {
this.timeout(300000);
let driver;
- let vars;
+ vars = {};
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
@@ -43,7 +44,7 @@ async function TransactionFlow(){
}
beforeEach(async function() {
driver = await new Builder().forBrowser('chrome').build();
- vars = {};
+
driver.manage().window().maximize();
let step = util.getStep()
});
@@ -97,12 +98,15 @@ async function TransactionFlow(){
console.log(step++,' | storeText | css=.with_price-block_amount-value | before');
vars['before'] = await driver.findElement(By.css('.with_price-block_amount-value')).getText()
+ console.log("before:")
console.log(vars['before'])
console.log('This is the EndOfTest');
});
it('From Alice to Bob', async function() {
-
+ console.log("before:")
+ console.log(vars['before'])
+
console.log(' Test name: BobLogIn');
console.log(' Step # | action | target | value');
console.log(step++,' | open | '+ logInPage + '| ');
@@ -185,10 +189,10 @@ async function TransactionFlow(){
});
it('CheckMail', async function() {
-
+
console.log('Test name: Confirmation');
console.log('Step # | name | target | value');
- await util.emailLogIn(step,driver,admin,passWord);
+ await util.emailLogIn(step,driver,admin,emailPass);
console.log(step++,' | Click | css=.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1) | ');
await driver.wait(until.elementIsEnabled(await driver.findElement(By.css('.x-grid3-row:nth-child(1) .subject:nth-child(1) > .grid_compact:nth-child(1)'))), 50000);
@@ -241,6 +245,7 @@ async function TransactionFlow(){
});
it('BobLoginSecondTime', async function() {
+
console.log(' Test name: BobLogIn');
console.log(' Step # | action | target | value');
@@ -278,16 +283,18 @@ async function TransactionFlow(){
console.log(step++,' | type | name=search-assets | hollaex');
await driver.findElement(By.name('search-assets')).sendKeys('hollaex');
-
+ await sleep(2000);
+
console.log(step++,' | sendKeys | name=search-assets | ${KEY_ENTER}');
await driver.findElement(By.name('search-assets')).sendKeys(Key.ENTER);
console.log(step++,' | click | css=.td-amount > .d-flex | ');
await driver.findElement(By.linkText('HollaEx')).click()
-
- console.log(step++,' | storeText | css=.with_price-block_amount-value | before');
+
+ console.log(step++,' | storeText | css=.with_price-block_amount-value | before');
vars['after'] = await driver.findElement(By.css('.with_price-block_amount-value')).getText()
- expect(parseFloat(vars['after'])- parseFloat(vars['before'])).to.equal(0.0001);
+ let diff = (parseFloat(vars['after']).toFixed(4)- (parseFloat(vars['before'])).toFixed(4))
+ expect(diff).to.equal(0.0001);
console.log('This is the EndOfTest');
});
diff --git a/test/selenium/Wallet/wallet.js b/test/selenium/Wallet/wallet.js
index ce11fd13f4..7a8e9b472a 100644
--- a/test/selenium/Wallet/wallet.js
+++ b/test/selenium/Wallet/wallet.js
@@ -39,7 +39,7 @@ async function Wallet(){
vars = {};
driver.manage().window().maximize();
let step = util.getStep()
- util.kitLogIn(step,driver,userName,passWord)
+ await util.kitLogIn(step,driver,userName.toLowerCase(),passWord)
});
afterEach(async function() {
util.setStep(step);
@@ -53,19 +53,23 @@ async function Wallet(){
console.log(step++,' | click | name=search-assets | ');
await driver.findElement(By.name('search-assets')).click();
-
+ await sleep(3000);
+
console.log(step++,' | type | name=search-assets | USDT');
await driver.findElement(By.name('search-assets')).sendKeys('USDT');
-
+ await sleep(3000);
+
console.log(step++,' | sendKeys | name=search-assets | ${KEY_ENTER}');
await driver.findElement(By.name('search-assets')).sendKeys(Key.ENTER);
console.log(step++,' | click | css=.action-button-wrapper:nth-child(1) > .action_notification-text | ');
await driver.findElement(By.css('.action-button-wrapper:nth-child(1) > .action_notification-text')).click();
-
+ await sleep(3000);
+
console.log(step++,' | click | css=.dropdown-placeholder | ');
await driver.findElement(By.css('.dropdown-placeholder')).click();
-
+ await sleep(3000);
+
console.log(step++,' | click | id=network-eth-0 |');
await driver.findElement(By.id('network-eth-0')).click();
diff --git a/version b/version
index 6b4d157738..047615559c 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-2.2.3
\ No newline at end of file
+2.2.4
\ No newline at end of file
diff --git a/web/docker/Dockerfile b/web/docker/Dockerfile
index 6102e650de..25549bcb09 100644
--- a/web/docker/Dockerfile
+++ b/web/docker/Dockerfile
@@ -1,5 +1,5 @@
# build environment
-FROM node:12.18.3-buster as build
+FROM node:12.22.7-buster as build
ENV NODE_OPTIONS=--max_old_space_size=3072
WORKDIR /app
COPY package.json /app/package.json
diff --git a/web/package-lock.json b/web/package-lock.json
index 20d09495be..2929f3d615 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "hollaex-kit",
- "version": "2.2.3",
+ "version": "2.2.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/web/package.json b/web/package.json
index 262e0f92c8..9bc4360660 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "hollaex-kit",
- "version": "2.2.3",
+ "version": "2.2.4",
"private": true,
"dependencies": {
"@ant-design/compatible": "1.0.5",
diff --git a/web/src/components/Form/FormFields/FileField.js b/web/src/components/Form/FormFields/FileField.js
index dcb64c1d93..e4ef3631b6 100644
--- a/web/src/components/Form/FormFields/FileField.js
+++ b/web/src/components/Form/FormFields/FileField.js
@@ -35,8 +35,10 @@ class FileField extends Component {
}
} else {
const file = ev.target.files[0];
- this.setState({ filename: file.name });
- this.props.input.onChange(file);
+ if (file) {
+ this.setState({ filename: file.name });
+ this.props.input.onChange(file);
+ }
}
};
diff --git a/web/src/containers/Admin/AppWrapper/index.css b/web/src/containers/Admin/AppWrapper/index.css
index 7afb1a07e8..f56f865575 100644
--- a/web/src/containers/Admin/AppWrapper/index.css
+++ b/web/src/containers/Admin/AppWrapper/index.css
@@ -232,9 +232,10 @@
font-weight: bold;
}
.sub-label {
- position: absolute;
- top: 4rem;
- left: 11.2rem;
+ position: absolute;
+ top: 6rem;
+ left: 11.2rem;
+ line-height: 20px;
}
.sider-icons {
width: 85px;
diff --git a/web/src/containers/Admin/AppWrapper/index.js b/web/src/containers/Admin/AppWrapper/index.js
index 94632a0f1d..6569951fbf 100644
--- a/web/src/containers/Admin/AppWrapper/index.js
+++ b/web/src/containers/Admin/AppWrapper/index.js
@@ -453,7 +453,7 @@ class AppWrapper extends React.Component {