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 {

Role:
-
SuperVisor
+
Supervisor
); diff --git a/web/src/containers/Admin/Plugins/MyPlugins.js b/web/src/containers/Admin/Plugins/MyPlugins.js index 91e42bb26c..e1e6f53109 100644 --- a/web/src/containers/Admin/Plugins/MyPlugins.js +++ b/web/src/containers/Admin/Plugins/MyPlugins.js @@ -6,7 +6,7 @@ import { DownloadOutlined } from '@ant-design/icons'; import axios from 'axios'; import { STATIC_ICONS } from 'config/icons'; -import { addPlugin } from './action'; +import { addPlugin, updatePlugins } from './action'; class MyPlugins extends Component { constructor(props) { @@ -73,28 +73,55 @@ class MyPlugins extends Component { } }; + handleNavigation = (res) => { + message.success('Added third party plugin successfully'); + this.props.handleOpenPlugin(res, 'add_plugin'); + } + handleAddPlugin = async () => { - const { restart } = this.props; + const { restart, myPlugins } = this.props; const body = { ...this.state.thirdParty, enabled: true, }; - addPlugin(body) - .then((res) => { - if (res) { + const selectedPlugin = myPlugins.filter(data => data.name === body.name && data.author === body.author && data.version !== body.version); + const existPlugin = myPlugins.filter(data => data.name === body.name && data.author === body.author); + if (existPlugin.length && !selectedPlugin.length) { + message.warning('Plugin is already exist'); + } else if (selectedPlugin.length) { + updatePlugins({ name: body.name }, body) + .then((res) => { this.onCancel(); - this.props.handlePluginList(res); - restart(() => - message.success('Added third party plugin successfully') - ); - } - }) - .catch((err) => { - this.onCancel(); - const _error = - err.data && err.data.message ? err.data.message : err.message; - message.error(_error); - }); + if (res) { + restart(() => + message.success('Third party plugin updated successfully') + ); + }; + }) + .catch((err) => { + this.onCancel(); + const _error = + err.data && err.data.message ? err.data.message : err.message; + message.error(_error); + }); + } else { + addPlugin(body) + .then((res) => { + if (res) { + this.onCancel(); + this.props.handlePluginList(res); + restart(() => + this.handleNavigation(res) + ); + } + }) + .catch((err) => { + this.onCancel(); + const _error = + err.data && err.data.message ? err.data.message : err.message; + message.error(_error); + }); + } }; handleURL = (e) => { diff --git a/web/src/containers/Admin/Plugins/index.js b/web/src/containers/Admin/Plugins/index.js index f6052758a1..e0711e0b7e 100644 --- a/web/src/containers/Admin/Plugins/index.js +++ b/web/src/containers/Admin/Plugins/index.js @@ -164,8 +164,8 @@ class Plugins extends Component { } }; - handleOpenPlugin = (plugin) => { - const { pluginData, myPlugins } = this.state; + handleOpenPlugin = (plugin, plugin_type = '') => { + const { pluginData, myPlugins, isConfigure } = this.state; if (plugin.version === 0) { this.setState({ isVisible: true, @@ -180,6 +180,12 @@ class Plugins extends Component { showSelected: true, selectedPlugin: plugin, }); + if (plugin_type === 'add_plugin' && !isConfigure) { + this.setState({ + type: 'configure', + isConfigure: true + }); + } } else { this.setState({ isVisible: true, diff --git a/web/src/containers/Admin/Verification/DataDisplay.js b/web/src/containers/Admin/Verification/DataDisplay.js index ef631419a3..6a3e4d6dda 100644 --- a/web/src/containers/Admin/Verification/DataDisplay.js +++ b/web/src/containers/Admin/Verification/DataDisplay.js @@ -2,7 +2,8 @@ import React, { Fragment } from 'react'; import { isDate } from 'moment'; import classnames from 'classnames'; import _map from 'lodash/map'; -import { formatTimestampGregorian, DATETIME_FORMAT } from '../../../utils/date'; +import moment from 'moment'; +import { DATETIME_FORMAT } from '../../../utils/date'; import { ZoomInOutlined } from '@ant-design/icons'; export const KEYS_TO_HIDE = [ // 'email', @@ -50,7 +51,7 @@ export const renderRowInformation = ([key, value]) => export const renderJSONKey = (key, value) => { let valueText = ''; if (key === 'dob' && isDate(new Date(value))) { - valueText = `${formatTimestampGregorian(value, DATETIME_FORMAT)}`; + valueText = `${moment.parseZone(value).format(DATETIME_FORMAT)}`; } else if (key === 'wallet') { valueText = _map(value, (wallet, index) => { return ( diff --git a/web/src/containers/App/_App.scss b/web/src/containers/App/_App.scss index ddf298dffa..41f6573db2 100644 --- a/web/src/containers/App/_App.scss +++ b/web/src/containers/App/_App.scss @@ -81,6 +81,10 @@ $inner_container-border: 1px solid $colors-super-pale-black; // width: inherit; } + .ant-tabs { + color: $colors-black; + } + .ant-tabs-ink-bar { background: $colors-main-black; } @@ -91,11 +95,17 @@ $inner_container-border: 1px solid $colors-super-pale-black; .ant-tabs-tab { color: $colors-black; + + .ant-tabs-tab-btn { + color: $colors-black; + } } .ant-tabs-tab-active { color: $colors-main-black; + .ant-tabs-tab-btn { + color: $colors-main-black; font-weight: bold; } } diff --git a/web/src/containers/Home/index.js b/web/src/containers/Home/index.js index 78d16a25ce..66e8ae9db3 100644 --- a/web/src/containers/Home/index.js +++ b/web/src/containers/Home/index.js @@ -245,7 +245,7 @@ class Home extends Component { }; onSelectTarget = (selectedTarget) => { - const { tickers } = this.props; + const { tickers, pairs } = this.props; const { selectedSource } = this.state; const pairName = `${selectedTarget}-${selectedSource}`; @@ -254,12 +254,12 @@ class Home extends Component { let tickerClose; let side; let pair; - if (tickers[pairName]) { + if (pairs[pairName]) { const { close } = tickers[pairName]; tickerClose = close; side = 'buy'; pair = pairName; - } else if (tickers[reversePairName]) { + } else if (pairs[reversePairName]) { const { close } = tickers[reversePairName]; tickerClose = 1 / close; side = 'sell'; @@ -277,7 +277,7 @@ class Home extends Component { }; onSelectSource = (selectedSource) => { - const { tickers } = this.props; + const { tickers, pairs } = this.props; const targetOptions = this.getTargetOptions(selectedSource); const selectedTarget = targetOptions[0]; @@ -287,12 +287,12 @@ class Home extends Component { let tickerClose; let side; let pair; - if (tickers[pairName]) { + if (pairs[pairName]) { const { close } = tickers[pairName]; tickerClose = close; side = 'buy'; pair = pairName; - } else if (tickers[reversePairName]) { + } else if (pairs[reversePairName]) { const { close } = tickers[reversePairName]; tickerClose = 1 / close; side = 'sell'; diff --git a/web/src/containers/QuickTrade/index.js b/web/src/containers/QuickTrade/index.js index ec19a18f8f..ebd58e8519 100644 --- a/web/src/containers/QuickTrade/index.js +++ b/web/src/containers/QuickTrade/index.js @@ -28,19 +28,44 @@ import { BASE_CURRENCY, DEFAULT_COIN_DATA } from 'config/constants'; class QuickTradeContainer extends PureComponent { constructor(props) { super(props); - const { - routeParams: { pair }, - sourceOptions, - tickers, - } = this.props; - const [, selectedSource = sourceOptions[0]] = pair.split('-'); + const { routeParams, sourceOptions, tickers, pairs, router } = this.props; + + const pairKeys = Object.keys(pairs); + const flippedPair = this.flipPair(routeParams.pair); + + let pair; + let side; + let tickerClose; + let originalPair; + if (pairKeys.includes(routeParams.pair)) { + originalPair = routeParams.pair; + pair = routeParams.pair; + const { close } = tickers[pair] || {}; + side = 'buy'; + tickerClose = close; + } else if (pairKeys.includes(flippedPair)) { + originalPair = routeParams.pair; + pair = flippedPair; + const { close } = tickers[pair] || {}; + side = 'sell'; + tickerClose = 1 / close; + } else if (pairKeys.length) { + originalPair = pairKeys[0]; + pair = pairKeys[0]; + const { close } = tickers[pair] || {}; + side = 'buy'; + tickerClose = close; + } else { + router.push('/summary'); + } + + const [, selectedSource = sourceOptions[0]] = originalPair.split('-'); const targetOptions = this.getTargetOptions(selectedSource); - const [selectedTarget = targetOptions[0]] = pair.split('-'); - const { close: tickerClose } = tickers[pair] || {}; + const [selectedTarget = targetOptions[0]] = originalPair.split('-'); this.state = { pair, - side: 'buy', + side, tickerClose, showQuickTradeModal: false, targetOptions, @@ -60,6 +85,8 @@ class QuickTradeContainer extends PureComponent { pageSize: 12, searchValue: '', }; + + this.goToPair(pair); } getSearchPairs = (value) => { @@ -114,7 +141,7 @@ class QuickTradeContainer extends PureComponent { this.props.router.push('/account'); } if (this.props.sourceOptions && this.props.sourceOptions.length) { - this.constructTraget(); + this.constructTarget(); } this.handleMarket(pairs, tickers, this.state.searchValue); } @@ -131,7 +158,7 @@ class QuickTradeContainer extends PureComponent { JSON.stringify(prevProps.sourceOptions) !== JSON.stringify(this.props.sourceOptions) ) { - this.constructTraget(); + this.constructTarget(); } } @@ -217,8 +244,13 @@ class QuickTradeContainer extends PureComponent { this.props.router.push(`/trade/${this.state.pair}`); }; + flipPair = (pair) => { + const pairArray = pair.split('-'); + return pairArray.reverse().join('-'); + }; + onSelectTarget = (selectedTarget) => { - const { tickers } = this.props; + const { tickers, pairs } = this.props; const { selectedSource } = this.state; const pairName = `${selectedTarget}-${selectedSource}`; @@ -227,12 +259,12 @@ class QuickTradeContainer extends PureComponent { let tickerClose; let side; let pair; - if (tickers[pairName]) { + if (pairs[pairName]) { const { close } = tickers[pairName]; tickerClose = close; side = 'buy'; pair = pairName; - } else if (tickers[reversePairName]) { + } else if (pairs[reversePairName]) { const { close } = tickers[reversePairName]; tickerClose = 1 / close; side = 'sell'; @@ -250,7 +282,7 @@ class QuickTradeContainer extends PureComponent { }; onSelectSource = (selectedSource) => { - const { tickers } = this.props; + const { tickers, pairs } = this.props; const targetOptions = this.getTargetOptions(selectedSource); const selectedTarget = targetOptions[0]; @@ -260,12 +292,12 @@ class QuickTradeContainer extends PureComponent { let tickerClose; let side; let pair; - if (tickers[pairName]) { + if (pairs[pairName]) { const { close } = tickers[pairName]; tickerClose = close; side = 'buy'; pair = pairName; - } else if (tickers[reversePairName]) { + } else if (pairs[reversePairName]) { const { close } = tickers[reversePairName]; tickerClose = 1 / close; side = 'sell'; @@ -285,18 +317,49 @@ class QuickTradeContainer extends PureComponent { this.goToPair(pair); }; - constructTraget = () => { - const { - sourceOptions, - routeParams: { pair = '' }, - } = this.props; - const [, selectedSource = sourceOptions[0]] = pair.split('-'); + constructTarget = () => { + const { sourceOptions, routeParams, pairs, router, tickers } = this.props; + + const pairKeys = Object.keys(pairs); + const flippedPair = this.flipPair(routeParams.pair); + + let pair; + let side; + let tickerClose; + let originalPair; + if (pairKeys.includes(routeParams.pair)) { + originalPair = routeParams.pair; + pair = routeParams.pair; + const { close } = tickers[pair] || {}; + side = 'buy'; + tickerClose = close; + } else if (pairKeys.includes(flippedPair)) { + originalPair = routeParams.pair; + pair = flippedPair; + const { close } = tickers[pair] || {}; + side = 'sell'; + tickerClose = 1 / close; + } else if (pairKeys.length) { + originalPair = pairKeys[0]; + pair = pairKeys[0]; + const { close } = tickers[pair] || {}; + side = 'buy'; + tickerClose = close; + } else { + router.push('/summary'); + } + + const [, selectedSource = sourceOptions[0]] = originalPair.split('-'); const targetOptions = this.getTargetOptions(selectedSource); - const [selectedTarget = targetOptions[0]] = pair.split('-'); + const [selectedTarget = targetOptions[0]] = originalPair.split('-'); this.setState({ selectedTarget, targetOptions, + side, + tickerClose, }); + + this.goToPair(pair); }; getTargetOptions = (sourceKey) => { diff --git a/web/src/containers/Trade/components/ActiveOrders.js b/web/src/containers/Trade/components/ActiveOrders.js index 4e3cf89d3d..752db19db6 100644 --- a/web/src/containers/Trade/components/ActiveOrders.js +++ b/web/src/containers/Trade/components/ActiveOrders.js @@ -53,7 +53,7 @@ const generateHeaders = (pairs = {}, onCancel, onCancelAll, ICONS) => [ // ); // }, // }, - { + !isMobile && { label: STRINGS['TIME'], key: 'created_At', renderCell: ({ created_at = '' }, key, index) => { @@ -66,9 +66,7 @@ const generateHeaders = (pairs = {}, onCancel, onCancelAll, ICONS) => [ renderCell: ({ price = 0, symbol }, key, index) => { let pairData = pairs[symbol] || {}; return ( - - {formatToCurrency(price, pairData.increment_price)} - + {formatToCurrency(price, pairData.increment_price)} ); }, }, @@ -79,9 +77,7 @@ const generateHeaders = (pairs = {}, onCancel, onCancelAll, ICONS) => [ renderCell: ({ size = 0, symbol }, key, index) => { let pairData = pairs[symbol] || {}; return ( - - {formatToCurrency(size, pairData.increment_size)} - + {formatToCurrency(size, pairData.increment_size)} ); }, }, @@ -92,10 +88,7 @@ const generateHeaders = (pairs = {}, onCancel, onCancelAll, ICONS) => [ let pairData = pairs[symbol] || {}; return ( - {formatToCurrency( - subtract(size, filled), - pairData.increment_size - )} + {formatToCurrency(subtract(size, filled), pairData.increment_size)} ); }, @@ -122,12 +115,12 @@ const generateHeaders = (pairs = {}, onCancel, onCancelAll, ICONS) => [ ); }, }, - { + !isMobile && { label: STRINGS['TRIGGER_CONDITIONS'], key: 'type', exportToCsv: ({ stop, symbol }) => { let pairData = pairs[symbol] || {}; - return stop && formatToCurrency(stop, pairData.increment_price) + return stop && formatToCurrency(stop, pairData.increment_price); }, renderCell: ({ stop, symbol }, key, index) => { let pairData = pairs[symbol] || {}; diff --git a/web/src/containers/Verification/utils.js b/web/src/containers/Verification/utils.js index 45615e561c..e4bbe944be 100644 --- a/web/src/containers/Verification/utils.js +++ b/web/src/containers/Verification/utils.js @@ -1,11 +1,11 @@ import PhoneNumber from 'awesome-phonenumber'; import _get from 'lodash/get'; -import { initialCountry, COUNTRIES } from '../../utils/countries'; +import { initialCountry, COUNTRIES } from 'utils/countries'; export const mobileInitialValues = ({ country }, defaults) => { let countryVal = country ? country : _get(defaults, 'country'); - return { phone_country: getCountry(countryVal).phoneCode }; + return { phone_country: getCountry(countryVal).phoneCodes[0] || '' }; }; export const identityInitialValues = ( @@ -69,7 +69,16 @@ export const getCountryFromNumber = (phone = '') => { const phoneCode = `+${PhoneNumber.getCountryCodeForRegionCode( number.getRegionCode() )}`; - const filterValue = COUNTRIES.filter((data) => data.phoneCode === phoneCode); - if (filterValue.length) return filterValue[0]; - return initialCountry; + + let filterValue = initialCountry; + + COUNTRIES.forEach((country) => { + country.phoneCodes.forEach((code) => { + if (code === phoneCode) { + filterValue = country; + } + }); + }); + + return filterValue; }; diff --git a/web/src/index.css b/web/src/index.css index 4b11775913..e5b676768f 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -358,8 +358,8 @@ table th { height: auto; flex: 1; } .app_container.layout-mobile .app_container-content { - min-height: calc( 100vh - 10rem); - max-height: calc( 100vh - 10rem); + min-height: calc( 100vh - 10rem); + max-height: calc( 100vh - 10rem); max-width: 100vw; overflow-y: scroll; } .app_container.layout-mobile .app_container-content.no_bottom_navigation { @@ -372,8 +372,8 @@ table th { .app_container.layout-mobile .app_container-content .app_container-main.no_bottom_navigation { height: 100%; } .app_container.layout-mobile .content-with-bar { - min-height: calc( 100vh - 17rem); - max-height: calc( 100vh - 17rem); + min-height: calc( 100vh - 17rem); + max-height: calc( 100vh - 17rem); max-width: 100vw; padding: 1rem; margin: 0; @@ -385,15 +385,20 @@ table th { .app_container .app_container-content .app_container-main .presentation_container { width: 75rem; height: inherit; } + .app_container .app_container-content .app_container-main .ant-tabs { + color: var(--labels_secondary-inactive-label-text-graphics); } .app_container .app_container-content .app_container-main .ant-tabs-ink-bar { background: var(--labels_important-active-labels-text-graphics); } .app_container .app_container-content .app_container-main .ant-tabs-top > .ant-tabs-nav::before { border-color: var(--labels_secondary-inactive-label-text-graphics); } .app_container .app_container-content .app_container-main .ant-tabs-tab { color: var(--labels_secondary-inactive-label-text-graphics); } + .app_container .app_container-content .app_container-main .ant-tabs-tab .ant-tabs-tab-btn { + color: var(--labels_secondary-inactive-label-text-graphics); } .app_container .app_container-content .app_container-main .ant-tabs-tab-active { color: var(--labels_important-active-labels-text-graphics); } .app_container .app_container-content .app_container-main .ant-tabs-tab-active .ant-tabs-tab-btn { + color: var(--labels_important-active-labels-text-graphics); font-weight: bold; } .app_container .app_container-sidebar { display: flex; @@ -2736,8 +2741,8 @@ table th { .layout-mobile .quote-container { background-color: transparent; overflow-y: scroll; - min-height: calc( 100vh - 17rem); - max-height: calc( 100vh - 17rem); } + min-height: calc( 100vh - 17rem); + max-height: calc( 100vh - 17rem); } .layout-mobile .quote-container .quick_trade-wrapper { flex: 1 1; min-width: 95vw !important; @@ -2888,7 +2893,7 @@ table th { .layout-mobile .presentation_container.verification_container { padding-top: 0 !important; - max-height: calc( 100vh - 10rem); } + max-height: calc( 100vh - 10rem); } .layout-mobile .presentation_container.verification_container .header-content { font-size: 13px !important; } @@ -7357,14 +7362,14 @@ table th { right: 0 !important; min-width: 100vw; max-width: 100vw; - min-height: calc( 100vh - 4rem); - max-height: calc( 100vh - 4rem); + min-height: calc( 100vh - 4rem); + max-height: calc( 100vh - 4rem); border-radius: 0 !important; padding: 0 !important; } .layout-mobile .ReactModal__Content .dialog-mobile-content { padding: 2.5rem !important; - min-height: calc( 100vh - 8rem); - max-height: calc( 100vh - 8rem); + min-height: calc( 100vh - 8rem); + max-height: calc( 100vh - 8rem); display: flex; } .layout-mobile.LogoutModal .ReactModal__Content { diff --git a/web/src/index.js b/web/src/index.js index 3da960bfd4..11755755af 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -114,7 +114,7 @@ const drawFavIcon = (url) => { }; const getLocalBundle = async (pluginName) => { - const url = `/${pluginName}.json`; + const url = `http://localhost:8080/${pluginName}.json`; try { const response = await fetch(url); return await response.json(); diff --git a/web/src/utils/countries.js b/web/src/utils/countries.js index 89f3998bd9..208011f5e3 100644 --- a/web/src/utils/countries.js +++ b/web/src/utils/countries.js @@ -10,8 +10,8 @@ const convertCountry = (value = {}) => { value: value.alpha2, name: value.name, label: value.name, - phoneCode: - value.countryCallingCodes.length > 0 ? value.countryCallingCodes[0] : '', + phoneCodes: value.countryCallingCodes, + phoneCode: '', // Temporarily kept for compatibility purposes flag: ( ({ icon: country.flag, })); -export const PHONE_OPTIONS = COUNTRIES.map((country) => ({ - label: STRINGS.formatString( - STRINGS[ - 'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.PHONE_CODE_DISPLAY' - ], - country.phoneCode, - country.name - ).join(''), - value: country.phoneCode, - icon: country.flag, -})); +const getPhoneOptions = () => { + const phoneOptions = []; + + COUNTRIES.forEach((country) => { + country.phoneCodes.forEach((phoneCode) => { + phoneOptions.push({ + label: STRINGS.formatString( + STRINGS[ + 'USER_VERIFICATION.USER_DOCUMENTATION_FORM.FORM_FIELDS.PHONE_CODE_DISPLAY' + ], + phoneCode, + country.name + ).join(''), + value: phoneCode, + icon: country.flag, + }); + }); + }); + + return phoneOptions; +}; + +export const PHONE_OPTIONS = getPhoneOptions();