diff --git a/.gitignore b/.gitignore index 1d18f8a5..3676d790 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /api/dist +/api/cache .DS_Store # Logs diff --git a/README.md b/README.md index 2599d645..eb67550f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ - - -# BitMEX trading tool - -This project is a trading tool based on **BitMEX API** *(Binance coming next)*. This project has a long way to go before becoming an actual usable tool. If you notice any bugs, open an issue. +

+ +

## Table of Contents @@ -17,6 +15,10 @@ This project is a trading tool based on **BitMEX API** *(Binance coming next)*. - [License](#license) - [Useful material](#useful-material) +# BitMEX trading tool + +This project is a trading tool based on **BitMEX API**. This project has a long way to go before becoming an actual usable tool. If you notice any bugs, open an issue. + ### Current Features - **Scaled orders:** @@ -75,16 +77,6 @@ This project is a trading tool based on **BitMEX API** *(Binance coming next)*. - if you set a sell cross order price above current price, it will trigger a market sell order when the current price crosses up and then down of the set cross price; - if you set a buy cross order price below current price, it will trigger a market buy order when the current price crosses down and then up of the set cross price. -- **Open Orders:** - - - See currently open orders; - - Add profit targets for open orders (uses limit stop-loss orders to achieve that); - - Cancel any open/profit order(s); - -

- -

- ### Built With The Backend was built using **Node + Express** and the Frontend, **React + Redux**. Styled components were taken from **Chakra UI** @@ -172,15 +164,13 @@ npm run prod These are the available distributions to choose from: -

- -

+

+ +

Probability density function is used to calculate distributions: -

- -

+![formula](https://i.stack.imgur.com/bBIbn.png)
diff --git a/api/package.json b/api/package.json index c37528d2..55dcb077 100644 --- a/api/package.json +++ b/api/package.json @@ -4,12 +4,12 @@ "description": "server side of the application(express)", "author": "Juozas Rimantas ", "license": "MIT", - "main": "src/app", + "main": "src/server", "scripts": { "tsc": "tsc", - "start": "cross-env NODE_ENV=production tsc && node ./dist/app.js", + "start": "cross-env NODE_ENV=production tsc && node ./dist/server.js", "client": "(cd ../client && npm run start)", - "server": "nodemon src/app.ts", + "server": "nodemon src/server.ts", "clean": "rimraf ./dist ../client/build ", "eslint:ts": "eslint . -c .eslintrc --ignore-path .eslintignore --ext .ts --max-warnings 20 -f stylish", "dev": "NODE_ENV=development && concurrently \"npm run server\" \"npm run client\"", @@ -23,6 +23,7 @@ "crypto": "^1.0.1", "dotenv": "8.0.0", "express": "4.17.1", + "flat-cache": "^3.0.4", "helmet": "3.21.1", "morgan": "^1.10.0", "request": "2.88.0", @@ -33,6 +34,7 @@ "@types/cors": "2.8.6", "@types/dotenv": "6.1.1", "@types/express": "4.17.1", + "@types/flat-cache": "^2.0.0", "@types/helmet": "0.0.44", "@types/morgan": "^1.9.0", "@types/node": "12.11.1", diff --git a/api/src/app.ts b/api/src/app.ts index f84ff1a2..aa430019 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,109 +1,69 @@ import path from 'path'; -import dotenv from 'dotenv'; -dotenv.config({path: path.join(__dirname, '../../client/.env')}); - import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; import morgan from 'morgan'; +import flatCache from 'flat-cache'; import {logger} from './util/logger'; -import Router from './routes/bitmex'; - -const app: express.Application = express(); - -const port = process.env.PORT || 3001; - -app.set('port', port); -app.use(helmet()); -app.use(cors()); -app.disable('etag').disable('x-powered-by'); +import {SettingsRouter} from './routes/settingsRouter'; +import {BitmexRouter} from './routes/bitmexRouter'; + +export const cache = flatCache.create('apiKeyCache', path.resolve('./cache')); + +export function expressApp() { + const app: express.Application = express(); + + const port = process.env.PORT || 3003; + + app + .set('port', port) + .use(helmet()) + .use(cors()) + .disable('etag') + .disable('x-powered-by') + .use(express.urlencoded({extended: true})) + .use(express.json()) + .use('/bitmex', BitmexRouter()) + .use('/settings', SettingsRouter()); + + if (process.env.NODE_ENV != 'development') { + console.log(`Server is running at http://localhost:${app.get('port')} in ${app.get('env')} mode`); + console.log('Press CTRL-C to stop\n'); + // Serve any static files + app.use(express.static(path.join(__dirname, '../../client/build'))); + // Handle React routing, return all requests to React app + app.get('/*', function (req: express.Request, res: express.Response) { + res.sendFile(path.join(__dirname, '../../', 'client/build/index.html')); + }); + } -app.use(express.urlencoded({extended: true})); -app.use(express.json()); -app.use('/bitmex', Router); + const morganFormat = process.env.NODE_ENV !== 'production' ? 'dev' : 'combined'; + app.use(morgan(morganFormat, {skip: (req, res) => res.statusCode < 400, stream: process.stderr})); + app.use(morgan(morganFormat, {skip: (req, res) => res.statusCode >= 400, stream: process.stdout})); -if (process.env.NODE_ENV != 'development') { - console.log(`Server is running at http://localhost:${app.get('port')} in ${app.get('env')} mode`); - console.log('Press CTRL-C to stop\n'); - // Serve any static files - app.use(express.static(path.join(__dirname, '../../client/build'))); - // Handle React routing, return all requests to React app - app.get('/*', function (req: express.Request, res: express.Response) { - res.sendFile(path.join(__dirname, '../../', 'client/build/index.html')); + app.get('/', function (req, res) { + logger.debug('Debug statement'); + logger.info('Info statement'); + res.send(req.method + ' ' + req.originalUrl); }); -} - -// ============LOGGING============ -const morganFormat = process.env.NODE_ENV !== 'production' ? 'dev' : 'combined'; - -app.use( - morgan(morganFormat, { - skip: function (req, res) { - return res.statusCode < 400; - }, - stream: process.stderr, - }), -); - -app.use( - morgan(morganFormat, { - skip: function (req, res) { - return res.statusCode >= 400; - }, - stream: process.stdout, - }), -); -app.get('/', function (req, res) { - logger.debug('Debug statement'); - logger.info('Info statement'); - res.send(req.method + ' ' + req.originalUrl); -}); - -app.get('/error', function (req, res) { - throw new Error('Problem Here!'); -}); - -// // All errors are sent back as JSON -app.use((err: any, req: any, res: any, next: any) => { - // Fallback to default node handler - if (res.headersSent) { - next(err); - return; - } - - logger.error(err.message, {url: req.originalUrl}); - - res.status(500); - res.json({error: err.message}); -}); + app.get('/error', (req, res) => { + throw new Error('Problem Here!'); + }); -// eslint-disable-next-line @typescript-eslint/no-empty-function -const server = app.listen(app.get('port'), () => {}); + // // All errors are sent back as JSON + app.use((err: any, req: any, res: any, next: any) => { + // Fallback to default node handler + if (res.headersSent) { + next(err); + return; + } -// on kill -process.on('SIGTERM', () => { - logger.log('warn', 'process.on::SIGTERM'); - server.close(function () { - process.exit(0); - }); -}); + logger.error(err.message, {url: req.originalUrl}); -process.on('exit', () => { - logger.log('warn', 'process.on::exit'); - console.log('exit'); - server.close(function () { - process.exit(2); + res.status(500); + res.json({error: err.message}); }); -}); -// on crash -process.on('uncaughtException', (error) => { - logger.log('error', 'process.on::uncaughtException'); - logger.log('error', `Something terrible happened: ${error}`); - server.close(function () { - process.exit(1); - }); // exit application -}); - -export default app; + return app; +} diff --git a/api/src/controllers/bitmexController.ts b/api/src/controllers/bitmexController.ts deleted file mode 100644 index beec4817..00000000 --- a/api/src/controllers/bitmexController.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {Request, Response} from 'express'; -import {fetchBitmexExchange} from '../util/auth'; -import {logger} from '../util/logger'; - -export const fetch = async ({originalUrl, baseUrl, body: {data, method}, query}: Request, res: Response) => { - try { - const path = originalUrl.replace(`${baseUrl}/`, ''); - const body = method === 'GET' ? (query ? {filter: JSON.parse((query.filter as string) || '{}')} : undefined) : data; - - const response = await fetchBitmexExchange(path, method, body); - logger.info(`Successful ${method} request (${originalUrl})`); - - return res.send({data: response, statusCode: res.statusCode}); - } catch (error) { - return res.status(400).send({error: error}); - } -}; diff --git a/api/src/routes/bitmex.ts b/api/src/routes/bitmex.ts deleted file mode 100644 index c4b3bce1..00000000 --- a/api/src/routes/bitmex.ts +++ /dev/null @@ -1,8 +0,0 @@ -import express from 'express'; -import * as bitmexController from '../controllers/bitmexController'; - -const Router = express.Router(); - -Router.post('/*', bitmexController.fetch); - -export default Router; diff --git a/api/src/routes/bitmexRouter.ts b/api/src/routes/bitmexRouter.ts new file mode 100644 index 00000000..54980043 --- /dev/null +++ b/api/src/routes/bitmexRouter.ts @@ -0,0 +1,20 @@ +import {Request, Response, Router} from 'express'; +import {fetchBitmexExchange} from '../util/auth'; +import {logger} from '../util/logger'; + +export function BitmexRouter(): Router { + return Router().post('/*', async ({originalUrl, baseUrl, body: {data, method}, query}: Request, res: Response) => { + try { + const path = originalUrl.replace(`${baseUrl}/`, ''); + const body = + method === 'GET' ? (query ? {filter: JSON.parse((query.filter as string) || '{}')} : undefined) : data; + + const response = await fetchBitmexExchange(path, method, body); + logger.info(`Successful ${method} request (${originalUrl})`); + + return res.send({data: response, statusCode: res.statusCode}); + } catch (error) { + return res.status(400).send({error: error}); + } + }); +} diff --git a/api/src/routes/settingsRouter.ts b/api/src/routes/settingsRouter.ts new file mode 100644 index 00000000..dacf7a11 --- /dev/null +++ b/api/src/routes/settingsRouter.ts @@ -0,0 +1,54 @@ +import {Router} from 'express'; +import {cache} from '../app'; +import {logger} from '../util/logger'; + +export function SettingsRouter(): Router { + return Router() + .post('/apiKey', async function saveApiKey({body}, res) { + try { + cache.setKey(body.exchange, {key: body.key, secret: body.secret}); + cache.save(true); + console.log('GET API KEY', body); + logger.info('Successfully saved api key'); + return res.send({data: {exchange: body.exchange}, statusCode: res.statusCode}); + } catch (error) { + return res.status(400).send({error: error}); + } + }) + .get('/apiKey', async function getApiKey({query}, res) { + try { + const exchange = query.exchange as string; + const data = cache.getKey(exchange); + return res.send({data: {exchange, key: data.key ?? '', secret: data.secret ?? ''}, statusCode: res.statusCode}); + } catch (error) { + return res.status(400).send({error: error}); + } + }) + .get('/apiKeys', async function getAllApiKeys(req, res) { + try { + const data = cache.keys(); + return res.send({data: {exchanges: data}, statusCode: res.statusCode}); + } catch (error) { + return res.status(400).send({error: error}); + } + }) + .delete('/apiKey', async function deleteApiKey({body: {data}}, res) { + try { + cache.removeKey(data.exchange); + cache.save(true); + logger.info('Successfully deleted api key'); + return res.send({data: {exchange: data.exchange}, statusCode: res.statusCode}); + } catch (error) { + return res.status(400).send({error: error}); + } + }) + .delete('/apiKeys', async function deleteAllApiKeys(req, res) { + try { + cache.destroy(); + logger.info('Successfully deleted all api keys'); + return res.send({statusCode: res.statusCode}); + } catch (error) { + return res.status(400).send({error: error}); + } + }); +} diff --git a/api/src/server.ts b/api/src/server.ts new file mode 100644 index 00000000..0e092877 --- /dev/null +++ b/api/src/server.ts @@ -0,0 +1,28 @@ +import path from 'path'; +import dotenv from 'dotenv'; +dotenv.config({path: path.join(__dirname, '../../client/.env')}); + +import {logger} from './util/logger'; +import {expressApp} from './app'; + +const app = expressApp(); + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const server = app.listen(app.get('port'), () => {}); + +// on kill +process + .on('SIGTERM', () => { + logger.log('warn', 'process.on::SIGTERM'); + server.close(() => void process.exit(0)); + }) + .on('exit', () => { + logger.log('warn', 'process.on::exit'); + console.log('exit'); + server.close(() => void process.exit(2)); + }) + .on('uncaughtException', (error) => { + logger.log('error', 'process.on::uncaughtException'); + logger.log('error', `Something terrible happened: ${error}`); + server.close(() => void process.exit(1)); // exit application + }); diff --git a/api/src/util/auth.ts b/api/src/util/auth.ts index 62ad291e..0e57dcbb 100644 --- a/api/src/util/auth.ts +++ b/api/src/util/auth.ts @@ -2,6 +2,17 @@ import crypto from 'crypto'; import rq from 'request-promise'; import {logger} from './logger'; import {ErrorHandler} from './error'; +import {cache} from '../app'; + +enum Exchange { + BitMeX = 'bitmex', + BitMeXTEST = 'bitmexTEST', +} + +const baseUrls: {[key in Exchange]: string} = { + [Exchange.BitMeX]: 'https://www.bitmex.com/api/v1/', + [Exchange.BitMeXTEST]: 'https://testnet.bitmex.com/api/v1/', +}; type Method = 'GET' | 'POST'; @@ -12,9 +23,16 @@ interface RequestOptions { body: any; } -export const generate_requestOptions = (data: any, path: string, method: Method, url: string): RequestOptions => { - const api = process.env.REACT_APP___API_KEY || ''; - const secret = process.env.REACT_APP___API_SECRET || ''; +export const generate_requestOptions = ( + data: any, + path: string, + method: Method, + url: string, + exchange: Exchange, +): RequestOptions => { + const cacheData = cache.getKey(exchange); + const api = cacheData.key || ''; + const secret = cacheData.secret || ''; const expires = Math.round(new Date().getTime() / 1000) + 60; // 1 min in the future const body = data ? JSON.stringify(data) : ''; @@ -34,9 +52,16 @@ export const generate_requestOptions = (data: any, path: string, method: Method, return {headers, url, method, body}; }; -export const fetchBitmexExchange = async (path: string, method?: Method, postdict?: Record) => { - const url = `https://${process.env.REACT_APP___TESTNET == 'true' ? 'testnet' : 'www'}.bitmex.com/api/v1/${path}`; - const requestOptions = generate_requestOptions(postdict, path, method || (postdict ? 'POST' : 'GET'), url); +export const fetchBitmexExchange = async ( + exchange: Exchange, + path: string, + method?: Method, + postdict?: Record, +) => { + const url = baseUrls[exchange] + path; + const requestOptions = generate_requestOptions(postdict, path, method || (postdict ? 'POST' : 'GET'), url, exchange); + + console.log(requestOptions, 'RRRRRRRRRRRR'); try { logger.log('debug', `Sending request from _curl_bitmex(${path})...`); const response = await rq(requestOptions); diff --git a/api/tsconfig.json b/api/tsconfig.json index 4d621dbc..59cbc50f 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -43,12 +43,8 @@ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [ - // "./node_modules/@types" - // ] /* List of folders to include type definitions from. */, - // "types": [ - // "node" - // ] /* Type declaration files to be included in compilation. */, + "typeRoots": ["./node_modules/@types"] /* List of folders to include type definitions from. */, + "types": ["node", "jest"] /* Type declaration files to be included in compilation. */, "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ diff --git a/client/package-lock.json b/client/package-lock.json index 501ab438..d009682d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "2.5.0", + "version": "2.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -13,24 +13,24 @@ } }, "@babel/compat-data": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", - "integrity": "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==" + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==" }, "@babel/core": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.8.tgz", - "integrity": "sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", + "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", "requires": { "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.8", - "@babel/helper-compilation-targets": "^7.14.5", - "@babel/helper-module-transforms": "^7.14.8", + "@babel/generator": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.0", + "@babel/helper-module-transforms": "^7.15.0", "@babel/helpers": "^7.14.8", - "@babel/parser": "^7.14.8", + "@babel/parser": "^7.15.0", "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.8", - "@babel/types": "^7.14.8", + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -47,11 +47,11 @@ } }, "@babel/generator": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.8.tgz", - "integrity": "sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", + "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", "requires": { - "@babel/types": "^7.14.8", + "@babel/types": "^7.15.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -74,11 +74,11 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz", - "integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz", + "integrity": "sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A==", "requires": { - "@babel/compat-data": "^7.14.5", + "@babel/compat-data": "^7.15.0", "@babel/helper-validator-option": "^7.14.5", "browserslist": "^4.16.6", "semver": "^6.3.0" @@ -92,15 +92,15 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.8.tgz", - "integrity": "sha512-bpYvH8zJBWzeqi1o+co8qOrw+EXzQ/0c74gVmY205AWXy9nifHrOg77y+1zwxX5lXE7Icq4sPlSQ4O2kWBrteQ==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.0.tgz", + "integrity": "sha512-MdmDXgvTIi4heDVX/e9EFfeGpugqm9fobBVg/iioE8kueXrOHdRDe36FAY7SnE9xXLVeYCoJR/gdrBEIHRC83Q==", "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", "@babel/helper-function-name": "^7.14.5", - "@babel/helper-member-expression-to-functions": "^7.14.7", + "@babel/helper-member-expression-to-functions": "^7.15.0", "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.0", "@babel/helper-split-export-declaration": "^7.14.5" } }, @@ -170,11 +170,11 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", - "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz", + "integrity": "sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg==", "requires": { - "@babel/types": "^7.14.5" + "@babel/types": "^7.15.0" } }, "@babel/helper-module-imports": { @@ -186,18 +186,18 @@ } }, "@babel/helper-module-transforms": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.8.tgz", - "integrity": "sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz", + "integrity": "sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg==", "requires": { "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", + "@babel/helper-replace-supers": "^7.15.0", "@babel/helper-simple-access": "^7.14.8", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.8", + "@babel/helper-validator-identifier": "^7.14.9", "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.8", - "@babel/types": "^7.14.8" + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" } }, "@babel/helper-optimise-call-expression": { @@ -224,14 +224,14 @@ } }, "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz", + "integrity": "sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==", "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", + "@babel/helper-member-expression-to-functions": "^7.15.0", "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" + "@babel/traverse": "^7.15.0", + "@babel/types": "^7.15.0" } }, "@babel/helper-simple-access": { @@ -259,9 +259,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.8.tgz", - "integrity": "sha512-ZGy6/XQjllhYQrNw/3zfWRwZCTVSiBLZ9DHVZxn9n2gip/7ab8mv2TWlKPIBk26RwedCBoWdjLmn+t9na2Gcow==" + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==" }, "@babel/helper-validator-option": { "version": "7.14.5", @@ -300,9 +300,9 @@ } }, "@babel/parser": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.8.tgz", - "integrity": "sha512-syoCQFOoo/fzkWDeM0dLEZi5xqurb5vuyzwIMNZRNun+N/9A4cUZeQaE7dTrB8jGaKuJRBtEOajtnmw0I5hvvA==" + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.0.tgz", + "integrity": "sha512-0v7oNOjr6YT9Z2RAOTv4T9aP+ubfx4Q/OhVtAet7PFDt0t9Oy6Jn+/rfC6b8HJ5zEqrQCiMxJfgtHpmIminmJQ==" }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.14.5", @@ -315,9 +315,9 @@ } }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz", - "integrity": "sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz", + "integrity": "sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw==", "requires": { "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-remap-async-to-generator": "^7.14.5", @@ -662,9 +662,9 @@ } }, "@babel/plugin-transform-classes": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.5.tgz", - "integrity": "sha512-J4VxKAMykM06K/64z9rwiL6xnBHgB1+FVspqvlgCdwD1KUbQNfszeKVVOMh59w3sztHYIZDgnhOC4WbdEfHFDA==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz", + "integrity": "sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A==", "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", "@babel/helper-function-name": "^7.14.5", @@ -770,13 +770,13 @@ } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz", - "integrity": "sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.0.tgz", + "integrity": "sha512-3H/R9s8cXcOGE8kgMlmjYYC9nqr5ELiPkJn4q0mypBrjhYQoc+5/Maq69vV4xRPWnkzZuwJPf5rArxpB/35Cig==", "requires": { - "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-module-transforms": "^7.15.0", "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", + "@babel/helper-simple-access": "^7.14.8", "babel-plugin-dynamic-import-node": "^2.3.3" } }, @@ -802,9 +802,9 @@ } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.7.tgz", - "integrity": "sha512-DTNOTaS7TkW97xsDMrp7nycUVh6sn/eq22VaxWfEdzuEbRsiaOU0pqU7DlyUGHVsbQbSghvjKRpEl+nUCKGQSg==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz", + "integrity": "sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA==", "requires": { "@babel/helper-create-regexp-features-plugin": "^7.14.5" } @@ -851,23 +851,23 @@ } }, "@babel/plugin-transform-react-display-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.5.tgz", - "integrity": "sha512-07aqY1ChoPgIxsuDviptRpVkWCSbXWmzQqcgy65C6YSFOfPFvb/DX3bBRHh7pCd/PMEEYHYWUTSVkCbkVainYQ==", + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.15.1.tgz", + "integrity": "sha512-yQZ/i/pUCJAHI/LbtZr413S3VT26qNrEm0M5RRxQJA947/YNYwbZbBaXGDrq6CG5QsZycI1VIP6d7pQaBfP+8Q==", "requires": { "@babel/helper-plugin-utils": "^7.14.5" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.5.tgz", - "integrity": "sha512-7RylxNeDnxc1OleDm0F5Q/BSL+whYRbOAR+bwgCxIr0L32v7UFh/pz1DLMZideAUxKT6eMoS2zQH6fyODLEi8Q==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.9.tgz", + "integrity": "sha512-30PeETvS+AeD1f58i1OVyoDlVYQhap/K20ZrMjLmmzmC2AYR/G43D4sdJAaDAqCD3MYpSWbmrz3kES158QSLjw==", "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", "@babel/helper-module-imports": "^7.14.5", "@babel/helper-plugin-utils": "^7.14.5", "@babel/plugin-syntax-jsx": "^7.14.5", - "@babel/types": "^7.14.5" + "@babel/types": "^7.14.9" } }, "@babel/plugin-transform-react-jsx-development": { @@ -879,9 +879,9 @@ } }, "@babel/plugin-transform-react-jsx-self": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.14.5.tgz", - "integrity": "sha512-M/fmDX6n0cfHK/NLTcPmrfVAORKDhK8tyjDhyxlUjYyPYYO8FRWwuxBA3WBx8kWN/uBUuwGa3s/0+hQ9JIN3Tg==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.14.9.tgz", + "integrity": "sha512-Fqqu0f8zv9W+RyOnx29BX/RlEsBRANbOf5xs5oxb2aHP4FKbLXxIaVPUiCti56LAR1IixMH4EyaixhUsKqoBHw==", "requires": { "@babel/helper-plugin-utils": "^7.14.5" } @@ -979,11 +979,11 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.14.6.tgz", - "integrity": "sha512-XlTdBq7Awr4FYIzqhmYY80WN0V0azF74DMPyFqVHBvf81ZUgc4X7ZOpx6O8eLDK6iM5cCQzeyJw0ynTaefixRA==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.15.0.tgz", + "integrity": "sha512-WIIEazmngMEEHDaPTx0IZY48SaAmjVWe3TRSX7cmJXn0bEv9midFzAjxiruOWYIVf5iQ10vFx7ASDpgEO08L5w==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.14.6", + "@babel/helper-create-class-features-plugin": "^7.15.0", "@babel/helper-plugin-utils": "^7.14.5", "@babel/plugin-syntax-typescript": "^7.14.5" } @@ -1006,16 +1006,16 @@ } }, "@babel/preset-env": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.8.tgz", - "integrity": "sha512-a9aOppDU93oArQ51H+B8M1vH+tayZbuBqzjOhntGetZVa+4tTu5jp+XTwqHGG2lxslqomPYVSjIxQkFwXzgnxg==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.15.0.tgz", + "integrity": "sha512-FhEpCNFCcWW3iZLg0L2NPE9UerdtsCR6ZcsGHUX6Om6kbCQeL5QZDqFDmeNHC6/fy6UH3jEge7K4qG5uC9In0Q==", "requires": { - "@babel/compat-data": "^7.14.7", - "@babel/helper-compilation-targets": "^7.14.5", + "@babel/compat-data": "^7.15.0", + "@babel/helper-compilation-targets": "^7.15.0", "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-validator-option": "^7.14.5", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", - "@babel/plugin-proposal-async-generator-functions": "^7.14.7", + "@babel/plugin-proposal-async-generator-functions": "^7.14.9", "@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/plugin-proposal-class-static-block": "^7.14.5", "@babel/plugin-proposal-dynamic-import": "^7.14.5", @@ -1048,7 +1048,7 @@ "@babel/plugin-transform-async-to-generator": "^7.14.5", "@babel/plugin-transform-block-scoped-functions": "^7.14.5", "@babel/plugin-transform-block-scoping": "^7.14.5", - "@babel/plugin-transform-classes": "^7.14.5", + "@babel/plugin-transform-classes": "^7.14.9", "@babel/plugin-transform-computed-properties": "^7.14.5", "@babel/plugin-transform-destructuring": "^7.14.7", "@babel/plugin-transform-dotall-regex": "^7.14.5", @@ -1059,10 +1059,10 @@ "@babel/plugin-transform-literals": "^7.14.5", "@babel/plugin-transform-member-expression-literals": "^7.14.5", "@babel/plugin-transform-modules-amd": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.14.5", + "@babel/plugin-transform-modules-commonjs": "^7.15.0", "@babel/plugin-transform-modules-systemjs": "^7.14.5", "@babel/plugin-transform-modules-umd": "^7.14.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.9", "@babel/plugin-transform-new-target": "^7.14.5", "@babel/plugin-transform-object-super": "^7.14.5", "@babel/plugin-transform-parameters": "^7.14.5", @@ -1077,11 +1077,11 @@ "@babel/plugin-transform-unicode-escapes": "^7.14.5", "@babel/plugin-transform-unicode-regex": "^7.14.5", "@babel/preset-modules": "^0.1.4", - "@babel/types": "^7.14.8", + "@babel/types": "^7.15.0", "babel-plugin-polyfill-corejs2": "^0.2.2", "babel-plugin-polyfill-corejs3": "^0.2.2", "babel-plugin-polyfill-regenerator": "^0.2.2", - "core-js-compat": "^3.15.0", + "core-js-compat": "^3.16.0", "semver": "^6.3.0" }, "dependencies": { @@ -1135,11 +1135,11 @@ } }, "@babel/runtime-corejs3": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.8.tgz", - "integrity": "sha512-4dMD5QRBkumn45oweR0SxoNtt15oz3BUBAQ8cIx7HJqZTtE8zjpM0My8aHJHVnyf4XfRg6DNzaE1080WLBiC1w==", + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.14.9.tgz", + "integrity": "sha512-64RiH2ON4/y8qYtoa8rUiyam/tUVyGqRyNYhe+vCRGmjnV4bUlZvY+mwd0RrmLoCpJpdq3RsrNqKb7SJdw/4kw==", "requires": { - "core-js-pure": "^3.15.0", + "core-js-pure": "^3.16.0", "regenerator-runtime": "^0.13.4" } }, @@ -1154,27 +1154,27 @@ } }, "@babel/traverse": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.8.tgz", - "integrity": "sha512-kexHhzCljJcFNn1KYAQ6A5wxMRzq9ebYpEDV4+WdNyr3i7O44tanbDOR/xjiG2F3sllan+LgwK+7OMk0EmydHg==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", + "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", "requires": { "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.8", + "@babel/generator": "^7.15.0", "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.8", - "@babel/types": "^7.14.8", + "@babel/parser": "^7.15.0", + "@babel/types": "^7.15.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.8.tgz", - "integrity": "sha512-iob4soQa7dZw8nodR/KlOQkPh9S4I8RwCxwRIFuiMRYjOzH/KJzdUfDgz6cGi5dDaclXF4P2PAhCdrBJNIg68Q==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", + "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", "requires": { - "@babel/helper-validator-identifier": "^7.14.8", + "@babel/helper-validator-identifier": "^7.14.9", "to-fast-properties": "^2.0.0" } }, @@ -2029,9 +2029,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -2114,9 +2114,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -2223,9 +2223,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -2243,9 +2243,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -2514,9 +2514,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -2658,14 +2658,24 @@ } }, "@reduxjs/toolkit": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.5.1.tgz", - "integrity": "sha512-PngZKuwVZsd+mimnmhiOQzoD0FiMjqVks6ituO1//Ft5UEX5Ca9of13NEjo//pU22Jk7z/mdXVsmDfgsig1osA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.1.tgz", + "integrity": "sha512-pa3nqclCJaZPAyBhruQtiRwtTjottRrVJqziVZcWzI73i6L3miLTtUyWfauwv08HWtiXLx1xGyGt+yLFfW/d0A==", "requires": { - "immer": "^8.0.1", - "redux": "^4.0.0", + "immer": "^9.0.1", + "redux": "^4.1.0", "redux-thunk": "^2.3.0", "reselect": "^4.0.0" + }, + "dependencies": { + "redux": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + } } }, "@rollup/plugin-node-resolve": { @@ -3127,9 +3137,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -3142,12 +3152,18 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, + "@types/history": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", + "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", + "dev": true + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -3195,14 +3211,14 @@ } }, "@types/json-schema": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.8.tgz", - "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==" + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, "@types/lodash": { - "version": "4.14.171", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz", - "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==" + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==" }, "@types/lodash.mergewith": { "version": "4.6.6", @@ -3255,9 +3271,9 @@ "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" }, "@types/react": { - "version": "17.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz", - "integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==", + "version": "17.0.16", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.16.tgz", + "integrity": "sha512-3kCUiOOlQTwUUvjNFkbBTWMTxdTGybz/PfjCw9JmaRGcEDBQh+nGMg7/E9P2rklhJuYVd25IYLNcvqgSPCPksg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3285,6 +3301,27 @@ "redux": "^4.0.0" } }, + "@types/react-router": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz", + "integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz", + "integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/react-test-renderer": { "version": "16.9.5", "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.5.tgz", @@ -3295,9 +3332,9 @@ }, "dependencies": { "@types/react": { - "version": "16.14.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.11.tgz", - "integrity": "sha512-Don0MtsZZ3fjwTJ2BsoqkyOy7e176KplEAKOpr/4XDdzinlyJBn9yfsKn5mcSgn4kh1B22+3tBnzBC1z63ybtQ==", + "version": "16.14.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.12.tgz", + "integrity": "sha512-7nOJgNsRbARhZhvwPm7cnzahtzEi5VJ9OvcQk8ExEEb1t+zaFklwLVkJz7G1kfxX4X/mDa/icTmzE0vTmqsqBg==", "dev": true, "requires": { "@types/prop-types": "*", @@ -3317,9 +3354,9 @@ }, "dependencies": { "redux": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz", - "integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", + "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", "dev": true, "requires": { "@babel/runtime": "^7.9.2" @@ -3336,9 +3373,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -3410,9 +3447,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "source-map": { "version": "0.6.1", @@ -3422,9 +3459,9 @@ } }, "@types/webpack-sources": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.1.tgz", - "integrity": "sha512-MjM1R6iuw8XaVbtkCBz0N349cyqBjJHCbQiOeppe3VBeFvxqs74RKHAVt9LkxTnUWc7YLZOEsUfPUnmK6SBPKQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", "requires": { "@types/node": "*", "@types/source-list-map": "*", @@ -3432,9 +3469,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "source-map": { "version": "0.7.3", @@ -3460,6 +3497,7 @@ "version": "4.28.5", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz", "integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==", + "dev": true, "requires": { "@typescript-eslint/experimental-utils": "4.28.5", "@typescript-eslint/scope-manager": "4.28.5", @@ -3470,10 +3508,66 @@ "tsutils": "^3.21.0" }, "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz", + "integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.28.5", + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/typescript-estree": "4.28.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz", + "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5" + } + }, + "@typescript-eslint/types": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz", + "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz", + "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz", + "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "eslint-visitor-keys": "^2.0.0" + } + }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -3481,14 +3575,14 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz", - "integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.0.tgz", + "integrity": "sha512-FpNVKykfeaIxlArLUP/yQfv/5/3rhl1ov6RWgud4OgbqWLkEq7lqgQU9iiavZRzpzCRQV4XddyFz3wFXdkiX9w==", "requires": { "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.28.5", - "@typescript-eslint/types": "4.28.5", - "@typescript-eslint/typescript-estree": "4.28.5", + "@typescript-eslint/scope-manager": "4.29.0", + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/typescript-estree": "4.29.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } @@ -3497,34 +3591,87 @@ "version": "4.28.5", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz", "integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==", + "dev": true, "requires": { "@typescript-eslint/scope-manager": "4.28.5", "@typescript-eslint/types": "4.28.5", "@typescript-eslint/typescript-estree": "4.28.5", "debug": "^4.3.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz", + "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5" + } + }, + "@typescript-eslint/types": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz", + "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz", + "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/visitor-keys": "4.28.5", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz", + "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.28.5", + "eslint-visitor-keys": "^2.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "@typescript-eslint/scope-manager": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz", - "integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.0.tgz", + "integrity": "sha512-HPq7XAaDMM3DpmuijxLV9Io8/6pQnliiXMQUcAdjpJJSR+fdmbD/zHCd7hMkjJn04UQtCQBtshgxClzg6NIS2w==", "requires": { - "@typescript-eslint/types": "4.28.5", - "@typescript-eslint/visitor-keys": "4.28.5" + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/visitor-keys": "4.29.0" } }, "@typescript-eslint/types": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz", - "integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==" + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.0.tgz", + "integrity": "sha512-2YJM6XfWfi8pgU2HRhTp7WgRw78TCRO3dOmSpAvIQ8MOv4B46JD2chnhpNT7Jq8j0APlIbzO1Bach734xxUl4A==" }, "@typescript-eslint/typescript-estree": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz", - "integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.0.tgz", + "integrity": "sha512-8ZpNHDIOyqzzgZrQW9+xQ4k5hM62Xy2R4RPO3DQxMc5Rq5QkCdSpk/drka+DL9w6sXNzV5nrdlBmf8+x495QXQ==", "requires": { - "@typescript-eslint/types": "4.28.5", - "@typescript-eslint/visitor-keys": "4.28.5", + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/visitor-keys": "4.29.0", "debug": "^4.3.1", "globby": "^11.0.3", "is-glob": "^4.0.1", @@ -3543,11 +3690,11 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.28.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz", - "integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.0.tgz", + "integrity": "sha512-LoaofO1C/jAJYs0uEpYMXfHboGXzOJeV118X4OsZu9f7rG7Pr9B3+4HTU8+err81rADa4xfQmAxnRnPAI2jp+Q==", "requires": { - "@typescript-eslint/types": "4.28.5", + "@typescript-eslint/types": "4.29.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -4924,15 +5071,15 @@ } }, "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "version": "4.16.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.7.tgz", + "integrity": "sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA==", "requires": { - "caniuse-lite": "^1.0.30001219", + "caniuse-lite": "^1.0.30001248", "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", + "electron-to-chromium": "^1.3.793", "escalade": "^3.1.1", - "node-releases": "^1.1.71" + "node-releases": "^1.1.73" } }, "bs-logger": { @@ -4960,6 +5107,13 @@ "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } } }, "buffer-from": { @@ -5120,9 +5274,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001248", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz", - "integrity": "sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw==" + "version": "1.0.30001249", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz", + "integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==" }, "capture-exit": { "version": "2.0.0", @@ -6324,9 +6478,9 @@ } }, "dom-accessibility-api": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz", - "integrity": "sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.7.tgz", + "integrity": "sha512-ml3lJIq9YjUfM9TUnEPvEYWFSwivwIGBPKpewX7tii7fwCazA8yCioGdqQcNsItPpfFvSJ3VIdMQPj60LJhcQA==", "dev": true }, "dom-converter": { @@ -6475,9 +6629,9 @@ "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==" }, "electron-to-chromium": { - "version": "1.3.792", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.792.tgz", - "integrity": "sha512-RM2O2xrNarM7Cs+XF/OE2qX/aBROyOZqqgP+8FXMXSuWuUqCfUUzg7NytQrzZU3aSqk1Qq6zqnVkJsbfMkIatg==" + "version": "1.3.799", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.799.tgz", + "integrity": "sha512-V2rbYWdGvSqrg+95KjkVuSi41bGfrhrOzjl1tSi2VLnm0mRe3FsSvhiqidSiSll9WiMhrQAhpDcW/wcqK3c+Yw==" }, "elliptic": { "version": "6.5.4", @@ -6587,9 +6741,9 @@ } }, "es-abstract": { - "version": "1.18.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.4.tgz", - "integrity": "sha512-xjDAPJRxKc1uoTkdW8MEk7Fq/2bzz3YoCADYniDV7+KITCUdu9c90fj1aKI7nEZFZxRrHlDo3wtma/C6QkhlXQ==", + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", + "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", @@ -7000,9 +7154,9 @@ } }, "eslint-plugin-flowtype": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.8.2.tgz", - "integrity": "sha512-/aPTnNKNAYJbEU07HwvohnXp0itBT+P0r+7s80IG5eqfsrx4NLN+0rXNztJBc56u1RJegSn0GMt1cZnGZpCThw==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.9.0.tgz", + "integrity": "sha512-aBUVPA5Wt0XyuV3Wg8flfVqYJR6yR2nRLuyPwoTjCg5VTk4G1X1zQpInr39tUGgRxqrA+d+B9GYK4+/d1i0Rfw==", "requires": { "lodash": "^4.17.15", "string-natural-compare": "^3.0.1" @@ -7594,6 +7748,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" } } }, @@ -8393,9 +8552,9 @@ } }, "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "growly": { "version": "1.3.0", @@ -8478,6 +8637,14 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -8582,6 +8749,15 @@ "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" }, + "history": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/history/-/history-5.0.1.tgz", + "integrity": "sha512-5qC/tFUKfVci5kzgRxZxN5Mf1CV8NmJx9ByaPX0YTLx5Vz3Svh7NYp6eA4CpDq4iA9D0C1t8BNIfvQIrUI3mVw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.7.6" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -8973,9 +9149,9 @@ "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, "immer": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.4.tgz", - "integrity": "sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ==" + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.5.tgz", + "integrity": "sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ==" }, "import-cwd": { "version": "2.1.0", @@ -9064,9 +9240,9 @@ } }, "influnt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/influnt/-/influnt-1.2.0.tgz", - "integrity": "sha512-hc7sZh3VBAb0mnUb3nQdHWy1Famzg/LRLy+LNrVcPlrz8ioOweadxoQ1Luy/VtpTkjJ/sksmIccgHZ1BDk6TWw==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/influnt/-/influnt-1.3.7.tgz", + "integrity": "sha512-Nv1BpYInrrvc7+MbiPnUo43RhIwRI3E7Z7aa/am3YsihLVLj5WDEtl/zcEyVMvbxZvnKbdmK4On1yqCRhwRxsQ==", "dev": true }, "inherits": { @@ -9145,11 +9321,12 @@ } }, "is-arguments": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "requires": { - "call-bind": "^1.0.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, "is-arrayish": { @@ -9158,9 +9335,9 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.3.tgz", + "integrity": "sha512-ZU538ajmYJmzysE5yU4Y7uIrPQ2j704u+hXFiIPQExpqzzUbpe5jCPdTfmz7jXRxZdvjY3KZ3ZNenoXQovX+Dg==" }, "is-binary-path": { "version": "2.1.0", @@ -9172,11 +9349,12 @@ } }, "is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "requires": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, "is-buffer": { @@ -9185,9 +9363,9 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" }, "is-ci": { "version": "2.0.0", @@ -9237,9 +9415,12 @@ } }, "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-descriptor": { "version": "0.1.6", @@ -9318,9 +9499,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, "is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-obj": { "version": "2.0.0", @@ -9367,12 +9551,12 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "requires": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "has-tostringtag": "^1.0.0" } }, "is-regexp": { @@ -9396,9 +9580,12 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-symbol": { "version": "1.0.4", @@ -9433,9 +9620,9 @@ } }, "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "isexe": { "version": "2.0.0", @@ -9729,9 +9916,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -10032,9 +10219,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -10052,9 +10239,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -10085,9 +10272,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -10117,9 +10304,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -10302,9 +10489,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -10449,9 +10636,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -10688,9 +10875,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -10824,9 +11011,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -11005,9 +11192,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "ansi-styles": { "version": "4.3.0", @@ -11065,9 +11252,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" }, "has-flag": { "version": "4.0.0", @@ -11111,9 +11298,9 @@ "dev": true }, "jsdom": { - "version": "16.6.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.6.0.tgz", - "integrity": "sha512-Ty1vmF4NHJkolaEmdjtxTfSfkdb8Ywarwf63f+F8/mDD1uLSSWDxDuMiZxiPhwunLrn9LOSVItWj4bLYsLN3Dg==", + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", "requires": { "abab": "^2.0.5", "acorn": "^8.2.4", @@ -11140,7 +11327,7 @@ "whatwg-encoding": "^1.0.5", "whatwg-mimetype": "^2.3.0", "whatwg-url": "^8.5.0", - "ws": "^7.4.5", + "ws": "^7.4.6", "xml-name-validator": "^3.0.0" }, "dependencies": { @@ -11756,6 +11943,15 @@ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + } + }, "mini-css-extract-plugin": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz", @@ -11948,9 +12144,9 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" }, "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" }, "nanoid": { "version": "3.1.23", @@ -12752,9 +12948,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } }, "path-type": { "version": "4.0.0", @@ -14510,6 +14709,67 @@ } } }, + "react-router": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + } + } + }, + "react-router-dom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", + "requires": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.2.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "dependencies": { + "history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "requires": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + } + } + }, "react-scripts": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-4.0.3.tgz", @@ -14606,6 +14866,41 @@ } } }, + "@typescript-eslint/eslint-plugin": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.0.tgz", + "integrity": "sha512-eiREtqWRZ8aVJcNru7cT/AMVnYd9a2UHsfZT8MR1dW3UUEg6jDv9EQ9Cq4CUPZesyQ58YUpoAADGv71jY8RwgA==", + "requires": { + "@typescript-eslint/experimental-utils": "4.29.0", + "@typescript-eslint/scope-manager": "4.29.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.29.0.tgz", + "integrity": "sha512-+92YRNHFdXgq+GhWQPT2bmjX09X7EH36JfgN2/4wmhtwV/HPxozpCNst8jrWcngLtEVd/4zAwA6BKojAlf+YqA==", + "requires": { + "@typescript-eslint/scope-manager": "4.29.0", + "@typescript-eslint/types": "4.29.0", + "@typescript-eslint/typescript-estree": "4.29.0", + "debug": "^4.3.1" + } + }, "eslint-plugin-jest": { "version": "24.4.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.4.0.tgz", @@ -14778,6 +15073,13 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } } }, "readdirp": { @@ -15131,6 +15433,11 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, + "resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -15270,9 +15577,9 @@ }, "dependencies": { "@types/node": { - "version": "16.4.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.8.tgz", - "integrity": "sha512-VL7RZyCpfYEmbyd3/Eq5RNYhZt7yoL1JThZQ3KzimzhLya2Qa86U1ZZmioNWAAjiz99z1ED1xF9NUV2srvfVrA==" + "version": "16.4.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.13.tgz", + "integrity": "sha512-bLL69sKtd25w7p1nvg9pigE4gtKVpGTPojBFLMkGHXuUgap2sLqQt2qUnqmVCDfzGUL0DRNZP+1prIZJbMeAXg==" } } }, @@ -16699,9 +17006,9 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, "tar": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.2.tgz", - "integrity": "sha512-EwKEgqJ7nJoS+s8QfLYVGMDmAsj+StbI2AM/RTHeUSsOw6Z8bwNBRv5z3CY0m7laC5qUAqruLX5AhMuc5deY3Q==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz", + "integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -16917,6 +17224,11 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tinycolor2": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", @@ -17356,6 +17668,11 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" } } }, @@ -17533,6 +17850,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/client/package.json b/client/package.json index 1c023777..3f80ddde 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "2.6.0", + "version": "2.7.0", "private": true, "description": "front side of the application(reactjs)", "author": "Juozas Rimantas ", @@ -24,7 +24,7 @@ "@emotion/react": "^11.4.0", "@emotion/styled": "^11.3.0", "@giantmachines/redux-websocket": "1.1.6", - "@reduxjs/toolkit": "1.5.1", + "@reduxjs/toolkit": "^1.6.1", "axios": "^0.21.1", "bootstrap": "^4.4.1", "classnames": "^2.3.1", @@ -35,6 +35,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-redux": "7.1.0", + "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "react-toastify": "^6.0.8", "redux": "4.0.4", @@ -52,6 +53,7 @@ "@types/react": "^17.0.11", "@types/react-dom": "^17.0.8", "@types/react-redux": "7.1.4", + "@types/react-router-dom": "^5.1.8", "@types/react-test-renderer": "^16.9.3", "@types/redux-mock-store": "^1.0.2", "@typescript-eslint/eslint-plugin": "4.28.x", @@ -62,8 +64,9 @@ "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.1.0", + "history": "^5.0.1", "hoist-non-react-statics": "^3.3.2", - "influnt": "^1.2.0", + "influnt": "^1.3.7", "node-sass": "^4.14.1", "prettier": "^2.3.0", "react-test-renderer": "16.9.0", diff --git a/client/public/index.html b/client/public/index.html index 8aed4d3c..f612c290 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -21,7 +21,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - BitMEX trading tool - v2.6.0 + BitMEX trading tool - v2.7.0 diff --git a/client/src/@types/global.d.ts b/client/src/@types/global.d.ts index ede6f126..ac650649 100644 --- a/client/src/@types/global.d.ts +++ b/client/src/@types/global.d.ts @@ -9,6 +9,8 @@ type iterobject = {[key: string]: T}; type RequiredProperty = {[P in keyof T]: Required>}; +type RawType

> = P extends Promise ? U : never; + type ValueOf = T[keyof T]; type UnionToIntersection = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; diff --git a/client/src/App.tsx b/client/src/App.tsx index ae53a251..45eedb90 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,57 +1,29 @@ import React from 'react'; -import {useDispatch} from 'react-redux'; import {Box} from '@chakra-ui/react'; -import { - ScaledOrders, - MarketOrderContainer, - TrailingLimitOrder, - TickerPricesContainer, - // CrossOrderContainer, - OpenOrdersContainer, -} from 'containers'; -import {Spinner, ToastContainer} from 'components'; -import {useReduxSelector} from 'redux/helpers/hookHelpers'; -import {wsConnect, wsDisconnect, wsSubscribeTo, wsAuthenticate} from 'redux/modules/websocket/websocketModule'; -import {getBalance} from 'redux/modules/preview/previewModule'; -import 'scss/root.module.scss'; - -const App = React.memo(() => { - const dispatch = useDispatch(); - const {previewLoading, trailLoading, wsLoading, connected} = useReduxSelector( - 'previewLoading', - 'trailLoading', - 'wsLoading', - 'connected', - ); - - React.useEffect(() => { - dispatch(wsConnect()); +import {Route, Switch} from 'react-router-dom'; +import {ExchangeRoute, Header} from 'components'; +import {Exchange} from 'redux/modules/settings/types'; +import Home from 'pages/Home'; +import Settings from 'pages/Settings'; +import NotFound from 'pages/NotFound'; +import BitmexExchange from 'pages/Bitmex'; +import {RoutePath} from 'pages/paths'; - return () => { - dispatch(wsDisconnect()); - }; - }, [dispatch]); - - React.useEffect(() => { - if (connected) { - dispatch(getBalance()); - dispatch(wsAuthenticate()); - dispatch(wsSubscribeTo('order')); - } - }, [dispatch, connected]); +import 'scss/root.module.scss'; +export default React.memo(function App() { return ( - - - - - - {/* TODO: disabling for now */} - - - - + <> +

+ + + + + + + + + + ); }); - -export default App; diff --git a/client/src/components/Banner/Banner.tsx b/client/src/components/Banner/Banner.tsx new file mode 100644 index 00000000..9eeecce1 --- /dev/null +++ b/client/src/components/Banner/Banner.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {WarningIcon} from '@chakra-ui/icons'; +import {Box} from '@chakra-ui/react'; +import {Link} from 'react-router-dom'; +import {useExchange} from 'general/hooks'; +import {ExchangePresenter} from 'presenters/general-presenters'; + +export function Banner() { + const exchange = useExchange(); + return ( + + + + There is no API key for {ExchangePresenter[exchange]} exchange. Add it{' '} + + here + + + + ); +} diff --git a/client/src/components/Button/Button.module.scss b/client/src/components/Button/Button.module.scss index 9a60ef03..d216a3db 100644 --- a/client/src/components/Button/Button.module.scss +++ b/client/src/components/Button/Button.module.scss @@ -20,8 +20,9 @@ background-color: var(--primaryColor); border-radius: 2px; padding: 5px 5px; - width: 120px; - height: 30px; + width: auto; + min-width: 120px; + height: 34px; &:focus:enabled { -webkit-box-shadow: none; box-shadow: none; @@ -69,6 +70,16 @@ } } +.outline { + @extend %regular; + color: var(--stopColor); + border: 1px solid var(--stopColor); + &:hover:enabled { + border: 1px solid rgb(245, 157, 172); + color: rgb(245, 157, 172); + } +} + .button_buy { @extend %regular; border: 1px solid var(--accentColor); diff --git a/client/src/components/Button/Button.tsx b/client/src/components/Button/Button.tsx index e9d28383..1eb2e1d3 100644 --- a/client/src/components/Button/Button.tsx +++ b/client/src/components/Button/Button.tsx @@ -5,7 +5,7 @@ import {SIDE} from '../../redux/api/bitmex/types'; import {COMPONENTS} from 'data-test-ids'; import styles from './Button.module.scss'; -export type ButtonVariants = 'submit' | 'text' | 'custom' | 'textSell' | SIDE; +export type ButtonVariants = 'submit' | 'text' | 'custom' | 'textSell' | 'outline' | SIDE; interface Props { testID?: string; @@ -36,6 +36,7 @@ export function Button({ [styles.text_sell]: variant === 'textSell', [styles.button_buy]: variant === 'Buy', [styles.button_sell]: variant === 'Sell', + [styles.outline]: variant === 'outline', [className]: variant === 'custom', }); diff --git a/client/src/components/ExchangeRoute/ExchangeRoute.tsx b/client/src/components/ExchangeRoute/ExchangeRoute.tsx new file mode 100644 index 00000000..3e80c947 --- /dev/null +++ b/client/src/components/ExchangeRoute/ExchangeRoute.tsx @@ -0,0 +1,36 @@ +import {Heading} from '@chakra-ui/react'; +import React from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import {Route} from 'react-router-dom'; +import {activateExchange, getAllApiKeys} from 'redux/modules/settings/settingsModule'; +import {Exchange} from 'redux/modules/settings/types'; +import {AppState} from 'redux/modules/state'; + +interface Props { + path: string; + exact?: boolean; + exchange: Exchange; + component: React.ComponentType; +} + +export function ExchangeRoute({path, exact, exchange, component}: Props) { + const dispatch = useDispatch(); + + const currentExchange = useSelector((state: AppState) => state.settings.activeExchange); + const loading = useSelector((state: AppState) => state.settings.getAllApiKeysLoading); + + React.useEffect(() => { + dispatch(activateExchange(exchange)); + dispatch(getAllApiKeys()); + }, [dispatch, exchange]); + + return ( + + {loading ? ( + Loading... + ) : currentExchange === exchange ? ( + React.createElement(component) + ) : null} + + ); +} diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx new file mode 100644 index 00000000..853c7701 --- /dev/null +++ b/client/src/components/Header/Header.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import {Flex, Heading, Box} from '@chakra-ui/react'; +import {SettingsIcon} from '@chakra-ui/icons'; +import {NavLink} from 'react-router-dom'; +import {RoutePath} from 'pages/paths'; + +const MenuItem = ({children, to}: any) => ( + + + {children} + + +); + +export function Header() { + return ( + + + Home + BitMeX + BitMex Testnet + + + + + + + + + ); +} diff --git a/client/src/components/InputField/InputField.tsx b/client/src/components/InputField/InputField.tsx index 67a55f93..66bd25be 100644 --- a/client/src/components/InputField/InputField.tsx +++ b/client/src/components/InputField/InputField.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {Box, NumberInput, NumberInputField} from '@chakra-ui/react'; +import {Box, NumberInput, NumberInputField, Input} from '@chakra-ui/react'; import './InputField.module.scss'; interface Props { @@ -13,13 +13,18 @@ interface Props { tooltip?: string; onChange: (value: any, id: string) => void; step?: number; + type?: 'number' | 'text'; } export function InputField(props: Props) { - const {id, label, value, stop = false, placeholder, onChange, testID, step} = props; + const {id, label, value, stop = false, placeholder, onChange, testID, step, type = 'number'} = props; const invokeValueChange = React.useCallback( - (value: string) => onChange(step == undefined ? +value : value, id as string), + (value: string | any) => { + typeof value === 'string' + ? onChange(step == undefined ? +value : value, id as string) + : onChange(value?.target.value, id as string); + }, [onChange, id, step], ); @@ -28,28 +33,43 @@ export function InputField(props: Props) { {label} - - + + + ) : ( + - + )} ); } diff --git a/client/src/components/index.ts b/client/src/components/index.ts index f325cfbc..4af4ccfa 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -9,3 +9,5 @@ export * from './Toast/Toast'; export * from './Modal/Modal'; export * from './Row/Row'; export * from './modals'; +export * from './Header/Header'; +export * from './ExchangeRoute/ExchangeRoute'; diff --git a/client/src/components/modals/AddApiKeysModal.tsx b/client/src/components/modals/AddApiKeysModal.tsx new file mode 100644 index 00000000..390233ff --- /dev/null +++ b/client/src/components/modals/AddApiKeysModal.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import {useDispatch} from 'react-redux'; +import {Modal, InputField} from 'components'; +import {saveApiKey} from 'redux/modules/settings/settingsModule'; +import {Exchange} from 'redux/modules/settings/types'; +import {ExchangePresenter} from 'presenters/general-presenters'; +import {ADD_API_KEYS_MODAL} from 'data-test-ids'; + +interface Props { + exchange: Exchange; +} + +export function AddApiKeysModal({exchange}: Props) { + const dispatch = useDispatch(); + + const [key, setKey] = React.useState(''); + const [secret, setSecret] = React.useState(''); + + const addTarget = React.useCallback(() => { + dispatch(saveApiKey({exchange, key, secret})); + }, [dispatch, exchange, key, secret]); + + const isConfirmButtonDisabled = key.length < 15 || secret.length < 15; + + return ( + + + + + ); +} diff --git a/client/src/components/modals/AddProfitOrderModal.tsx b/client/src/components/modals/AddProfitOrderModal.tsx index c64ef3a8..f7d90219 100644 --- a/client/src/components/modals/AddProfitOrderModal.tsx +++ b/client/src/components/modals/AddProfitOrderModal.tsx @@ -1,19 +1,19 @@ import React from 'react'; -import {useDispatch, useSelector} from 'react-redux'; +import {useSelector} from 'react-redux'; import {Modal, InputField} from 'components'; -import {addProfitTarget} from 'redux/modules/orders/ordersModule'; import {SIDE} from 'redux/api/bitmex/types'; import {AppState} from 'redux/modules/state'; import {orderSelector} from 'redux/selectors'; import {ADD_ORDER_MODAL} from 'data-test-ids'; import {INSTRUMENT_PARAMS} from 'utils'; +import {useAppContext} from 'general/hooks'; interface Props { orderID: string; } export function AddProfitOrderModal({orderID}: Props) { - const dispatch = useDispatch(); + const {api} = useAppContext(); const order = useSelector((state: AppState) => orderSelector(state, {orderID})); const {symbol, side, price: stopPx} = order!; @@ -22,8 +22,8 @@ export function AddProfitOrderModal({orderID}: Props) { const [quantity, setQuantity] = React.useState(''); const addTarget = React.useCallback(() => { - dispatch(addProfitTarget({orderID, side, symbol, stop: stopPx, price: parseInt(price), orderQty: quantity})); - }, [dispatch, orderID, side, quantity, price, stopPx, symbol]); + api.addProfitTarget({orderID, side, symbol, stop: stopPx, price: parseInt(price), orderQty: quantity}); + }, [api, orderID, side, quantity, price, stopPx, symbol]); const isConfirmButtonDisabled = !parseInt(price) || diff --git a/client/src/components/modals/CancelAllOrdersModal.tsx b/client/src/components/modals/CancelAllOrdersModal.tsx index 6a04cbd9..65158794 100644 --- a/client/src/components/modals/CancelAllOrdersModal.tsx +++ b/client/src/components/modals/CancelAllOrdersModal.tsx @@ -1,19 +1,16 @@ import React from 'react'; -import {useDispatch} from 'react-redux'; import {Modal} from 'components'; -import {cancelAllOrders} from 'redux/modules/orders/ordersModule'; +import {useAppContext} from 'general/hooks'; interface Props { totalOrders: number; } export function CancelAllOrdersModal({totalOrders}: Props) { - const dispatch = useDispatch(); - - const emitConfirm = React.useCallback(() => void dispatch(cancelAllOrders()), [dispatch]); + const {api} = useAppContext(); return ( - + {`This will cancel ${totalOrders} order${totalOrders > 1 ? 's' : ''}`} ); diff --git a/client/src/components/modals/CancelAllProfitOrdersModal.tsx b/client/src/components/modals/CancelAllProfitOrdersModal.tsx index 63f408b2..34852b73 100644 --- a/client/src/components/modals/CancelAllProfitOrdersModal.tsx +++ b/client/src/components/modals/CancelAllProfitOrdersModal.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import {useDispatch} from 'react-redux'; import {Modal} from 'components'; -import {cancelAllProfitOrders} from 'redux/modules/orders/ordersModule'; +import {useAppContext} from 'general/hooks'; interface Props { totalOrders: number; @@ -9,11 +8,12 @@ interface Props { } export function CancelAllProfitOrdersModal({totalOrders, profitOrderIds}: Props) { - const dispatch = useDispatch(); + const {api} = useAppContext(); - const emitConfirm = React.useCallback(() => { - dispatch(cancelAllProfitOrders({orderID: profitOrderIds})); - }, [dispatch, profitOrderIds]); + const emitConfirm = React.useCallback( + () => api.cancelAllProfitOrders({orderID: profitOrderIds}), + [api, profitOrderIds], + ); return ( diff --git a/client/src/components/modals/CancelOrderModal.tsx b/client/src/components/modals/CancelOrderModal.tsx index f5f4cd7b..011b1c95 100644 --- a/client/src/components/modals/CancelOrderModal.tsx +++ b/client/src/components/modals/CancelOrderModal.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import {useDispatch, useSelector} from 'react-redux'; +import {useSelector} from 'react-redux'; import {Modal} from 'components'; -import {cancelOrder} from 'redux/modules/orders/ordersModule'; import {AppState} from 'redux/modules/state'; import {groupedOrdersSelector, orderSelector} from 'redux/selectors'; +import {useAppContext} from 'general/hooks'; interface Props { orderID: string; } export function CancelOrderModal({orderID}: Props) { - const dispatch = useDispatch(); + const {api} = useAppContext(); const order = useSelector((state: AppState) => orderSelector(state, {orderID})); const groupedOrders = useSelector(groupedOrdersSelector); @@ -22,9 +22,9 @@ export function CancelOrderModal({orderID}: Props) { const emitConfirm = React.useCallback(() => { if (order) { - dispatch(cancelOrder({orderID: [order.orderID, ...profitOrderIDs]})); + api.cancelOrder({orderID: [order.orderID, ...profitOrderIDs]}); } - }, [dispatch, profitOrderIDs, order]); + }, [api, profitOrderIDs, order]); if (!order) { return null; diff --git a/client/src/components/modals/CancelProfitOrderModal.tsx b/client/src/components/modals/CancelProfitOrderModal.tsx index 548b2585..8890558c 100644 --- a/client/src/components/modals/CancelProfitOrderModal.tsx +++ b/client/src/components/modals/CancelProfitOrderModal.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import {useDispatch} from 'react-redux'; import {Modal} from 'components'; -import {cancelProfitOrder} from 'redux/modules/orders/ordersModule'; import {SYMBOL} from 'redux/api/bitmex/types'; +import {useAppContext} from 'general/hooks'; interface Props { symbol: SYMBOL; @@ -12,9 +11,9 @@ interface Props { } export function CancelProfitOrderModal({symbol, price, quantity, orderID}: Props) { - const dispatch = useDispatch(); + const {api} = useAppContext(); - const emitConfirm = React.useCallback(() => void dispatch(cancelProfitOrder({orderID})), [dispatch, orderID]); + const emitConfirm = React.useCallback(() => api.cancelProfitOrder({orderID}), [api, orderID]); return ( diff --git a/client/src/components/modals/GeneralModal.tsx b/client/src/components/modals/GeneralModal.tsx new file mode 100644 index 00000000..adb3a658 --- /dev/null +++ b/client/src/components/modals/GeneralModal.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import {Modal} from 'components'; + +interface Props { + title: string; + subtitle: string; + onConfirm: () => void; +} + +export function GeneralModal({title, subtitle, onConfirm}: Props) { + return ( + + {subtitle} + + ); +} diff --git a/client/src/components/modals/index.tsx b/client/src/components/modals/index.tsx index 3c1e8fd4..b22b5084 100644 --- a/client/src/components/modals/index.tsx +++ b/client/src/components/modals/index.tsx @@ -3,3 +3,5 @@ export * from './CancelProfitOrderModal'; export * from './CancelAllOrdersModal'; export * from './CancelAllProfitOrdersModal'; export * from './AddProfitOrderModal'; +export * from './GeneralModal'; +export * from './AddApiKeysModal'; diff --git a/client/src/containers/CrossOrder/CrossOrderContainer.spec.ts b/client/src/containers/CrossOrder/CrossOrderContainer.spec.ts index e8004e2d..17cf0f03 100644 --- a/client/src/containers/CrossOrder/CrossOrderContainer.spec.ts +++ b/client/src/containers/CrossOrder/CrossOrderContainer.spec.ts @@ -2,16 +2,16 @@ import {CROSS_ORDER_CONTAINER} from 'data-test-ids'; import {SIDE, SYMBOL} from 'redux/api/bitmex/types'; import {partialInstrument, updateInstrument} from 'tests/websocketData/instrument'; import CrossOrderContainer from './CrossOrderContainer'; -import {createRenderer} from 'tests/influnt'; +import {createMainRenderer} from 'tests/influnt'; import {getState, openWebsocket, sendWebsocketMessage, storeActions} from 'tests/helpers'; -import {createMockedStore} from 'tests/mockStore'; import {textOf, isDisabled, respond} from 'influnt'; import {forgeMarketOrder} from 'tests/responses'; +import {Exchange} from 'redux/modules/settings/types'; // eslint-disable-next-line @typescript-eslint/no-empty-function const forceRerender = () => {}; -const render = createRenderer(CrossOrderContainer, {extraArgs: () => createMockedStore({})}); +const render = createMainRenderer(CrossOrderContainer, {passProps: {exchange: Exchange.BitMeX}}); describe('CrossOrderContainer', () => { it('should render submit button as disabled when not subscribed to ws', async () => { @@ -39,7 +39,7 @@ describe('CrossOrderContainer', () => { }); expect(result).toEqual({ - actions: ['REDUX_WEBSOCKET::OPEN', 'REDUX_WEBSOCKET::MESSAGE'], + actions: ['bitmex::OPEN', 'bitmex::MESSAGE'], isDisabled: true, submitButtonLabel: 'Place a crossunder-market sell order', }); @@ -59,12 +59,7 @@ describe('CrossOrderContainer', () => { .inspect({actions: storeActions(), cross: getState('cross')}); expect(result).toEqual({ - actions: [ - 'REDUX_WEBSOCKET::OPEN', - 'REDUX_WEBSOCKET::MESSAGE', - 'cross/CREATE_CROSS_ORDER', - 'cross/ORDER_CROSSED_ONCE', - ], + actions: ['bitmex::OPEN', 'bitmex::MESSAGE', 'cross/CREATE_CROSS_ORDER', 'cross/ORDER_CROSSED_ONCE'], cross: { crossOrderPrice: 10000, crossOrderQuantity: 200, @@ -95,10 +90,10 @@ describe('CrossOrderContainer', () => { expect(result).toEqual({ actions: [ - 'REDUX_WEBSOCKET::OPEN', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::OPEN', + 'bitmex::MESSAGE', 'cross/CREATE_CROSS_ORDER', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::MESSAGE', 'cross/ORDER_CROSSED_ONCE', 'cross/CROSS_POST_MARKET_ORDER/pending', 'cross/CROSS_POST_MARKET_ORDER/fulfilled', @@ -128,8 +123,8 @@ describe('CrossOrderContainer', () => { expect(result).toEqual({ actions: [ - 'REDUX_WEBSOCKET::OPEN', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::OPEN', + 'bitmex::MESSAGE', 'cross/CREATE_CROSS_ORDER', 'cross/ORDER_CROSSED_ONCE', 'cross/CLEAR_CROSS_ORDER', @@ -170,12 +165,12 @@ describe('CrossOrderContainer', () => { expect(result).toEqual({ actions: [ - 'REDUX_WEBSOCKET::OPEN', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::OPEN', + 'bitmex::MESSAGE', 'cross/CREATE_CROSS_ORDER', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::MESSAGE', 'cross/ORDER_CROSSED_ONCE', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::MESSAGE', 'cross/CROSS_POST_MARKET_ORDER/pending', 'cross/CROSS_POST_MARKET_ORDER/fulfilled', ], diff --git a/client/src/containers/CrossOrder/CrossOrderContainer.tsx b/client/src/containers/CrossOrder/CrossOrderContainer.tsx index 8ba33a3a..7a2afcdc 100644 --- a/client/src/containers/CrossOrder/CrossOrderContainer.tsx +++ b/client/src/containers/CrossOrder/CrossOrderContainer.tsx @@ -4,11 +4,18 @@ import {SelectDropdown, InputField, Button, SideRadioButtons, Row, MainContainer import {SYMBOL, SIDE} from 'redux/api/bitmex/types'; import {CROSS_ORDER_CONTAINER} from 'data-test-ids'; import buildOrderPresenter from '../../presenters/cross-label-presenter'; -import {clearCrossOrder, createCrossOrder} from 'redux/modules/cross/crossModule'; +import {clearCrossOrder} from 'redux/modules/cross/crossModule'; import {useHooks} from './useHooks'; import {INSTRUMENT_PARAMS} from 'utils'; +import {useAppContext} from 'general/hooks'; +import {Exchange} from 'redux/modules/settings/types'; -export default React.memo(function CrossOrderContainer() { +interface Props { + exchange: Exchange; +} + +export default React.memo(function CrossOrderContainer({exchange}: Props) { + const {api} = useAppContext(); const dispatch = useDispatch(); const [symbol, setSymbol] = React.useState(SYMBOL.XBTUSD); @@ -16,15 +23,15 @@ export default React.memo(function CrossOrderContainer() { const [quantity, setQuantity] = React.useState(''); const [side, setSide] = React.useState(SIDE.SELL); - const {wsCrossPrice, connected, crossOrderPrice} = useHooks(); + const {wsCrossPrice, connected, crossOrderPrice} = useHooks(exchange); const createOrder = React.useCallback(() => { if (price && +price > 0 && quantity && +quantity > 0) { - dispatch(createCrossOrder({price: +price, symbol, side, orderQty: +quantity})); + api.createCrossOrder({price: +price, symbol, side, orderQty: +quantity}); setPrice(''); setQuantity(''); } - }, [dispatch, price, quantity, side, symbol]); + }, [api, price, quantity, side, symbol]); const cancelCrossOrder = React.useCallback(() => void dispatch(clearCrossOrder()), [dispatch]); diff --git a/client/src/containers/CrossOrder/useHooks.ts b/client/src/containers/CrossOrder/useHooks.ts index d9d1f5c5..d88162e6 100644 --- a/client/src/containers/CrossOrder/useHooks.ts +++ b/client/src/containers/CrossOrder/useHooks.ts @@ -1,18 +1,21 @@ import {useEffect} from 'react'; import {shallowEqual, useDispatch, useSelector} from 'react-redux'; import {AppState} from 'redux/modules/state'; -import {orderCrossedOnce, postMarketCrossOrder} from 'redux/modules/cross/crossModule'; +import {orderCrossedOnce} from 'redux/modules/cross/crossModule'; import {hasCrossedOnceSelector, hasCrossedSecondTimeSelector, websocketCrossPriceSelector} from 'redux/selectors'; +import {useAppContext} from 'general/hooks'; +import {Exchange} from 'redux/modules/settings/types'; -export function useHooks() { +export function useHooks(exchange: Exchange) { + const {api} = useAppContext(); const {hasCrossedOnce, hasCrossedSecondTime, wsCrossPrice, connected, crossOrderPrice, hasPriceCrossedOnce} = useSelector((state: AppState) => { const {websocket, cross} = state; return { - hasCrossedOnce: hasCrossedOnceSelector(state), - hasCrossedSecondTime: hasCrossedSecondTimeSelector(state), - wsCrossPrice: websocketCrossPriceSelector(state), - connected: websocket.connected, + hasCrossedOnce: hasCrossedOnceSelector(state, exchange), + hasCrossedSecondTime: hasCrossedSecondTimeSelector(state, exchange), + wsCrossPrice: websocketCrossPriceSelector(state, exchange), + connected: websocket[exchange].connected, crossOrderPrice: cross.crossOrderPrice, hasPriceCrossedOnce: cross.hasPriceCrossedOnce, }; @@ -30,9 +33,9 @@ export function useHooks() { useEffect(() => { if (hasCrossedSecondTime) { //@ts-expect-error - dispatch(postMarketCrossOrder()); + api.postMarketCrossOrder(); } - }, [dispatch, hasCrossedSecondTime]); + }, [api, hasCrossedSecondTime]); return { wsCrossPrice, diff --git a/client/src/containers/MarketOrder/MarketOrderContainer.spec.tsx b/client/src/containers/MarketOrder/MarketOrderContainer.spec.tsx index 64abf8bf..39257bc8 100644 --- a/client/src/containers/MarketOrder/MarketOrderContainer.spec.tsx +++ b/client/src/containers/MarketOrder/MarketOrderContainer.spec.tsx @@ -2,12 +2,11 @@ import MarketOrderContainer from './MarketOrderContainer'; import {COMPONENTS, MARKET_CONTAINER} from 'data-test-ids'; import {forgeMarketOrder} from 'tests/responses'; import {SIDE, SYMBOL} from 'redux/api/bitmex/types'; -import {createRenderer} from 'tests/influnt'; -import {createMockedStore} from 'tests/mockStore'; +import {createMainRenderer} from 'tests/influnt'; import {isDisabled, respond, exists} from 'influnt'; import {storeActions} from 'tests/helpers'; -const render = createRenderer(MarketOrderContainer, {extraArgs: () => createMockedStore({})}); +const render = createMainRenderer(MarketOrderContainer); describe('MarketOrder', () => { it('should disable market buy and market sell buttons by default', async () => { diff --git a/client/src/containers/MarketOrder/MarketOrderContainer.tsx b/client/src/containers/MarketOrder/MarketOrderContainer.tsx index 7b20b03d..bbdbb60c 100644 --- a/client/src/containers/MarketOrder/MarketOrderContainer.tsx +++ b/client/src/containers/MarketOrder/MarketOrderContainer.tsx @@ -1,30 +1,30 @@ import React from 'react'; import {WarningTwoIcon} from '@chakra-ui/icons'; -import {useDispatch} from 'react-redux'; -import {postMarketOrder} from 'redux/modules/preview/previewModule'; -import {useReduxSelector} from 'redux/helpers/hookHelpers'; import {SYMBOL, SIDE} from 'redux/api/bitmex/types'; import {MARKET_CONTAINER} from 'data-test-ids'; import {SelectDropdown, InputField, Button, Row, MainContainer} from 'components'; +import {useAppContext} from 'general/hooks'; +import {useSelector} from 'react-redux'; +import {AppState} from 'redux/modules/state'; const icons = [{element: WarningTwoIcon, color: 'red', onHoverMessage: 'Minimum lotsize for XBT is 100'}]; export default React.memo(function MarketOrderContainer() { - const dispatch = useDispatch(); + const {api} = useAppContext(); const [symbol, setSymbol] = React.useState(SYMBOL.XBTUSD); const [quantity, setQuantity] = React.useState(''); - const {previewLoading} = useReduxSelector('previewLoading'); + const loading = useSelector((state: AppState) => state.preview.previewLoading); const submitMarketOrder = React.useCallback( (id: SIDE) => { if (quantity) { - dispatch(postMarketOrder({symbol, orderQty: +quantity, side: id})); + api.postMarketOrder({symbol, orderQty: +quantity, side: id}); } setQuantity(''); }, - [dispatch, symbol, quantity], + [symbol, quantity, api], ); return ( @@ -41,7 +41,7 @@ export default React.memo(function MarketOrderContainer() { id={SIDE.BUY} label="MARKET Buy" onClick={submitMarketOrder} - isLoading={previewLoading} + isLoading={loading} variant={SIDE.BUY} disabled={!quantity || +quantity > 20e6} /> @@ -50,7 +50,7 @@ export default React.memo(function MarketOrderContainer() { id={SIDE.SELL} label="MARKET Sell" onClick={submitMarketOrder} - isLoading={previewLoading} + isLoading={loading} variant={SIDE.SELL} disabled={!quantity || +quantity > 20e6} /> diff --git a/client/src/containers/OpenOrders/OpenOrdersContainer.spec.ts b/client/src/containers/OpenOrders/OpenOrdersContainer.spec.ts index 6d573e30..13996583 100644 --- a/client/src/containers/OpenOrders/OpenOrdersContainer.spec.ts +++ b/client/src/containers/OpenOrders/OpenOrdersContainer.spec.ts @@ -2,14 +2,13 @@ import {OpenOrdersContainer} from 'containers'; import {ADD_ORDER_MODAL, GLOBAL, OPEN_ORDERS_CONTAINER} from 'data-test-ids'; import {SIDE, SYMBOL} from 'redux/api/bitmex/types'; import {builderProfitOrder, buildOrder} from 'tests/builders'; -import {createRenderer} from 'tests/influnt'; +import {createMainRenderer} from 'tests/influnt'; import {countOf, exists, respond} from 'influnt'; import {forgeOpenOrders, forgeOrderCancel, forgeOrderCancelAll, forgeProfitTargetOrder} from 'tests/responses'; -import {createMockedStore} from 'tests/mockStore'; import {createProfitTarget} from 'utils'; import {getState, storeActions} from 'tests/helpers'; -const render = createRenderer(OpenOrdersContainer, {extraArgs: () => createMockedStore()}); +const render = createMainRenderer(OpenOrdersContainer); describe('OpenOrders', () => { it('should show empty cta when there are no open orders', async () => { @@ -56,15 +55,15 @@ describe('OpenOrders', () => { }); it('should cancel an open order', async () => { - const orderID1 = 'OrderID1'; + const orderID = 'OrderID1'; const [getOpenOrdersPromise, orderCancelPromise] = [ - respond('getOpenOrders', [undefined]).with(forgeOpenOrders([buildOrder({orderID: orderID1}), buildOrder()])), - respond('orderCancel', [{orderID: [orderID1]}]).with(forgeOrderCancel([{orderID: orderID1}])), + respond('getOpenOrders', [undefined]).with(forgeOpenOrders([buildOrder({orderID}), buildOrder()])), + respond('orderCancel', [{orderID: [orderID]}]).with(forgeOrderCancel([{orderID}])), ]; const result = await render({mocks: [getOpenOrdersPromise]}) - .press(`${OPEN_ORDERS_CONTAINER.CANCEL}.${orderID1}`) + .press(`${OPEN_ORDERS_CONTAINER.CANCEL}.${orderID}`) .press(GLOBAL.MODAL_CONFIRM) .inspect({orderRowCountBefore: countOf(OPEN_ORDERS_CONTAINER.ORDER_ROW)}) .resolve(orderCancelPromise) @@ -81,7 +80,8 @@ describe('OpenOrders', () => { 'orders/CANCEL_ORDER/pending', 'orders/CANCEL_ORDER/fulfilled', ], - network: [{getOpenOrders: [undefined]}, {orderCancel: [{orderID: ['OrderID1']}]}], + network: [{getOpenOrders: [undefined]}, {orderCancel: [{orderID: [orderID]}]}], + modal: [{showCancelOrder: {orderID}}], emptyCtaVisible: false, orderRowCountBefore: 2, orderRowCountAfter: 1, @@ -123,6 +123,7 @@ describe('OpenOrders', () => { 'orders/CANCEL_ALL_ORDERS/pending', 'orders/CANCEL_ALL_ORDERS/fulfilled', ], + modal: [{showCancelAllOrders: {totalOrders: 2}}], emptyCtaVisible: true, orderRowCountBefore: 1, orderRowCountAfter: 0, @@ -135,11 +136,10 @@ describe('OpenOrders', () => { const profitOrderID1 = 'ProfitOrderID1'; const order = buildOrder({orderID: orderID1}); + const profitOrder = builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderID1}); const [getOpenOrdersPromise, orderCancelPromise] = [ - respond('getOpenOrders', [undefined]).with( - forgeOpenOrders([order, builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderID1})]), - ), + respond('getOpenOrders', [undefined]).with(forgeOpenOrders([order, profitOrder])), respond('orderCancel', [{orderID: profitOrderID1}]).with(forgeOrderCancel([{orderID: profitOrderID1}])), ]; @@ -167,6 +167,17 @@ describe('OpenOrders', () => { emptyCtaVisible: false, orderRowCountAfter: 1, orderRowCountBefore: 1, + modal: [ + { + showCancelProfitOrder: { + orderID: 'ProfitOrderID1', + price: profitOrder.price, + quantity: profitOrder.orderQty, + side: profitOrder.side, + symbol: profitOrder.symbol, + }, + }, + ], orders: { openOrders: [order], ordersError: '', @@ -179,7 +190,7 @@ describe('OpenOrders', () => { it('should cancel all profit orders of one of the open order`s', async () => { const orderID1 = 'OrderID1'; - const profitOrderIDs = ['ProfitOrderID1', 'ProfitOrderID2', 'ProfitOrderID3']; + const profitOrderIds = ['ProfitOrderID1', 'ProfitOrderID2', 'ProfitOrderID3']; const order = buildOrder({orderID: orderID1}); @@ -187,12 +198,12 @@ describe('OpenOrders', () => { respond('getOpenOrders', [undefined]).with( forgeOpenOrders([ order, - builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderIDs[0]}), - builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderIDs[1]}), - builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderIDs[2]}), + builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderIds[0]}), + builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderIds[1]}), + builderProfitOrder({orderID: orderID1, profitOrderID: profitOrderIds[2]}), ]), ), - respond('orderCancel', [{orderID: profitOrderIDs}]).with(forgeOrderCancel([{orderID: profitOrderIDs}])), + respond('orderCancel', [{orderID: profitOrderIds}]).with(forgeOrderCancel([{orderID: profitOrderIds}])), ]; const result = await render({mocks: [getOpenOrdersPromise]}) @@ -215,10 +226,11 @@ describe('OpenOrders', () => { 'orders/CANCEL_ALL_PROFIT_ORDERS/pending', 'orders/CANCEL_ALL_PROFIT_ORDERS/fulfilled', ], - network: [{getOpenOrders: [undefined]}, {orderCancel: [{orderID: profitOrderIDs}]}], + network: [{getOpenOrders: [undefined]}, {orderCancel: [{orderID: profitOrderIds}]}], emptyCtaVisible: false, orderRowCountAfter: 1, orderRowCountBefore: 1, + modal: [{showCancelAllProfitOrders: {profitOrderIds, totalOrders: 3}}], orders: { openOrders: [order], ordersError: '', @@ -264,6 +276,7 @@ describe('OpenOrders', () => { 'orders/CANCEL_ORDER/pending', 'orders/CANCEL_ORDER/fulfilled', ], + modal: [{showCancelOrder: {orderID: 'OrderID1'}}], network: [{getOpenOrders: [undefined]}, {orderCancel: [{orderID: ['OrderID1', 'ProfitOrderID1']}]}], emptyCtaVisible: true, orderRowCountAfter: 0, @@ -324,6 +337,7 @@ describe('OpenOrders', () => { ], }, ], + modal: [{showAddProfitTarget: {orderID: 'OrderID1'}}], emptyCtaVisible: false, orderRowCountAfter: 1, orderRowCountBefore: 1, @@ -402,6 +416,7 @@ describe('OpenOrders', () => { ], }, ], + modal: [{showAddProfitTarget: {orderID: 'OrderID1'}}], emptyCtaVisible: false, orderRowCountAfter: 1, orderRowCountBefore: 1, diff --git a/client/src/containers/OpenOrders/OpenOrdersContainer.tsx b/client/src/containers/OpenOrders/OpenOrdersContainer.tsx index 36d3da42..aff5c4ce 100644 --- a/client/src/containers/OpenOrders/OpenOrdersContainer.tsx +++ b/client/src/containers/OpenOrders/OpenOrdersContainer.tsx @@ -1,16 +1,16 @@ import React from 'react'; import {Tbody, Th, Thead, Tr, Table, Box} from '@chakra-ui/react'; import {RepeatIcon} from '@chakra-ui/icons'; -import {useDispatch} from 'react-redux'; -import {useReduxSelector} from 'redux/helpers/hookHelpers'; import {formatPrice} from 'general/formatting'; import {Order, ORD_TYPE} from 'redux/api/bitmex/types'; import {MainContainer} from 'components'; -import {getOpenOrders} from 'redux/modules/orders/ordersModule'; -import {useModal} from 'general/hooks'; +import {useAppContext, useModal} from 'general/hooks'; import {OPEN_ORDERS_CONTAINER} from 'data-test-ids'; import OpenOrderRow from './OpenOrderRow'; import ProfitOrderInActionRow from './ProfitOrderInActionRow'; +import {useSelector} from 'react-redux'; +import {AppState} from 'redux/modules/state'; +import {groupedOrdersSelector} from 'redux/selectors'; function Text({children}: {children: React.ReactNode}) { return ( @@ -47,24 +47,20 @@ export const presentOrderPrice = (order: Order) => { } }; -export default function OpenOrdersContainer() { - const dispatch = useDispatch(); +export default React.memo(function OpenOrdersContainer() { + const {api} = useAppContext(); const {modals} = useModal(); - const {openOrders, profitOrders, profitOrdersInAction, groupedOrders, ordersLoading, ordersError} = useReduxSelector( - 'openOrders', - 'profitOrders', - 'profitOrdersInAction', - 'groupedOrders', - 'ordersLoading', - 'ordersError', - ); - - const fetchOpenOrders = React.useCallback(() => void dispatch(getOpenOrders()), [dispatch]); + const openOrders = useSelector((state: AppState) => state.orders.openOrders); + const profitOrders = useSelector((state: AppState) => state.orders.profitOrders); + const profitOrdersInAction = useSelector((state: AppState) => state.orders.profitOrdersInAction); + const ordersLoading = useSelector((state: AppState) => state.orders.ordersLoading); + const ordersError = useSelector((state: AppState) => state.orders.ordersError); + const groupedOrders = useSelector(groupedOrdersSelector); React.useEffect(() => { - fetchOpenOrders(); - }, [fetchOpenOrders]); + api.getOpenOrders(); + }, [api]); const showCancelAllOrdersModal = React.useCallback(() => { const totalOrders = openOrders.length + profitOrders.length + profitOrdersInAction.length; @@ -85,8 +81,8 @@ export default function OpenOrdersContainer() { }, [ordersError, ordersLoading, openOrders, profitOrdersInAction]); const icons = React.useMemo( - () => [{element: RepeatIcon, onClick: !ordersLoading ? fetchOpenOrders : undefined, color: 'green'}], - [fetchOpenOrders, ordersLoading], + () => [{element: RepeatIcon, onClick: !ordersLoading ? api.getOpenOrders : undefined, color: 'green'}], + [api, ordersLoading], ); return ( @@ -126,4 +122,4 @@ export default function OpenOrdersContainer() { ); -} +}); diff --git a/client/src/containers/ScaledOrders/OrdersPreviewTable/OrdersPreviewTable.tsx b/client/src/containers/ScaledOrders/OrdersPreviewTable/OrdersPreviewTable.tsx index 9e9324b9..1eae0cec 100644 --- a/client/src/containers/ScaledOrders/OrdersPreviewTable/OrdersPreviewTable.tsx +++ b/client/src/containers/ScaledOrders/OrdersPreviewTable/OrdersPreviewTable.tsx @@ -4,11 +4,12 @@ import OrdersTable from './orders-table'; import DetailsTable from './details-table'; import styles from './OrdersPreviewTable.module.scss'; import {SCALED_CONTAINER} from 'data-test-ids'; -import {useReduxSelector} from 'redux/helpers/hookHelpers'; import {SYMBOL} from 'redux/api/bitmex/types'; +import {useSelector} from 'react-redux'; +import {AppState} from 'redux/modules/state'; export default function OrdersPreviewTable() { - const {orders} = useReduxSelector('orders'); + const orders = useSelector((state: AppState) => state.preview.orders); return ( { return ( diff --git a/client/src/containers/ScaledOrders/ScaledOrders.spec.ts b/client/src/containers/ScaledOrders/ScaledOrders.spec.ts index 5b92d8f5..e36cf415 100644 --- a/client/src/containers/ScaledOrders/ScaledOrders.spec.ts +++ b/client/src/containers/ScaledOrders/ScaledOrders.spec.ts @@ -3,8 +3,7 @@ import {COMPONENTS, SCALED_CONTAINER} from 'data-test-ids'; import {createScaledOrders, DISTRIBUTION} from 'utils'; import {SIDE, SYMBOL} from 'redux/api/bitmex/types'; import {forgeResult} from 'tests/responses'; -import {createRenderer} from 'tests/influnt'; -import {createMockedStore} from 'tests/mockStore'; +import {createMainRenderer} from 'tests/influnt'; import {InfluntEngine, respond, isDisabled, exists, countOf} from 'influnt'; import {storeActions} from 'tests/helpers'; @@ -30,7 +29,7 @@ function fillInputs({orderQty, n_tp, start, end, stop, symbol, side}: ScaledInpu }; } -const render = createRenderer(ScaledContainer, {extraArgs: () => createMockedStore()}); +const render = createMainRenderer(ScaledContainer); describe('ScaledOrders', () => { it('should render submit button as disabled', async () => { @@ -42,7 +41,7 @@ describe('ScaledOrders', () => { it('should submit sell scaled orders without stoploss', async () => { const input = {orderQty: 1000, n_tp: 2, start: 1000, end: 2000, side: SIDE.SELL, symbol: SYMBOL.XBTUSD, stop: 0}; const orders = createScaledOrders({ordersProps: input, distribution: DISTRIBUTION.Uniform}); - const promise = respond('orderBulk', [orders]).with(forgeResult(orders)); + const promise = respond('orderBulk', [{orders}]).with(forgeResult(orders)); const result = await render() .apply(fillInputs(input)) @@ -53,7 +52,7 @@ describe('ScaledOrders', () => { expect(result).toEqual({ actions: ['preview/PREVIEW_POST_ORDER/pending', 'preview/PREVIEW_POST_ORDER/fulfilled'], - network: [{orderBulk: [orders]}], + network: [{orderBulk: [{orders}]}], spinnerVisible: true, toast: [{message: 'Submitted Scaled Orders', toastPreset: 'success'}], }); @@ -62,7 +61,7 @@ describe('ScaledOrders', () => { it('should submit buy scaled orders without stoploss', async () => { const input = {orderQty: 1000, n_tp: 2, start: 1000, end: 2000, side: SIDE.BUY, symbol: SYMBOL.XBTUSD, stop: 0}; const orders = createScaledOrders({ordersProps: input, distribution: DISTRIBUTION.Uniform}); - const promise = respond('orderBulk', [orders]).with(forgeResult(orders)); + const promise = respond('orderBulk', [{orders}]).with(forgeResult(orders)); const result = await render() .apply(fillInputs(input)) @@ -73,7 +72,7 @@ describe('ScaledOrders', () => { expect(result).toEqual({ actions: ['preview/PREVIEW_POST_ORDER/pending', 'preview/PREVIEW_POST_ORDER/fulfilled'], - network: [{orderBulk: [orders]}], + network: [{orderBulk: [{orders}]}], spinnerVisible: true, toast: [{message: 'Submitted Scaled Orders', toastPreset: 'success'}], }); @@ -90,7 +89,7 @@ describe('ScaledOrders', () => { stop: 3000, }; const orders = createScaledOrders({ordersProps: input, distribution: DISTRIBUTION.Uniform}); - const promise = respond('orderBulk', [orders]).with(forgeResult(orders)); + const promise = respond('orderBulk', [{orders}]).with(forgeResult(orders)); const result = await render() .apply(fillInputs({orderQty: 1000, n_tp: 2, start: 1000, end: 20, stop: 3000})) @@ -101,7 +100,7 @@ describe('ScaledOrders', () => { expect(result).toEqual({ actions: ['preview/PREVIEW_POST_ORDER/pending', 'preview/PREVIEW_POST_ORDER/fulfilled'], - network: [{orderBulk: [orders]}], + network: [{orderBulk: [{orders}]}], spinnerVisible: true, toast: [{message: 'Submitted Scaled Orders', toastPreset: 'success'}], }); diff --git a/client/src/containers/ScaledOrders/ScaledOrders.tsx b/client/src/containers/ScaledOrders/ScaledOrders.tsx index 876dfeb8..05264a84 100644 --- a/client/src/containers/ScaledOrders/ScaledOrders.tsx +++ b/client/src/containers/ScaledOrders/ScaledOrders.tsx @@ -1,15 +1,16 @@ import React from 'react'; -import {useDispatch} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import {Box, Tooltip} from '@chakra-ui/react'; import {WarningTwoIcon, WarningIcon} from '@chakra-ui/icons'; import OrdersPreviewTable from './OrdersPreviewTable/OrdersPreviewTable'; -import {previewOrders, previewToggle, postOrderBulk} from 'redux/modules/preview/previewModule'; +import {previewOrders, previewToggle} from 'redux/modules/preview/previewModule'; import {InputField, SelectDropdown, MainContainer, Button, SideRadioButtons, Row} from 'components'; import DistributionsRadioGroup from './DistributionsRadioGroup'; import {createScaledOrders, DISTRIBUTION, INSTRUMENT_PARAMS} from 'utils'; import {SIDE, SYMBOL} from 'redux/api/bitmex/types'; import {SCALED_CONTAINER} from 'data-test-ids'; -import {useReduxSelector} from 'redux/helpers/hookHelpers'; +import {useAppContext} from 'general/hooks'; +import {AppState} from 'redux/modules/state'; const icons = [{element: WarningTwoIcon, color: 'red', onHoverMessage: 'Minimum lotsize for XBT is 100'}]; @@ -36,8 +37,10 @@ const initialState: Readonly = { }; export default React.memo(function ScaledContainer() { + const {api} = useAppContext(); const dispatch = useDispatch(); - const {showPreview, previewLoading} = useReduxSelector('showPreview', 'previewLoading'); + const showPreview = useSelector((state: AppState) => state.preview.showPreview); + const previewLoading = useSelector((state: AppState) => state.preview.previewLoading); const [state, setState] = React.useState(initialState); const [isDirty, setDirty] = React.useState(false); @@ -65,10 +68,10 @@ export default React.memo(function ScaledContainer() { const onOrderSubmit = React.useCallback((): void => { const {distribution, ...rest} = state as RequiredProperty; const ordersProps = {...rest, start: +rest.start, end: +rest.end, stop: rest.stop != undefined ? +rest.stop : 0}; - dispatch(postOrderBulk(createScaledOrders({ordersProps, distribution}))); + api.postOrderBulk({orders: createScaledOrders({ordersProps, distribution})}); setState(initialState); setDirty(true); - }, [dispatch, state]); + }, [api, state]); const onPreviewOrders = React.useCallback((): void => { if (!isDirty) { diff --git a/client/src/containers/TickerPrices/TickerPricesContainer.tsx b/client/src/containers/TickerPrices/TickerPricesContainer.tsx index dc742096..c93f6cb7 100644 --- a/client/src/containers/TickerPrices/TickerPricesContainer.tsx +++ b/client/src/containers/TickerPrices/TickerPricesContainer.tsx @@ -8,6 +8,7 @@ import {MainContainer, Row} from 'components'; import styles from './TickerPricesContainer.module.scss'; import {AppState} from 'redux/modules/state'; import {formatPrice} from 'general/formatting'; +import {useExchange} from 'general/hooks'; const none = '---' as unknown as number; @@ -18,8 +19,9 @@ const defaultData: SymbolPrices[] = [ ]; export default React.memo(function TickerPricesContainer() { - const wsMessage = useSelector((state: AppState) => state.websocket.message); - const allPrices = useSelector(allWebsocketBidAskPrices, isEqual); + const exchange = useExchange(); + const wsMessage = useSelector((state: AppState) => state.websocket[exchange].message); + const allPrices = useSelector((state: AppState) => allWebsocketBidAskPrices(state, exchange), isEqual); const data = allPrices?.length ? allPrices : defaultData; diff --git a/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.spec.ts b/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.spec.ts index e031d9b5..a9a7fd2b 100644 --- a/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.spec.ts +++ b/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.spec.ts @@ -6,13 +6,15 @@ import {partialInstrument, updateInstrument} from 'tests/websocketData/instrumen import {partialOrder} from 'tests/websocketData/order'; import {forgeAmendOrder, forgeLimitOrder} from 'tests/responses'; import {getState, openWebsocket, sendWebsocketMessage, storeActions} from 'tests/helpers'; -import {createRenderer} from 'tests/influnt'; +import {createMainRenderer} from 'tests/influnt'; import {textOf, isDisabled, respond} from 'influnt'; import {createMockedStore} from 'tests/mockStore'; +import {createMemoryHistory} from 'history'; +import {Exchange} from 'redux/modules/settings/types'; const orderID = 'OrderId'; -const render = createRenderer(TrailingLimitOrderContainer, {extraArgs: () => createMockedStore()}); +const render = createMainRenderer(TrailingLimitOrderContainer, {passProps: {exchange: Exchange.BitMeX}}); describe('TrailingLimitContainer', () => { const commonOrder = ({orderQty, price}: {orderQty: number; price: number}) => ({ @@ -48,7 +50,7 @@ describe('TrailingLimitContainer', () => { }); expect(result).toEqual({ - actions: ['REDUX_WEBSOCKET::OPEN', 'REDUX_WEBSOCKET::MESSAGE'], + actions: ['bitmex::OPEN', 'bitmex::MESSAGE'], isDisabled: true, submitButtonLabel: 'Submit order at 10,322.0', }); @@ -64,7 +66,21 @@ describe('TrailingLimitContainer', () => { instrument: [{symbol: SYMBOL.XBTUSD, askPrice: 501, bidPrice: 500.5}], }); - const result = await render({extraArgs: createMockedStore({websocket})}) + const result = await render({ + extraArgs: { + store: createMockedStore({ + websocket, + settings: { + activeExchange: Exchange.BitMeX, + activeApiKeys: {bitmex: true, bitmexTEST: false}, + settingsLoading: false, + settingsError: '', + getAllApiKeysLoading: false, + }, + }), + history: createMemoryHistory(), + }, + }) .inputText(TRAILING_LIMIT_CONTAINER.QUANTITY_INPUT, '200') .press(TRAILING_LIMIT_CONTAINER.SUBMIT_TRAILING_ORDER) .resolve(mock) @@ -72,11 +88,7 @@ describe('TrailingLimitContainer', () => { .inspect({actions: storeActions(), trailing: getState('trailing')}); expect(result).toEqual({ - actions: [ - 'trailing/POST_TRAILING_ORDER/pending', - 'trailing/POST_TRAILING_ORDER/fulfilled', - 'REDUX_WEBSOCKET::MESSAGE', - ], + actions: ['trailing/POST_TRAILING_ORDER/pending', 'trailing/POST_TRAILING_ORDER/fulfilled', 'bitmex::MESSAGE'], network: [{limitOrder: [{orderQty: 200, price: 501, side: 'Sell', symbol: 'XBTUSD', text: 'best_order'}]}], toast: [{message: 'Trailing Order placed at 501', toastPreset: 'success'}], trailing: { @@ -107,11 +119,11 @@ describe('TrailingLimitContainer', () => { expect(result).toEqual({ actions: [ - 'REDUX_WEBSOCKET::OPEN', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::OPEN', + 'bitmex::MESSAGE', 'trailing/POST_TRAILING_ORDER/pending', 'trailing/POST_TRAILING_ORDER/fulfilled', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::MESSAGE', 'trailing/__CLEAR_TRAILING_ORDER', ], network: [{limitOrder: [{orderQty: 200, price: 10322, side: 'Sell', symbol: 'XBTUSD', text: 'best_order'}]}], @@ -149,12 +161,12 @@ describe('TrailingLimitContainer', () => { expect(result).toEqual({ actions: [ - 'REDUX_WEBSOCKET::OPEN', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::OPEN', + 'bitmex::MESSAGE', 'trailing/POST_TRAILING_ORDER/pending', 'trailing/POST_TRAILING_ORDER/fulfilled', - 'REDUX_WEBSOCKET::MESSAGE', - 'REDUX_WEBSOCKET::MESSAGE', + 'bitmex::MESSAGE', + 'bitmex::MESSAGE', 'trailing/PUT_TRAILING_ORDER/pending', 'trailing/PUT_TRAILING_ORDER/fulfilled', ], diff --git a/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.tsx b/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.tsx index dd3fa581..c234231f 100644 --- a/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.tsx +++ b/client/src/containers/TrailingLimitOrder/TrailingLimitOrderContainer.tsx @@ -2,25 +2,32 @@ import React from 'react'; import {useDispatch} from 'react-redux'; import {Text} from '@chakra-ui/react'; import {WarningTwoIcon} from '@chakra-ui/icons'; -import {postTrailingOrder, cancelTrailingOrder, changeTrailingOrderSymbol} from 'redux/modules/trailing/trailingModule'; +import {changeTrailingOrderSymbol} from 'redux/modules/trailing/trailingModule'; import {SYMBOL, SIDE} from 'redux/api/bitmex/types'; import {SelectDropdown, InputField, Button, SideRadioButtons, Row, MainContainer} from 'components'; import {TRAILING_LIMIT_CONTAINER} from 'data-test-ids'; import buildOrderPresenter from '../../presenters/trailing-label-presenter'; import {useHooks} from './useHooks'; import {INSTRUMENT_PARAMS} from 'utils'; +import {useAppContext} from 'general/hooks'; +import {Exchange} from 'redux/modules/settings/types'; const icons = [{element: WarningTwoIcon, color: 'red', onHoverMessage: 'Minimum lotsize for XBT is 100'}]; -export default React.memo(function TrailingLimitOrderContainer() { +interface Props { + exchange: Exchange; +} + +export default React.memo(function TrailingLimitOrderContainer({exchange}: Props) { const dispatch = useDispatch(); + const {api} = useAppContext(); const [symbol, setSymbol] = React.useState(SYMBOL.XBTUSD); const [side, setSide] = React.useState(SIDE.SELL); const [quantity, setQuantity] = React.useState(''); const {wsCurrentPrice, wsBidAskPrices, trailOrderId, trailOrderStatus, trailOrderPrice, status, connected} = - useHooks(); + useHooks(exchange); const spread = 1 / INSTRUMENT_PARAMS[symbol].ticksize; const trailingOrderPrice = @@ -28,13 +35,12 @@ export default React.memo(function TrailingLimitOrderContainer() { const submitTrailingOrder = React.useCallback(() => { if (trailingOrderPrice && quantity) { - const payload = {symbol, side, orderQty: +quantity, price: trailingOrderPrice, text: 'best_order'}; - dispatch(postTrailingOrder(payload)); + api.postTrailingOrder({symbol, side, orderQty: +quantity, price: trailingOrderPrice, text: 'best_order'}); setQuantity(''); } - }, [dispatch, trailingOrderPrice, quantity, side, symbol]); + }, [api, trailingOrderPrice, quantity, side, symbol]); - const cancelOrder = React.useCallback(() => void dispatch(cancelTrailingOrder({} as any)), [dispatch]); + const cancelOrder = React.useCallback(() => void api.cancelTrailingOrder({} as any), [api]); const toggleInstrument = React.useCallback( (symbol: SYMBOL) => { diff --git a/client/src/containers/TrailingLimitOrder/useHooks.ts b/client/src/containers/TrailingLimitOrder/useHooks.ts index a258a415..b004edf5 100644 --- a/client/src/containers/TrailingLimitOrder/useHooks.ts +++ b/client/src/containers/TrailingLimitOrder/useHooks.ts @@ -7,9 +7,12 @@ import { websocketCurrentPrice, websocketTrailingPriceSelector, } from 'redux/selectors'; -import {ammendTrailingOrder, __clearTrailingOrder} from 'redux/modules/trailing/trailingModule'; +import {__clearTrailingOrder} from 'redux/modules/trailing/trailingModule'; +import {useAppContext} from 'general/hooks'; +import {Exchange} from 'redux/modules/settings/types'; -export function useHooks() { +export function useHooks(exchange: Exchange) { + const {api} = useAppContext(); const { wsTrailingPrice, wsCurrentPrice, @@ -23,11 +26,11 @@ export function useHooks() { } = useSelector((state: AppState) => { const {websocket, trailing} = state; return { - wsCurrentPrice: websocketCurrentPrice(state), - wsTrailingPrice: websocketTrailingPriceSelector(state), - wsBidAskPrices: websocketBidAskPrices(state), - status: trailingOrderStatusSelector(state), - connected: websocket.connected, + wsCurrentPrice: websocketCurrentPrice(state, exchange), + wsTrailingPrice: websocketTrailingPriceSelector(state, exchange), + wsBidAskPrices: websocketBidAskPrices(state, exchange), + status: trailingOrderStatusSelector(state, exchange), + connected: websocket[exchange].connected, trailOrderId: trailing.trailOrderId, trailOrderPrice: trailing.trailOrderPrice, trailOrderStatus: trailing.trailOrderStatus, @@ -42,10 +45,10 @@ export function useHooks() { if (wsTrailingPrice && trailOrderPrice && !statuses.includes(status)) { const toAmmend = wsTrailingPrice !== trailOrderPrice; if (toAmmend) { - dispatch(ammendTrailingOrder({orderID: trailOrderId, price: wsTrailingPrice})); + api.ammendTrailingOrder({orderID: trailOrderId, price: wsTrailingPrice}); } } - }, [dispatch, trailOrderPrice, trailOrderId, trailOrderSide, status, wsTrailingPrice]); + }, [api, trailOrderPrice, trailOrderId, trailOrderSide, status, wsTrailingPrice]); useEffect(() => { const statuses = ['Filled', 'Canceled', 'Order not placed.']; diff --git a/client/src/context/app-context.tsx b/client/src/context/app-context.tsx new file mode 100644 index 00000000..05d04567 --- /dev/null +++ b/client/src/context/app-context.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import {useAppDispatch} from 'redux/store'; +import {Exchange} from 'redux/modules/settings/types'; +import * as ordersModule from 'redux/modules/orders/ordersModule'; +import * as previewModule from 'redux/modules/preview/previewModule'; +import * as trailingModule from 'redux/modules/trailing/trailingModule'; +import * as crossModule from 'redux/modules/cross/crossModule'; + +const modules = {...ordersModule, ...previewModule, ...trailingModule, ...crossModule}; + +type ReduxModules = typeof modules; + +type ApiActions = { + [key in keyof ReduxModules]: ( + params: ReduxModules[key] extends (...args: any) => any + ? Omit[number], 'exchange'> extends {exchange?: Exchange} + ? void + : Omit[number], 'exchange'> + : void, + ) => void; +}; + +export interface AppContext { + api: ApiActions; +} + +const initialContext = { + api: undefined, +} as unknown as AppContext; + +export const AppContext = React.createContext(initialContext); + +export const AppProvider = React.memo(({children}: {children: React.ReactNode}) => { + const dispatch = useAppDispatch(); + + const context: AppContext = React.useMemo( + () => ({ + api: new Proxy(modules, { + get(target: ReduxModules, key: keyof ReduxModules) { + if (typeof target[key] === 'function') { + return (params: any) => { + //@ts-ignore + dispatch(target[key](params)); + }; + } + return undefined; + }, + }) as unknown as ApiActions, + }), + [dispatch], + ); + + return {children}; +}); diff --git a/client/src/context/registerModals.ts b/client/src/context/registerModals.ts index d37dc01c..56dab6e4 100644 --- a/client/src/context/registerModals.ts +++ b/client/src/context/registerModals.ts @@ -5,6 +5,8 @@ import { CancelProfitOrderModal, CancelAllProfitOrdersModal, AddProfitOrderModal, + GeneralModal, + AddApiKeysModal, } from 'components/modals'; export type ShowModalArgs = { @@ -23,6 +25,8 @@ const registeredModals = { showCancelAllOrders: CancelAllOrdersModal, showCancelAllProfitOrders: CancelAllProfitOrdersModal, showAddProfitTarget: AddProfitOrderModal, + showGeneralModal: GeneralModal, + showAddApiKeys: AddApiKeysModal, }; export function showRegisteredModal

(type: keyof RegisteredModals, modalProps: P) { @@ -33,7 +37,7 @@ export function createModals(showModal: ({type, props}: ShowModalArgs) => void): return Object.assign( {}, ...Object.keys(registeredModals).map((modalName) => ({ - [modalName]: (props: any) => showModal({type: modalName as any, props}), + [modalName]: (props: any) => showModal({type: modalName as keyof typeof registeredModals, props}), })), ); } diff --git a/client/src/data-test-ids.ts b/client/src/data-test-ids.ts index c0bea201..9f8ebc87 100644 --- a/client/src/data-test-ids.ts +++ b/client/src/data-test-ids.ts @@ -35,6 +35,11 @@ export const OPEN_ORDERS_CONTAINER = { ADD_PROFIT: 'OpenOrdersContainer.addProfit', }; +export const ADD_API_KEYS_MODAL = { + API_KEY: 'AddApiKeysModal.ApiKey', + API_SECRET: 'AddApiKeysModal.ApiSecret', +}; + export const ADD_ORDER_MODAL = { PRICE: 'AddOrderModal.price', QUANTITY: 'AddOrderModal.quantity', @@ -60,6 +65,16 @@ export const CROSS_ORDER_CONTAINER = { SIDE: 'CrossOrderContainer.side', }; +export const SETTINGS = { + API_KEY_ROW_STATUS: 'Settings.ApiKeyRowStatus', + API_KEY_ROW: 'Settings.ApiKeyRow', +}; + +export const HOME = { + ROW: 'Home.Row', + ICON: 'Home.Icon', +}; + export const GLOBAL = { SNACKBAR: 'Global.SnackBar', TOAST: 'Global.toast', diff --git a/client/src/general/hooks.ts b/client/src/general/hooks.ts index b80a3072..e142172d 100644 --- a/client/src/general/hooks.ts +++ b/client/src/general/hooks.ts @@ -1,8 +1,22 @@ import React from 'react'; import {ModalContext} from 'context/modal-context'; +import {useLocation} from 'react-router-dom'; +import {Exchange} from 'redux/modules/settings/types'; +import {AppContext} from 'context/app-context'; export function useModal() { const context = React.useContext(ModalContext); if (!context) throw new Error('Modal context: add wrapper'); return context; } + +export function useAppContext() { + const context = React.useContext(AppContext); + if (!context) throw new Error('App context: add wrapper'); + return context; +} + +export function useExchange(): Exchange { + const location = useLocation(); + return location.pathname.slice(1) as Exchange; +} diff --git a/client/src/index.tsx b/client/src/index.tsx index 737ec54a..41edda34 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,11 +1,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import {BrowserRouter} from 'react-router-dom'; import {Provider} from 'react-redux'; import {ChakraProvider, extendTheme} from '@chakra-ui/react'; import {createStore} from 'redux/store'; import App from './App'; import {ModalProvider} from 'context/modal-context'; - +import {AppProvider} from 'context/app-context'; import * as serviceWorker from './serviceWorker'; import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; @@ -108,11 +109,15 @@ const theme = extendTheme({ ReactDOM.render( - - - - - + + + + + + + + + , document.getElementById('root') as HTMLElement, ); diff --git a/client/src/pages/Bitmex.tsx b/client/src/pages/Bitmex.tsx new file mode 100644 index 00000000..95fb7373 --- /dev/null +++ b/client/src/pages/Bitmex.tsx @@ -0,0 +1,66 @@ +import 'scss/root.module.scss'; +import React from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import {Box} from '@chakra-ui/react'; +import { + ScaledOrders, + MarketOrderContainer, + TrailingLimitOrder, + TickerPricesContainer, + // CrossOrderContainer, + OpenOrdersContainer, +} from 'containers'; +import {Spinner, ToastContainer} from 'components'; +import {useReduxSelector} from 'redux/helpers/hookHelpers'; +import {wsConnect, wsDisconnect, wsSubscribeTo, wsAuthenticate} from 'redux/modules/websocket/websocketModule'; +import {useAppContext} from 'general/hooks'; +import {Banner} from 'components/Banner/Banner'; +import {Exchange} from 'redux/modules/settings/types'; +import {activeApiKeySelector} from 'redux/selectors'; + +const exchange = Exchange.BitMeX; + +const BitmexExchange = React.memo(() => { + const {api} = useAppContext(); + const isApiKeyActive = useSelector(activeApiKeySelector); + const dispatch = useDispatch(); + const {previewLoading, trailLoading, wsLoading, connected} = useReduxSelector( + exchange, + 'previewLoading', + 'trailLoading', + 'wsLoading', + 'connected', + ); + + React.useEffect(() => { + dispatch(wsConnect(exchange)); + + return () => { + dispatch(wsDisconnect(exchange)); + }; + }, [dispatch]); + + React.useEffect(() => { + if (connected && isApiKeyActive) { + api.getBalance(); + dispatch(wsAuthenticate()); + dispatch(wsSubscribeTo('order')); + } + }, [dispatch, api, connected, isApiKeyActive]); + + return ( + + {!isApiKeyActive && } + + + + + {/* TODO: disabling for now */} + + + + + ); +}); + +export default BitmexExchange; diff --git a/client/src/pages/Home.spec.tsx b/client/src/pages/Home.spec.tsx new file mode 100644 index 00000000..ac57122c --- /dev/null +++ b/client/src/pages/Home.spec.tsx @@ -0,0 +1,55 @@ +import {HOME} from 'data-test-ids'; +import {Exchange} from 'redux/modules/settings/types'; +import {respondBasic, history} from 'tests/helpers'; +import {createMainRenderer} from 'tests/influnt'; +import {RoutePath} from './paths'; +import Home from './Home'; +import {createMockedStore} from 'tests/mockStore'; +import {createMemoryHistory} from 'history'; + +const forgeResult = (data: R) => ({data: {data: data, statusCode: 200}}); + +const render = createMainRenderer(Home, { + extraArgs: () => ({store: createMockedStore(), history: createMemoryHistory()}), +}); + +describe('Home page', () => { + it('should navigate to exchange on row press even if the api key is not active', async () => { + const mock = respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: []})); + + const result = await render({mocks: [mock]}) + .press(HOME.ROW, {index: 0}) + .inspect({history: history()}); + + expect(result).toEqual({ + history: RoutePath.BitMex, + network: [{getAllApiKeys: [undefined]}], + }); + }); + + it('should navigate to exchange if api key is active and pressed on icon', async () => { + const mock = respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: [Exchange.BitMeX]})); + + const result = await render({mocks: [mock]}) + .press(HOME.ICON, {index: 0}) + .inspect({history: history()}); + + expect(result).toEqual({ + history: RoutePath.BitMex, + network: [{getAllApiKeys: [undefined]}], + }); + }); + + it('should navigate to settings if api key is not active and pressed on icon', async () => { + const mock = respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: []})); + + const result = await render({mocks: [mock]}) + .press(HOME.ICON, {index: 0}) + .inspect({history: history()}); + + expect(result).toEqual({ + history: RoutePath.Settings, + network: [{getAllApiKeys: [undefined]}], + }); + }); +}); diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx new file mode 100644 index 00000000..fd32519a --- /dev/null +++ b/client/src/pages/Home.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import {Link} from 'react-router-dom'; +import {CheckIcon, WarningIcon} from '@chakra-ui/icons'; +import {Box, Divider, Heading, Text, Tooltip} from '@chakra-ui/react'; +import {ExchangePresenter} from 'presenters/general-presenters'; +import {Exchange} from 'redux/modules/settings/types'; +import {useDispatch, useSelector} from 'react-redux'; +import {AppState} from 'redux/modules/state'; +import {getAllApiKeys} from 'redux/modules/settings/settingsModule'; +import {HOME} from 'data-test-ids'; + +interface ExchangeRowProps { + exchange: Exchange; + isActive: boolean; +} + +function ExchangeRow({exchange, isActive}: ExchangeRowProps) { + return ( + + + + + {ExchangePresenter[exchange]} + + + + + + + {isActive ? ( + + + + ) : ( + + + + )} + + + + ); +} + +const Home = React.memo(() => { + const dispatch = useDispatch(); + const activeApiKeys = useSelector((state: AppState) => state.settings.activeApiKeys); + + React.useEffect(() => { + dispatch(getAllApiKeys()); + }, [dispatch]); + + return ( + + + Available Exchanges + + {Object.entries(activeApiKeys).map(([exchange, isActive]) => ( + + ))} + + + ); +}); + +export default Home; diff --git a/client/src/pages/NotFound.tsx b/client/src/pages/NotFound.tsx new file mode 100644 index 00000000..84cda840 --- /dev/null +++ b/client/src/pages/NotFound.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import {Heading, Text} from '@chakra-ui/react'; +import {Link} from 'react-router-dom'; +import {RoutePath} from './paths'; + +export default function NotFound() { + return ( + <> + + 404 Not Found + + + Go back to Home + + + ); +} diff --git a/client/src/pages/Settings.spec.ts b/client/src/pages/Settings.spec.ts new file mode 100644 index 00000000..1a8ec741 --- /dev/null +++ b/client/src/pages/Settings.spec.ts @@ -0,0 +1,107 @@ +import {ADD_API_KEYS_MODAL, GLOBAL, SETTINGS} from 'data-test-ids'; +import {createMemoryHistory} from 'history'; +import {textOfAll} from 'influnt'; +import {Exchange} from 'redux/modules/settings/types'; +import {respondBasic} from 'tests/helpers'; +import {createMainRenderer} from 'tests/influnt'; +import {createMockedStore} from 'tests/mockStore'; +import Settings from './Settings'; + +const forgeResult = (data: R) => ({data: {data: data, statusCode: 200}}); + +const render = createMainRenderer(Settings, { + extraArgs: () => ({store: createMockedStore(), history: createMemoryHistory()}), +}); + +describe('Settings page', () => { + it('should display all api keys as inactive', async () => { + const mock = respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: []})); + + const result = await render({mocks: [mock]}).inspect({itemStatuses: textOfAll(SETTINGS.API_KEY_ROW_STATUS)}); + + expect(result).toEqual({ + itemStatuses: ['Empty', 'Empty'], + network: [{getAllApiKeys: [undefined]}], + }); + }); + + it('should display one api keys as active', async () => { + const mock = respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: [Exchange.BitMeX]})); + + const result = await render({mocks: [mock]}).inspect({itemStatuses: textOfAll(SETTINGS.API_KEY_ROW_STATUS)}); + + expect(result).toEqual({ + itemStatuses: ['Active', 'Empty'], + network: [{getAllApiKeys: [undefined]}], + }); + }); + + it('should show modal for clearing the api key if pressed on the active one', async () => { + const mock = respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: [Exchange.BitMeX]})); + + const result = await render({mocks: [mock]}).press(SETTINGS.API_KEY_ROW, {index: 0}); + + expect(result).toEqual({ + network: [{getAllApiKeys: [undefined]}], + modal: [{showGeneralModal: ['Clear BITMEX API Key', 'This will clear api key entry of BITMEX exchange']}], + }); + }); + + it('should show modal for adding the api key if pressed on the empty one', async () => { + const mock = respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: [Exchange.BitMeX]})); + + const result = await render({mocks: [mock]}).press(SETTINGS.API_KEY_ROW, {index: 1}); + + expect(result).toEqual({ + network: [{getAllApiKeys: [undefined]}], + modal: [{showAddApiKeys: {exchange: Exchange.BitMeXTEST}}], + }); + }); + + it('should add api key', async () => { + const key = '12312314141414144414'; + const secret = '12312314141414144414'; + const [getAllApiKeysResponse, saveApiKeyResponse] = [ + respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: []})), + //@ts-expect-error + respondBasic('saveApiKey', [{key, secret}]).with(forgeResult({exchange: Exchange.BitMeX})), + ]; + + const result = await render({mocks: [getAllApiKeysResponse]}) + .inspect({itemStatuses: textOfAll(SETTINGS.API_KEY_ROW_STATUS)}) + .press(SETTINGS.API_KEY_ROW, {index: 0}) + .inputText(ADD_API_KEYS_MODAL.API_KEY, key) + .inputText(ADD_API_KEYS_MODAL.API_SECRET, secret) + .press(GLOBAL.MODAL_CONFIRM) + .resolve(saveApiKeyResponse) + .inspect({itemStatusesAfter: textOfAll(SETTINGS.API_KEY_ROW_STATUS)}); + + expect(result).toEqual({ + itemStatuses: ['Empty', 'Empty'], + itemStatusesAfter: ['Active', 'Empty'], + modal: [{showAddApiKeys: {exchange: Exchange.BitMeX}}], + network: [{getAllApiKeys: [undefined]}, {saveApiKey: [{key, secret}]}], + }); + }); + + it('should remove api key', async () => { + const [getAllApiKeysResponse, deleteApiKeyResponse] = [ + respondBasic('getAllApiKeys', [undefined]).with(forgeResult({exchanges: [Exchange.BitMeX]})), + respondBasic('deleteApiKey', [Exchange.BitMeX]).with(forgeResult(Exchange.BitMeX)), + ]; + + const result = await render({mocks: [getAllApiKeysResponse]}) + .inspect({itemStatuses: textOfAll(SETTINGS.API_KEY_ROW_STATUS)}) + .press(SETTINGS.API_KEY_ROW, {index: 0}) + .press(GLOBAL.MODAL_CONFIRM) + .resolve(deleteApiKeyResponse) + .inspect({itemStatusesAfter: textOfAll(SETTINGS.API_KEY_ROW_STATUS)}); + + expect(result).toEqual({ + itemStatuses: ['Active', 'Empty'], + itemStatusesAfter: ['Empty', 'Empty'], + modal: [{showGeneralModal: ['Clear BITMEX API Key', 'This will clear api key entry of BITMEX exchange']}], + network: [{getAllApiKeys: [undefined]}, {deleteApiKey: [Exchange.BitMeX]}], + }); + }); +}); diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx new file mode 100644 index 00000000..4f4ad457 --- /dev/null +++ b/client/src/pages/Settings.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import {Badge, Box, Divider, Heading, Text} from '@chakra-ui/react'; +import {Button} from 'components'; +import {AppState} from 'redux/modules/state'; +import {Exchange} from 'redux/modules/settings/types'; +import {useModal} from 'general/hooks'; +import {deleteAllApiKeys, deleteApiKey, getAllApiKeys} from 'redux/modules/settings/settingsModule'; +import {ExchangePresenter} from 'presenters/general-presenters'; +import {SETTINGS} from 'data-test-ids'; + +interface ItemProps { + title: string; + isActive: boolean; + exchange: Exchange; + onClick: (isActive: boolean, exchange: Exchange) => void; +} + +const ApiKeySettingRow = React.memo(({title, isActive, exchange, onClick}: ItemProps) => { + const color = isActive ? '#4caf50' : 'grey'; + return ( + onClick(isActive, exchange)} + > + + + {title} + + Add api keys for authenticated requests + + + {isActive ? Active : Empty} + + + ); +}); + +export default function Settings() { + const dispatch = useDispatch(); + const {modals} = useModal(); + + const activeApiKeys = useSelector((state: AppState) => state.settings.activeApiKeys); + + React.useEffect(() => { + dispatch(getAllApiKeys()); + }, [dispatch]); + + const confirmDeleteAllApiKeys = React.useCallback(() => { + modals.showGeneralModal({ + title: 'Clear all API Keys', + subtitle: 'This will clear all api keys and remove the folder saving them', + onConfirm: () => dispatch(deleteAllApiKeys()), + }); + }, [dispatch, modals]); + + const configureApiKey = React.useCallback( + (isActive: boolean, exchange: Exchange) => { + isActive + ? modals.showGeneralModal({ + title: `Clear ${ExchangePresenter[exchange]} API Key`, + subtitle: `This will clear api key entry of ${ExchangePresenter[exchange]} exchange`, + onConfirm: () => dispatch(deleteApiKey(exchange)), + }) + : modals.showAddApiKeys({exchange}); + }, + [modals, dispatch], + ); + + return ( + + + Settings + + {Object.entries(activeApiKeys).map(([exchange, isActive]) => ( + + ))} + +