diff --git a/client/asset/interface.go b/client/asset/interface.go index 6823904441..2621913512 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -119,7 +119,7 @@ type WalletInfo struct { // Version is the Wallet's version number, which is used to signal when // major changes are made to internal details such as coin ID encoding and // contract structure that must be common to a server's. - Version uint32 + Version uint32 `json:"version"` // AvailableWallets is an ordered list of available WalletDefinition. The // first WalletDefinition is considered the default, and might, for instance // be the initial form offered to the user for configuration, with others diff --git a/client/webserver/site/.babelrc b/client/webserver/site/.babelrc index a9b68d19ea..7fe0b14360 100644 --- a/client/webserver/site/.babelrc +++ b/client/webserver/site/.babelrc @@ -1,3 +1,9 @@ { - "plugins": ["@babel/plugin-syntax-dynamic-import"] + "presets": [ + "@babel/typescript", + "@babel/env" + ], + "plugins": [ + "@babel/plugin-transform-runtime" + ] } diff --git a/client/webserver/site/.eslintrc.js b/client/webserver/site/.eslintrc.js new file mode 100644 index 0000000000..bcb86674fd --- /dev/null +++ b/client/webserver/site/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', // Required to have rules that rely on Types. + tsconfigRootDir: './' + }, + extends: [ + 'standard', + 'plugin:@typescript-eslint/recommended' + ], + env: { + browser: true, + node: true + }, + plugins: [ + '@typescript-eslint' // Let's us override rules below. + ], + rules: { + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/indent': 'off', + 'no-use-before-define': 'off', + 'no-trailing-spaces': 'error', + 'no-console': ['off'], + 'no-alert': 'error', + 'no-eval': 'error', + 'no-implied-eval': 'error' + } +} diff --git a/client/webserver/site/.eslintrc.json b/client/webserver/site/.eslintrc.json deleted file mode 100644 index bbe395502d..0000000000 --- a/client/webserver/site/.eslintrc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "standard", - "parser": "@babel/eslint-parser", - "rules": { - "no-trailing-spaces": "error", - "no-console": ["off"], - "no-alert": "error", - "no-eval": "error", - "no-implied-eval": "error" - } -} diff --git a/client/webserver/site/package-lock.json b/client/webserver/site/package-lock.json index e65ee56a7e..e78d0a1507 100644 --- a/client/webserver/site/package-lock.json +++ b/client/webserver/site/package-lock.json @@ -10,15 +10,18 @@ "license": "Blue Oak 1.0.0", "devDependencies": { "@babel/core": "^7.17.2", - "@babel/eslint-parser": "^7.17.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", + "@babel/preset-typescript": "^7.16.7", + "@babel/runtime": "^7.17.8", + "@typescript-eslint/eslint-plugin": "^5.15.0", + "@typescript-eslint/parser": "^5.15.0", "babel-loader": "^8.2.3", "bootstrap": "^5.1.3", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.6.0", "css-minimizer-webpack-plugin": "^3.4.1", - "eslint": "^8.9.0", + "eslint": "^8.11.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.25.4", "eslint-plugin-node": "^11.1.0", @@ -31,6 +34,8 @@ "stylelint-config-standard": "^25.0.0", "stylelint-config-standard-scss": "^3.0.0", "stylelint-webpack-plugin": "^3.1.1", + "ts-loader": "^9.2.8", + "typescript": "^4.6.2", "webpack": "^5.69.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", @@ -62,9 +67,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", - "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", + "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -100,28 +105,10 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/eslint-parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz", - "integrity": "sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==", - "dev": true, - "dependencies": { - "eslint-scope": "^5.1.1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.11.0", - "eslint": "^7.5.0 || ^8.0.0" - } - }, "node_modules/@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", "dev": true, "dependencies": { "@babel/types": "^7.17.0", @@ -176,9 +163,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.10.tgz", - "integrity": "sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg==", + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz", + "integrity": "sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.16.7", @@ -197,13 +184,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz", - "integrity": "sha512-fk5A6ymfp+O5+p2yCkXAu5Kyj6v0xh0RBeNcAkYUMDvvAAoxvSKXn+Jb37t/yWFiQVDFK1ELpUTD8/aLhCPu+g==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", + "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.16.7", - "regexpu-core": "^4.7.1" + "regexpu-core": "^5.0.1" }, "engines": { "node": ">=6.9.0" @@ -318,19 +305,19 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", "@babel/helper-split-export-declaration": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" }, "engines": { "node": ">=6.9.0" @@ -388,12 +375,12 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", "dev": true, "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.17.0" }, "engines": { "node": ">=6.9.0" @@ -485,9 +472,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz", + "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -562,12 +549,12 @@ } }, "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz", - "integrity": "sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw==", + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz", + "integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-create-class-features-plugin": "^7.17.6", "@babel/helper-plugin-utils": "^7.16.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -675,12 +662,12 @@ } }, "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz", - "integrity": "sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA==", + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz", + "integrity": "sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.16.4", + "@babel/compat-data": "^7.17.0", "@babel/helper-compilation-targets": "^7.16.7", "@babel/helper-plugin-utils": "^7.16.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", @@ -953,6 +940,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", + "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", @@ -1053,9 +1055,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz", - "integrity": "sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz", + "integrity": "sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.16.7" @@ -1194,14 +1196,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz", - "integrity": "sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz", + "integrity": "sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-module-transforms": "^7.17.7", "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", "babel-plugin-dynamic-import-node": "^2.3.3" }, "engines": { @@ -1212,13 +1214,13 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz", - "integrity": "sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw==", + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz", + "integrity": "sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-module-transforms": "^7.17.7", "@babel/helper-plugin-utils": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7", "babel-plugin-dynamic-import-node": "^2.3.3" @@ -1352,6 +1354,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz", + "integrity": "sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", @@ -1428,6 +1450,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz", + "integrity": "sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-typescript": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", @@ -1563,10 +1602,27 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { + "node_modules/@babel/preset-typescript": { "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz", + "integrity": "sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-option": "^7.16.7", + "@babel/plugin-transform-typescript": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", + "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.4" @@ -1590,18 +1646,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", + "@babel/generator": "^7.17.3", "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-function-name": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", + "@babel/parser": "^7.17.3", "@babel/types": "^7.17.0", "debug": "^4.1.0", "globals": "^11.1.0" @@ -1633,16 +1689,16 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz", - "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", + "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.3.1", "globals": "^13.9.0", - "ignore": "^4.0.6", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.0.4", @@ -1653,9 +1709,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1667,15 +1723,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -1872,6 +1919,276 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.15.0.tgz", + "integrity": "sha512-u6Db5JfF0Esn3tiAKELvoU5TpXVSkOpZ78cEGn/wXtT2RVqs2vkt4ge6N8cRCyw7YVKhmmLDbwI2pg92mlv7cA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.15.0", + "@typescript-eslint/type-utils": "5.15.0", + "@typescript-eslint/utils": "5.15.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.15.0.tgz", + "integrity": "sha512-NGAYP/+RDM2sVfmKiKOCgJYPstAO40vPAgACoWPO/+yoYKSgAXIFaBKsV8P0Cc7fwKgvj27SjRNX4L7f4/jCKQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.15.0", + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/typescript-estree": "5.15.0", + "debug": "^4.3.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.15.0.tgz", + "integrity": "sha512-EFiZcSKrHh4kWk0pZaa+YNJosvKE50EnmN4IfgjkA3bTHElPtYcd2U37QQkNTqwMCS7LXeDeZzEqnsOH8chjSg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/visitor-keys": "5.15.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.15.0.tgz", + "integrity": "sha512-KGeDoEQ7gHieLydujGEFLyLofipe9PIzfvA/41urz4hv+xVxPEbmMQonKSynZ0Ks2xDhJQ4VYjB3DnRiywvKDA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.15.0", + "debug": "^4.3.2", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.15.0.tgz", + "integrity": "sha512-yEiTN4MDy23vvsIksrShjNwQl2vl6kJeG9YkVJXjXZnkJElzVK8nfPsWKYxcsGWG8GhurYXP4/KGj3aZAxbeOA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.15.0.tgz", + "integrity": "sha512-Hb0e3dGc35b75xLzixM3cSbG1sSbrTBQDfIScqdyvrfJZVEi4XWAT+UL/HMxEdrJNB8Yk28SKxPLtAhfCbBInA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/visitor-keys": "5.15.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.15.0.tgz", + "integrity": "sha512-081rWu2IPKOgTOhHUk/QfxuFog8m4wxW43sXNOMSCdh578tGJ1PAaWPsj42LOa7pguh173tNlMigsbrHvh/mtA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.15.0", + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/typescript-estree": "5.15.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.15.0.tgz", + "integrity": "sha512-+vX5FKtgvyHbmIJdxMJ2jKm9z2BIlXJiuewI8dsDYMp5LzPUcuTT78Ya5iwvQg3VqSVdmxyM8Anj1Jeq7733ZQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.15.0", + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -2301,13 +2618,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz", - "integrity": "sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", + "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", "dev": true, "dependencies": { "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.0", + "@babel/helper-define-polyfill-provider": "^0.3.1", "semver": "^6.1.1" }, "peerDependencies": { @@ -2315,25 +2632,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz", - "integrity": "sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", + "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", "dev": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.20.0" + "core-js-compat": "^3.21.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz", - "integrity": "sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", + "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.0" + "@babel/helper-define-polyfill-provider": "^0.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -2640,9 +2957,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz", - "integrity": "sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw==", + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", + "integrity": "sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==", "dev": true, "dependencies": { "browserslist": "^4.19.1", @@ -3090,9 +3407,9 @@ } }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -3334,12 +3651,12 @@ } }, "node_modules/eslint": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz", - "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz", + "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.1.0", + "@eslint/eslintrc": "^1.2.1", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -3777,9 +4094,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6708,9 +7025,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", - "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -6747,15 +7064,15 @@ } }, "node_modules/regexpu-core": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", - "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", + "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", "dev": true, "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^9.0.0", - "regjsgen": "^0.5.2", - "regjsparser": "^0.7.0", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.0.0" }, @@ -6764,15 +7081,15 @@ } }, "node_modules/regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", "dev": true }, "node_modules/regjsparser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", - "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", "dev": true, "dependencies": { "jsesc": "~0.5.0" @@ -7949,6 +8266,110 @@ "node": ">=8" } }, + "node_modules/ts-loader": { + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", + "integrity": "sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tsconfig-paths": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", @@ -7973,6 +8394,27 @@ "json5": "lib/cli.js" } }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8017,6 +8459,19 @@ } ] }, + "node_modules/typescript": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", + "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -8515,9 +8970,9 @@ } }, "@babel/compat-data": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", - "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", + "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", "dev": true }, "@babel/core": { @@ -8543,21 +8998,10 @@ "semver": "^6.3.0" } }, - "@babel/eslint-parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.17.0.tgz", - "integrity": "sha512-PUEJ7ZBXbRkbq3qqM/jZ2nIuakUBqCYc7Qf52Lj7dlZ6zERnqisdHioL0l4wwQZnmskMeasqUNzLBFKs3nylXA==", - "dev": true, - "requires": { - "eslint-scope": "^5.1.1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" - } - }, "@babel/generator": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.0.tgz", - "integrity": "sha512-I3Omiv6FGOC29dtlZhkfXO6pgkmukJSlT26QjVvS1DGZe/NzSVCPG41X0tS21oZkJYlovfj9qDWgKP+Cn4bXxw==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", "dev": true, "requires": { "@babel/types": "^7.17.0", @@ -8597,9 +9041,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.10.tgz", - "integrity": "sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg==", + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz", + "integrity": "sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.16.7", @@ -8612,13 +9056,13 @@ } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz", - "integrity": "sha512-fk5A6ymfp+O5+p2yCkXAu5Kyj6v0xh0RBeNcAkYUMDvvAAoxvSKXn+Jb37t/yWFiQVDFK1ELpUTD8/aLhCPu+g==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", + "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.16.7", - "regexpu-core": "^4.7.1" + "regexpu-core": "^5.0.1" } }, "@babel/helper-define-polyfill-provider": { @@ -8703,19 +9147,19 @@ } }, "@babel/helper-module-transforms": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", - "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", "@babel/helper-split-export-declaration": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7" + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" } }, "@babel/helper-optimise-call-expression": { @@ -8758,12 +9202,12 @@ } }, "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.17.0" } }, "@babel/helper-skip-transparent-expression-wrappers": { @@ -8831,9 +9275,9 @@ } }, "@babel/parser": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.0.tgz", - "integrity": "sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==", + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz", + "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -8878,12 +9322,12 @@ } }, "@babel/plugin-proposal-class-static-block": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz", - "integrity": "sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw==", + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz", + "integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-create-class-features-plugin": "^7.17.6", "@babel/helper-plugin-utils": "^7.16.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" } @@ -8949,12 +9393,12 @@ } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz", - "integrity": "sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA==", + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz", + "integrity": "sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==", "dev": true, "requires": { - "@babel/compat-data": "^7.16.4", + "@babel/compat-data": "^7.17.0", "@babel/helper-compilation-targets": "^7.16.7", "@babel/helper-plugin-utils": "^7.16.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", @@ -9140,6 +9584,15 @@ "@babel/helper-plugin-utils": "^7.14.5" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", + "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", @@ -9204,9 +9657,9 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz", - "integrity": "sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz", + "integrity": "sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.16.7" @@ -9291,25 +9744,25 @@ } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz", - "integrity": "sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz", + "integrity": "sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-module-transforms": "^7.17.7", "@babel/helper-plugin-utils": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz", - "integrity": "sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw==", + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz", + "integrity": "sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==", "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-module-transforms": "^7.17.7", "@babel/helper-plugin-utils": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7", "babel-plugin-dynamic-import-node": "^2.3.3" @@ -9389,6 +9842,20 @@ "@babel/helper-plugin-utils": "^7.16.7" } }, + "@babel/plugin-transform-runtime": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz", + "integrity": "sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "semver": "^6.3.0" + } + }, "@babel/plugin-transform-shorthand-properties": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", @@ -9435,6 +9902,17 @@ "@babel/helper-plugin-utils": "^7.16.7" } }, + "@babel/plugin-transform-typescript": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz", + "integrity": "sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-typescript": "^7.16.7" + } + }, "@babel/plugin-transform-unicode-escapes": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", @@ -9549,10 +10027,21 @@ "esutils": "^2.0.2" } }, - "@babel/runtime": { + "@babel/preset-typescript": { "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz", + "integrity": "sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-option": "^7.16.7", + "@babel/plugin-transform-typescript": "^7.16.7" + } + }, + "@babel/runtime": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", + "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" @@ -9570,18 +10059,18 @@ } }, "@babel/traverse": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.0.tgz", - "integrity": "sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==", + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.0", + "@babel/generator": "^7.17.3", "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-function-name": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.0", + "@babel/parser": "^7.17.3", "@babel/types": "^7.17.0", "debug": "^4.1.0", "globals": "^11.1.0" @@ -9604,16 +10093,16 @@ "dev": true }, "@eslint/eslintrc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz", - "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", + "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.3.1", "globals": "^13.9.0", - "ignore": "^4.0.6", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.0.4", @@ -9621,20 +10110,14 @@ }, "dependencies": { "globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -9805,6 +10288,162 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.15.0.tgz", + "integrity": "sha512-u6Db5JfF0Esn3tiAKELvoU5TpXVSkOpZ78cEGn/wXtT2RVqs2vkt4ge6N8cRCyw7YVKhmmLDbwI2pg92mlv7cA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.15.0", + "@typescript-eslint/type-utils": "5.15.0", + "@typescript-eslint/utils": "5.15.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.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==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.15.0.tgz", + "integrity": "sha512-NGAYP/+RDM2sVfmKiKOCgJYPstAO40vPAgACoWPO/+yoYKSgAXIFaBKsV8P0Cc7fwKgvj27SjRNX4L7f4/jCKQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.15.0", + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/typescript-estree": "5.15.0", + "debug": "^4.3.2" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.15.0.tgz", + "integrity": "sha512-EFiZcSKrHh4kWk0pZaa+YNJosvKE50EnmN4IfgjkA3bTHElPtYcd2U37QQkNTqwMCS7LXeDeZzEqnsOH8chjSg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/visitor-keys": "5.15.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.15.0.tgz", + "integrity": "sha512-KGeDoEQ7gHieLydujGEFLyLofipe9PIzfvA/41urz4hv+xVxPEbmMQonKSynZ0Ks2xDhJQ4VYjB3DnRiywvKDA==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.15.0", + "debug": "^4.3.2", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.15.0.tgz", + "integrity": "sha512-yEiTN4MDy23vvsIksrShjNwQl2vl6kJeG9YkVJXjXZnkJElzVK8nfPsWKYxcsGWG8GhurYXP4/KGj3aZAxbeOA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.15.0.tgz", + "integrity": "sha512-Hb0e3dGc35b75xLzixM3cSbG1sSbrTBQDfIScqdyvrfJZVEi4XWAT+UL/HMxEdrJNB8Yk28SKxPLtAhfCbBInA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/visitor-keys": "5.15.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.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/utils": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.15.0.tgz", + "integrity": "sha512-081rWu2IPKOgTOhHUk/QfxuFog8m4wxW43sXNOMSCdh578tGJ1PAaWPsj42LOa7pguh173tNlMigsbrHvh/mtA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.15.0", + "@typescript-eslint/types": "5.15.0", + "@typescript-eslint/typescript-estree": "5.15.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.15.0.tgz", + "integrity": "sha512-+vX5FKtgvyHbmIJdxMJ2jKm9z2BIlXJiuewI8dsDYMp5LzPUcuTT78Ya5iwvQg3VqSVdmxyM8Anj1Jeq7733ZQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.15.0", + "eslint-visitor-keys": "^3.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + } + } + }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -10157,33 +10796,33 @@ } }, "babel-plugin-polyfill-corejs2": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz", - "integrity": "sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", + "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", "dev": true, "requires": { "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.3.0", + "@babel/helper-define-polyfill-provider": "^0.3.1", "semver": "^6.1.1" } }, "babel-plugin-polyfill-corejs3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz", - "integrity": "sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", + "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", "dev": true, "requires": { "@babel/helper-define-polyfill-provider": "^0.3.1", - "core-js-compat": "^3.20.0" + "core-js-compat": "^3.21.0" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz", - "integrity": "sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", + "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.0" + "@babel/helper-define-polyfill-provider": "^0.3.1" } }, "balanced-match": { @@ -10417,9 +11056,9 @@ } }, "core-js-compat": { - "version": "3.20.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz", - "integrity": "sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw==", + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", + "integrity": "sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==", "dev": true, "requires": { "browserslist": "^4.19.1", @@ -10729,9 +11368,9 @@ } }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -10916,12 +11555,12 @@ "dev": true }, "eslint": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz", - "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz", + "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.1.0", + "@eslint/eslintrc": "^1.2.1", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -11047,9 +11686,9 @@ } }, "globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -13338,9 +13977,9 @@ "dev": true }, "regenerate-unicode-properties": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", - "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", "dev": true, "requires": { "regenerate": "^1.4.2" @@ -13368,29 +14007,29 @@ "dev": true }, "regexpu-core": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", - "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", + "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", "dev": true, "requires": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^9.0.0", - "regjsgen": "^0.5.2", - "regjsparser": "^0.7.0", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.0.0" } }, "regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", "dev": true }, "regjsparser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", - "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -14261,6 +14900,78 @@ "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true }, + "ts-loader": { + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.2.8.tgz", + "integrity": "sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "tsconfig-paths": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", @@ -14284,6 +14995,21 @@ } } }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14305,6 +15031,12 @@ "integrity": "sha512-6dOYeZfS3O9RtRD1caom0sMxgK59b27+IwoNy8RDPsmslSGOyU+mpTamlaIW7aNKi90ZQZ9DFaZL3YRoiSCULQ==", "dev": true }, + "typescript": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz", + "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", + "dev": true + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", diff --git a/client/webserver/site/package.json b/client/webserver/site/package.json index 9ca112c1a4..8bc5041b51 100644 --- a/client/webserver/site/package.json +++ b/client/webserver/site/package.json @@ -7,24 +7,27 @@ "test": "echo \"Error: no test specified\" && exit 1", "watch": "webpack --watch --config webpack/dev.js", "analyze": "webpack --config webpack/analyze.js", - "build": "webpack --config webpack/prod.js --progress --color", - "lint": "./node_modules/.bin/eslint src --ext .js", - "lint-fix": "./node_modules/.bin/eslint src --ext .js --fix" + "build": "./node_modules/.bin/tsc && webpack --config webpack/prod.js --progress --color", + "lint": "./node_modules/.bin/tsc && ./node_modules/.bin/eslint src --ext .js --ext .ts", + "check-types": "./node_modules/.bin/tsc" }, "keywords": [], "author": "The Decred developers", "license": "Blue Oak 1.0.0", "devDependencies": { "@babel/core": "^7.17.2", - "@babel/eslint-parser": "^7.17.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", + "@babel/preset-typescript": "^7.16.7", + "@babel/runtime": "^7.17.8", + "@typescript-eslint/eslint-plugin": "^5.15.0", + "@typescript-eslint/parser": "^5.15.0", "babel-loader": "^8.2.3", "bootstrap": "^5.1.3", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.6.0", "css-minimizer-webpack-plugin": "^3.4.1", - "eslint": "^8.9.0", + "eslint": "^8.11.0", "eslint-config-standard": "^16.0.3", "eslint-plugin-import": "^2.25.4", "eslint-plugin-node": "^11.1.0", @@ -37,6 +40,8 @@ "stylelint-config-standard": "^25.0.0", "stylelint-config-standard-scss": "^3.0.0", "stylelint-webpack-plugin": "^3.1.1", + "ts-loader": "^9.2.8", + "typescript": "^4.6.2", "webpack": "^5.69.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", diff --git a/client/webserver/site/src/index.js b/client/webserver/site/src/index.ts similarity index 100% rename from client/webserver/site/src/index.js rename to client/webserver/site/src/index.ts diff --git a/client/webserver/site/src/js/app.js b/client/webserver/site/src/js/app.ts similarity index 76% rename from client/webserver/site/src/js/app.js rename to client/webserver/site/src/js/app.ts index 917ef283c4..d5fcc92b1c 100644 --- a/client/webserver/site/src/js/app.js +++ b/client/webserver/site/src/js/app.ts @@ -12,6 +12,30 @@ import { getJSON, postJSON } from './http' import * as ntfn from './notifications' import ws from './ws' import * as intl from './locales' +import { + User, + SupportedAsset, + Exchange, + WalletState, + FeePaymentNote, + CoreNote, + OrderNote, + Market, + Order, + Match, + BalanceNote, + WalletConfigNote, + MatchNote, + ConnEventNote, + SpotPriceNote, + UnitInfo, + WalletDefinition, + WalletBalance, + LogMessage, + NoteElement, + BalanceResponse, + APIResponse +} from './registry' const idel = Doc.idel // = element by id const bind = Doc.bind @@ -22,8 +46,21 @@ const loggersKey = 'loggers' const recordersKey = 'recorders' const noteCacheSize = 100 +interface Page { + unload (): void + notify (n: CoreNote): void +} + +interface PageClass { + new (main: HTMLElement, data: any): Page; +} + +interface CoreNotePlus extends CoreNote { + el: HTMLElement // Added in app +} + /* constructors is a map to page constructors. */ -const constructors = { +const constructors: Record = { login: LoginPage, register: RegistrationPage, markets: MarketsPage, @@ -39,17 +76,40 @@ const unauthedPages = ['register', 'login', 'settings'] // Application is the main javascript web application for the Decred DEX client. export default class Application { + notes: CoreNotePlus[] + pokes: CoreNotePlus[] + user: User + seedGenTime: number + commitHash: string + showPopups: boolean + loggers: Record + recorders: Record + main: HTMLElement + header: HTMLElement + assets: Record + exchanges: Record + walletMap: Record + tooltip: HTMLElement + page: Record + loadedPage: Page | null + popupNotes: HTMLElement + popupTmpl: HTMLElement + constructor () { this.notes = [] this.pokes = [] // The "user" is a large data structure that contains nearly all state // information, including exchanges, markets, wallets, and orders. this.user = { - accounts: {}, - wallets: {} + exchanges: {}, + inited: false, + seedgentime: 0, + assets: {}, + authed: false, + ok: true } this.seedGenTime = 0 - this.commitHash = process.env.COMMITHASH + this.commitHash = process.env.COMMITHASH || '' this.showPopups = State.getCookie('popups') === '1' console.log('Decred DEX Client App, Build', this.commitHash.substring(0, 7)) @@ -64,7 +124,7 @@ export default class Application { return `${loggerID} logger ${state ? 'enabled' : 'disabled'}` } // Enable logging from anywhere. - window.log = (...a) => { this.log(...a) } + window.log = (loggerID, ...a) => { this.log(loggerID, ...a) } // Recorders can record log messages, and then save them to file on request. const recorderKeys = State.fetch(recordersKey) || [] @@ -102,7 +162,7 @@ export default class Application { */ async start () { // Handle back navigation from the browser. - bind(window, 'popstate', (e) => { + bind(window, 'popstate', (e: PopStateEvent) => { const page = e.state.page if (!page && page !== '') return this.loadPage(page, e.state.data, true) @@ -117,7 +177,7 @@ export default class Application { // The application is free to respond with a page that differs from the // one requested in the omnibox, e.g. routing though a login page. Set the // current URL state based on the actual page. - const url = new URL(window.location) + const url = new URL(window.location.href) if (handlerFromPath(url.pathname) !== handler) { url.pathname = `/${handler}` url.search = '' @@ -126,13 +186,13 @@ export default class Application { // Attach stuff. this.attachHeader() this.attachCommon(this.header) - this.attach() + this.attach({}) // Load recent notifications from Window.localStorage. const notes = State.fetch('notifications') this.setNotes(notes || []) // Connect the websocket and register the notification route. ws.connect(getSocketURI(), this.reconnected) - ws.registerRoute(notificationRoute, note => { + ws.registerRoute(notificationRoute, (note: CoreNote) => { this.notify(note) }) } @@ -149,18 +209,18 @@ export default class Application { * Fetch and save the user, which is the primary core state that must be * maintained by the Application. */ - async fetchUser () { - const user = await getJSON('/api/user') + async fetchUser (): Promise { + const resp: APIResponse = await getJSON('/api/user') // If it's not a page that requires auth, skip the error notification. - const skipNote = unauthedPages.indexOf(this.main.dataset.handler) > -1 - if (!this.checkResponse(user, skipNote)) return + const skipNote = unauthedPages.indexOf(this.main.dataset.handler || '') > -1 + if (!this.checkResponse(resp, skipNote)) return + const user = (resp as any) as User this.seedGenTime = user.seedgentime this.user = user this.assets = user.assets this.exchanges = user.exchanges this.walletMap = {} - this.locale = user.locale - for (const [assetID, asset] of Object.entries(user.assets)) { + for (const [assetID, asset] of (Object.entries(user.assets) as [any, SupportedAsset][])) { if (asset.wallet) { this.walletMap[assetID] = asset.wallet } @@ -170,7 +230,7 @@ export default class Application { } /* Load the page from the server. Insert and bind the DOM. */ - async loadPage (page, data, skipPush) { + async loadPage (page: string, data?: any, skipPush?: boolean): Promise { // Close some menus and tooltips. this.tooltip.style.left = '-10000px' Doc.hide(this.page.noteBox, this.page.profileBox) @@ -178,7 +238,7 @@ export default class Application { const url = new URL(`/${page}`, window.location.origin) const requestedHandler = handlerFromPath(page) // Fetch and parse the page. - const response = await window.fetch(url) + const response = await window.fetch(url.toString()) if (!response.ok) return false const html = await response.text() const doc = Doc.noderize(html) @@ -187,7 +247,7 @@ export default class Application { // Append the request to the page history. if (!skipPush) { const path = delivered === requestedHandler ? url.toString() : `/${delivered}` - window.history.pushState({ page: page, data: data }, delivered, path) + window.history.pushState({ page: page, data: data }, '', path) } // Insert page and attach handlers. document.title = doc.title @@ -198,7 +258,7 @@ export default class Application { } /* attach binds the common handlers and calls the page constructor. */ - attach (data) { + attach (data: any) { const handlerID = this.main.dataset.handler if (!handlerID) { console.error('cannot attach to content with no specified handler') @@ -214,10 +274,10 @@ export default class Application { this.bindTooltips(this.main) } - bindTooltips (ancestor) { - ancestor.querySelectorAll('[data-tooltip]').forEach(el => { + bindTooltips (ancestor: HTMLElement) { + ancestor.querySelectorAll('[data-tooltip]').forEach((el: HTMLElement) => { bind(el, 'mouseenter', () => { - this.tooltip.textContent = el.dataset.tooltip + this.tooltip.textContent = el.dataset.tooltip || '' const lyt = Doc.layoutMetrics(el) let left = lyt.centerX - this.tooltip.offsetWidth / 2 if (left < 0) left = 5 @@ -240,12 +300,13 @@ export default class Application { this.header = idel(document.body, 'header') this.popupNotes = idel(document.body, 'popupNotes') this.popupTmpl = Doc.tmplElement(this.popupNotes, 'note') - this.popupTmpl.remove() + if (this.popupTmpl) this.popupTmpl.remove() + else console.error('popupTmpl element not found') this.tooltip = idel(document.body, 'tooltip') const page = this.page = Doc.idDescendants(this.header) - delete page.noteTmpl.id + page.noteTmpl.removeAttribute('id') page.noteTmpl.remove() - delete page.pokeTmpl.id + page.pokeTmpl.removeAttribute('id') page.pokeTmpl.remove() page.loader.remove() Doc.show(page.loader) @@ -275,7 +336,7 @@ export default class Application { bind(page.innerNoteIcon, 'click', () => { Doc.hide(page.noteBox) }) bind(page.innerProfileIcon, 'click', () => { Doc.hide(page.profileBox) }) - bind(page.profileSignout, 'click', async e => await this.signOut()) + bind(page.profileSignout, 'click', async () => await this.signOut()) bind(page.pokeCat, 'click', () => { this.setNoteTimes(page.pokeList) @@ -300,14 +361,14 @@ export default class Application { * showDropdown sets the position and visibility of the specified dropdown * dialog according to the position of its icon button. */ - showDropdown (icon, dialog) { + showDropdown (icon: HTMLElement, dialog: HTMLElement) { const ico = icon.getBoundingClientRect() Doc.hide(this.page.noteBox, this.page.profileBox) Doc.show(dialog) dialog.style.right = `${window.innerWidth - ico.left - ico.width + 11}px` dialog.style.top = `${ico.top - 9}px` - const hide = e => { + const hide = (e: MouseEvent) => { if (!Doc.mouseInElement(e, dialog)) { Doc.hide(dialog) unbind(document, 'click', hide) @@ -333,9 +394,9 @@ export default class Application { Doc.hide(this.page.noteIndicator) } - setNoteTimes (noteList) { - for (const el of Array.from(noteList.children)) { - el.querySelector('span.note-time').textContent = Doc.timeSince(el.note.stamp) + setNoteTimes (noteList: HTMLElement) { + for (const el of (Array.from(noteList.children) as NoteElement[])) { + Doc.safeSelector(el, 'span.note-time').textContent = Doc.timeSince(el.note.stamp) } } @@ -343,20 +404,20 @@ export default class Application { * bindInternalNavigation hijacks navigation by click on any local links that * are descendants of ancestor. */ - bindInternalNavigation (ancestor) { - const pageURL = new URL(window.location) + bindInternalNavigation (ancestor: HTMLElement) { + const pageURL = new URL(window.location.href) ancestor.querySelectorAll('a').forEach(a => { if (!a.href) return const url = new URL(a.href) if (url.origin === pageURL.origin) { const token = url.pathname.substring(1) - const params = {} + const params: Record = {} if (url.search) { url.searchParams.forEach((v, k) => { params[k] = v }) } - Doc.bind(a, 'click', e => { + Doc.bind(a, 'click', (e: Event) => { e.preventDefault() this.loadPage(token, params) }) @@ -405,7 +466,7 @@ export default class Application { } /* attachCommon scans the provided node and handles some common bindings. */ - attachCommon (node) { + attachCommon (node: HTMLElement) { this.bindInternalNavigation(node) } @@ -413,31 +474,29 @@ export default class Application { * updateExchangeRegistration updates the information for the exchange * registration payment */ - updateExchangeRegistration (dexAddr, isPaid, confs, asset) { + updateExchangeRegistration (dexAddr: string, confs: number, assetID: number) { const dex = this.exchanges[dexAddr] + const symbol = this.assets[assetID].symbol + dex.pendingFee = { confs, assetID, symbol } + } - if (isPaid) { - // setting the null value in the 'confs' field indicates that the fee - // payment was completed - dex.pendingFee = null - return - } - - const symbol = this.assets[asset].symbol - dex.pendingFee = { confs, asset, symbol } + setDEXPaid (host: string) { + // setting the null value in the 'confs' field indicates that the fee + // payment was completed + this.exchanges[host].pendingFee = null } /* * handleFeePaymentNote is the handler for the 'feepayment'-type notification, which * is used to update the dex registration status. */ - handleFeePaymentNote (note) { + handleFeePaymentNote (note: FeePaymentNote) { switch (note.topic) { case 'RegUpdate': - this.updateExchangeRegistration(note.dex, false, note.confirmations, note.asset) + this.updateExchangeRegistration(note.dex, note.confirmations, note.asset) break case 'AccountRegistered': - this.updateExchangeRegistration(note.dex, true) + this.setDEXPaid(note.dex) break default: break @@ -448,7 +507,7 @@ export default class Application { * setNotes sets the current notification cache and populates the notification * display. */ - setNotes (notes) { + setNotes (notes: CoreNote[]) { this.log('notes', 'setNotes', notes) this.notes = [] Doc.empty(this.page.noteList) @@ -462,16 +521,16 @@ export default class Application { * notify is the top-level handler for notifications received from the client. * Notifications are propagated to the loadedPage. */ - notify (note) { + notify (note: CoreNote) { // Handle type-specific updates. this.log('notes', 'notify', note) switch (note.type) { case 'order': { - const order = note.order + const order = (note as OrderNote).order const mkt = this.user.exchanges[order.host].markets[order.market] // Updates given order in market's orders list if it finds it. // Returns a bool which indicates if order was found. - const updateOrder = (mkt, ord) => { + const updateOrder = (mkt: Market, ord: Order) => { for (const i in mkt.orders || []) { if (mkt.orders[i].id === ord.id) { mkt.orders[i] = ord @@ -489,19 +548,20 @@ export default class Application { break } case 'balance': { + const n: BalanceNote = note as BalanceNote const wallet = this.user.assets && - this.user.assets[note.assetID].wallet - if (wallet) wallet.balance = note.balance + this.user.assets[n.assetID].wallet + if (wallet) wallet.balance = n.balance break } case 'feepayment': - this.handleFeePaymentNote(note) + this.handleFeePaymentNote(note as FeePaymentNote) break case 'walletstate': case 'walletconfig': { // assets can be null if failed to connect to dex server. if (!this.assets) return - const wallet = note.wallet + const wallet = (note as WalletConfigNote).wallet const asset = this.assets[wallet.assetID] asset.wallet = wallet this.walletMap[wallet.assetID] = wallet @@ -510,18 +570,21 @@ export default class Application { break } case 'match': { - const ord = this.order(note.orderID) - if (ord) updateMatch(ord, note.match) + const n = note as MatchNote + const ord = this.order(n.orderID) + if (ord) updateMatch(ord, n.match) break } case 'conn': { - const xc = this.user.exchanges[note.host] - if (xc) xc.connected = note.connected + const n = note as ConnEventNote + const xc = this.user.exchanges[n.host] + if (xc) xc.connected = n.connected break } case 'spots': { - const xc = this.user.exchanges[note.host] - for (const [mktName, spot] of Object.entries(note.spots)) xc.markets[mktName].spot = spot + const n = note as SpotPriceNote + const xc = this.user.exchanges[n.host] + for (const [mktName, spot] of Object.entries(n.spots)) xc.markets[mktName].spot = spot } } @@ -531,7 +594,7 @@ export default class Application { if (note.severity < ntfn.POKE) return // Poke notifications have their own display. if (this.showPopups) { - const span = this.popupTmpl.cloneNode(true) + const span = this.popupTmpl.cloneNode(true) as HTMLElement Doc.tmplElement(span, 'text').textContent = `${note.subject}: ${note.details}` const indicator = Doc.tmplElement(span, 'indicator') if (note.severity === ntfn.POKE) { @@ -540,10 +603,10 @@ export default class Application { const pn = this.popupNotes pn.appendChild(span) // These take up screen space. Only show max 5 at a time. - while (pn.children.length > 5) pn.removeChild(pn.firstChild) + while (pn.children.length > 5) pn.removeChild(pn.firstChild as Node) setTimeout(async () => { - await Doc.animate(500, progress => { - span.style.opacity = 1 - progress + await Doc.animate(500, (progress: number) => { + span.style.opacity = String(1 - progress) }) span.remove() }, 6000) @@ -567,7 +630,7 @@ export default class Application { * book Order book feed. * ws.........Websocket connection status changes. */ - log (loggerID, ...msg) { + log (loggerID: string, ...msg: any) { if (this.loggers[loggerID]) console.log(`${nowString()}[${loggerID}]:`, ...msg) if (this.recorders[loggerID]) { this.recorders[loggerID].push({ @@ -577,18 +640,18 @@ export default class Application { } } - prependPokeElement (note) { + prependPokeElement (cn: CoreNote) { + const [el, note] = this.makePoke(cn) this.pokes.push(note) while (this.pokes.length > noteCacheSize) this.pokes.shift() - const el = this.makePoke(note) this.prependListElement(this.page.pokeList, note, el) } - prependNoteElement (note, skipSave) { + prependNoteElement (cn: CoreNote, skipSave?: boolean) { + const [el, note] = this.makeNote(cn) this.notes.push(note) while (this.notes.length > noteCacheSize) this.notes.shift() const noteList = this.page.noteList - const el = this.makeNote(note) this.prependListElement(noteList, note, el) if (!skipSave) this.storeNotes() // Set the indicator color. @@ -602,16 +665,15 @@ export default class Application { const ni = this.page.noteIndicator setSeverityClass(ni, severity) if (unacked) { - ni.textContent = (unacked > noteCacheSize - 1) ? `${noteCacheSize - 1}+` : unacked + ni.textContent = String((unacked > noteCacheSize - 1) ? `${noteCacheSize - 1}+` : unacked) Doc.show(ni) } else Doc.hide(ni) } - prependListElement (noteList, note, el) { - note.el = el + prependListElement (noteList: HTMLElement, note: CoreNotePlus, el: NoteElement) { el.note = note noteList.prepend(el) - while (noteList.children.length > noteCacheSize) noteList.removeChild(noteList.lastChild) + while (noteList.children.length > noteCacheSize) noteList.removeChild(noteList.lastChild as Node) this.setNoteTimes(noteList) } @@ -619,24 +681,26 @@ export default class Application { * makeNote constructs a single notification element for the drop-down * notification list. */ - makeNote (note) { - const el = this.page.noteTmpl.cloneNode(true) + makeNote (note: CoreNote): [NoteElement, CoreNotePlus] { + const el = this.page.noteTmpl.cloneNode(true) as NoteElement if (note.severity > ntfn.POKE) { const cls = note.severity === ntfn.SUCCESS ? 'good' : note.severity === ntfn.WARNING ? 'warn' : 'bad' - el.querySelector('div.note-indicator').classList.add(cls) + Doc.safeSelector(el, 'div.note-indicator').classList.add(cls) } - el.querySelector('div.note-subject').textContent = note.subject - el.querySelector('div.note-details').textContent = note.details - return el + Doc.safeSelector(el, 'div.note-subject').textContent = note.subject + Doc.safeSelector(el, 'div.note-details').textContent = note.details + const np: CoreNotePlus = { el, ...note } + return [el, np] } - makePoke (note) { - const el = this.page.pokeTmpl.cloneNode(true) + makePoke (note: CoreNote): [NoteElement, CoreNotePlus] { + const el = this.page.pokeTmpl.cloneNode(true) as NoteElement const d = new Date(note.stamp) Doc.tmplElement(el, 'dateTime').textContent = `${d.toLocaleDateString()}, ${d.toLocaleTimeString()}` Doc.tmplElement(el, 'details').textContent = `${note.subject}: ${note.details}` - return el + const np: CoreNotePlus = { el, ...note } + return [el, np] } /* @@ -644,14 +708,14 @@ export default class Application { * loading icon. The loader will block all interaction with the specified * element until Application.loaded is called. */ - loading (el) { - const loader = this.page.loader.cloneNode(true) + loading (el: HTMLElement): () => void { + const loader = this.page.loader.cloneNode(true) as HTMLElement el.appendChild(loader) return () => { loader.remove() } } /* orders retrieves a list of orders for the specified dex and market. */ - orders (host, mktID) { + orders (host: string, mktID: string): Order[] { let o = this.user.exchanges[host].markets[mktID].orders if (!o) { o = [] @@ -664,7 +728,7 @@ export default class Application { * haveActiveOrders returns whether or not the there are active orders * involving a certain asset. */ - haveAssetOrders (assetID) { + haveAssetOrders (assetID: number): boolean { for (const xc of Object.values(this.user.exchanges)) { for (const market of Object.values(xc.markets)) { if (!market.orders) continue @@ -678,7 +742,7 @@ export default class Application { } /* order attempts to locate an order by order ID. */ - order (oid) { + order (oid: string): Order | null { for (const xc of Object.values(this.user.exchanges)) { for (const market of Object.values(xc.markets)) { if (!market.orders) continue @@ -687,6 +751,7 @@ export default class Application { } } } + return null } /* @@ -694,26 +759,37 @@ export default class Application { * [core.Exchange] is provided, and this is not a SupportedAsset, the UnitInfo * sent from the exchange's assets map [dex.Asset] will be used. */ - unitInfo (assetID, xc) { + unitInfo (assetID: number, xc?: Exchange): UnitInfo { const supportedAsset = this.assets[assetID] if (supportedAsset) return supportedAsset.info.unitinfo + if (!xc) { + console.error(`no supported asset info for id = ${assetID}, and no exchange info provided`) + return { + atomicUnit: '', + conventional: { + unit: '', + conversionFactor: 1e8 + }, + denominations: [] + } + } return xc.assets[assetID].unitInfo } /* conventionalRate converts the encoded atomic rate to a conventional rate */ - conventionalRate (baseID, quoteID, encRate) { + conventionalRate (baseID: number, quoteID: number, encRate: number): number { const [b, q] = [this.unitInfo(baseID), this.unitInfo(quoteID)] const r = b.conventional.conversionFactor / q.conventional.conversionFactor return encRate / RateEncodingFactor * r } - walletDefinition (assetID, walletType) { + walletDefinition (assetID: number, walletType: string): WalletDefinition { const assetInfo = this.assets[assetID].info if (walletType === '') return assetInfo.availablewallets[assetInfo.emptyidx] return assetInfo.availablewallets.filter(def => def.type === walletType)[0] } - currentWalletDefinition (assetID) { + currentWalletDefinition (assetID: number): WalletDefinition { return this.walletDefinition(assetID, this.assets[assetID].wallet.type) } @@ -722,8 +798,8 @@ export default class Application { * include the balance, but we're ignoring it, since a balance update * notification is received via the Application anyways. */ - async fetchBalance (assetID) { - const res = await postJSON('/api/balance', { assetID: assetID }) + async fetchBalance (assetID: number): Promise { + const res: BalanceResponse = await postJSON('/api/balance', { assetID: assetID }) if (!this.checkResponse(res)) { throw new Error(`failed to fetch balance for asset ID ${assetID}`) } @@ -736,7 +812,7 @@ export default class Application { * message will be displayed in the drop-down notifications and false will be * returned. */ - checkResponse (resp, skipNote) { + checkResponse (resp: APIResponse, skipNote?: boolean): boolean { if (!resp.requestSuccessful || !resp.ok) { if (this.user.inited && !skipNote) this.notify(ntfn.make(intl.prep(intl.ID_API_ERROR), resp.msg, ntfn.ERROR)) return false @@ -762,7 +838,7 @@ export default class Application { } /* getSocketURI returns the websocket URI for the client. */ -function getSocketURI () { +function getSocketURI (): string { const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws' return `${protocol}://${window.location.host}/ws` } @@ -771,19 +847,19 @@ function getSocketURI () { * severityClassMap maps a notification severity level to a CSS class that * assigns a background color. */ -const severityClassMap = { +const severityClassMap: Record = { [ntfn.SUCCESS]: 'good', [ntfn.ERROR]: 'bad', [ntfn.WARNING]: 'warn' } /* handlerFromPath parses the handler name from the path. */ -function handlerFromPath (path) { +function handlerFromPath (path: string): string { return path.replace(/^\//, '').split('/')[0].split('?')[0].split('#')[0] } /* nowString creates a string formatted like HH:MM:SS.xxx */ -function nowString () { +function nowString (): string { const stamp = new Date() const h = stamp.getHours().toString().padStart(2, '0') const m = stamp.getMinutes().toString().padStart(2, '0') @@ -792,13 +868,13 @@ function nowString () { return `${h}:${m}:${s}.${ms}` } -function setSeverityClass (el, severity) { +function setSeverityClass (el: HTMLElement, severity: number) { el.classList.remove('bad', 'warn', 'good') el.classList.add(severityClassMap[severity]) } /* updateMatch updates the match in or adds the match to the order. */ -function updateMatch (order, match) { +function updateMatch (order: Order, match: Match) { for (const i in order.matches) { const m = order.matches[i] if (m.matchID === match.matchID) { diff --git a/client/webserver/site/src/js/basepage.js b/client/webserver/site/src/js/basepage.ts similarity index 59% rename from client/webserver/site/src/js/basepage.js rename to client/webserver/site/src/js/basepage.ts index b0191412bd..b9bd22ac53 100644 --- a/client/webserver/site/src/js/basepage.js +++ b/client/webserver/site/src/js/basepage.ts @@ -1,10 +1,16 @@ +import { CoreNote } from './registry' + export default class BasePage { + notifiers?: Record void> + /* notify is called when a notification is received by the app. */ - notify (note) { + notify (note: CoreNote) { if (!this.notifiers || !this.notifiers[note.type]) return this.notifiers[note.type](note) } /* unload is called when the user navigates away from the page. */ - unload () {} + unload () { + // should be implemented by inheriting class. + } } diff --git a/client/webserver/site/src/js/charts.js b/client/webserver/site/src/js/charts.ts similarity index 80% rename from client/webserver/site/src/js/charts.js rename to client/webserver/site/src/js/charts.ts index 42d0bb498f..d04e0eac4a 100644 --- a/client/webserver/site/src/js/charts.js +++ b/client/webserver/site/src/js/charts.ts @@ -1,6 +1,8 @@ import Doc from './doc' import { RateEncodingFactor } from './orderutil' +import OrderBook from './orderbook' import State from './state' +import { UnitInfo, Market, Candle, CandlesPayload } from './registry' const bind = Doc.bind const unbind = Doc.unbind @@ -8,7 +10,100 @@ const PIPI = 2 * Math.PI const plusChar = String.fromCharCode(59914) const minusChar = String.fromCharCode(59915) -const darkTheme = { +interface Point { + x: number + y: number +} + +interface MinMax { + min: number + max: number +} + +interface Label { + val: number + txt: string +} + +interface LabelSet { + widest?: number + lbls: Label[] +} + +interface Translator { + x: (x: number) => number + y: (y: number) => number + unx: (x: number) => number + uny: (y: number) => number + w: (w: number) => number + h: (h: number) => number + dataCoords: (f: () => void) => void +} + +export interface MouseReport { + rate: number + depth: number + dotColor: string + hoverMarkers: number[] +} + +export interface VolumeReport { + buyBase: number + buyQuote: number + sellBase: number + sellQuote: number +} + +export interface DepthReporters { + mouse: (r: MouseReport | null) => void + click: (x: number) => void + volume: (r: VolumeReport) => void + zoom: (z: number) => void +} + +export interface CandleReporters { + mouse: (r: Candle | null) => void +} + +export interface ChartReporters { + resize: () => void, + click: (e: MouseEvent) => void, + zoom: (bigger: boolean) => void +} + +export interface DepthLine { + rate: number + color: string +} + +export interface DepthMarker { + rate: number + active: boolean +} + +interface DepthMark extends DepthMarker { + qty: number + sell: boolean +} + +interface Theme { + axisLabel: string + gridBorder: string + gridLines: string + gapLine: string + value: string + zoom: string + zoomHover: string + sellLine: string + buyLine: string + sellFill: string + buyFill: string + crosshairs: string + legendFill: string + legendText: string +} + +const darkTheme: Theme = { axisLabel: '#b1b1b1', gridBorder: '#3a3a3a', gridLines: '#2a2a2a', @@ -25,7 +120,7 @@ const darkTheme = { legendText: '#d5d5d5' } -const lightTheme = { +const lightTheme: Theme = { axisLabel: '#1b1b1b', gridBorder: '#3a3a3a', gridLines: '#dadada', @@ -44,19 +139,40 @@ const lightTheme = { // Chart is the base class for charts. class Chart { - constructor (parent) { + parent: HTMLElement + report: ChartReporters + theme: Theme + canvas: HTMLCanvasElement + visible: boolean + ctx: CanvasRenderingContext2D + mousePos: Point | null + rect: DOMRect + wheelLimiter: number | null + boundResizer: () => void + plotRegion: Region + xRegion: Region + yRegion: Region + dataExtents: Extents + + constructor (parent: HTMLElement, reporters: ChartReporters) { this.parent = parent + this.report = reporters this.theme = State.isDark() ? darkTheme : lightTheme this.canvas = document.createElement('canvas') this.visible = true parent.appendChild(this.canvas) - this.ctx = this.canvas.getContext('2d') + const ctx = this.canvas.getContext('2d') + if (!ctx) { + console.error('error getting canvas context') + return + } + this.ctx = ctx this.ctx.textAlign = 'center' this.ctx.textBaseline = 'middle' this.setZoomBttns() // Mouse handling this.mousePos = null - bind(this.canvas, 'mousemove', e => { + bind(this.canvas, 'mousemove', (e: MouseEvent) => { // this.rect will be set in resize(). this.mousePos = { x: e.clientX - this.rect.left, @@ -70,13 +186,14 @@ class Chart { }) // Scrolling by wheel is smoother when the rate is slightly limited. this.wheelLimiter = null - this.wheeled = () => { - this.wheelLimiter = setTimeout(() => { this.wheelLimiter = null }, 100) - } - bind(this.canvas, 'wheel', e => { this.wheel(e) }) + bind(this.canvas, 'wheel', (e: WheelEvent) => { this.wheel(e) }) this.boundResizer = () => { this.resize(parent.clientHeight) } bind(window, 'resize', this.boundResizer) - bind(this.canvas, 'click', e => { this.click(e) }) + bind(this.canvas, 'click', (e: MouseEvent) => { this.click(e) }) + } + + wheeled () { + this.wheelLimiter = window.setTimeout(() => { this.wheelLimiter = null }, 100) } /* clear the canvas. */ @@ -91,15 +208,17 @@ class Chart { // setZoomBttns is run before drawing and should be used for setup of zoom // buttons. - setZoomBttns () {} + setZoomBttns () { + // should be implemented by inheriting class. + } /* click is the handler for a click event on the canvas. */ - click (e) { - this.clicked(e) + click (e: MouseEvent) { + this.report.click(e) } /* wheel is a mousewheel event handler. */ - wheel (e) { + wheel (e: WheelEvent) { this.zoom(e.deltaY < 0) e.preventDefault() } @@ -109,7 +228,7 @@ class Chart { * updating the height programatically after the caller sets a style.height * but before the clientHeight has been updated. */ - resize (parentHeight) { + resize (parentHeight: number) { this.canvas.width = this.parent.clientWidth this.canvas.height = parentHeight - 20 // magic number derived from a soup of css values. const xLblHeight = 30 @@ -124,14 +243,14 @@ class Chart { // return nonsense until a render. window.requestAnimationFrame(() => { this.rect = this.canvas.getBoundingClientRect() - this.resized() + this.report.resize() }) } /* zoom is called when the user scrolls the mouse wheel on the canvas. */ - zoom (bigger) { + zoom (bigger: boolean) { if (this.wheelLimiter) return - this.zoomed(bigger) + this.report.zoom(bigger) } /* hide hides the canvas */ @@ -166,9 +285,9 @@ class Chart { } /* plotXLabels applies the provided labels to the x axis and draws the grid. */ - plotXLabels (labels, minX, maxX, unitLines) { + plotXLabels (labels: LabelSet, minX: number, maxX: number, unitLines: string[]) { const extents = new Extents(minX, maxX, 0, 1) - this.xRegion.plot(extents, (ctx, tools) => { + this.xRegion.plot(extents, (ctx: CanvasRenderingContext2D, tools: Translator) => { this.applyLabelStyle() const centerX = (maxX + minX) / 2 let lastX = minX @@ -188,7 +307,7 @@ class Chart { ctx.fillText(unitLines[0], tools.x(unitCenter), tools.y(0.5)) } }, true) - this.plotRegion.plot(extents, (ctx, tools) => { + this.plotRegion.plot(extents, (ctx: CanvasRenderingContext2D, tools: Translator) => { ctx.lineWidth = 1 ctx.strokeStyle = this.theme.gridLines labels.lbls.forEach(lbl => { @@ -201,9 +320,9 @@ class Chart { * plotYLabels applies the y labels based on the provided plot region, and * draws the grid. */ - plotYLabels (region, labels, minY, maxY, unit) { + plotYLabels (region: Region, labels: LabelSet, minY: number, maxY: number, unit: string) { const extents = new Extents(0, 1, minY, maxY) - this.yRegion.plot(extents, (ctx, tools) => { + this.yRegion.plot(extents, (ctx: CanvasRenderingContext2D, tools: Translator) => { this.applyLabelStyle() const centerY = maxY / 2 let lastY = 0 @@ -217,7 +336,7 @@ class Chart { }) ctx.fillText(unit, tools.x(0.5), tools.y(unitCenter)) }, true) - region.plot(extents, (ctx, tools) => { + region.plot(extents, (ctx: CanvasRenderingContext2D, tools: Translator) => { ctx.lineWidth = 1 ctx.strokeStyle = this.theme.gridLines labels.lbls.forEach(lbl => { @@ -230,12 +349,12 @@ class Chart { * doYLabels generates and applies the y-axis labels, based upon the * provided plot region. */ - doYLabels (region, step, unit, valFmt) { + doYLabels (region: Region, step: number, unit: string, valFmt?: (v: number) => string) { const yLabels = makeLabels(this.ctx, region.height(), this.dataExtents.y.min, this.dataExtents.y.max, 50, step, unit, valFmt) // Reassign the width of the y-label column to accommodate the widest text. - const yAxisWidth = yLabels.widest * 1.5 + const yAxisWidth = (yLabels.widest || 0) * 1.5 this.yRegion.extents.x.max = yAxisWidth this.yRegion.extents.y.max = region.extents.y.max @@ -248,7 +367,7 @@ class Chart { // drawFrame draws an outline around the plotRegion. drawFrame () { - this.plotRegion.plot(new Extents(0, 1, 0, 1), (ctx, tools) => { + this.plotRegion.plot(new Extents(0, 1, 0, 1), (ctx: CanvasRenderingContext2D, tools: Translator) => { ctx.lineWidth = 1 ctx.strokeStyle = this.theme.gridBorder ctx.beginPath() @@ -266,14 +385,26 @@ class Chart { /* DepthChart is a javascript Canvas-based depth chart renderer. */ export class DepthChart extends Chart { - constructor (parent, reporters, zoom) { - super(parent) + reporters: DepthReporters + book: OrderBook + zoomLevel: number + lotSize: number + rateStep: number + lines: DepthLine[] + markers: Record + zoomInBttn: Region + zoomOutBttn: Region + baseUnit: string + quoteUnit: string + + constructor (parent: HTMLElement, reporters: DepthReporters, zoom: number) { + super(parent, { + resize: () => this.resized(), + click: (e: MouseEvent) => this.clicked(e), + zoom: (bigger: boolean) => this.zoomed(bigger) + }) this.reporters = reporters - this.book = null - this.dataExtents = null this.zoomLevel = zoom - this.lotSize = null - this.rateStep = null this.lines = [] this.markers = { buys: [], @@ -297,7 +428,7 @@ export class DepthChart extends Chart { } /* zoomed zooms the current view in or out. bigger=true is zoom in. */ - zoomed (bigger) { + zoomed (bigger: boolean) { if (!this.zoomLevel) return if (!this.book.buys || !this.book.sells) return this.wheeled() @@ -310,7 +441,7 @@ export class DepthChart extends Chart { } /* clicked is the canvas 'click' event handler. */ - clicked (e) { + clicked (e: MouseEvent) { if (!this.dataExtents) return const x = e.clientX - this.rect.left const y = e.clientY - this.rect.y @@ -326,7 +457,7 @@ export class DepthChart extends Chart { } // set sets the current data set and draws. - set (book, lotSize, rateStep, baseUnitInfo, quoteUnitInfo) { + set (book: OrderBook, lotSize: number, rateStep: number, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) { this.book = book this.lotSize = lotSize / baseUnitInfo.conventional.conversionFactor const [qFactor, bFactor] = [quoteUnitInfo.conventional.conversionFactor, baseUnitInfo.conventional.conversionFactor] @@ -376,12 +507,12 @@ export class DepthChart extends Chart { const sellMarkers = [...this.markers.sells] buyMarkers.sort((a, b) => b.rate - a.rate) sellMarkers.sort((a, b) => a.rate - b.rate) - const markers = [] + const markers: DepthMark[] = [] - const buyDepth = [] - const buyEpoch = [] - const sellDepth = [] - const sellEpoch = [] + const buyDepth: [number, number][] = [] + const buyEpoch: [number, number][] = [] + const sellDepth: [number, number][] = [] + const sellEpoch: [number, number][] = [] const volumeReport = { buyBase: 0, buyQuote: 0, @@ -404,6 +535,7 @@ export class DepthChart extends Chart { volumeReport.buyQuote += ord.qty * ord.rate while (buyMarkers.length && floatCompare(buyMarkers[0].rate, ord.rate)) { const mark = buyMarkers.shift() + if (!mark) continue markers.push({ rate: mark.rate, qty: ord.epoch ? epochSum : sum, @@ -429,6 +561,7 @@ export class DepthChart extends Chart { volumeReport.sellQuote += ord.qty * ord.rate while (sellMarkers.length && floatCompare(sellMarkers[0].rate, ord.rate)) { const mark = sellMarkers.shift() + if (!mark) continue markers.push({ rate: mark.rate, qty: ord.epoch ? epochSum : sum, @@ -461,7 +594,7 @@ export class DepthChart extends Chart { this.plotXLabels(xLabels, low, high, [`${this.quoteUnit}/`, this.baseUnit]) // A function to be run at the end if there is legend data to display. - let mouseData + let mouseData: MouseReport | null = null // Draw the grid. this.drawFrame() @@ -507,7 +640,7 @@ export class DepthChart extends Chart { bttnTop + bttnSize ) let hover = mousePos && this.zoomOutBttn.contains(mousePos.x, mousePos.y) - this.zoomOutBttn.plot(new Extents(0, 1, 0, 1), (ctx, tools) => { + this.zoomOutBttn.plot(new Extents(0, 1, 0, 1), ctx => { ctx.font = '12px \'icomoon\'' ctx.fillStyle = this.theme.zoom if (hover) { @@ -524,7 +657,7 @@ export class DepthChart extends Chart { bttnTop + bttnSize ) hover = mousePos && this.zoomInBttn.contains(mousePos.x, mousePos.y) - this.zoomInBttn.plot(new Extents(0, 1, 0, 1), (ctx, tools) => { + this.zoomInBttn.plot(new Extents(0, 1, 0, 1), ctx => { ctx.font = '12px \'icomoon\'' ctx.fillStyle = this.theme.zoom if (hover) { @@ -536,7 +669,7 @@ export class DepthChart extends Chart { // Draw a dotted vertical line where the mouse is, and a dot at the level // of the depth line. - const drawLine = (x, color) => { + const drawLine = (x: number, color: string) => { if (x > high || x < low) return ctx.save() ctx.setLineDash([3, 5]) @@ -582,7 +715,7 @@ export class DepthChart extends Chart { // side and depth for the x value. const dataX = tools.unx(mousePos.x) let evalSide = sellDepth - let trigger = (ptX) => ptX >= dataX + let trigger = (ptX: number) => ptX >= dataX let dotColor = this.theme.sellLine if (dataX < midGap) { evalSide = buyDepth @@ -595,7 +728,7 @@ export class DepthChart extends Chart { if (trigger(pt[0])) break bestDepth = pt } - drawLine(dataX, this.theme.crosshairs, true) + drawLine(dataX, this.theme.crosshairs) mouseData = { rate: dataX, depth: bestDepth[1], @@ -632,6 +765,7 @@ export class DepthChart extends Chart { // line. This should be drawn after the depths. if (mouseData) { this.plotRegion.plot(dataExtents, (ctx, tools) => { + if (!mouseData) return // For TypeScript. Duh. dot(ctx, tools.x(mouseData.rate), tools.y(mouseData.depth), mouseData.dotColor, 5) }) } @@ -642,10 +776,10 @@ export class DepthChart extends Chart { } /* drawDepth draws a single side's depth chart data. */ - drawDepth (depth) { + drawDepth (depth: [number, number][]) { const firstPt = depth[0] let y = firstPt[1] - let x + let x: number this.plotRegion.plot(this.dataExtents, (ctx, tools) => { tools.dataCoords(() => { ctx.beginPath() @@ -681,23 +815,36 @@ export class DepthChart extends Chart { } /* setLines stores the indicator lines to draw. */ - setLines (lines) { + setLines (lines: DepthLine[]) { this.lines = lines } /* setMarkers sets the indicator markers to draw. */ - setMarkers (markers) { + setMarkers (markers: Record) { this.markers = markers } } /* CandleChart is a candlestick data renderer. */ export class CandleChart extends Chart { - constructor (parent, reporters) { - super(parent) + reporters: CandleReporters + data: CandlesPayload + zoomLevel: number + numToShow: number + candleRegion: Region + volumeRegion: Region + resizeTimer: number + zoomLevels: number[] + market: Market + rateConversionFactor: number + + constructor (parent: HTMLElement, reporters: CandleReporters) { + super(parent, { + resize: () => this.resized(), + click: (/* e: MouseEvent */) => { this.clicked() }, + zoom: (bigger: boolean) => this.zoomed(bigger) + }) this.reporters = reporters - this.data = null - this.dataExtents = null this.zoomLevel = 1 this.numToShow = 100 this.resize(parent.clientHeight) @@ -712,13 +859,15 @@ export class CandleChart extends Chart { this.volumeRegion = new Region(this.ctx, volumeExtents) // Set a delay on the render to prevent lag. if (this.resizeTimer) clearTimeout(this.resizeTimer) - this.resizeTimer = setTimeout(() => this.draw(), 100) + this.resizeTimer = window.setTimeout(() => this.draw(), 100) } - clicked (e) {} + clicked (/* e: MouseEvent */) { + // handle clicks + } /* zoomed zooms the current view in or out. bigger=true is zoom in. */ - zoomed (bigger) { + zoomed (bigger: boolean) { // bigger actually means fewer candles -> reduce zoomLevels index. const idx = this.zoomLevels.indexOf(this.numToShow) if (bigger) { @@ -749,9 +898,9 @@ export class CandleChart extends Chart { // padding definition and some helper functions to parse candles. const candleWidthPadding = 0.2 - const start = c => truncate(c.endStamp, candleWidth) - const end = c => start(c) + candleWidth - const paddedStart = c => start(c) + candleWidthPadding * candleWidth + const start = (c: Candle) => truncate(c.endStamp, candleWidth) + const end = (c: Candle) => start(c) + candleWidth + const paddedStart = (c: Candle) => start(c) + candleWidthPadding * candleWidth const paddedWidth = (1 - 2 * candleWidthPadding) * candleWidth const first = candles[0] @@ -788,7 +937,7 @@ export class CandleChart extends Chart { this.drawFrame() // Highlight the candle if the user mouse is over the canvas. - let mouseCandle + let mouseCandle: Candle | null = null if (mousePos) { this.plotRegion.plot(new Extents(dataExtents.x.min, dataExtents.x.max, 0, 1), (ctx, tools) => { const selectedStartStamp = truncate(tools.unx(mousePos.x), candleWidth) @@ -804,6 +953,7 @@ export class CandleChart extends Chart { if (mouseCandle) { const yExt = this.xRegion.extents.y this.xRegion.plot(new Extents(dataExtents.x.min, dataExtents.x.max, yExt.min, yExt.max), (ctx, tools) => { + if (!mouseCandle) return // For TypeScript. Duh. this.applyLabelStyle() const rangeTxt = `${new Date(start(mouseCandle)).toLocaleString()} - ${new Date(end(mouseCandle)).toLocaleString()}` const [xPad, yPad] = [25, 2] @@ -818,7 +968,7 @@ export class CandleChart extends Chart { const top = yExt.min + (this.xRegion.height() - rangeHeight) / 2 ctx.fillStyle = this.theme.legendFill ctx.strokeStyle = this.theme.gridBorder - const rectArgs = [left - xPad, top - yPad, rangeWidth + 2 * xPad, rangeHeight + 2 * yPad] + const rectArgs: [number, number, number, number] = [left - xPad, top - yPad, rangeWidth + 2 * xPad, rangeHeight + 2 * yPad] ctx.fillRect(...rectArgs) ctx.strokeRect(...rectArgs) this.applyLabelStyle() @@ -861,7 +1011,7 @@ export class CandleChart extends Chart { } /* setCandles sets the candle data and redraws the chart. */ - setCandles (data, market, baseUnitInfo, quoteUnitInfo) { + setCandles (data: CandlesPayload, market: Market, baseUnitInfo: UnitInfo, quoteUnitInfo: UnitInfo) { this.data = data if (!data.candles) return this.market = market @@ -884,11 +1034,14 @@ export class CandleChart extends Chart { * getters for related data. */ class Extents { - constructor (xMin, xMax, yMin, yMax) { + x: MinMax + y: MinMax + + constructor (xMin: number, xMax: number, yMin: number, yMax: number) { this.setExtents(xMin, xMax, yMin, yMax) } - setExtents (xMin, xMax, yMin, yMax) { + setExtents (xMin: number, xMax: number, yMin: number, yMax: number) { this.x = { min: xMin, max: xMax @@ -899,19 +1052,19 @@ class Extents { } } - get xRange () { + get xRange (): number { return this.x.max - this.x.min } - get midX () { + get midX (): number { return (this.x.max + this.x.min) / 2 } - get yRange () { + get yRange (): number { return this.y.max - this.y.min } - get midY () { + get midY (): number { return (this.y.max + this.y.min) / 2 } } @@ -921,24 +1074,27 @@ class Extents { * transformations and restricting drawing to a specified region of the canvas. */ class Region { - constructor (context, extents) { + context: CanvasRenderingContext2D + extents: Extents + + constructor (context: CanvasRenderingContext2D, extents: Extents) { this.context = context this.extents = extents } - setExtents (xMin, xMax, yMin, yMax) { + setExtents (xMin: number, xMax: number, yMin: number, yMax: number) { this.extents.setExtents(xMin, xMax, yMin, yMax) } - width () { + width (): number { return this.extents.xRange } - height () { + height (): number { return this.extents.yRange } - contains (x, y) { + contains (x: number, y: number): boolean { const ext = this.extents return (x < ext.x.max && x > ext.x.min && y < ext.y.max && y > ext.y.min) @@ -949,7 +1105,7 @@ class Region { * translate data coordinates to canvas coordinates for the specified data * Extents. unx and uny translate canvas coordinates to data coordinates. */ - translator (dataExtents) { + translator (dataExtents: Extents): Translator { const region = this.extents const xMin = dataExtents.x.min // const xMax = dataExtents.x.max @@ -964,23 +1120,24 @@ class Region { const xFactor = screenW / xRange const yFactor = screenH / yRange return { - x: x => (x - xMin) * xFactor + screenMinX, - y: y => screenMaxY - (y - yMin) * yFactor, - unx: x => (x - screenMinX) / xFactor + xMin, - uny: y => yMin - (y - screenMaxY) / yFactor, - w: w => w / xRange * screenW, - h: h => -h / yRange * screenH + x: (x: number) => (x - xMin) * xFactor + screenMinX, + y: (y: number) => screenMaxY - (y - yMin) * yFactor, + unx: (x: number) => (x - screenMinX) / xFactor + xMin, + uny: (y: number) => yMin - (y - screenMaxY) / yFactor, + w: (w: number) => w / xRange * screenW, + h: (h: number) => -h / yRange * screenH, + dataCoords: () => { /* Added when using plot() */ } } } /* clear clears the region. */ clear () { const ext = this.extents - this.ctx.clearRect(ext.x.min, ext.y.min, ext.xRange, ext.yRange) + this.context.clearRect(ext.x.min, ext.y.min, ext.xRange, ext.yRange) } /* plot prepares tools for drawing using data coordinates. */ - plot (dataExtents, drawFunc, skipMask) { + plot (dataExtents: Extents, drawFunc: (ctx: CanvasRenderingContext2D, tools: Translator) => void, skipMask?: boolean) { const ctx = this.context const region = this.extents ctx.save() // Save the original state @@ -1037,7 +1194,16 @@ class Region { * makeLabels attempts to create the appropriate labels for the specified * screen size, context, and label spacing. */ -function makeLabels (ctx, screenW, min, max, spacingGuess, step, unit, valFmt) { +function makeLabels ( + ctx: CanvasRenderingContext2D, + screenW: number, + min: number, + max: number, + spacingGuess: number, + step: number, + unit: string, + valFmt?: (v: number) => string +): LabelSet { valFmt = valFmt || formatLabelValue const n = screenW / spacingGuess const diff = max - min @@ -1049,7 +1215,7 @@ function makeLabels (ctx, screenW, min, max, spacingGuess, step, unit, valFmt) { // The Math.round part is the minimum precision required to see the change in the numbers. // The 2 accounts for the precision of the tick. const sigFigs = Math.round(Math.log10(absMax / tick)) + 2 - const pts = [] + const pts: Label[] = [] let widest = 0 while (x < max) { x = Number(x.toPrecision(sigFigs)) @@ -1072,7 +1238,7 @@ function makeLabels (ctx, screenW, min, max, spacingGuess, step, unit, valFmt) { const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] /* makeCandleTimeLabels prepares labels for candlestick data. */ -function makeCandleTimeLabels (candles, dur, screenW, spacingGuess) { +function makeCandleTimeLabels (candles: Candle[], dur: number, screenW: number, spacingGuess: number): LabelSet { const first = candles[0] const last = candles[candles.length - 1] const start = truncate(first.endStamp, dur) @@ -1080,26 +1246,29 @@ function makeCandleTimeLabels (candles, dur, screenW, spacingGuess) { const diff = end - start const n = Math.min(candles.length, screenW / spacingGuess) const tick = truncate(diff / n, dur) - if (tick === 0) return console.error('zero tick', dur, diff, n) // probably won't happen, but it'd suck if it did + if (tick === 0) { + console.error('zero tick', dur, diff, n) // probably won't happen, but it'd suck if it did + return { lbls: [] } + } let x = start const zoneOffset = new Date().getTimezoneOffset() - const dayStamp = x => { + const dayStamp = (x: number) => { x = x - zoneOffset * 60000 return x - (x % 86400000) } let lastDay = dayStamp(start) let lastYear = 0 // new Date(start).getFullYear() - if (dayStamp(first) === dayStamp(last)) lastDay = 0 // Force at least one day stamp. + if (dayStamp(first.endStamp) === dayStamp(last.endStamp)) lastDay = 0 // Force at least one day stamp. const pts = [] let label if (dur < 86400000) { - label = (d, x) => { + label = (d: Date, x: number) => { const day = dayStamp(x) if (day !== lastDay) return `${months[d.getMonth()]}${d.getDate()} ${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}` else return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}` } } else { - label = d => { + label = (d: Date) => { const year = d.getFullYear() if (year !== lastYear) return `${months[d.getMonth()]}${d.getDate()} '${String(year).slice(2, 4)}` else return `${months[d.getMonth()]}${d.getDate()}` @@ -1119,12 +1288,12 @@ function makeCandleTimeLabels (candles, dur, screenW, spacingGuess) { } /* The last element of an array. */ -function last (arr) { +function last (arr: any[]): any { return arr[arr.length - 1] } /* line draws a line with the provided context. */ -function line (ctx, x0, y0, x1, y1, skipStroke) { +function line (ctx: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number, skipStroke?: boolean) { ctx.beginPath() ctx.moveTo(x0, y0) ctx.lineTo(x1, y1) @@ -1132,7 +1301,7 @@ function line (ctx, x0, y0, x1, y1, skipStroke) { } /* dot draws a circle with the provided context. */ -function dot (ctx, x, y, color, radius) { +function dot (ctx: CanvasRenderingContext2D, x: number, y: number, color: string, radius: number) { ctx.fillStyle = color ctx.beginPath() ctx.arc(x, y, radius, 0, PIPI) @@ -1140,7 +1309,7 @@ function dot (ctx, x, y, color, radius) { } /* clamp returns v if min <= v <= max, else min or max. */ -function clamp (v, min, max) { +function clamp (v: number, min: number, max: number): number { if (v < min) return min if (v > max) return max return v @@ -1153,12 +1322,12 @@ const labelSpecs = { } /* formatLabelValue formats the provided value using the labelSpecs format. */ -function formatLabelValue (x) { +function formatLabelValue (x: number) { return x.toLocaleString('en-us', labelSpecs) } /* floatCompare compares two floats to within a tolerance of 1e-8. */ -function floatCompare (a, b) { +function floatCompare (a: number, b: number) { return withinTolerance(a, b, 1e-8) } @@ -1166,10 +1335,10 @@ function floatCompare (a, b) { * withinTolerance returns true if the difference between a and b are with * the specified tolerance. */ -function withinTolerance (a, b, tolerance) { +function withinTolerance (a: number, b: number, tolerance: number) { return Math.abs(a - b) < Math.abs(tolerance) } -function truncate (v, w) { +function truncate (v: number, w: number): number { return v - (v % w) } diff --git a/client/webserver/site/src/js/doc.js b/client/webserver/site/src/js/doc.ts similarity index 76% rename from client/webserver/site/src/js/doc.js rename to client/webserver/site/src/js/doc.ts index 80d5670a62..a934d91eeb 100644 --- a/client/webserver/site/src/js/doc.js +++ b/client/webserver/site/src/js/doc.ts @@ -1,4 +1,10 @@ import * as intl from './locales' +import { + UnitInfo, + LayoutMetrics, + WalletState, + PageElement +} from './registry' const parser = new window.DOMParser() @@ -17,7 +23,7 @@ const BipIDs = { const BipSymbols = Object.values(BipIDs) -const intFormatter = new Intl.NumberFormat(navigator.languages) +const intFormatter = new Intl.NumberFormat((navigator.languages as string[])) /* A cache for formatters used for Doc.formatCoinValue. */ const decimalFormatters = {} @@ -26,7 +32,7 @@ const decimalFormatters = {} * decimalFormatter gets the formatCoinValue formatter for the specified decimal * precision. */ -function decimalFormatter (prec) { +function decimalFormatter (prec: number) { return formatter(decimalFormatters, 2, prec) } @@ -37,7 +43,7 @@ const fullPrecisionFormatters = {} * fullPrecisionFormatter gets the formatFullPrecision formatter for the * specified decimal precision. */ -function fullPrecisionFormatter (prec) { +function fullPrecisionFormatter (prec: number) { return formatter(fullPrecisionFormatters, prec, prec) } @@ -45,11 +51,11 @@ function fullPrecisionFormatter (prec) { * formatter gets the formatter from the supplied cache if it already exists, * else creates it. */ -function formatter (formatters, min, max) { +function formatter (formatters: Record, min: number, max: number): Intl.NumberFormat { const k = `${min}-${max}` let fmt = formatters[k] if (!fmt) { - fmt = new Intl.NumberFormat(navigator.languages, { + fmt = new Intl.NumberFormat((navigator.languages as string[]), { minimumFractionDigits: min, maximumFractionDigits: max }) @@ -62,7 +68,7 @@ function formatter (formatters, min, max) { * convertToConventional converts the value in atomic units to conventional * units. */ -function convertToConventional (v, unitInfo) { +function convertToConventional (v: number, unitInfo?: UnitInfo) { let prec = 8 if (unitInfo) { const f = unitInfo.conventional.conversionFactor @@ -78,22 +84,22 @@ export default class Doc { * idel is the element with the specified id that is the descendent of the * specified node. */ - static idel (el, id) { - return el.querySelector(`#${id}`) + static idel (el: Document | Element, id: string): HTMLElement { + return el.querySelector(`#${id}`) as HTMLElement } /* bind binds the function to the event for the element. */ - static bind (el, ev, f) { + static bind (el: EventTarget, ev: string, f: (e: Event) => void) { el.addEventListener(ev, f) } /* unbind removes the handler for the event from the element. */ - static unbind (el, ev, f) { + static unbind (el: EventTarget, ev: string, f: (e: Event) => void) { el.removeEventListener(ev, f) } /* noderize creates a Document object from a string of HTML. */ - static noderize (html) { + static noderize (html: string): Document { return parser.parseFromString(html, 'text/html') } @@ -101,7 +107,7 @@ export default class Doc { * mouseInElement returns true if the position of mouse event, e, is within * the bounds of the specified element. */ - static mouseInElement (e, el) { + static mouseInElement (e: MouseEvent, el: HTMLElement): boolean { const rect = el.getBoundingClientRect() return e.pageX >= rect.left && e.pageX <= rect.right && e.pageY >= rect.top && e.pageY <= rect.bottom @@ -110,7 +116,7 @@ export default class Doc { /* * layoutMetrics gets information about the elements position on the page. */ - static layoutMetrics (el) { + static layoutMetrics (el: HTMLElement): LayoutMetrics { const box = el.getBoundingClientRect() const docEl = document.documentElement const top = box.top + docEl.scrollTop @@ -128,7 +134,7 @@ export default class Doc { } /* empty removes all child nodes from the specified element. */ - static empty (...els) { + static empty (...els: Element[]) { for (const el of els) while (el.firstChild) el.removeChild(el.firstChild) } @@ -136,7 +142,7 @@ export default class Doc { * hide hides the specified elements. This is accomplished by adding the * bootstrap d-hide class to the element. Use Doc.show to undo. */ - static hide (...els) { + static hide (...els: Element[]) { for (const el of els) el.classList.add('d-hide') } @@ -144,17 +150,17 @@ export default class Doc { * show shows the specified elements. This is accomplished by removing the * bootstrap d-hide class as added with Doc.hide. */ - static show (...els) { + static show (...els: Element[]) { for (const el of els) el.classList.remove('d-hide') } /* isHidden returns true if the specified element is hidden */ - static isHidden (el) { + static isHidden (el: Element): boolean { return el.classList.contains('d-hide') } /* isDisplayed returns true if the specified element is not hidden */ - static isDisplayed (el) { + static isDisplayed (el: Element): boolean { return !el.classList.contains('d-hide') } @@ -166,7 +172,7 @@ export default class Doc { * algorithm. See the Easing object for the available easing algo choices. The * default easing algorithm is linear. */ - static async animate (duration, f, easingAlgo) { + static async animate (duration: number, f: (progress: number) => void, easingAlgo?: string) { const easer = easingAlgo ? Easing[easingAlgo] : Easing.linear const start = new Date().getTime() const end = start + duration @@ -181,14 +187,29 @@ export default class Doc { f(1) } + static applySelector (ancestor: HTMLElement, k: string): PageElement[] { + return Array.from(ancestor.querySelectorAll(k)) as PageElement[] + } + + static kids (ancestor: HTMLElement): PageElement[] { + return Array.from(ancestor.children) as PageElement[] + } + + static safeSelector (ancestor: HTMLElement, k: string): PageElement { + const el = ancestor.querySelector(k) + if (el) return el as PageElement + console.warn(`no element found for selector '${k}' on element ->`, ancestor) + return document.createElement('div') + } + /* * idDescendants creates an object mapping to elements which are descendants * of the ancestor and have id attributes. Elements are keyed by their id * value. */ - static idDescendants (ancestor) { - const d = {} - for (const el of ancestor.querySelectorAll('[id]')) d[el.id] = el + static idDescendants (ancestor: HTMLElement): Record { + const d: Record = {} + for (const el of Doc.applySelector(ancestor, '[id]')) d[el.id] = el return d } @@ -197,7 +218,7 @@ export default class Doc { * representation in conventional units. If the value happens to be an * integer, no decimals are displayed. Trailing zeros may be truncated. */ - static formatCoinValue (vAtomic, unitInfo) { + static formatCoinValue (vAtomic: number, unitInfo?: UnitInfo): string { const [v, prec] = convertToConventional(vAtomic, unitInfo) if (Number.isInteger(v)) return intFormatter.format(v) return decimalFormatter(prec).format(v) @@ -208,7 +229,7 @@ export default class Doc { * representation in conventional units using the full decimal precision * associated with the conventional unit's conversion factor. */ - static formatFullPrecision (vAtomic, unitInfo) { + static formatFullPrecision (vAtomic: number, unitInfo?: UnitInfo): string { const [v, prec] = convertToConventional(vAtomic, unitInfo) return fullPrecisionFormatter(prec).format(v) } @@ -218,7 +239,7 @@ export default class Doc { * the symbol is not a supported asset, the generic letter logo will be * requested instead. */ - static logoPath (symbol) { + static logoPath (symbol: string): string { if (BipSymbols.indexOf(symbol) === -1) symbol = symbol.substring(0, 1) return `/img/coins/${symbol}.png` } @@ -227,7 +248,7 @@ export default class Doc { * cleanTemplates removes the elements from the DOM and deletes the id * attribute. */ - static cleanTemplates (...tmpls) { + static cleanTemplates (...tmpls: HTMLElement[]) { tmpls.forEach(tmpl => { tmpl.remove() tmpl.removeAttribute('id') @@ -238,17 +259,17 @@ export default class Doc { * tmplElement is a helper function for grabbing sub-elements of the market list * template. */ - static tmplElement (ancestor, s) { - return ancestor.querySelector(`[data-tmpl="${s}"]`) + static tmplElement (ancestor: Document | Element, s: string): PageElement { + return ancestor.querySelector(`[data-tmpl="${s}"]`) || document.createElement('div') } /* * parseTemplate returns an object of data-tmpl elements, keyed by their * data-tmpl values. */ - static parseTemplate (ancestor) { - const d = {} - for (const el of ancestor.querySelectorAll('[data-tmpl]')) d[el.dataset.tmpl] = el + static parseTemplate (ancestor: HTMLElement): Record { + const d: Record = {} + for (const el of Doc.applySelector(ancestor, '[data-tmpl]')) d[el.dataset.tmpl || ''] = el return d } @@ -256,16 +277,16 @@ export default class Doc { * timeSince returns a string representation of the duration since the * specified unix timestamp. */ - static timeSince (t) { + static timeSince (t: number): string { return Doc.formatDuration((new Date().getTime()) - t) } /* formatDuration returns a string representation of the duration */ - static formatDuration (dur) { + static formatDuration (dur: number): string { let seconds = Math.floor(dur) let result = '' let count = 0 - const add = (n, s) => { + const add = (n: number, s: string) => { if (n > 0 || count > 0) count++ if (n > 0) result += `${n} ${s} ` return count >= 2 @@ -293,7 +314,7 @@ export default class Doc { * scroll increment/decrement behavior for a wheel action on a * number input. */ - static disableMouseWheel (...inputFields) { + static disableMouseWheel (...inputFields: Element[]) { for (const inputField of inputFields) { inputField.addEventListener('wheel', (ev) => { ev.preventDefault() @@ -303,7 +324,7 @@ export default class Doc { } /* Easing algorithms for animations. */ -const Easing = { +const Easing: Record number> = { linear: t => t, easeIn: t => t * t, easeOut: t => t * (2 - t), @@ -313,8 +334,11 @@ const Easing = { /* WalletIcons are used for controlling wallets in various places. */ export class WalletIcons { - constructor (box) { - const stateElement = (name) => box.querySelector(`[data-state=${name}]`) + icons: Record + status: Element + + constructor (box: HTMLElement) { + const stateElement = (name: string) => box.querySelector(`[data-state=${name}]`) as HTMLElement this.icons = {} this.icons.sleeping = stateElement('sleeping') this.icons.locked = stateElement('locked') @@ -362,7 +386,7 @@ export class WalletIcons { if (this.status) this.status.textContent = intl.prep(intl.ID_NOWALLET) } - setSyncing (wallet) { + setSyncing (wallet: WalletState | null) { const syncIcon = this.icons.syncing if (!wallet || !wallet.running) { Doc.hide(syncIcon) @@ -385,12 +409,10 @@ export class WalletIcons { } /* reads the core.Wallet state and sets the icon visibility. */ - readWallet (wallet) { + readWallet (wallet: WalletState | null) { this.setSyncing(wallet) + if (!wallet) return this.nowallet() switch (true) { - case (!wallet): - this.nowallet() - break case (!wallet.running): this.sleeping() break @@ -407,7 +429,7 @@ export class WalletIcons { } /* sleep can be used by async functions to pause for a specified period. */ -function sleep (ms) { +function sleep (ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } @@ -418,7 +440,7 @@ const anHour = 3600000 const aMinute = 60000 /* timeMod returns the quotient and remainder of t / dur. */ -function timeMod (t, dur) { +function timeMod (t: number, dur: number) { const n = Math.floor(t / dur) return [n, t - n * dur] } diff --git a/client/webserver/site/src/js/forms.js b/client/webserver/site/src/js/forms.ts similarity index 80% rename from client/webserver/site/src/js/forms.js rename to client/webserver/site/src/js/forms.ts index a3217432ff..df8f8b3c32 100644 --- a/client/webserver/site/src/js/forms.js +++ b/client/webserver/site/src/js/forms.ts @@ -1,20 +1,50 @@ -import { app } from './registry' import Doc from './doc' import { postJSON } from './http' import State from './state' import * as intl from './locales' import { RateEncodingFactor } from './orderutil' +import { + app, + PasswordCache, + SupportedAsset, + PageElement, + WalletDefinition, + ConfigOption, + Exchange, + Market, + UnitInfo, + FeeAsset, + WalletState, + WalletBalance +} from './registry' + +interface ConfigOptionInput extends HTMLInputElement { + configOpt: ConfigOption +} + +interface ProgressPoint { + stamp: number + progress: number +} /* * NewWalletForm should be used with the "newWalletForm" template. The enclosing *
element should be the second argument of the constructor. */ export class NewWalletForm { - constructor (form, success, pwCache, backFunc) { + page: Record + form: HTMLElement + pwCache: PasswordCache | null + success: (assetID: number) => void + currentAsset: SupportedAsset + pwHiders: HTMLElement[] + subform: WalletConfigForm + currentWalletType: string + + constructor (form: HTMLElement, success: (assetID: number) => void, pwCache?: PasswordCache, backFunc?: () => void) { this.form = form this.success = success - this.pwCache = pwCache - this.currentAsset = null + this.pwCache = pwCache || null const page = this.page = Doc.parseTemplate(form) this.pwHiders = Array.from(form.querySelectorAll('.hide-pw')) this.refresh() @@ -44,22 +74,24 @@ export class NewWalletForm { async submit () { const page = this.page - const pw = page.appPass.value || (this.pwCache ? this.pwCache.pw : '') + const appPass = page.appPass as HTMLInputElement + const newWalletPass = page.newWalletPass as HTMLInputElement + const pw = appPass.value || (this.pwCache ? this.pwCache.pw : '') if (!pw && !State.passwordIsCached()) { page.newWalletErr.textContent = intl.prep(intl.ID_NO_APP_PASS_ERROR_MSG) Doc.show(page.newWalletErr) return } Doc.hide(page.newWalletErr) - const assetID = parseInt(this.currentAsset.id) + const assetID = this.currentAsset.id const createForm = { assetID: assetID, - pass: page.newWalletPass.value || '', + pass: newWalletPass.value || '', config: this.subform.map(), appPass: pw, walletType: this.currentWalletType } - page.appPass.value = '' + appPass.value = '' const loaded = app().loading(page.mainForm) const res = await postJSON('/api/newwallet', createForm) loaded() @@ -68,11 +100,11 @@ export class NewWalletForm { return } if (this.pwCache) this.pwCache.pw = pw - page.newWalletPass.value = '' + newWalletPass.value = '' this.success(assetID) } - async setAsset (assetID) { + async setAsset (assetID: number) { const page = this.page const asset = app().assets[assetID] const tabs = page.walletTypeTabs @@ -92,24 +124,25 @@ export class NewWalletForm { if (asset.info.availablewallets.length > 1) { Doc.show(tabs) for (const wDef of asset.info.availablewallets) { - const tab = page.walletTabTmpl.cloneNode(true) + const tab = page.walletTabTmpl.cloneNode(true) as HTMLElement tab.dataset.tooltip = wDef.description tab.textContent = wDef.tab tabs.appendChild(tab) Doc.bind(tab, 'click', () => { - for (const t of tabs.children) t.classList.remove('selected') + for (const t of Doc.kids(tabs)) t.classList.remove('selected') tab.classList.add('selected') this.update(wDef) }) } app().bindTooltips(tabs) - tabs.firstChild.classList.add('selected') + const first = tabs.firstChild as HTMLElement + first.classList.add('selected') } await this.update(walletDef) } - async update (walletDef) { + async update (walletDef: WalletDefinition) { const page = this.page this.currentWalletType = walletDef.type const appPwCached = State.passwordIsCached() || (this.pwCache && this.pwCache.pw) @@ -145,7 +178,7 @@ export class NewWalletForm { } /* setError sets and shows the in-form error message. */ - async setError (errMsg) { + async setError (errMsg: string) { this.page.newWalletErr.textContent = errMsg Doc.show(this.page.newWalletErr) } @@ -178,7 +211,29 @@ export class NewWalletForm { * asset-specific wallet configuration options. */ export class WalletConfigForm { - constructor (form, sectionize) { + form: HTMLElement + configElements: Record + configOpts: ConfigOption[] + sectionize: boolean + allSettings: PageElement + dynamicOpts: PageElement + textInputTmpl: PageElement + dateInputTmpl: PageElement + checkboxTmpl: PageElement + fileSelector: PageElement + fileInput: PageElement + errMsg: PageElement + showOther: PageElement + showIcon: PageElement + hideIcon: PageElement + showHideMsg: PageElement + otherSettings: PageElement + loadedSettingsMsg: PageElement + loadedSettings: PageElement + defaultSettingsMsg: PageElement + defaultSettings: PageElement + + constructor (form: HTMLElement, sectionize: boolean) { this.form = form // A configElement is a div containing an input and its label. this.configElements = {} @@ -228,8 +283,10 @@ export class WalletConfigForm { async fileInputChanged () { Doc.hide(this.errMsg) if (!this.fileInput.value) return + const files = this.fileInput.files + if (!files || files.length === 0) return const loaded = app().loading(this.form) - const config = await this.fileInput.files[0].text() + const config = await files[0].text() if (!config) return const res = await postJSON('/api/parseconfig', { configtext: config @@ -252,7 +309,7 @@ export class WalletConfigForm { /* * update creates the dynamic form. */ - update (configOpts, assetHasActiveOrders) { + update (configOpts: ConfigOption[], assetHasActiveOrders?: boolean) { this.configElements = {} this.configOpts = configOpts Doc.empty(this.dynamicOpts, this.defaultSettings, this.loadedSettings) @@ -267,17 +324,17 @@ export class WalletConfigForm { this.defaultSettings, this.errMsg ) const defaultedOpts = [] - const addOpt = (box, opt) => { + const addOpt = (box: HTMLElement, opt: ConfigOption) => { const elID = 'wcfg-' + opt.key - let el - if (opt.isboolean) el = this.checkboxTmpl.cloneNode(true) - else if (opt.isdate) el = this.dateInputTmpl.cloneNode(true) - else el = this.textInputTmpl.cloneNode(true) + let el: HTMLElement + if (opt.isboolean) el = this.checkboxTmpl.cloneNode(true) as HTMLElement + else if (opt.isdate) el = this.dateInputTmpl.cloneNode(true) as HTMLElement + else el = this.textInputTmpl.cloneNode(true) as HTMLElement this.configElements[opt.key] = el - const input = el.querySelector('input') + const input = el.querySelector('input') as ConfigOptionInput input.id = elID input.configOpt = opt - const label = el.querySelector('label') + const label = Doc.safeSelector(el, 'label') label.htmlFor = elID // 'for' attribute, but 'for' is a keyword label.prepend(opt.displayname) box.appendChild(el) @@ -285,16 +342,16 @@ export class WalletConfigForm { if (opt.description) label.dataset.tooltip = opt.description if (opt.isboolean) input.checked = opt.default else if (opt.isdate) { - const getMinMaxVal = (minMax) => { - if (!minMax) return undefined + const getMinMaxVal = (minMax: string | number) => { + if (!minMax) return '' if (minMax === 'now') return dateToString(new Date()) - return dateToString(new Date(minMax * 1000)) + return dateToString(new Date((minMax as number) * 1000)) } input.max = getMinMaxVal(opt.max) input.min = getMinMaxVal(opt.min) input.valueAsDate = opt.default ? new Date(opt.default * 1000) : new Date() } else input.value = opt.default !== null ? opt.default : '' - input.disabled = opt.disablewhenactive && assetHasActiveOrders + input.disabled = Boolean(opt.disablewhenactive && assetHasActiveOrders) } for (const opt of this.configOpts) { if (this.sectionize && opt.default !== null) defaultedOpts.push(opt) @@ -316,7 +373,7 @@ export class WalletConfigForm { /* * setOtherSettingsViz sets the visibility of the additional settings section. */ - setOtherSettingsViz (visible) { + setOtherSettingsViz (visible: boolean) { if (visible) { Doc.hide(this.showIcon) Doc.show(this.hideIcon, this.otherSettings) @@ -333,15 +390,15 @@ export class WalletConfigForm { * sets the inputs value to the corresponding cfg value. A list of matching * configElements is returned. */ - setConfig (cfg) { - const finds = [] - this.allSettings.querySelectorAll('input').forEach(input => { + setConfig (cfg: Record) { + const finds: HTMLElement[] = [] + this.allSettings.querySelectorAll('input').forEach((input: ConfigOptionInput) => { const k = input.configOpt.key const v = cfg[k] if (typeof v === 'undefined') return finds.push(this.configElements[k]) if (input.configOpt.isboolean) input.checked = isTruthyString(v) - else if (input.configOpt.isdate) input.valueAsDate = new Date(v * 1000) + else if (input.configOpt.isdate) input.valueAsDate = new Date(parseInt(v) * 1000) else input.value = v }) return finds @@ -351,7 +408,7 @@ export class WalletConfigForm { * setLoadedConfig sets the input values for the entries in cfg, and moves * them to the loadedSettings box. */ - setLoadedConfig (cfg) { + setLoadedConfig (cfg: Record) { const finds = this.setConfig(cfg) if (!this.sectionize || finds.length === 0) return this.loadedSettings.append(...finds) @@ -364,9 +421,9 @@ export class WalletConfigForm { * map reads all inputs and constructs an object from the configOpt keys and * values. */ - map () { - const config = {} - this.allSettings.querySelectorAll('input').forEach(input => { + map (): Record { + const config: Record = {} + this.allSettings.querySelectorAll('input').forEach((input: ConfigOptionInput) => { if (input.configOpt.isboolean && input.configOpt.key) { config[input.configOpt.key] = input.checked ? '1' : '0' } else if (input.configOpt.isdate && input.configOpt.key) { @@ -388,15 +445,15 @@ export class WalletConfigForm { * reorder sorts the configElements in the box by the order of the * server-provided configOpts array. */ - reorder (box) { - const els = {} - box.querySelectorAll('input').forEach(el => { - const k = el.configOpt.key - els[k] = this.configElements[k] + reorder (box: HTMLElement) { + const inputs: Record = {} + box.querySelectorAll('input').forEach((input: ConfigOptionInput) => { + const k = input.configOpt.key + inputs[k] = this.configElements[k] }) for (const opt of this.configOpts) { - const el = els[opt.key] - if (el) box.append(el) + const input = inputs[opt.key] + if (input) box.append(input) } } } @@ -406,20 +463,26 @@ export class WalletConfigForm { * template. */ export class ConfirmRegistrationForm { - constructor (form, success, goBack, pwCache) { + form: HTMLElement + success: () => void + page: Record + xc: Exchange + certFile: string + feeAssetID: number + pwCache: PasswordCache + + constructor (form: HTMLElement, success: () => void, goBack: () => void, pwCache: PasswordCache) { this.form = form this.success = success this.page = Doc.parseTemplate(form) - this.xc = null this.certFile = '' - this.feeAssetID = null this.pwCache = pwCache Doc.bind(this.page.goBack, 'click', () => goBack()) bind(form, this.page.submit, () => this.submitForm()) } - setExchange (xc, certFile) { + setExchange (xc: Exchange, certFile: string) { this.xc = xc this.certFile = certFile const page = this.page @@ -428,7 +491,7 @@ export class ConfirmRegistrationForm { page.host.textContent = xc.host } - setAsset (assetID) { + setAsset (assetID: number) { const asset = app().assets[assetID] const unitInfo = asset.info.unitinfo this.feeAssetID = asset.id @@ -444,7 +507,7 @@ export class ConfirmRegistrationForm { const form = this.form Doc.animate(400, prog => { form.style.transform = `scale(${prog})` - form.style.opacity = Math.pow(prog, 4) + form.style.opacity = String(Math.pow(prog, 4)) const offset = `${(1 - prog) * 500}px` form.style.top = offset form.style.left = offset @@ -495,24 +558,28 @@ export class ConfirmRegistrationForm { * FeeAssetSelectionForm should be used with the "regAssetForm" template. */ export class FeeAssetSelectionForm { - constructor (form, success) { + form: HTMLElement + success: (assetID: number) => void + xc: Exchange + page: Record + + constructor (form: HTMLElement, success: (assetID: number) => void) { this.form = form this.success = success - this.xc = null this.page = Doc.parseTemplate(form) Doc.cleanTemplates(this.page.marketTmpl, this.page.assetTmpl) } - setExchange (xc) { + setExchange (xc: Exchange) { this.xc = xc const page = this.page Doc.empty(page.assets, page.allMarkets) - const cFactor = ui => ui.conventional.conversionFactor + const cFactor = (ui: UnitInfo) => ui.conventional.conversionFactor - const marketNode = (mkt, excludeIcon) => { - const marketNode = page.marketTmpl.cloneNode(true) - const marketTmpl = Doc.parseTemplate(marketNode) + const marketNode = (mkt: Market, excludeIcon?: number) => { + const n = page.marketTmpl.cloneNode(true) as HTMLElement + const marketTmpl = Doc.parseTemplate(n) const baseAsset = xc.assets[mkt.baseid] const baseUnitInfo = app().unitInfo(mkt.baseid, xc) @@ -526,10 +593,11 @@ export class FeeAssetSelectionForm { const otherSymbol = xc.assets[excludeBase ? mkt.quoteid : mkt.baseid].symbol marketTmpl.logo.src = Doc.logoPath(otherSymbol) } else { - const otherLogo = marketTmpl.logo.cloneNode(true) + const otherLogo = marketTmpl.logo.cloneNode(true) as PageElement marketTmpl.logo.src = Doc.logoPath(baseAsset.symbol) otherLogo.src = Doc.logoPath(quoteAsset.symbol) - marketTmpl.logo.parentNode.insertBefore(otherLogo, marketTmpl.logo.nextSibling) + const parent = marketTmpl.logo.parentNode + if (parent) parent.insertBefore(otherLogo, marketTmpl.logo.nextSibling) } const baseSymbol = baseAsset.symbol.toUpperCase() @@ -546,7 +614,7 @@ export class FeeAssetSelectionForm { const s = Doc.formatCoinValue(quoteLot, quoteUnitInfo) marketTmpl.quoteLotSize.textContent = `(~${s} ${quoteSymbol})` } - return marketNode + return n } for (const [symbol, feeAsset] of Object.entries(xc.regFees)) { @@ -554,14 +622,14 @@ export class FeeAssetSelectionForm { if (!asset) continue const haveWallet = asset.wallet const unitInfo = asset.info.unitinfo - const assetNode = page.assetTmpl.cloneNode(true) + const assetNode = page.assetTmpl.cloneNode(true) as HTMLElement Doc.bind(assetNode, 'click', () => { this.success(feeAsset.id) }) const assetTmpl = Doc.parseTemplate(assetNode) page.assets.appendChild(assetNode) assetTmpl.logo.src = Doc.logoPath(symbol) const fee = Doc.formatCoinValue(feeAsset.amount, unitInfo) assetTmpl.fee.textContent = `${fee} ${unitInfo.conventional.unit}` - assetTmpl.confs.textContent = feeAsset.confs + assetTmpl.confs.textContent = String(feeAsset.confs) assetTmpl.ready.textContent = haveWallet ? intl.prep(intl.WALLET_READY) : intl.prep(intl.SETUP_NEEDED) assetTmpl.ready.classList.add(haveWallet ? 'readygreen' : 'setuporange') @@ -598,7 +666,7 @@ export class FeeAssetSelectionForm { const extraMargin = 75 const extraTop = 50 const fontSize = 24 - const regAssetElements = Array.from(page.assets.children) + const regAssetElements = Array.from(page.assets.children) as PageElement[] regAssetElements.push(page.allmkts) form.style.opacity = '0' @@ -620,13 +688,22 @@ export class FeeAssetSelectionForm { * in preparation for paying the registration fee. */ export class WalletWaitForm { - constructor (form, success, goBack) { + form: HTMLElement + success: () => void + goBack: () => void + page: Record + assetID: number + xc: Exchange + regFee: FeeAsset + progressCache: ProgressPoint[] + progressed: boolean + funded: boolean + + constructor (form: HTMLElement, success: () => void, goBack: () => void) { this.form = form this.success = success this.page = Doc.parseTemplate(form) this.assetID = -1 - this.xc = null - this.regFee = null this.progressCache = [] this.progressed = false this.funded = false @@ -638,12 +715,12 @@ export class WalletWaitForm { } /* setExchange sets the exchange for which the fee is being paid. */ - setExchange (xc) { + setExchange (xc: Exchange) { this.xc = xc } /* setWallet must be called before showing the form. */ - setWallet (wallet, txFee) { + setWallet (wallet: WalletState, txFee: number) { this.assetID = wallet.assetID this.progressCache = [] this.progressed = false @@ -652,7 +729,7 @@ export class WalletWaitForm { const asset = app().assets[wallet.assetID] const fee = this.regFee = this.xc.regFees[asset.symbol] - for (const span of this.form.querySelectorAll('.unit')) span.textContent = asset.symbol.toUpperCase() + for (const span of Doc.applySelector(this.form, '.unit')) span.textContent = asset.symbol.toUpperCase() page.logo.src = Doc.logoPath(asset.symbol) page.depoAddr.textContent = wallet.address page.fee.textContent = Doc.formatCoinValue(fee.amount, asset.info.unitinfo) @@ -672,7 +749,7 @@ export class WalletWaitForm { Doc.show(wallet.synced ? page.syncCheck : wallet.syncProgress >= 1 ? page.syncSpinner : page.syncUncheck) Doc.show(wallet.balance.available > fee.amount ? page.balCheck : page.balUncheck) - page.progress.textContent = Math.round(wallet.syncProgress * 100) + page.progress.textContent = String(Math.round(wallet.syncProgress * 100)) if (wallet.synced) { this.progressed = true @@ -684,7 +761,7 @@ export class WalletWaitForm { * reportWalletState sets the progress and balance, ultimately calling the * success function if conditions are met. */ - reportWalletState (wallet) { + reportWalletState (wallet: WalletState) { if (wallet.assetID !== this.assetID) return if (this.progressed && this.funded) return this.reportProgress(wallet.synced, wallet.syncProgress) @@ -695,7 +772,7 @@ export class WalletWaitForm { * reportBalance sets the balance display and calls success if we go over the * threshold. */ - reportBalance (bal, assetID) { + reportBalance (bal: WalletBalance, assetID: number) { if (this.funded || this.assetID === -1 || this.assetID !== assetID) return const page = this.page const asset = app().assets[this.assetID] @@ -716,7 +793,7 @@ export class WalletWaitForm { * reportProgress sets the progress display and calls success if we are fully * synced. */ - reportProgress (synced, prog) { + reportProgress (synced: boolean, prog: number) { const page = this.page if (synced) { page.progress.textContent = '100' @@ -732,7 +809,7 @@ export class WalletWaitForm { Doc.hide(page.syncSpinner) Doc.show(page.syncUncheck) } - page.progress.textContent = Math.round(prog * 100) + page.progress.textContent = String(Math.round(prog * 100)) // The remaining time estimate must be based on more than one progress // report. We'll cache up to the last 20 and look at the difference between @@ -761,16 +838,21 @@ export class WalletWaitForm { } export class UnlockWalletForm { - constructor (form, success, pwCache) { + form: HTMLElement + success: () => void + pwCache: PasswordCache | null + page: Record + currentAsset: SupportedAsset + + constructor (form: HTMLElement, success: () => void, pwCache?: PasswordCache) { this.page = Doc.idDescendants(form) this.form = form - this.pwCache = pwCache - this.currentAsset = null + this.pwCache = pwCache || null this.success = success bind(form, this.page.submitUnlock, () => this.submit()) } - setAsset (asset) { + setAsset (asset: SupportedAsset) { const page = this.page this.currentAsset = asset page.uwAssetLogo.src = Doc.logoPath(asset.symbol) @@ -784,7 +866,7 @@ export class UnlockWalletForm { /* * setError displays an error on the form. */ - setError (msg) { + setError (msg: string) { this.page.unlockErr.textContent = msg Doc.show(this.page.unlockErr) } @@ -793,7 +875,7 @@ export class UnlockWalletForm { * showErrorOnly displays only an error on the form. Hides the * app pass field and the submit button. */ - showErrorOnly (msg) { + showErrorOnly (msg: string) { this.setError(msg) Doc.hide(this.page.uwAppPassBox) Doc.hide(this.page.submitUnlockDiv) @@ -809,7 +891,7 @@ export class UnlockWalletForm { } Doc.hide(this.page.unlockErr) const open = { - assetID: parseInt(this.currentAsset.id), + assetID: this.currentAsset.id, pass: pw } page.uwAppPass.value = '' @@ -827,10 +909,17 @@ export class UnlockWalletForm { /* DEXAddressForm accepts a DEX address and performs account discovery. */ export class DEXAddressForm { - constructor (form, success, pwCache) { + form: HTMLElement + success: (xc: Exchange, cert: string) => void + pwCache: PasswordCache | null + defaultTLSText: string + page: Record + knownExchanges: HTMLElement[] + + constructor (form: HTMLElement, success: (xc: Exchange, cert: string) => void, pwCache?: PasswordCache) { this.form = form this.success = success - this.pwCache = pwCache + this.pwCache = pwCache || null this.defaultTLSText = 'none selected' const page = this.page = Doc.parseTemplate(form) @@ -879,11 +968,11 @@ export class DEXAddressForm { const form = this.form Doc.animate(550, prog => { form.style.transform = `scale(${0.9 + 0.1 * prog})` - form.style.opacity = Math.pow(prog, 4) + form.style.opacity = String(Math.pow(prog, 4)) }, 'easeOut') } - async checkDEX (addr) { + async checkDEX (addr?: string) { const page = this.page Doc.hide(page.err) addr = addr || page.addr.value @@ -895,12 +984,15 @@ export class DEXAddressForm { let cert = '' if (page.certFile.value) { - cert = await page.certFile.files[0].text() + const files = page.certFile.files + if (files && files.length) { + cert = await files[0].text() + } } let pw = '' if (!State.passwordIsCached()) { - pw = page.appPW.value || this.pwCache.pw + pw = page.appPW.value || (this.pwCache ? this.pwCache.pw : '') } const loaded = app().loading(this.form) @@ -939,7 +1031,7 @@ export class DEXAddressForm { async onCertFileChange () { const page = this.page const files = page.certFile.files - if (!files.length) return + if (!files || !files.length) return page.selectedCert.textContent = files[0].name Doc.show(page.removeCert) Doc.hide(page.addCert) @@ -957,12 +1049,18 @@ export class DEXAddressForm { /* LoginForm is used to sign into the app. */ export class LoginForm { - constructor (form, success, pwCache) { + form: HTMLElement + success: () => void + pwCache: PasswordCache | null + headerTxt: string + page: Record + + constructor (form: HTMLElement, success: () => void, pwCache?: PasswordCache) { this.success = success this.form = form - this.pwCache = pwCache + this.pwCache = pwCache || null const page = this.page = Doc.parseTemplate(form) - this.headerTxt = page.header.textContent + this.headerTxt = page.header.textContent || '' bind(form, page.submit, () => { this.submit() }) } @@ -971,10 +1069,10 @@ export class LoginForm { this.page.pw.focus() } - async submit (e) { + async submit () { const page = this.page Doc.hide(page.errMsg) - const pw = page.pw.value + const pw = page.pw.value || '' page.pw.value = '' const rememberPass = page.rememberPass.checked if (pw === '') { @@ -1003,7 +1101,7 @@ export class LoginForm { const form = this.form Doc.animate(550, prog => { form.style.transform = `scale(${0.9 + 0.1 * prog})` - form.style.opacity = Math.pow(prog, 4) + form.style.opacity = String(Math.pow(prog, 4)) }, 'easeOut') } } @@ -1011,17 +1109,17 @@ export class LoginForm { const animationLength = 300 /* Swap form1 for form2 with an animation. */ -export async function slideSwap (form1, form2) { +export async function slideSwap (form1: HTMLElement, form2: HTMLElement) { const shift = document.body.offsetWidth / 2 await Doc.animate(animationLength, progress => { form1.style.right = `${progress * shift}px` }, 'easeInHard') Doc.hide(form1) form1.style.right = '0' - form2.style.right = -shift + form2.style.right = String(-shift) Doc.show(form2) if (form2.querySelector('input')) { - form2.querySelector('input').focus() + Doc.safeSelector(form2, 'input').focus() } await Doc.animate(animationLength, progress => { form2.style.right = `${-shift + progress * shift}px` @@ -1033,8 +1131,8 @@ export async function slideSwap (form1, form2) { * bind binds the click and submit events and prevents page reloading on * submission. */ -export function bind (form, submitBttn, handler) { - const wrapper = e => { +export function bind (form: HTMLElement, submitBttn: HTMLElement, handler: (e: Event) => void) { + const wrapper = (e: Event) => { if (e.preventDefault) e.preventDefault() handler(e) } @@ -1044,17 +1142,17 @@ export function bind (form, submitBttn, handler) { // isTruthyString will be true if the provided string is recognized as a // value representing true. -function isTruthyString (s) { +function isTruthyString (s: string) { return s === '1' || s.toLowerCase() === 'true' } // toUnixDate converts a javscript date object to a unix date, which is // the number of *seconds* since the start of the epoch. -function toUnixDate (date) { +function toUnixDate (date: Date) { return Math.floor(date.getTime() / 1000) } // dateToString converts a javascript date object to a YYYY-MM-DD format string. -function dateToString (date) { +function dateToString (date: Date) { return date.toISOString().split('T')[0] } diff --git a/client/webserver/site/src/js/http.js b/client/webserver/site/src/js/http.ts similarity index 79% rename from client/webserver/site/src/js/http.js rename to client/webserver/site/src/js/http.ts index 2cc62a6e55..db1a5139e3 100644 --- a/client/webserver/site/src/js/http.js +++ b/client/webserver/site/src/js/http.ts @@ -1,7 +1,7 @@ /* * requestJSON encodes the object and sends the JSON to the specified address. */ -export async function requestJSON (method, addr, reqBody) { +export async function requestJSON (method: string, addr: string, reqBody?: any): Promise { try { const response = await window.fetch(addr, { method: method, @@ -24,13 +24,13 @@ export async function requestJSON (method, addr, reqBody) { * postJSON sends a POST request with JSON-formatted data and returns the * response. */ -export async function postJSON (addr, data) { +export async function postJSON (addr: string, data?: any) { return requestJSON('POST', addr, JSON.stringify(data)) } /* * getJSON sends a GET request and returns the response. */ -export async function getJSON (addr) { +export async function getJSON (addr: string): Promise { return requestJSON('GET', addr) } diff --git a/client/webserver/site/src/js/locales.js b/client/webserver/site/src/js/locales.ts similarity index 96% rename from client/webserver/site/src/js/locales.js rename to client/webserver/site/src/js/locales.ts index 57206f5dae..7d2ed92391 100644 --- a/client/webserver/site/src/js/locales.js +++ b/client/webserver/site/src/js/locales.ts @@ -1,3 +1,5 @@ +type Locale = Record + export const ID_NO_PASS_ERROR_MSG = 'ID_NO_PASS_ERROR_MSG' export const ID_NO_APP_PASS_ERROR_MSG = 'ID_NO_APP_PASS_ERROR_MSG' export const ID_SET_BUTTON_BUY = 'ID_SET_BUTTON_BUY' @@ -48,7 +50,7 @@ export const ID_KEEP_WALLET_TYPE = 'ID_KEEP_WALLET_TYPE' export const WALLET_READY = 'WALLET_READY' export const SETUP_NEEDED = 'SETUP_NEEDED' -export const enUS = { +export const enUS: Locale = { [ID_NO_PASS_ERROR_MSG]: 'password cannot be empty', [ID_NO_APP_PASS_ERROR_MSG]: 'app password cannot be empty', [ID_PASSWORD_NOT_MATCH]: 'passwords do not match', @@ -100,7 +102,7 @@ export const enUS = { [SETUP_NEEDED]: 'Setup Needed' } -export const ptBR = { +export const ptBR: Locale = { [ID_NO_PASS_ERROR_MSG]: 'senha não pode ser vazia', [ID_NO_APP_PASS_ERROR_MSG]: 'senha do app não pode ser vazia', [ID_PASSWORD_NOT_MATCH]: 'senhas diferentes', @@ -152,7 +154,7 @@ export const ptBR = { [SETUP_NEEDED]: 'Configuração Necessária' } -export const zhCN = { +export const zhCN: Locale = { [ID_NO_PASS_ERROR_MSG]: '密码不能为空', [ID_NO_APP_PASS_ERROR_MSG]: '应用密码不能为空', [ID_PASSWORD_NOT_MATCH]: '密码不相同', @@ -200,7 +202,7 @@ export const zhCN = { [ID_SETUP_WALLET]: 'Setup' // xxx translate } -export const plPL = { +export const plPL: Locale = { [ID_NO_PASS_ERROR_MSG]: 'hasło nie może być puste', [ID_NO_APP_PASS_ERROR_MSG]: 'hasło aplikacji nie może być puste', [ID_PASSWORD_NOT_MATCH]: 'hasła nie są jednakowe', @@ -252,7 +254,7 @@ export const plPL = { [SETUP_NEEDED]: 'Potrzebna konfiguracja' } -const localesMap = { +const localesMap: Record = { 'en-us': enUS, 'pt-br': ptBR, 'zh-cn': zhCN, @@ -260,7 +262,7 @@ const localesMap = { } /* locale will hold the locale loaded via setLocale. */ -let locale +let locale: Locale const defaultLocale = enUS @@ -272,24 +274,24 @@ const defaultLocale = enUS export function setLocale () { locale = localesMap[document.documentElement.lang.toLowerCase()] } /* prep will format the message to the current locale. */ -export function prep (k, args = undefined) { - return stringTemplateParser(locale[k] || defaultLocale[k], args) +export function prep (k: string, args?: Record) { + return stringTemplateParser(locale[k] || defaultLocale[k], args || {}) } /* * stringTemplateParser is a template string matcher, where expression is any * text. It switches what is inside double brackets (e.g. 'buy {{ asset }}') - * for the value described into valueObj. valueObj is an object with keys + * for the value described into args. args is an object with keys * equal to the placeholder keys. (e.g. {"asset": "dcr"}). * So that will be switched for: 'asset dcr'. */ -function stringTemplateParser (expression, valueObj) { +function stringTemplateParser (expression: string, args: Record) { // templateMatcher matches any text which: // is some {{ text }} between two brackets, and a space between them. // It is global, therefore it will change all occurrences found. // text can be anything, but brackets '{}' and space '\s' const templateMatcher = /{{\s?([^{}\s]*)\s?}}/g - return expression.replace(templateMatcher, (_, value) => valueObj[value]) + return expression.replace(templateMatcher, (_, value) => args[value]) } window.localeDiscrepancies = () => { diff --git a/client/webserver/site/src/js/login.js b/client/webserver/site/src/js/login.ts similarity index 82% rename from client/webserver/site/src/js/login.js rename to client/webserver/site/src/js/login.ts index 2b3f91aa3d..a471e52644 100644 --- a/client/webserver/site/src/js/login.js +++ b/client/webserver/site/src/js/login.ts @@ -4,7 +4,10 @@ import BasePage from './basepage' import { LoginForm } from './forms' export default class LoginPage extends BasePage { - constructor (body) { + form: HTMLElement + loginForm: LoginForm + + constructor (body: HTMLElement) { super() this.form = Doc.idel(body, 'loginForm') Doc.show(this.form) @@ -13,7 +16,7 @@ export default class LoginPage extends BasePage { } /* login submits the sign-in form and parses the result. */ - async loggedIn (e) { + async loggedIn () { await app().fetchUser() app().loadPage('markets') } diff --git a/client/webserver/site/src/js/markets.js b/client/webserver/site/src/js/markets.ts similarity index 81% rename from client/webserver/site/src/js/markets.js rename to client/webserver/site/src/js/markets.ts index 552f0e2ea3..12d6c7851e 100644 --- a/client/webserver/site/src/js/markets.js +++ b/client/webserver/site/src/js/markets.ts @@ -1,14 +1,55 @@ -import { app } from './registry' import Doc, { WalletIcons } from './doc' import State from './state' import BasePage from './basepage' import OrderBook from './orderbook' -import { CandleChart, DepthChart } from './charts' +import { + CandleChart, + DepthChart, + DepthLine, + CandleReporters, + MouseReport, + VolumeReport, + DepthMarker +} from './charts' import { postJSON } from './http' import { NewWalletForm, UnlockWalletForm, bind as bindForm } from './forms' -import * as Order from './orderutil' +import * as OrderUtil from './orderutil' import ws from './ws' import * as intl from './locales' +import { + app, + SupportedAsset, + PageElement, + Order, + Market, + OrderEstimate, + MaxOrderEstimate, + Exchange, + UnitInfo, + Asset, + Candle, + CandlesPayload, + TradeForm, + BookUpdate, + MaxSell, + MaxBuy, + SwapEstimate, + MarketOrderBook, + APIResponse, + PreSwap, + PreRedeem, + WalletStateNote, + SpotPriceNote, + FeePaymentNote, + OrderNote, + EpochNote, + BalanceNote, + MiniOrder, + RemainderUpdate, + ConnEventNote, + Spot, + OrderOption +} from './registry' const bind = Doc.bind @@ -40,34 +81,114 @@ const percentFormatter = new Intl.NumberFormat(document.documentElement.lang, { maximumFractionDigits: 2 }) +interface MetaOrder { + row: HTMLElement + order: Order + cancelling?: boolean + status?: number +} + +interface CancelData { + bttn: PageElement + order: Order +} + +interface CurrentMarket { + dex: Exchange + sid: string // A string market identifier used by the DEX. + cfg: Market + base: SupportedAsset + quote: SupportedAsset + baseUnitInfo: UnitInfo + quoteUnitInfo: UnitInfo + maxSell: MaxOrderEstimate | null + sellBalance: number + buyBalance: number + maxBuys: Record + candleCaches: Record + baseCfg: Asset + quoteCfg: Asset + rateConversionFactor: number +} + +interface BalanceWidgetElement { + id: number + cfg: Asset | null + logo: PageElement + avail: PageElement + newWalletRow: PageElement + newWalletBttn: PageElement + locked: PageElement + immature: PageElement + unsupported: PageElement + expired: PageElement + connect: PageElement + spinner: PageElement + iconBox: PageElement + stateIcons: WalletIcons +} + +interface LoadTracker { + loaded: () => void, + timer: number +} + +interface OrderRow extends HTMLElement { + manager: OrderTableRowManager +} + export default class MarketsPage extends BasePage { - constructor (main, data) { + page: Record + main: HTMLElement + loaded: (() => void) | null + maxLoaded: (() => void) | null + maxOrderUpdateCounter: number + market: CurrentMarket + currentForm: HTMLElement + openAsset: SupportedAsset + openFunc: () => void + currentCreate: SupportedAsset + maxEstimateTimer: number | null + book: OrderBook + cancelData: CancelData + metaOrders: Record + preorderCache: Record + currentOrder: TradeForm + depthLines: Record + activeMarkerRate: number | null + hovers: HTMLElement[] + ordersSortKey: string + ordersSortDirection: 1 | -1 + ogTitle: string + depthChart: DepthChart + candleChart: CandleChart + currentChart: string + candleDur: string + balanceWgt: BalanceWidget + marketList: MarketList + quoteUnits: NodeListOf + baseUnits: NodeListOf + unlockForm: UnlockWalletForm + newWalletForm: NewWalletForm + keyup: (e: KeyboardEvent) => void + secondTicker: number + candlesLoading: LoadTracker | null + + constructor (main: HTMLElement, data: any) { super() const page = this.page = Doc.idDescendants(main) this.main = main + if (!this.main.parentElement) return // Not gonna happen, but TypeScript cares. this.loaded = app().loading(this.main.parentElement) - this.maxLoaded = null // There may be multiple pending updates to the max order. This makes sure // that the screen is updated with the most recent one. this.maxOrderUpdateCounter = 0 - this.market = null - this.registrationStatus = {} - this.currentForm = null - this.openAsset = null - this.openFunc = null - this.currentCreate = null - this.maxEstimateTimer = null - this.book = null - this.cancelData = null this.metaOrders = {} - this.orderOpts = {} this.preorderCache = {} - this.currentOrder = null this.depthLines = { hover: [], input: [] } - this.activeMarkerRate = null this.hovers = [] // 'Your Orders' list sort key and direction. this.ordersSortKey = 'stamp' @@ -77,14 +198,14 @@ export default class MarketsPage extends BasePage { this.ogTitle = document.title const depthReporters = { - click: p => { this.reportDepthClick(p) }, - volume: d => { this.reportDepthVolume(d) }, - mouse: d => { this.reportDepthMouse(d) }, - zoom: z => { this.reportDepthZoom(z) } + click: (x: number) => { this.reportDepthClick(x) }, + volume: (r: VolumeReport) => { this.reportDepthVolume(r) }, + mouse: (r: MouseReport) => { this.reportDepthMouse(r) }, + zoom: (z: number) => { this.reportDepthZoom(z) } } this.depthChart = new DepthChart(page.marketChart, depthReporters, State.fetch(depthZoomKey)) - const candleReporters = { + const candleReporters: CandleReporters = { mouse: c => { this.reportMouseCandle(c) } } this.candleChart = new CandleChart(page.marketChart, candleReporters) @@ -113,7 +234,7 @@ export default class MarketsPage extends BasePage { // Prepare templates for the buy and sell tables and the user's order table. Doc.cleanTemplates(page.rowTemplate, page.liveTemplate, page.durBttnTemplate, page.booleanOptTmpl, page.rangeOptTmpl, page.orderOptTmpl) - Order.setOptionTemplates(page) + OrderUtil.setOptionTemplates(page) // Prepare the list of markets. this.marketList = new MarketList(page.marketList) @@ -154,7 +275,7 @@ export default class MarketsPage extends BasePage { this.setOrderVisibility() if (!page.rateField.value) return this.depthLines.input = [{ - rate: page.rateField.value, + rate: parseFloat(page.rateField.value || '0'), color: this.isSell() ? this.depthChart.theme.sellLine : this.depthChart.theme.buyLine }] this.drawChartLines() @@ -167,8 +288,11 @@ export default class MarketsPage extends BasePage { this.drawChartLines() }) bind(page.maxOrd, 'click', () => { - if (this.isSell()) page.lotField.value = this.market.maxSell.swap.lots - else page.lotField.value = this.market.maxBuys[this.adjustedRate()].swap.lots + if (this.isSell()) { + const maxSell = this.market.maxSell + if (!maxSell) return + page.lotField.value = String(maxSell.swap.lots) + } else page.lotField.value = String(this.market.maxBuys[this.adjustedRate()].swap.lots) this.lotChanged() }) bind(page.depthBttn, 'click', () => { @@ -181,19 +305,19 @@ export default class MarketsPage extends BasePage { Doc.disableMouseWheel(page.rateField, page.lotField, page.qtyField, page.mktBuyField) // Handle the full orderbook sent on the 'book' route. - ws.registerRoute(bookRoute, data => { this.handleBookRoute(data) }) + ws.registerRoute(bookRoute, (data: BookUpdate) => { this.handleBookRoute(data) }) // Handle the new order for the order book on the 'book_order' route. - ws.registerRoute(bookOrderRoute, data => { this.handleBookOrderRoute(data) }) + ws.registerRoute(bookOrderRoute, (data: BookUpdate) => { this.handleBookOrderRoute(data) }) // Remove the order sent on the 'unbook_order' route from the orderbook. - ws.registerRoute(unbookOrderRoute, data => { this.handleUnbookOrderRoute(data) }) + ws.registerRoute(unbookOrderRoute, (data: BookUpdate) => { this.handleUnbookOrderRoute(data) }) // Update the remaining quantity on a booked order. - ws.registerRoute(updateRemainingRoute, data => { this.handleUpdateRemainingRoute(data) }) + ws.registerRoute(updateRemainingRoute, (data: BookUpdate) => { this.handleUpdateRemainingRoute(data) }) // Handle the new order for the order book on the 'epoch_order' route. - ws.registerRoute(epochOrderRoute, data => { this.handleEpochOrderRoute(data) }) + ws.registerRoute(epochOrderRoute, (data: BookUpdate) => { this.handleEpochOrderRoute(data) }) // Handle the intial candlestick data on the 'candles' route. - ws.registerRoute(candleUpdateRoute, data => { this.handleCandleUpdateRoute(data) }) + ws.registerRoute(candleUpdateRoute, (data: BookUpdate) => { this.handleCandleUpdateRoute(data) }) // Handle the candles update on the 'candles' route. - ws.registerRoute(candlesRoute, data => { this.handleCandlesRoute(data) }) + ws.registerRoute(candlesRoute, (data: BookUpdate) => { this.handleCandlesRoute(data) }) // Bind the wallet unlock form. this.unlockForm = new UnlockWalletForm(page.unlockWalletForm, async () => { this.openFunc() }) // Create a wallet @@ -211,9 +335,9 @@ export default class MarketsPage extends BasePage { Doc.bind(page.closeDetailPane, 'click', () => this.showForm(page.verifyForm)) // Bind active orders list's header sort events. page.liveTable.querySelectorAll('[data-ordercol]') - .forEach(th => bind(th, 'click', () => setOrdersSortCol(th.dataset.ordercol))) + .forEach((th: HTMLElement) => bind(th, 'click', () => setOrdersSortCol(th.dataset.ordercol || ''))) - const setOrdersSortCol = (key) => { + const setOrdersSortCol = (key: string) => { // First unset header's current sorted col classes. unsetOrdersSortColClasses() // If already sorting by key change sort direction. @@ -242,7 +366,7 @@ export default class MarketsPage extends BasePage { const setOrdersSortColClasses = () => { const key = this.ordersSortKey const sortCls = sortClassByDirection() - page.liveTable.querySelector(`[data-ordercol=${key}]`).classList.add(sortCls) + Doc.safeSelector(page.liveTable, `[data-ordercol=${key}]`).classList.add(sortCls) } // Set default's sorted col header classes. @@ -255,14 +379,14 @@ export default class MarketsPage extends BasePage { } // If the user clicks outside of a form, it should close the page overlay. - bind(page.forms, 'mousedown', e => { + bind(page.forms, 'mousedown', (e: MouseEvent) => { if (Doc.isDisplayed(page.vDetailPane) && !Doc.mouseInElement(e, page.vDetailPane)) return this.showForm(page.verifyForm) if (!Doc.mouseInElement(e, this.currentForm)) { closePopups() } }) - this.keyup = e => { + this.keyup = (e: KeyboardEvent) => { if (e.key === 'Escape') { closePopups() } @@ -299,7 +423,7 @@ export default class MarketsPage extends BasePage { }) // Load the user's layout preferences. - const setChartRatio = r => { + const setChartRatio = (r: number) => { if (r > 0.7) r = 0.7 else if (r < 0.25) r = 0.25 @@ -314,11 +438,11 @@ export default class MarketsPage extends BasePage { setChartRatio(chartDivRatio) } // Bind chart resizing. - bind(page.chartResizer, 'mousedown', e => { + bind(page.chartResizer, 'mousedown', (e: MouseEvent) => { if (e.button !== 0) return e.preventDefault() - let chartRatio - const trackMouse = ee => { + let chartRatio: number + const trackMouse = (ee: MouseEvent) => { ee.preventDefault() const box = page.rightSide.getBoundingClientRect() const h = box.bottom - box.top @@ -334,13 +458,13 @@ export default class MarketsPage extends BasePage { // Notification filters. this.notifiers = { - order: note => { this.handleOrderNote(note) }, - epoch: note => { this.handleEpochNote(note) }, - conn: note => { this.handleConnNote(note) }, - balance: note => { this.handleBalanceNote(note) }, - feepayment: note => { this.handleFeePayment(note) }, - walletstate: note => { this.handleWalletStateNote(note) }, - spots: note => { this.handlePriceUpdate(note) } + order: (note: OrderNote) => { this.handleOrderNote(note) }, + epoch: (note: EpochNote) => { this.handleEpochNote(note) }, + conn: (note: ConnEventNote) => { this.handleConnNote(note) }, + balance: (note: BalanceNote) => { this.handleBalanceNote(note) }, + feepayment: (note: FeePaymentNote) => { this.handleFeePayment(note) }, + walletstate: (note: WalletStateNote) => { this.handleWalletStateNote(note) }, + spots: (note: SpotPriceNote) => { this.handlePriceUpdate(note) } } // Fetch the first market in the list, or the users last selected market, if @@ -357,7 +481,7 @@ export default class MarketsPage extends BasePage { this.setMarket(selected.host, selected.base, selected.quote) // Start a ticker to update time-since values. - this.secondTicker = setInterval(() => { + this.secondTicker = window.setInterval(() => { for (const metaOrder of Object.values(this.metaOrders)) { const td = Doc.tmplElement(metaOrder.row, 'age') td.textContent = Doc.timeSince(metaOrder.order.stamp) @@ -438,16 +562,14 @@ export default class MarketsPage extends BasePage { * supported */ setLoaderMsgVisibility () { - const page = this.page - const { base, quote } = this.market + const { page, market } = this if (this.assetsAreSupported()) { // make sure to hide the loader msg Doc.hide(page.loaderMsg) return } - const symbol = (!base && this.market.baseCfg.symbol) || (!quote && this.market.quoteCfg.symbol) - + const symbol = market.base ? market.quoteCfg.symbol : market.baseCfg.symbol page.loaderMsg.textContent = intl.prep(intl.ID_NOT_SUPPORTED, { asset: symbol.toUpperCase() }) Doc.show(page.loaderMsg) } @@ -455,7 +577,7 @@ export default class MarketsPage extends BasePage { /* setRegistrationStatusView sets the text content and class for the * registration status view */ - setRegistrationStatusView (titleContent, confStatusMsg, titleClass) { + setRegistrationStatusView (titleContent: string, confStatusMsg: string, titleClass: string) { const page = this.page page.regStatusTitle.textContent = titleContent page.regStatusConfsDisplay.textContent = confStatusMsg @@ -478,7 +600,7 @@ export default class MarketsPage extends BasePage { } const confirmationsRequired = dex.regFees[pending.symbol].confs - page.confReq.textContent = confirmationsRequired + page.confReq.textContent = String(confirmationsRequired) const confStatusMsg = `${pending.confs} / ${confirmationsRequired}` this.setRegistrationStatusView(intl.prep(intl.ID_WAITING_FOR_CONFS), confStatusMsg, 'waiting') } @@ -531,7 +653,7 @@ export default class MarketsPage extends BasePage { } /* setMarket sets the currently displayed market. */ - async setMarket (host, base, quote) { + async setMarket (host: string, base: number, quote: number) { const dex = app().user.exchanges[host] const page = this.page @@ -544,11 +666,13 @@ export default class MarketsPage extends BasePage { // exchange data, so just put up a message and wait for the connection to be // established, at which time handleConnNote will refresh and reload. if (!dex.connected) { - this.market = { dex: dex } + // TODO: Figure out why this was like this. + // this.market = { dex: dex } + page.chartErrMsg.textContent = intl.prep(intl.ID_CONNECTION_FAILED) Doc.show(page.chartErrMsg) - this.loaded() - this.main.style.opacity = 1 + if (this.loaded) this.loaded() + this.main.style.opacity = '1' Doc.hide(page.marketLoader) return } @@ -558,7 +682,7 @@ export default class MarketsPage extends BasePage { const [bui, qui] = [app().unitInfo(base, dex), app().unitInfo(quote, dex)] - const rateConversionFactor = Order.RateEncodingFactor / bui.conventional.conversionFactor * qui.conventional.conversionFactor + const rateConversionFactor = OrderUtil.RateEncodingFactor / bui.conventional.conversionFactor * qui.conventional.conversionFactor Doc.hide(page.maxOrd, page.chartErrMsg) if (this.maxEstimateTimer) { window.clearTimeout(this.maxEstimateTimer) @@ -580,7 +704,9 @@ export default class MarketsPage extends BasePage { candleCaches: {}, baseCfg, quoteCfg, - rateConversionFactor + rateConversionFactor, + sellBalance: 0, + buyBalance: 0 } Doc.show(page.marketLoader) @@ -602,8 +728,8 @@ export default class MarketsPage extends BasePage { * reportDepthClick is a callback used by the DepthChart when the user clicks * on the chart area. The rate field is set to the x-value of the click. */ - reportDepthClick (r) { - this.page.rateField.value = r + reportDepthClick (r: number) { + this.page.rateField.value = String(r) this.rateFieldChanged() } @@ -611,25 +737,25 @@ export default class MarketsPage extends BasePage { * reportDepthVolume accepts a volume report from the DepthChart and sets the * values in the chart legend. */ - reportDepthVolume (d) { + reportDepthVolume (r: VolumeReport) { const page = this.page const { baseUnitInfo: b, quoteUnitInfo: q } = this.market // DepthChart reports volumes in conventional units. We'll still use // formatCoinValue for formatting though. - page.sellBookedBase.textContent = Doc.formatCoinValue(d.sellBase * b.conventional.conversionFactor, b) - page.sellBookedQuote.textContent = Doc.formatCoinValue(d.sellQuote * q.conventional.conversionFactor, q) - page.buyBookedBase.textContent = Doc.formatCoinValue(d.buyBase * b.conventional.conversionFactor, b) - page.buyBookedQuote.textContent = Doc.formatCoinValue(d.buyQuote * q.conventional.conversionFactor, q) + page.sellBookedBase.textContent = Doc.formatCoinValue(r.sellBase * b.conventional.conversionFactor, b) + page.sellBookedQuote.textContent = Doc.formatCoinValue(r.sellQuote * q.conventional.conversionFactor, q) + page.buyBookedBase.textContent = Doc.formatCoinValue(r.buyBase * b.conventional.conversionFactor, b) + page.buyBookedQuote.textContent = Doc.formatCoinValue(r.buyQuote * q.conventional.conversionFactor, q) } /* * reportDepthMouse accepts informations about the mouse position on the * chart area. */ - reportDepthMouse (d) { - while (this.hovers.length) this.hovers.shift().classList.remove('hover') + reportDepthMouse (r: MouseReport) { + while (this.hovers.length) (this.hovers.shift() as HTMLElement).classList.remove('hover') const page = this.page - if (!d) { + if (!r) { Doc.hide(page.hoverData) return } @@ -638,16 +764,16 @@ export default class MarketsPage extends BasePage { // of a user order, highlight that order's row. for (const metaOrd of Object.values(this.metaOrders)) { const [row, ord] = [metaOrd.row, metaOrd.order] - if (ord.status !== Order.StatusBooked) continue - if (d.hoverMarkers.indexOf(ord.rate) > -1) { + if (ord.status !== OrderUtil.StatusBooked) continue + if (r.hoverMarkers.indexOf(ord.rate) > -1) { row.classList.add('hover') this.hovers.push(row) } } - page.hoverPrice.textContent = Doc.formatCoinValue(d.rate) - page.hoverVolume.textContent = Doc.formatCoinValue(d.depth) - page.hoverVolume.style.color = d.dotColor + page.hoverPrice.textContent = Doc.formatCoinValue(r.rate) + page.hoverVolume.textContent = Doc.formatCoinValue(r.depth) + page.hoverVolume.style.color = r.dotColor Doc.show(page.hoverData) } @@ -656,11 +782,11 @@ export default class MarketsPage extends BasePage { * This information is saved to disk so that the zoom level can be maintained * across reloads. */ - reportDepthZoom (zoom) { + reportDepthZoom (zoom: number) { State.store(depthZoomKey, zoom) } - reportMouseCandle (candle) { + reportMouseCandle (candle: Candle | null) { const page = this.page if (!candle) { Doc.hide(page.hoverData) @@ -679,7 +805,7 @@ export default class MarketsPage extends BasePage { * parseOrder pulls the order information from the form fields. Data is not * validated in any way. */ - parseOrder () { + parseOrder (): TradeForm { const page = this.page let qtyField = page.qtyField const limit = this.isLimit() @@ -694,17 +820,17 @@ export default class MarketsPage extends BasePage { sell: sell, base: market.base.id, quote: market.quote.id, - qty: convertToAtoms(qtyField.value, market.baseUnitInfo.conventional.conversionFactor), - rate: convertToAtoms(page.rateField.value, market.rateConversionFactor), // message-rate - tifnow: page.tifNow.checked, - options: this.orderOpts + qty: convertToAtoms(qtyField.value || '', market.baseUnitInfo.conventional.conversionFactor), + rate: convertToAtoms(page.rateField.value || '', market.rateConversionFactor), // message-rate + tifnow: page.tifNow.checked || false, + options: {} } } /** * previewQuoteAmt shows quote amount when rate or quantity input are changed */ - previewQuoteAmt (show) { + previewQuoteAmt (show: boolean) { const page = this.page if (!this.market.base || !this.market.quote) return // Not a supported asset const order = this.parseOrder() @@ -728,7 +854,7 @@ export default class MarketsPage extends BasePage { return } const quoteAsset = app().assets[order.quote] - const quoteQty = order.qty * order.rate / Order.RateEncodingFactor + const quoteQty = order.qty * order.rate / OrderUtil.RateEncodingFactor const total = Doc.formatCoinValue(quoteQty, this.market.quoteUnitInfo) page.orderPreview.textContent = intl.prep(intl.ID_ORDER_PREVIEW, { total, asset: quoteAsset.symbol.toUpperCase() }) @@ -743,8 +869,8 @@ export default class MarketsPage extends BasePage { this.maxOrderUpdateCounter++ const mkt = this.market const baseWallet = app().assets[mkt.base.id].wallet - if (baseWallet.available < mkt.cfg.lotsize) { - this.setMaxOrder({ lots: 0 }) + if (baseWallet.balance.available < mkt.cfg.lotsize) { + this.setMaxOrder(null) return } if (mkt.maxSell) { @@ -752,7 +878,7 @@ export default class MarketsPage extends BasePage { return } // We only fetch pre-sell once per balance update, so don't delay. - this.scheduleMaxEstimate('/api/maxsell', {}, 0, res => { + this.scheduleMaxEstimate('/api/maxsell', {}, 0, (res: MaxSell) => { mkt.maxSell = res.maxSell mkt.sellBalance = baseWallet.balance.available this.setMaxOrder(res.maxSell.swap) @@ -767,9 +893,9 @@ export default class MarketsPage extends BasePage { const mkt = this.market const rate = this.adjustedRate() const quoteWallet = app().assets[mkt.quote.id].wallet - const aLot = mkt.cfg.lotsize * (rate / Order.RateEncodingFactor) + const aLot = mkt.cfg.lotsize * (rate / OrderUtil.RateEncodingFactor) if (quoteWallet.balance.available < aLot) { - this.setMaxOrder({ lots: 0 }) + this.setMaxOrder(null) return } if (mkt.maxBuys[rate]) { @@ -779,7 +905,7 @@ export default class MarketsPage extends BasePage { // 0 delay for first fetch after balance update or market change, otherwise // meter these at 1 / sec. const delay = mkt.maxBuys ? 1000 : 0 - this.scheduleMaxEstimate('/api/maxbuy', { rate: rate }, delay, res => { + this.scheduleMaxEstimate('/api/maxbuy', { rate: rate }, delay, (res: MaxBuy) => { mkt.maxBuys[rate] = res.maxBuy mkt.buyBalance = app().assets[mkt.quote.id].wallet.balance.available this.setMaxOrder(res.maxBuy.swap) @@ -791,7 +917,7 @@ export default class MarketsPage extends BasePage { * estimate api endpoint. If another call to scheduleMaxEstimate is made before * this one is fired (after delay), this call will be canceled. */ - scheduleMaxEstimate (path, args, delay, success) { + scheduleMaxEstimate (path: string, args: any, delay: number, success: (res: any) => void) { const page = this.page if (!this.maxLoaded) this.maxLoaded = app().loading(page.maxOrd) const [bid, qid] = [this.market.base.id, this.market.quote.id] @@ -828,7 +954,7 @@ export default class MarketsPage extends BasePage { } /* setMaxOrder sets the max order text. */ - setMaxOrder (maxOrder, toConverter) { + setMaxOrder (maxOrder: SwapEstimate | null) { const page = this.page if (this.maxLoaded) { this.maxLoaded() @@ -836,21 +962,25 @@ export default class MarketsPage extends BasePage { } Doc.show(page.maxOrd, page.maxLotBox, page.maxAboveZero) const sell = this.isSell() - page.maxFromLots.textContent = maxOrder.lots.toString() + + let lots = 0 + if (maxOrder) lots = maxOrder.lots + + page.maxFromLots.textContent = lots.toString() // XXX add plural into format details, so we don't need this - page.maxFromLotsLbl.textContent = maxOrder.lots === 1 ? 'lot' : 'lots' - if (maxOrder.lots === 0) { + page.maxFromLotsLbl.textContent = lots === 1 ? 'lot' : 'lots' + if (!maxOrder) { Doc.hide(page.maxAboveZero) return } // Could add the estimatedFees here, but that might also be // confusing. const [fromAsset, toAsset] = sell ? [this.market.base, this.market.quote] : [this.market.quote, this.market.base] - page.maxFromAmt.textContent = Doc.formatCoinValue(maxOrder.value, fromAsset.info.unitinfo) + page.maxFromAmt.textContent = Doc.formatCoinValue(maxOrder.value || 0, fromAsset.info.unitinfo) page.maxFromTicker.textContent = fromAsset.symbol.toUpperCase() // Could subtract the maxOrder.redemptionFees here. - const toConversion = sell ? this.adjustedRate() / Order.RateEncodingFactor : Order.RateEncodingFactor / this.adjustedRate() - page.maxToAmt.textContent = Doc.formatCoinValue(maxOrder.value * toConversion, toAsset.info.unitinfo) + const toConversion = sell ? this.adjustedRate() / OrderUtil.RateEncodingFactor : OrderUtil.RateEncodingFactor / this.adjustedRate() + page.maxToAmt.textContent = Doc.formatCoinValue((maxOrder.value || 0) * toConversion, toAsset.info.unitinfo) page.maxToTicker.textContent = toAsset.symbol.toUpperCase() } @@ -858,7 +988,7 @@ export default class MarketsPage extends BasePage { * validateOrder performs some basic order sanity checks, returning boolean * true if the order appears valid. */ - validateOrder (order) { + validateOrder (order: TradeForm) { const page = this.page if (order.isLimit && !order.rate) { Doc.show(page.orderErr) @@ -874,7 +1004,7 @@ export default class MarketsPage extends BasePage { } /* handleBook accepts the data sent in the 'book' notification. */ - handleBook (data) { + handleBook (data: MarketOrderBook) { const { cfg, baseUnitInfo, quoteUnitInfo, baseCfg, quoteCfg } = this.market this.book = new OrderBook(data, baseCfg.symbol, quoteCfg.symbol) this.loadTable() @@ -914,12 +1044,12 @@ export default class MarketsPage extends BasePage { if (!book) return if (book.buys && book.buys.length) { if (book.sells && book.sells.length) { - return (book.buys[0].msgRate + book.sells[0].msgRate) / 2 / Order.RateEncodingFactor + return (book.buys[0].msgRate + book.sells[0].msgRate) / 2 / OrderUtil.RateEncodingFactor } - return book.buys[0].msgRate / Order.RateEncodingFactor + return book.buys[0].msgRate / OrderUtil.RateEncodingFactor } if (book.sells && book.sells.length) { - return book.sells[0].msgRate / Order.RateEncodingFactor + return book.sells[0].msgRate / OrderUtil.RateEncodingFactor } return null } @@ -946,25 +1076,25 @@ export default class MarketsPage extends BasePage { ordersSortCompare () { switch (this.ordersSortKey) { case 'stamp': - return (a, b) => this.ordersSortDirection * (b.stamp - a.stamp) + return (a: Order, b: Order) => this.ordersSortDirection * (b.stamp - a.stamp) case 'rate': - return (a, b) => this.ordersSortDirection * (a.rate - b.rate) + return (a: Order, b: Order) => this.ordersSortDirection * (a.rate - b.rate) case 'qty': - return (a, b) => this.ordersSortDirection * (a.qty - b.qty) + return (a: Order, b: Order) => this.ordersSortDirection * (a.qty - b.qty) case 'type': - return (a, b) => this.ordersSortDirection * - Order.typeString(a).localeCompare(Order.typeString(b)) + return (a: Order, b: Order) => this.ordersSortDirection * + OrderUtil.typeString(a).localeCompare(OrderUtil.typeString(b)) case 'sell': - return (a, b) => this.ordersSortDirection * - (Order.sellString(a)).localeCompare(Order.sellString(b)) + return (a: Order, b: Order) => this.ordersSortDirection * + (OrderUtil.sellString(a)).localeCompare(OrderUtil.sellString(b)) case 'status': - return (a, b) => this.ordersSortDirection * - (Order.statusString(a)).localeCompare(Order.statusString(b)) + return (a: Order, b: Order) => this.ordersSortDirection * + (OrderUtil.statusString(a)).localeCompare(OrderUtil.statusString(b)) case 'settled': - return (a, b) => this.ordersSortDirection * - ((Order.settled(a) * 100 / a.qty) - (Order.settled(b) * 100 / b.qty)) + return (a: Order, b: Order) => this.ordersSortDirection * + ((OrderUtil.settled(a) * 100 / a.qty) - (OrderUtil.settled(b) * 100 / b.qty)) case 'filled': - return (a, b) => this.ordersSortDirection * + return (a: Order, b: Order) => this.ordersSortDirection * ((a.filled * 100 / a.qty) - (b.filled * 100 / b.qty)) } } @@ -982,17 +1112,17 @@ export default class MarketsPage extends BasePage { Doc.empty(page.liveList) for (const ord of orders) { - const row = page.liveTemplate.cloneNode(true) + const row = page.liveTemplate.cloneNode(true) as HTMLElement metaOrders[ord.id] = { row: row, order: ord } - Doc.bind(row, 'mouseenter', e => { + Doc.bind(row, 'mouseenter', () => { this.activeMarkerRate = ord.rate this.setDepthMarkers() }) this.updateUserOrderRow(row, ord) - if (ord.type === Order.Limit && (ord.tif === Order.StandingTiF && ord.status < Order.StatusExecuted)) { + if (ord.type === OrderUtil.Limit && (ord.tif === OrderUtil.StandingTiF && ord.status < OrderUtil.StatusExecuted)) { const icon = Doc.tmplElement(row, 'cancelBttn') Doc.show(icon) bind(icon, 'click', e => { @@ -1014,27 +1144,27 @@ export default class MarketsPage extends BasePage { /* * updateUserOrderRow sets the td contents of the user's order table row. */ - updateUserOrderRow (tr, ord) { - updateDataCol(tr, 'type', Order.typeString(ord)) - updateDataCol(tr, 'side', Order.sellString(ord)) + updateUserOrderRow (tr: HTMLElement, ord: Order) { + updateDataCol(tr, 'type', OrderUtil.typeString(ord)) + updateDataCol(tr, 'side', OrderUtil.sellString(ord)) updateDataCol(tr, 'age', Doc.timeSince(ord.stamp)) updateDataCol(tr, 'rate', Doc.formatCoinValue(ord.rate / this.market.rateConversionFactor)) updateDataCol(tr, 'qty', Doc.formatCoinValue(ord.qty, this.market.baseUnitInfo)) updateDataCol(tr, 'filled', `${(ord.filled / ord.qty * 100).toFixed(1)}%`) - updateDataCol(tr, 'settled', `${(Order.settled(ord) / ord.qty * 100).toFixed(1)}%`) - updateDataCol(tr, 'status', Order.statusString(ord)) + updateDataCol(tr, 'settled', `${(OrderUtil.settled(ord) / ord.qty * 100).toFixed(1)}%`) + updateDataCol(tr, 'status', OrderUtil.statusString(ord)) } /* setMarkers sets the depth chart markers for booked orders. */ setDepthMarkers () { - const markers = { + const markers: Record = { buys: [], sells: [] } const rateFactor = this.market.rateConversionFactor for (const mo of Object.values(this.metaOrders)) { const ord = mo.order - if (ord.rate && ord.status === Order.StatusBooked) { + if (ord.rate && ord.status === OrderUtil.StatusBooked) { if (ord.sell) { markers.sells.push({ rate: ord.rate / rateFactor, @@ -1072,7 +1202,7 @@ export default class MarketsPage extends BasePage { * in response to a new market subscription. The data received will contain * the entire order book. */ - handleBookRoute (note) { + handleBookRoute (note: BookUpdate) { app().log('book', 'handleBookRoute:', note) const mktBook = note.payload const market = this.market @@ -1103,13 +1233,13 @@ export default class MarketsPage extends BasePage { this.loaded() this.loaded = null Doc.animate(250, progress => { - this.main.style.opacity = progress + this.main.style.opacity = String(progress) }) } } /* handleBookOrderRoute is the handler for 'book_order' notifications. */ - handleBookOrderRoute (data) { + handleBookOrderRoute (data: BookUpdate) { app().log('book', 'handleBookOrderRoute:', data) if (data.host !== this.market.dex.host || data.marketID !== this.market.sid) return const order = data.payload @@ -1120,7 +1250,7 @@ export default class MarketsPage extends BasePage { } /* handleUnbookOrderRoute is the handler for 'unbook_order' notifications. */ - handleUnbookOrderRoute (data) { + handleUnbookOrderRoute (data: BookUpdate) { app().log('book', 'handleUnbookOrderRoute:', data) if (data.host !== this.market.dex.host || data.marketID !== this.market.sid) return const order = data.payload @@ -1134,7 +1264,7 @@ export default class MarketsPage extends BasePage { * handleUpdateRemainingRoute is the handler for 'update_remaining' * notifications. */ - handleUpdateRemainingRoute (data) { + handleUpdateRemainingRoute (data: BookUpdate) { app().log('book', 'handleUpdateRemainingRoute:', data) if (data.host !== this.market.dex.host || data.marketID !== this.market.sid) return const update = data.payload @@ -1144,7 +1274,7 @@ export default class MarketsPage extends BasePage { } /* handleEpochOrderRoute is the handler for 'epoch_order' notifications. */ - handleEpochOrderRoute (data) { + handleEpochOrderRoute (data: BookUpdate) { app().log('book', 'handleEpochOrderRoute:', data) if (data.host !== this.market.dex.host || data.marketID !== this.market.sid) return const order = data.payload @@ -1154,7 +1284,7 @@ export default class MarketsPage extends BasePage { } /* handleCandlesRoute is the handler for 'candles' notifications. */ - handleCandlesRoute (data) { + handleCandlesRoute (data: BookUpdate) { if (this.candlesLoading) { clearTimeout(this.candlesLoading.timer) this.candlesLoading.loaded() @@ -1170,7 +1300,7 @@ export default class MarketsPage extends BasePage { } /* handleCandleUpdateRoute is the handler for 'candle_update' notifications. */ - handleCandleUpdateRoute (data) { + handleCandleUpdateRoute (data: BookUpdate) { if (data.host !== this.market.dex.host) return const { dur, candle } = data.payload const cache = this.market.candleCaches[dur] @@ -1187,7 +1317,7 @@ export default class MarketsPage extends BasePage { } /* showForm shows a modal form with a little animation. */ - async showForm (form) { + async showForm (form: HTMLElement) { this.currentForm = form const page = this.page Doc.hide(page.unlockWalletForm, page.verifyForm, page.newWalletForm, page.cancelForm, page.vDetailPane) @@ -1201,7 +1331,7 @@ export default class MarketsPage extends BasePage { } /* showOpen shows the form to unlock a wallet. */ - async showOpen (asset, f) { + async showOpen (asset: SupportedAsset, f: () => void) { const page = this.page this.openAsset = asset this.openFunc = f @@ -1214,7 +1344,6 @@ export default class MarketsPage extends BasePage { * and confirm submission of the order to the dex. */ showVerify () { - this.orderOpts = {} this.preorderCache = {} const page = this.page const order = this.currentOrder = this.parseOrder() @@ -1225,7 +1354,7 @@ export default class MarketsPage extends BasePage { const fromAsset = isSell ? baseAsset : quoteAsset // Set the to and from icons in the fee details pane. - for (const icon of page.vDetailPane.querySelectorAll('[data-icon]')) { + for (const icon of Doc.applySelector(page.vDetailPane, '[data-icon]')) { switch (icon.dataset.icon) { case 'from': icon.src = Doc.logoPath(fromAsset.symbol) @@ -1249,7 +1378,7 @@ export default class MarketsPage extends BasePage { page.vOrderType.textContent = order.tifnow ? orderDesc + ' (immediate)' : orderDesc page.vRate.textContent = Doc.formatCoinValue(order.rate / this.market.rateConversionFactor) page.vQty.textContent = Doc.formatCoinValue(order.qty, baseAsset.info.unitinfo) - page.vTotal.textContent = Doc.formatCoinValue(order.rate / Order.RateEncodingFactor * order.qty, quoteAsset.info.unitinfo) + page.vTotal.textContent = Doc.formatCoinValue(order.rate / OrderUtil.RateEncodingFactor * order.qty, quoteAsset.info.unitinfo) } else { Doc.hide(page.verifyLimit) Doc.show(page.verifyMarket) @@ -1286,7 +1415,7 @@ export default class MarketsPage extends BasePage { if (baseAsset.wallet.open && quoteAsset.wallet.open) this.preOrder(order) else { Doc.hide(page.vPreorder) - if (State.passwordIsCached()) this.unlockWalletsForEstimates() + if (State.passwordIsCached()) this.unlockWalletsForEstimates('') else Doc.show(page.vUnlockPreorder) } } @@ -1296,7 +1425,7 @@ export default class MarketsPage extends BasePage { * wallets. */ async submitEstimateUnlock () { - const pw = this.page.vUnlockPass.value + const pw = this.page.vUnlockPass.value || '' return await this.unlockWalletsForEstimates(pw) } @@ -1304,7 +1433,7 @@ export default class MarketsPage extends BasePage { * unlockWalletsForEstimates unlocks any locked wallets with the provided * password. */ - async unlockWalletsForEstimates (pw) { + async unlockWalletsForEstimates (pw: string) { const page = this.page const loaded = app().loading(page.verifyForm) const err = await this.attemptWalletUnlock(pw) @@ -1319,13 +1448,14 @@ export default class MarketsPage extends BasePage { * attemptWalletUnlock unlocks both the base and quote wallets for the current * market, if locked. */ - async attemptWalletUnlock (pw) { + async attemptWalletUnlock (pw: string) { const { base, quote } = this.market const assetIDs = [] if (!base.wallet.open) assetIDs.push(base.id) if (!quote.wallet.open) assetIDs.push(quote.id) const req = { - pass: pw + pass: pw, + assetID: -1 } for (const assetID of assetIDs) { req.assetID = assetID @@ -1337,7 +1467,7 @@ export default class MarketsPage extends BasePage { } /* fetchPreorder fetches the pre-order estimates and options. */ - async fetchPreorder (order) { + async fetchPreorder (order: TradeForm) { const page = this.page const cacheKey = JSON.stringify(order.options) const cached = this.preorderCache[cacheKey] @@ -1356,7 +1486,7 @@ export default class MarketsPage extends BasePage { * setPreorderErr sets and displays the pre-order error message and hides the * pre-order details box. */ - setPreorderErr (msg) { + setPreorderErr (msg: string) { const page = this.page Doc.hide(page.vPreorder) Doc.show(page.vPreorderErr) @@ -1364,17 +1494,18 @@ export default class MarketsPage extends BasePage { } /* preOrder loads the options and fetches pre-order estimates */ - async preOrder (order) { + async preOrder (order: TradeForm) { // if (!this.validateOrder(order)) return const page = this.page // Add swap options. const refreshPreorder = async () => { - const res = await this.fetchPreorder(order) + const res: APIResponse = await this.fetchPreorder(order) if (res.err) return this.setPreorderErr(res.err) + const est = (res as any) as OrderEstimate Doc.hide(page.vPreorderErr) Doc.show(page.vPreorder) - const { swap, redeem } = res + const { swap, redeem } = est Doc.empty(page.vOrderOpts) swap.options = swap.options || [] redeem.options = redeem.options || [] @@ -1386,7 +1517,7 @@ export default class MarketsPage extends BasePage { page.vFeeSummary.style.backgroundColor = `rgba(128, 128, 128, ${0.5 - 0.5 * progress})` }) } - const addOption = (opt, isSwap) => page.vOrderOpts.appendChild(Order.optionElement(opt, order, changed, isSwap)) + const addOption = (opt: OrderOption, isSwap: boolean) => page.vOrderOpts.appendChild(OrderUtil.optionElement(opt, order, changed, isSwap)) for (const opt of swap.options || []) addOption(opt, true) for (const opt of redeem.options || []) addOption(opt, false) app().bindTooltips(page.vOrderOpts) @@ -1396,9 +1527,9 @@ export default class MarketsPage extends BasePage { } /* setFeeEstimates sets all of the pre-order estimate fields */ - setFeeEstimates (swap, redeem, order) { + setFeeEstimates (swap: PreSwap, redeem: PreRedeem, order: TradeForm) { const page = this.page - const swapped = swap.estimate.value + const swapped = swap.estimate.value || 0 const fmtPct = percentFormatter.format // Set swap fee estimates in the details pane. @@ -1445,11 +1576,11 @@ export default class MarketsPage extends BasePage { } /* showCancel shows a form to confirm submission of a cancel order. */ - showCancel (row, orderID) { + showCancel (row: HTMLElement, orderID: string) { const order = this.metaOrders[orderID].order const page = this.page const remaining = order.qty - order.filled - const asset = Order.isMarketBuy(order) ? this.market.quote : this.market.base + const asset = OrderUtil.isMarketBuy(order) ? this.market.quote : this.market.base page.cancelRemain.textContent = Doc.formatCoinValue(remaining, asset.info.unitinfo) page.cancelUnit.textContent = asset.symbol.toUpperCase() this.showForm(page.cancelForm) @@ -1461,7 +1592,7 @@ export default class MarketsPage extends BasePage { } /* showCreate shows the new wallet creation form. */ - showCreate (asset) { + showCreate (asset: SupportedAsset) { const page = this.page this.currentCreate = asset this.newWalletForm.setAsset(asset.id) @@ -1500,12 +1631,13 @@ export default class MarketsPage extends BasePage { * handleWalletStateNote is the handler for the 'walletstate' notification * type. */ - handleWalletStateNote (note) { + handleWalletStateNote (note: WalletStateNote) { this.balanceWgt.updateAsset(note.wallet.assetID) } - handlePriceUpdate (note) { + handlePriceUpdate (note: SpotPriceNote) { const xcSection = this.marketList.xcSection(note.host) + if (!xcSection) return for (const spot of Object.values(note.spots)) { const marketRow = xcSection.marketRow(spot.baseID, spot.quoteID) if (marketRow) marketRow.setSpot(spot) @@ -1516,7 +1648,7 @@ export default class MarketsPage extends BasePage { * handleFeePayment is the handler for the 'feepayment' notification type. * This is used to update the registration status of the current exchange. */ - handleFeePayment (note) { + handleFeePayment (note: FeePaymentNote) { const dexAddr = note.dex if (dexAddr !== this.market.dex.host) return // update local dex @@ -1528,7 +1660,7 @@ export default class MarketsPage extends BasePage { * handleOrderNote is the handler for the 'order'-type notification, which are * used to update a user's order's status. */ - handleOrderNote (note) { + handleOrderNote (note: OrderNote) { const order = note.order const metaOrder = this.metaOrders[order.id] // If metaOrder doesn't exist for the given order it means it was @@ -1547,14 +1679,14 @@ export default class MarketsPage extends BasePage { } this.updateUserOrderRow(metaOrder.row, order) // Only reset markers if there is a change, since the chart is redrawn. - if ((oldStatus === Order.StatusEpoch && order.status === Order.StatusBooked) || - (oldStatus === Order.StatusBooked && order.status > Order.StatusBooked)) this.setDepthMarkers() + if ((oldStatus === OrderUtil.StatusEpoch && order.status === OrderUtil.StatusBooked) || + (oldStatus === OrderUtil.StatusBooked && order.status > OrderUtil.StatusBooked)) this.setDepthMarkers() } /* * handleEpochNote handles notifications signalling the start of a new epoch. */ - handleEpochNote (note) { + handleEpochNote (note: EpochNote) { app().log('book', 'handleEpochNote:', note) if (note.host !== this.market.dex.host || note.marketID !== this.market.sid) return if (this.book) { @@ -1562,20 +1694,20 @@ export default class MarketsPage extends BasePage { this.depthChart.draw() } - this.clearOrderTableEpochs(note.epoch) + this.clearOrderTableEpochs() for (const metaOrder of Object.values(this.metaOrders)) { const order = metaOrder.order const alreadyMatched = note.epoch > order.epoch const statusTD = Doc.tmplElement(metaOrder.row, 'status') switch (true) { - case order.type === Order.Limit && order.status === Order.StatusEpoch && alreadyMatched: - statusTD.textContent = order.tif === Order.ImmediateTiF ? intl.prep(intl.ID_EXECUTED) : intl.prep(intl.ID_BOOKED) - order.status = order.tif === Order.ImmediateTiF ? Order.StatusExecuted : Order.StatusBooked + case order.type === OrderUtil.Limit && order.status === OrderUtil.StatusEpoch && alreadyMatched: + statusTD.textContent = order.tif === OrderUtil.ImmediateTiF ? intl.prep(intl.ID_EXECUTED) : intl.prep(intl.ID_BOOKED) + order.status = order.tif === OrderUtil.ImmediateTiF ? OrderUtil.StatusExecuted : OrderUtil.StatusBooked break - case order.type === Order.Market && order.status === Order.StatusEpoch: + case order.type === OrderUtil.Market && order.status === OrderUtil.StatusEpoch: // Technically don't know if this should be 'executed' or 'settling'. statusTD.textContent = intl.prep(intl.ID_EXECUTED) - order.status = Order.StatusExecuted + order.status = OrderUtil.StatusExecuted break } } @@ -1590,7 +1722,7 @@ export default class MarketsPage extends BasePage { } /* handleBalanceNote handles notifications updating a wallet's balance. */ - handleBalanceNote (note) { + handleBalanceNote (note: BalanceNote) { this.setBalanceVisibility() // if connection to dex server fails, it is not possible to retrieve // markets. @@ -1624,7 +1756,7 @@ export default class MarketsPage extends BasePage { const pw = page.vPass.value page.vPass.value = '' // order options must be a string -> string map. - const stringyOptions = {} + const stringyOptions: Record = {} for (const [k, v] of Object.entries(order.options)) stringyOptions[k] = JSON.stringify(v) const req = { order: wireOrder(order), @@ -1657,6 +1789,7 @@ export default class MarketsPage extends BasePage { */ async createWallet () { const user = await app().fetchUser() + if (!user) return const asset = user.assets[this.currentCreate.id] Doc.hide(this.page.forms) this.balanceWgt.updateAsset(asset.id) @@ -1676,17 +1809,17 @@ export default class MarketsPage extends BasePage { /* lotChanged is attached to the keyup and change events of the lots input. */ lotChanged () { const page = this.page - const lots = parseInt(page.lotField.value || 0) + const lots = parseInt(page.lotField.value || '0') if (lots <= 0) { - page.lotField.value = 0 + page.lotField.value = '0' page.qtyField.value = '' this.previewQuoteAmt(false) return } const lotSize = this.market.cfg.lotsize - page.lotField.value = lots + page.lotField.value = String(lots) // Conversion factor must be a multiple of 10. - page.qtyField.value = lots * lotSize / this.market.baseUnitInfo.conventional.conversionFactor + page.qtyField.value = String(lots * lotSize / this.market.baseUnitInfo.conventional.conversionFactor) this.previewQuoteAmt(true) } @@ -1694,11 +1827,11 @@ export default class MarketsPage extends BasePage { * quantityChanged is attached to the keyup and change events of the quantity * input. */ - quantityChanged (finalize) { + quantityChanged (finalize: boolean) { const page = this.page const order = this.parseOrder() if (order.qty < 0) { - page.lotField.value = 0 + page.lotField.value = '0' page.qtyField.value = '' this.previewQuoteAmt(false) return @@ -1706,10 +1839,10 @@ export default class MarketsPage extends BasePage { const lotSize = this.market.cfg.lotsize const lots = Math.floor(order.qty / lotSize) const adjusted = lots * lotSize - page.lotField.value = lots + page.lotField.value = String(lots) if (!order.isLimit && !order.sell) return // Conversion factor must be a multiple of 10. - if (finalize) page.qtyField.value = adjusted / this.market.baseUnitInfo.conventional.conversionFactor + if (finalize) page.qtyField.value = String(adjusted / this.market.baseUnitInfo.conventional.conversionFactor) this.previewQuoteAmt(true) } @@ -1719,7 +1852,7 @@ export default class MarketsPage extends BasePage { */ marketBuyChanged () { const page = this.page - const qty = convertToAtoms(page.mktBuyField.value, this.market.quoteUnitInfo.conventional.conversionFactor) + const qty = convertToAtoms(page.mktBuyField.value || '', this.market.quoteUnitInfo.conventional.conversionFactor) const gap = this.midGap() if (!gap || !qty) { page.mktBuyLots.textContent = '0' @@ -1742,12 +1875,12 @@ export default class MarketsPage extends BasePage { if (adjusted <= 0) { this.depthLines.input = [] this.drawChartLines() - this.page.rateField.value = 0 + this.page.rateField.value = '0' return } const order = this.parseOrder() const r = adjusted / this.market.rateConversionFactor - this.page.rateField.value = r + this.page.rateField.value = String(r) this.depthLines.input = [{ rate: r, color: order.sell ? this.depthChart.theme.sellLine : this.depthChart.theme.buyLine @@ -1760,9 +1893,9 @@ export default class MarketsPage extends BasePage { * adjustedRate is the current rate field rate, rounded down to a * multiple of rateStep. */ - adjustedRate () { + adjustedRate (): number { const v = this.page.rateField.value - if (!v) return null + if (!v) return NaN const rate = convertToAtoms(v, this.market.rateConversionFactor) const rateStep = this.market.cfg.ratestep return rate - (rate % rateStep) @@ -1778,7 +1911,7 @@ export default class MarketsPage extends BasePage { same orders grouped into arrays. The orders are grouped by their rate and whether or not they are epoch queue orders. Epoch queue orders will come after non epoch queue orders with the same rate. */ - binOrdersByRateAndEpoch (orders) { + binOrdersByRateAndEpoch (orders: MiniOrder[]) { if (!orders || !orders.length) return [] const bins = [] let currEpochBin = [] @@ -1803,7 +1936,7 @@ export default class MarketsPage extends BasePage { } /* loadTables loads the order book side into its table. */ - loadTableSide (sell) { + loadTableSide (sell: boolean) { const bookSide = sell ? this.book.sells : this.book.buys const tbody = sell ? this.page.sellRows : this.page.buyRows Doc.empty(tbody) @@ -1813,9 +1946,9 @@ export default class MarketsPage extends BasePage { } /* addTableOrder adds a single order to the appropriate table. */ - addTableOrder (order) { + addTableOrder (order: MiniOrder) { const tbody = order.sell ? this.page.sellRows : this.page.buyRows - let row = tbody.firstChild + let row = tbody.firstChild as OrderRow // Handle market order differently. if (order.rate === 0) { // This is a market order. @@ -1828,7 +1961,7 @@ export default class MarketsPage extends BasePage { return } // Must be a limit order. Sort by rate. Skip the market order row. - if (row && row.manager.getRate() === 0) row = row.nextSibling + if (row && row.manager.getRate() === 0) row = row.nextSibling as OrderRow while (row) { if (row.manager.compare(order) === 0) { row.manager.insertOrder(order) @@ -1838,17 +1971,17 @@ export default class MarketsPage extends BasePage { tbody.insertBefore(tr, row) return } - row = row.nextSibling + row = row.nextSibling as OrderRow } const tr = this.orderTableRow([order]) tbody.appendChild(tr) } /* removeTableOrder removes a single order from its table. */ - removeTableOrder (order) { + removeTableOrder (order: MiniOrder) { const token = order.token for (const tbody of [this.page.sellRows, this.page.buyRows]) { - for (const tr of Array.from(tbody.children)) { + for (const tr of (Array.from(tbody.children) as OrderRow[])) { if (tr.manager.removeOrder(token)) { return } @@ -1857,10 +1990,10 @@ export default class MarketsPage extends BasePage { } /* updateTableOrder looks for the order in the table and updates the qty */ - updateTableOrder (update) { + updateTableOrder (u: RemainderUpdate) { for (const tbody of [this.page.sellRows, this.page.buyRows]) { - for (const tr of Array.from(tbody.children)) { - if (tr.manager.updateOrderQty(update)) { + for (const tr of (Array.from(tbody.children) as OrderRow[])) { + if (tr.manager.updateOrderQty(u)) { return } } @@ -1870,7 +2003,7 @@ export default class MarketsPage extends BasePage { /* * clearOrderTableEpochs removes immediate-tif orders whose epoch has expired. */ - clearOrderTableEpochs (newEpoch) { + clearOrderTableEpochs () { this.clearOrderTableEpochSide(this.page.sellRows) this.clearOrderTableEpochSide(this.page.buyRows) } @@ -1879,8 +2012,8 @@ export default class MarketsPage extends BasePage { * clearOrderTableEpochs removes immediate-tif orders whose epoch has expired * for a single side. */ - clearOrderTableEpochSide (tbody, newEpoch) { - for (const tr of Array.from(tbody.children)) { + clearOrderTableEpochSide (tbody: HTMLElement) { + for (const tr of (Array.from(tbody.children)) as OrderRow[]) { tr.manager.removeEpochOrders() } } @@ -1889,8 +2022,8 @@ export default class MarketsPage extends BasePage { * orderTableRow creates a new element to insert into an order table. Takes a bin of orders with the same rate, and displays the total quantity. */ - orderTableRow (orderBin) { - const tr = this.page.rowTemplate.cloneNode(true) + orderTableRow (orderBin: MiniOrder[]): OrderRow { + const tr = this.page.rowTemplate.cloneNode(true) as OrderRow const { baseUnitInfo, rateConversionFactor } = this.market const manager = new OrderTableRowManager(tr, orderBin, baseUnitInfo, rateConversionFactor) tr.manager = manager @@ -1898,7 +2031,7 @@ export default class MarketsPage extends BasePage { this.reportDepthClick(tr.manager.getRate() / rateConversionFactor) }) if (tr.manager.getRate() !== 0) { - Doc.bind(tr, 'mouseenter', e => { + Doc.bind(tr, 'mouseenter', () => { const chart = this.depthChart this.depthLines.hover = [{ rate: tr.manager.getRate() / rateConversionFactor, @@ -1912,7 +2045,7 @@ export default class MarketsPage extends BasePage { /* handleConnNote handles the 'conn' notification. */ - async handleConnNote (note) { + async handleConnNote (note: ConnEventNote) { this.marketList.setConnectionStatus(note) if (note.connected) { // Having been disconnected from a DEX server, anything may have changed, @@ -1929,7 +2062,7 @@ export default class MarketsPage extends BasePage { */ filterMarkets () { const filterTxt = this.page.marketSearch.value - const filter = filterTxt ? mkt => mkt.name.includes(filterTxt) : () => true + const filter = filterTxt ? (mkt: MarketRow) => mkt.name.includes(filterTxt) : () => true this.marketList.setFilter(filter) } @@ -1967,7 +2100,7 @@ export default class MarketsPage extends BasePage { } /* candleDurationSelected sets the candleDur and loads the candles. */ - candleDurationSelected (dur) { + candleDurationSelected (dur: string) { this.candleDur = dur this.loadCandles() } @@ -1977,16 +2110,16 @@ export default class MarketsPage extends BasePage { * active, the cache will be used without a loadcandles request. */ loadCandles () { - for (const bttn of this.page.durBttnBox.children) { + for (const bttn of Doc.kids(this.page.durBttnBox)) { if (bttn.textContent === this.candleDur) bttn.classList.add('selected') else bttn.classList.remove('selected') } - const { candleCaches, cfg } = this.market + const { candleCaches, cfg, baseUnitInfo, quoteUnitInfo } = this.market const cache = candleCaches[this.candleDur] if (cache) { this.depthChart.hide() this.candleChart.show() - this.candleChart.setCandles(cache, cfg) + this.candleChart.setCandles(cache, cfg, baseUnitInfo, quoteUnitInfo) return } this.requestCandles() @@ -1996,7 +2129,7 @@ export default class MarketsPage extends BasePage { requestCandles () { this.candlesLoading = { loaded: () => { Doc.hide(this.page.marketLoader) }, - timer: setTimeout(() => { + timer: window.setTimeout(() => { if (this.candlesLoading) { this.candlesLoading = null Doc.hide(this.page.marketLoader) @@ -2033,8 +2166,10 @@ export default class MarketsPage extends BasePage { * and sort order of markets. */ class MarketList { - constructor (div) { - this.selected = null + xcSections: ExchangeSection[] + selected: MarketRow + + constructor (div: HTMLElement) { const xcTmpl = Doc.tmplElement(div, 'xc') Doc.cleanTemplates(xcTmpl) this.xcSections = [] @@ -2058,7 +2193,7 @@ class MarketList { /* * xcSection is a getter for the ExchangeSection for a specified host. */ - xcSection (host) { + xcSection (host: string) { for (const xc of this.xcSections) { if (xc.host === host) return xc } @@ -2066,7 +2201,7 @@ class MarketList { } /* exists will be true if the specified market exists. */ - exists (host, baseID, quoteID) { + exists (host: string, baseID: number, quoteID: number) { const xc = this.xcSection(host) if (!xc) return false for (const mkt of xc.marketRows) { @@ -2085,23 +2220,29 @@ class MarketList { } /* select sets the specified market as selected. */ - select (host, baseID, quoteID) { + select (host: string, baseID: number, quoteID: number) { if (this.selected) this.selected.node.classList.remove('selected') - this.selected = this.xcSection(host).marketRow(baseID, quoteID) + const xcSection = this.xcSection(host) + if (!xcSection) return console.error(`select: no exchange section for ${host}`) + const marketRow = xcSection.marketRow(baseID, quoteID) + if (!marketRow) return console.error(`select: no market row for ${host}, ${baseID}-${quoteID}`) + this.selected = marketRow this.selected.node.classList.add('selected') } /* setConnectionStatus sets the visibility of the disconnected icon based * on the core.ConnEventNote. */ - setConnectionStatus (note) { - this.xcSection(note.host).setConnected(note.connected) + setConnectionStatus (note: ConnEventNote) { + const xcSection = this.xcSection(note.host) + if (!xcSection) return console.error(`setConnectionStatus: no exchange section for ${note.host}`) + xcSection.setConnected(note.connected) } /* * setFilter sets the visibility of market rows based on the provided filter. */ - setFilter (filter) { + setFilter (filter: (mkt: MarketRow) => boolean) { for (const xc of this.xcSections) { xc.setFilter(filter) } @@ -2112,10 +2253,16 @@ class MarketList { * ExchangeSection is a top level section of the MarketList. */ class ExchangeSection { - constructor (template, dex) { + marketRows: MarketRow[] + host: string + dex: Exchange + node: HTMLElement + disconnectedIco: PageElement + + constructor (template: HTMLElement, dex: Exchange) { this.dex = dex this.host = dex.host - this.node = template.cloneNode(true) + this.node = template.cloneNode(true) as HTMLElement const tmpl = Doc.parseTemplate(this.node) tmpl.header.textContent = dex.host @@ -2155,15 +2302,14 @@ class ExchangeSection { /* * marketRow gets the MarketRow for the specified market. */ - marketRow (baseID, quoteID) { + marketRow (baseID: number, quoteID: number) { for (const mkt of this.marketRows) { if (mkt.baseID === baseID && mkt.quoteID === quoteID) return mkt } - return null } /* setConnected sets the visiblity of the disconnected icon. */ - setConnected (isConnected) { + setConnected (isConnected: boolean) { if (isConnected) Doc.hide(this.disconnectedIco) else Doc.show(this.disconnectedIco) } @@ -2171,7 +2317,7 @@ class ExchangeSection { /* * setFilter sets the visibility of market rows based on the provided filter. */ - setFilter (filter) { + setFilter (filter: (mkt: MarketRow) => boolean) { for (const mkt of this.marketRows) { if (filter(mkt)) Doc.show(mkt.node) else Doc.hide(mkt.node) @@ -2184,14 +2330,23 @@ class ExchangeSection { * of the ExchangeSection. */ class MarketRow { - constructor (template, mkt) { + node: HTMLElement + mkt: Market + name: string + baseID: number + quoteID: number + lotSize: number + rateStep: number + tmpl: Record + + constructor (template: HTMLElement, mkt: Market) { this.mkt = mkt this.name = mkt.name this.baseID = mkt.baseid this.quoteID = mkt.quoteid this.lotSize = mkt.lotsize this.rateStep = mkt.ratestep - this.node = template.cloneNode(true) + this.node = template.cloneNode(true) as HTMLElement const tmpl = this.tmpl = Doc.parseTemplate(this.node) tmpl.baseIcon.src = Doc.logoPath(mkt.basesymbol) tmpl.quoteIcon.src = Doc.logoPath(mkt.quotesymbol) @@ -2200,7 +2355,7 @@ class MarketRow { this.setSpot(mkt.spot) } - setSpot (spot) { + setSpot (spot: Spot) { if (!spot) return const { tmpl, mkt } = this @@ -2228,7 +2383,11 @@ class MarketRow { * locked and immature balance. */ class BalanceWidget { - constructor (table) { + base: BalanceWidgetElement + quote: BalanceWidgetElement + dex: Exchange + + constructor (table: HTMLElement) { const els = Doc.idDescendants(table) this.base = { id: 0, @@ -2262,13 +2421,12 @@ class BalanceWidget { iconBox: els.quoteWalletState, stateIcons: new WalletIcons(els.quoteWalletState) } - this.dex = null } /* * setWallet sets the balance widget to display data for specified market. */ - setWallets (host, baseID, quoteID) { + setWallets (host: string, baseID: number, quoteID: number) { this.dex = app().user.exchanges[host] this.base.id = baseID this.base.cfg = this.dex.assets[baseID] @@ -2282,7 +2440,8 @@ class BalanceWidget { * updateWallet updates the displayed wallet information based on the * core.Wallet state. */ - updateWallet (side) { + updateWallet (side: BalanceWidgetElement) { + if (!side.cfg) return // no wallet set yet const asset = app().assets[side.id] // Just hide everything to start. Doc.hide( @@ -2335,14 +2494,14 @@ class BalanceWidget { * specified asset ID is not one of the current market's base or quote assets, * it is silently ignored. */ - updateAsset (assetID) { + updateAsset (assetID: number) { if (assetID === this.base.id) this.updateWallet(this.base) else if (assetID === this.quote.id) this.updateWallet(this.quote) } } /* makeMarket creates a market object that specifies basic market details. */ -function makeMarket (host, base, quote) { +function makeMarket (host: string, base?: number, quote?: number) { return { host: host, base: base, @@ -2351,16 +2510,16 @@ function makeMarket (host, base, quote) { } /* marketID creates a DEX-compatible market name from the ticker symbols. */ -export function marketID (b, q) { return `${b}_${q}` } +export function marketID (b: string, q: string) { return `${b}_${q}` } /* convertToAtoms converts the float string to the basic unit of a coin. */ -function convertToAtoms (s, conversionFactor) { +function convertToAtoms (s: string, conversionFactor: number) { if (!s) return 0 return Math.round(parseFloat(s) * conversionFactor) } /* swapBttns changes the 'selected' class of the buttons. */ -function swapBttns (before, now) { +function swapBttns (before: HTMLElement, now: HTMLElement) { before.classList.remove('selected') now.classList.add('selected') } @@ -2368,7 +2527,7 @@ function swapBttns (before, now) { /* * updateDataCol sets the textContent of descendent template element. */ -function updateDataCol (tr, col, s) { +function updateDataCol (tr: HTMLElement, col: string, s: string) { Doc.tmplElement(tr, col).textContent = s } @@ -2376,8 +2535,8 @@ function updateDataCol (tr, col, s) { * wireOrder prepares a copy of the order with the options field converted to a * string -> string map. */ -function wireOrder (order) { - const stringyOptions = {} +function wireOrder (order: TradeForm) { + const stringyOptions: Record = {} for (const [k, v] of Object.entries(order.options)) stringyOptions[k] = JSON.stringify(v) return Object.assign({}, order, { options: stringyOptions }) } @@ -2386,7 +2545,15 @@ function wireOrder (order) { // represents all the orders in the order book with the same rate, but orders that // are booked or still in the epoch queue are displayed in separate rows. class OrderTableRowManager { - constructor (tableRow, orderBin, baseUnitInfo, rateConversionFactor) { + tableRow: HTMLElement + orderBin: MiniOrder[] + sell: boolean + msgRate: number + epoch: boolean + baseUnitInfo: UnitInfo + rateConversionFactor: number + + constructor (tableRow: HTMLElement, orderBin: MiniOrder[], baseUnitInfo: UnitInfo, rateConversionFactor: number) { this.tableRow = tableRow this.orderBin = orderBin this.sell = orderBin[0].sell @@ -2429,16 +2596,16 @@ class OrderTableRowManager { qtyEl.innerText = Doc.formatFullPrecision(qty, this.baseUnitInfo) if (numOrders > 1) { numOrdersEl.removeAttribute('hidden') - numOrdersEl.innerText = numOrders + numOrdersEl.innerText = String(numOrders) numOrdersEl.title = `quantity is comprised of ${numOrders} orders` } else { - numOrdersEl.setAttribute('hidden', true) + numOrdersEl.setAttribute('hidden', 'true') } } // insertOrder adds an order to the order bin and updates the row elements // accordingly. - insertOrder (order) { + insertOrder (order: MiniOrder) { this.orderBin.push(order) this.updateQtyNumOrdersEl() } @@ -2446,7 +2613,7 @@ class OrderTableRowManager { // updateOrderQuantity updates the quantity of the order identified by a token, // if it exists in the row, and updates the row elements accordingly. The function // returns true if the order is in the bin, and false otherwise. - updateOrderQty (update) { + updateOrderQty (update: RemainderUpdate) { const { token, qty, qtyAtomic } = update for (let i = 0; i < this.orderBin.length; i++) { if (this.orderBin[i].token === token) { @@ -2463,7 +2630,7 @@ class OrderTableRowManager { // and updates the row elements accordingly. If the order bin is empty, the row is // removed from the screen. The function returns true if an order was removed, and // false otherwise. - removeOrder (token) { + removeOrder (token: string) { const index = this.orderBin.findIndex(order => order.token === token) if (index < 0) return false this.orderBin.splice(index, 1) @@ -2474,7 +2641,7 @@ class OrderTableRowManager { // removeEpochOrders removes all the orders from the row that are not in the // new epoch's epoch queue and updates the elements accordingly. - removeEpochOrders (newEpoch) { + removeEpochOrders (newEpoch?: number) { this.orderBin = this.orderBin.filter((order) => { return !(order.epoch && order.epoch !== newEpoch) }) @@ -2502,7 +2669,7 @@ class OrderTableRowManager { // be before this row in the table. Sell orders are displayed in ascending order, // buy orders are displayed in descending order, and epoch orders always come // after booked orders. - compare (order) { + compare (order: MiniOrder) { if (this.getRate() === order.msgRate && this.isEpoch() === !!order.epoch) { return 0 } else if (this.getRate() !== order.msgRate) { diff --git a/client/webserver/site/src/js/notifications.js b/client/webserver/site/src/js/notifications.ts similarity index 74% rename from client/webserver/site/src/js/notifications.js rename to client/webserver/site/src/js/notifications.ts index 5dd0176cf8..9faf2a38a5 100644 --- a/client/webserver/site/src/js/notifications.js +++ b/client/webserver/site/src/js/notifications.ts @@ -1,3 +1,5 @@ +import { CoreNote } from './registry' + export const IGNORE = 0 export const DATA = 1 export const POKE = 2 @@ -13,11 +15,15 @@ export const ERROR = 5 * if the error is generated during submission of a form, the error should be * displayed on or near the form itself, not in the notifications. */ -export function make (subject, details, severity) { +export function make (subject: string, details: string, severity: number): CoreNote { return { subject: subject, details: details, severity: severity, - stamp: new Date().getTime() + stamp: new Date().getTime(), + acked: false, + type: 'internal', + topic: 'internal', + id: '' } } diff --git a/client/webserver/site/src/js/order.js b/client/webserver/site/src/js/order.ts similarity index 69% rename from client/webserver/site/src/js/order.js rename to client/webserver/site/src/js/order.ts index 87350ff356..6f2690b58b 100644 --- a/client/webserver/site/src/js/order.js +++ b/client/webserver/site/src/js/order.ts @@ -1,10 +1,18 @@ -import { app } from './registry' import Doc from './doc' import BasePage from './basepage' -import * as Order from './orderutil' +import * as OrderUtil from './orderutil' import { bind as bindForm } from './forms' import { postJSON } from './http' import * as intl from './locales' +import { + app, + Order, + PageElement, + OrderNote, + MatchNote, + Match, + Coin +} from './registry' const Mainnet = 0 const Testnet = 1 @@ -12,19 +20,27 @@ const Testnet = 1 const animationLength = 500 -let net +let net: number export default class OrderPage extends BasePage { - constructor (main) { + orderID: string + order: Order + page: Record + currentForm: HTMLElement + secondTicker: number + + constructor (main: HTMLElement) { super() - const stampers = main.querySelectorAll('[data-stamp]') - net = parseInt(main.dataset.net) + const stampers = Doc.applySelector(main, '[data-stamp]') + net = parseInt(main.dataset.net || '') // Find the order - this.orderID = main.dataset.oid - this.order = app().order(this.orderID) + this.orderID = main.dataset.oid || '' + const ord = app().order(this.orderID) // app().order can only access active orders. If the order is not active, // we'll need to get the data from the database. - if (!this.order) this.fetchOrder() + if (ord) this.order = ord + else this.fetchOrder() + const page = this.page = Doc.idDescendants(main) if (page.cancelBttn) { @@ -34,7 +50,7 @@ export default class OrderPage extends BasePage { } // If the user clicks outside of a form, it should close the page overlay. - Doc.bind(page.forms, 'mousedown', e => { + Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { if (!Doc.mouseInElement(e, this.currentForm)) { Doc.hide(page.forms) page.cancelPass.value = '' @@ -44,24 +60,24 @@ export default class OrderPage extends BasePage { // Cancel order form bindForm(page.cancelForm, page.cancelSubmit, async () => { this.submitCancel() }) - main.querySelectorAll('[data-explorer-id]').forEach(link => { + main.querySelectorAll('[data-explorer-id]').forEach((link: PageElement) => { setCoinHref(link) }) const setStamp = () => { for (const span of stampers) { - span.textContent = Doc.timeSince(parseInt(span.dataset.stamp)) + span.textContent = Doc.timeSince(parseInt(span.dataset.stamp || '')) } } setStamp() - this.secondTicker = setInterval(() => { + this.secondTicker = window.setInterval(() => { setStamp() }, 10000) // update every 10 seconds this.notifiers = { - order: note => { this.handleOrderNote(note) }, - match: note => { this.handleMatchNote(note) } + order: (note: OrderNote) => { this.handleOrderNote(note) }, + match: (note: MatchNote) => { this.handleMatchNote(note) } } } @@ -81,14 +97,14 @@ export default class OrderPage extends BasePage { const order = this.order const page = this.page const remaining = order.qty - order.filled - const asset = Order.isMarketBuy(order) ? app().assets[order.quoteID] : app().assets[order.baseID] + const asset = OrderUtil.isMarketBuy(order) ? app().assets[order.quoteID] : app().assets[order.baseID] page.cancelRemain.textContent = Doc.formatCoinValue(remaining, asset.info.unitinfo) page.cancelUnit.textContent = asset.info.unitinfo.conventional.unit.toUpperCase() this.showForm(page.cancelForm) } /* showForm shows a modal form with a little animation. */ - async showForm (form) { + async showForm (form: HTMLElement) { this.currentForm = form const page = this.page Doc.hide(page.cancelForm) @@ -124,18 +140,18 @@ export default class OrderPage extends BasePage { * handleOrderNote is the handler for the 'order'-type notification, which are * used to update an order's status. */ - handleOrderNote (note) { + handleOrderNote (note: OrderNote) { const order = note.order const bttn = this.page.cancelBttn if (bttn && order.id === this.orderID) { - if (bttn && order.status > Order.StatusBooked) Doc.hide(bttn) - this.page.status.textContent = Order.statusString(order) + if (bttn && order.status > OrderUtil.StatusBooked) Doc.hide(bttn) + this.page.status.textContent = OrderUtil.statusString(order) } for (const m of order.matches || []) this.processMatch(m) } /* handleMatchNote handles a 'match' notification. */ - handleMatchNote (note) { + handleMatchNote (note: MatchNote) { if (note.orderID !== this.orderID) return this.processMatch(note.match) } @@ -144,9 +160,9 @@ export default class OrderPage extends BasePage { * processMatch synchronizes a match's card with a match received in a * 'order' or 'match' notification. */ - processMatch (m) { - let card - for (const div of Array.from(this.page.matchBox.querySelectorAll('.match-card'))) { + processMatch (m: Match) { + let card: HTMLElement | null = null + for (const div of Doc.applySelector(this.page.matchBox, '.match-card')) { if (div.dataset.matchID === m.matchID) { card = div break @@ -157,7 +173,8 @@ export default class OrderPage extends BasePage { return } - const setCoin = (divName, linkName, coin) => { + const setCoin = (divName: string, linkName: string, coin: Coin) => { + if (!card) return // Ugh if (!coin) return Doc.show(Doc.tmplElement(card, divName)) const coinLink = Doc.tmplElement(card, linkName) @@ -187,7 +204,7 @@ export default class OrderPage extends BasePage { Doc.hide(swapSpan, cSwapSpan) } - Doc.tmplElement(card, 'status').textContent = Order.matchStatusString(m.status, m.side) + Doc.tmplElement(card, 'status').textContent = OrderUtil.matchStatusString(m.status, m.side) } } @@ -195,7 +212,7 @@ export default class OrderPage extends BasePage { * confirmationString is a string describing the state of confirmations for a * coin * */ -function confirmationString (coin) { +function confirmationString (coin: Coin) { if (!coin.confs) return '' return `${coin.confs.count} / ${coin.confs.required} confirmations` } @@ -204,49 +221,49 @@ function confirmationString (coin) { * inCounterSwapCast will be true if we are waiting on confirmations for the * counterparty's swap. */ -function inCounterSwapCast (m) { - return (m.side === Order.Taker && m.status === Order.MakerSwapCast) || (m.side === Order.Maker && m.status === Order.TakerSwapCast) +function inCounterSwapCast (m: Match) { + return (m.side === OrderUtil.Taker && m.status === OrderUtil.MakerSwapCast) || (m.side === OrderUtil.Maker && m.status === OrderUtil.TakerSwapCast) } /* * inCounterSwapCast will be true if we are waiting on confirmations for our own * swap. */ -function inSwapCast (m) { - return (m.side === Order.Maker && m.status === Order.MakerSwapCast) || (m.side === Order.Taker && m.status === Order.TakerSwapCast) +function inSwapCast (m: Match) { + return (m.side === OrderUtil.Maker && m.status === OrderUtil.MakerSwapCast) || (m.side === OrderUtil.Taker && m.status === OrderUtil.TakerSwapCast) } /* * setCoinHref sets the hyperlink element's href attribute based on its * data-explorer-id and data-explorer-coin values. */ -function setCoinHref (link) { - const assetExplorer = CoinExplorers[parseInt(link.dataset.explorerId)] +function setCoinHref (link: PageElement) { + const assetExplorer = CoinExplorers[parseInt(link.dataset.explorerId || '')] if (!assetExplorer) return const formatter = assetExplorer[net] if (!formatter) return link.classList.remove('plainlink') link.classList.add('subtlelink') - link.href = formatter(link.dataset.explorerCoin) + link.href = formatter(link.dataset.explorerCoin || '') } -const CoinExplorers = { +const CoinExplorers: Record string>> = { 42: { // dcr - [Mainnet]: cid => { + [Mainnet]: (cid: string) => { const [txid, vout] = cid.split(':') return `https://explorer.dcrdata.org/tx/${txid}/out/${vout}` }, - [Testnet]: cid => { + [Testnet]: (cid: string) => { const [txid, vout] = cid.split(':') return `https://testnet.dcrdata.org/tx/${txid}/out/${vout}` } }, 0: { // btc - [Mainnet]: cid => `https://bitaps.com/${cid.split(':')[0]}`, - [Testnet]: cid => `https://tbtc.bitaps.com/${cid.split(':')[0]}` + [Mainnet]: (cid: string) => `https://bitaps.com/${cid.split(':')[0]}`, + [Testnet]: (cid: string) => `https://tbtc.bitaps.com/${cid.split(':')[0]}` }, 2: { // ltc - [Mainnet]: cid => `https://ltc.bitaps.com/${cid.split(':')[0]}`, - [Testnet]: cid => `https://tltc.bitaps.com/${cid.split(':')[0]}` + [Mainnet]: (cid: string) => `https://ltc.bitaps.com/${cid.split(':')[0]}`, + [Testnet]: (cid: string) => `https://tltc.bitaps.com/${cid.split(':')[0]}` } } diff --git a/client/webserver/site/src/js/orderbook.js b/client/webserver/site/src/js/orderbook.ts similarity index 72% rename from client/webserver/site/src/js/orderbook.js rename to client/webserver/site/src/js/orderbook.ts index e0658b1cd2..9ed511ce78 100644 --- a/client/webserver/site/src/js/orderbook.js +++ b/client/webserver/site/src/js/orderbook.ts @@ -1,5 +1,17 @@ +import { + MarketOrderBook, + MiniOrder +} from './registry' + export default class OrderBook { - constructor (mktBook, baseSymbol, quoteSymbol) { + base: number + baseSymbol: string + quote: number + quoteSymbol: string + buys: MiniOrder[] + sells: MiniOrder[] + + constructor (mktBook: MarketOrderBook, baseSymbol: string, quoteSymbol: string) { this.base = mktBook.base this.baseSymbol = baseSymbol this.quote = mktBook.quote @@ -10,19 +22,19 @@ export default class OrderBook { } /* add adds an order to the order book. */ - add (ord) { + add (ord: MiniOrder) { const side = ord.sell ? this.sells : this.buys side.splice(findIdx(side, ord.rate, !ord.sell), 0, ord) } /* remove removes an order from the order book. */ - remove (token) { + remove (token: string) { if (this.removeFromSide(this.sells, token)) return this.removeFromSide(this.buys, token) } /* removeFromSide removes an order from the list of orders. */ - removeFromSide (side, token) { + removeFromSide (side: MiniOrder[], token: string) { const [ord, i] = this.findOrder(side, token) if (ord) { side.splice(i, 1) @@ -32,8 +44,8 @@ export default class OrderBook { } /* findOrder finds an order in a specified side */ - findOrder (side, token) { - for (const i in side) { + findOrder (side: MiniOrder[], token: string): [MiniOrder | null, number] { + for (let i = 0; i < side.length; i++) { if (side[i].token === token) { return [side[i], i] } @@ -42,7 +54,7 @@ export default class OrderBook { } /* updates the remaining quantity of an order. */ - updateRemaining (token, qty, qtyAtomic) { + updateRemaining (token: string, qty: number, qtyAtomic: number) { if (this.updateRemainingSide(this.sells, token, qty, qtyAtomic)) return this.updateRemainingSide(this.buys, token, qty, qtyAtomic) } @@ -51,7 +63,7 @@ export default class OrderBook { * updateRemainingSide looks for the order in the side and updates the * quantity, returning true on success, false if order not found. */ - updateRemainingSide (side, token, qty, qtyAtomic) { + updateRemainingSide (side: MiniOrder[], token: string, qty: number, qtyAtomic: number) { const ord = this.findOrder(side, token)[0] if (ord) { ord.qty = qty @@ -64,8 +76,8 @@ export default class OrderBook { /* * setEpoch sets the current epoch and clear any orders from previous epochs. */ - setEpoch (epochIdx) { - const approve = ord => ord.epoch === undefined || ord.epoch === 0 || ord.epoch === epochIdx + setEpoch (epochIdx: number) { + const approve = (ord: MiniOrder) => ord.epoch === undefined || ord.epoch === 0 || ord.epoch === epochIdx this.sells = this.sells.filter(approve) this.buys = this.buys.filter(approve) } @@ -84,7 +96,7 @@ export default class OrderBook { * best epoch order if there are only epoch orders, or null if there are no * orders. */ - bestGapOrder (side) { + bestGapOrder (side: MiniOrder[]) { let best = null for (const ord of side) { if (!ord.epoch) return ord @@ -107,8 +119,8 @@ export default class OrderBook { /* * findIdx find the index at which to insert the order into the list of orders. */ -function findIdx (side, rate, less) { - for (const i in side) { +function findIdx (side: MiniOrder[], rate: number, less: boolean): number { + for (let i = 0; i < side.length; i++) { if ((side[i].rate < rate) === less) return i } return side.length diff --git a/client/webserver/site/src/js/orders.js b/client/webserver/site/src/js/orders.ts similarity index 73% rename from client/webserver/site/src/js/orders.js rename to client/webserver/site/src/js/orders.ts index 9435190705..408d2c7fa6 100644 --- a/client/webserver/site/src/js/orders.js +++ b/client/webserver/site/src/js/orders.ts @@ -1,13 +1,25 @@ -import { app } from './registry' import Doc from './doc' import BasePage from './basepage' -import * as Order from './orderutil' +import * as OrderUtil from './orderutil' import { postJSON } from './http' +import { + app, + PageElement, + OrderFilter, + Order +} from './registry' const orderBatchSize = 50 export default class OrdersPage extends BasePage { - constructor (main) { + main: HTMLElement + offset: string + loading: boolean + orderTmpl: PageElement + filterState: OrderFilter + page: Record + + constructor (main: HTMLElement) { super() this.main = main // if offset is '', there are no more orders available to auto-load for @@ -20,19 +32,19 @@ export default class OrdersPage extends BasePage { // filterState will store arrays of strings. The assets and statuses // sub-filters will need to be converted to ints for JSON encoding. - const filterState = this.filterState = { + const filterState: OrderFilter = this.filterState = { hosts: [], assets: [], statuses: [] } const search = new URLSearchParams(window.location.search) - const readFilter = (form, filterKey) => { + const readFilter = (form: HTMLElement, filterKey: string) => { const v = search.get(filterKey) if (!v || v.length === 0) return const subFilter = v.split(',') if (v) { - filterState[filterKey] = subFilter + (filterState as any)[filterKey] = subFilter // Kinda janky } form.querySelectorAll('input').forEach(bttn => { if (subFilter.indexOf(bttn.value) >= 0) bttn.checked = true @@ -42,9 +54,9 @@ export default class OrdersPage extends BasePage { readFilter(page.assetFilter, 'assets') readFilter(page.statusFilter, 'statuses') - const applyButtons = [] - const monitorFilter = (form, filterKey) => { - const applyBttn = form.querySelector('.apply-bttn') + const applyButtons: HTMLElement[] = [] + const monitorFilter = (form: HTMLElement, filterKey: string) => { + const applyBttn = form.querySelector('.apply-bttn') as HTMLElement applyButtons.push(applyBttn) Doc.bind(applyBttn, 'click', () => { this.submitFilter() @@ -53,7 +65,7 @@ export default class OrdersPage extends BasePage { form.querySelectorAll('input').forEach(bttn => { Doc.bind(bttn, 'change', () => { const subFilter = parseSubFilter(form) - if (compareSubFilter(subFilter, filterState[filterKey])) { + if (compareSubFilter(subFilter, (filterState as any)[filterKey])) { // Same as currently loaded. Hide the apply button. Doc.hide(applyBttn) } else { @@ -83,17 +95,17 @@ export default class OrdersPage extends BasePage { } /* setOrders empties the order table and appends the specified orders. */ - setOrders (orders) { + setOrders (orders: Order[]) { Doc.empty(this.page.tableBody) this.appendOrders(orders) } /* appendOrders appends orders to the orders table. */ - appendOrders (orders) { + appendOrders (orders: Order[]) { const tbody = this.page.tableBody for (const ord of orders) { - const tr = this.orderTmpl.cloneNode(true) - const set = (tmplID, s) => { Doc.tmplElement(tr, tmplID).textContent = s } + const tr = this.orderTmpl.cloneNode(true) as HTMLElement + const set = (tmplID: string, s: string) => { Doc.tmplElement(tr, tmplID).textContent = s } const mktID = `${ord.baseSymbol.toUpperCase()}-${ord.quoteSymbol.toUpperCase()}` set('host', `${mktID} @ ${ord.host}`) let from, to, fromQty @@ -102,15 +114,15 @@ export default class OrdersPage extends BasePage { if (ord.sell) { [from, to] = [ord.baseSymbol, ord.quoteSymbol] fromQty = Doc.formatCoinValue(ord.qty, baseUnitInfo) - if (ord.type === Order.Limit) { - toQty = Doc.formatCoinValue(ord.qty / Order.RateEncodingFactor * ord.rate, quoteUnitInfo) + if (ord.type === OrderUtil.Limit) { + toQty = Doc.formatCoinValue(ord.qty / OrderUtil.RateEncodingFactor * ord.rate, quoteUnitInfo) } } else { [from, to] = [ord.quoteSymbol, ord.baseSymbol] - if (ord.type === Order.Market) { + if (ord.type === OrderUtil.Market) { fromQty = Doc.formatCoinValue(ord.qty, baseUnitInfo) } else { - fromQty = Doc.formatCoinValue(ord.qty / Order.RateEncodingFactor * ord.rate, quoteUnitInfo) + fromQty = Doc.formatCoinValue(ord.qty / OrderUtil.RateEncodingFactor * ord.rate, quoteUnitInfo) toQty = Doc.formatCoinValue(ord.qty, baseUnitInfo) } } @@ -121,11 +133,11 @@ export default class OrdersPage extends BasePage { set('toQty', toQty) Doc.tmplElement(tr, 'toLogo').src = Doc.logoPath(to) set('toSymbol', to) - set('type', `${Order.typeString(ord)} ${Order.sellString(ord)}`) + set('type', `${OrderUtil.typeString(ord)} ${OrderUtil.sellString(ord)}`) set('rate', Doc.formatCoinValue(app().conventionalRate(ord.baseID, ord.quoteID, ord.rate))) - set('status', Order.statusString(ord)) + set('status', OrderUtil.statusString(ord)) set('filled', `${(ord.filled / ord.qty * 100).toFixed(1)}%`) - set('settled', `${(Order.settled(ord) / ord.qty * 100).toFixed(1)}%`) + set('settled', `${(OrderUtil.settled(ord) / ord.qty * 100).toFixed(1)}%`) const dateTime = new Date(ord.stamp).toLocaleString() set('time', `${Doc.timeSince(ord.stamp)} ago, ${dateTime}`) const link = Doc.tmplElement(tr, 'link') @@ -146,8 +158,8 @@ export default class OrdersPage extends BasePage { this.offset = '' const filterState = this.filterState filterState.hosts = parseSubFilter(page.hostFilter) - filterState.assets = parseSubFilter(page.assetFilter) - filterState.statuses = parseSubFilter(page.statusFilter) + filterState.assets = parseSubFilter(page.assetFilter).map((s: string) => parseInt(s)) + filterState.statuses = parseSubFilter(page.statusFilter).map((s: string) => parseInt(s)) this.setOrders(await this.fetchOrders()) } @@ -163,12 +175,12 @@ export default class OrdersPage extends BasePage { exportOrders () { this.offset = '' const filterState = this.currentFilter() - const url = new URL(window.location) + const url = new URL(window.location.href) const search = new URLSearchParams('') - const setQuery = (k) => { - const subFilter = filterState[k] - subFilter.forEach(e => { - search.append(k, e) + const setQuery = (k: string) => { + const subFilter = (filterState as any)[k] + subFilter.forEach((v: any) => { + search.append(k, v) }) } setQuery('hosts') @@ -183,12 +195,12 @@ export default class OrdersPage extends BasePage { * currentFilter converts the local filter type (which is all strings) to the * server's filter type. */ - currentFilter () { + currentFilter (): OrderFilter { const filterState = this.filterState return { hosts: filterState.hosts, - assets: filterState.assets.map(s => parseInt(s)), - statuses: filterState.statuses.map(s => parseInt(s)), + assets: filterState.assets.map((s: any) => parseInt(s)), + statuses: filterState.statuses.map((s: any) => parseInt(s)), n: orderBatchSize, offset: this.offset } @@ -212,8 +224,8 @@ export default class OrdersPage extends BasePage { * parseSubFilter parses a bool-map from the checkbox inputs in the specified * ancestor element. */ -function parseSubFilter (form) { - const entries = [] +function parseSubFilter (form: HTMLElement): string[] { + const entries: string[] = [] form.querySelectorAll('input').forEach(box => { if (box.checked) entries.push(box.value) }) @@ -221,7 +233,7 @@ function parseSubFilter (form) { } /* compareSubFilter compares the two filter arrays for unordered equivalence. */ -function compareSubFilter (filter1, filter2) { +function compareSubFilter (filter1: any[], filter2: any[]): boolean { if (filter1.length !== filter2.length) return false for (const entry of filter1) { if (filter2.indexOf(entry) === -1) return false diff --git a/client/webserver/site/src/js/orderutil.js b/client/webserver/site/src/js/orderutil.ts similarity index 78% rename from client/webserver/site/src/js/orderutil.js rename to client/webserver/site/src/js/orderutil.ts index ddbc1cc604..a6e1ee02a4 100644 --- a/client/webserver/site/src/js/orderutil.js +++ b/client/webserver/site/src/js/orderutil.ts @@ -1,6 +1,13 @@ import Doc from './doc' import * as intl from './locales' -import { app } from './registry' +import { + app, + Order, + TradeForm, + PageElement, + OrderOption as OrderOpt, + Match +} from './registry' export const Limit = 1 export const Market = 2 @@ -36,11 +43,11 @@ export const Taker = 1 */ export const RateEncodingFactor = 1e8 -export function sellString (ord) { return ord.sell ? 'sell' : 'buy' } -export function typeString (ord) { return ord.type === Limit ? (ord.tif === ImmediateTiF ? 'limit (i)' : 'limit') : 'market' } +export function sellString (ord: Order) { return ord.sell ? 'sell' : 'buy' } +export function typeString (ord: Order) { return ord.type === Limit ? (ord.tif === ImmediateTiF ? 'limit (i)' : 'limit') : 'market' } /* isMarketBuy will return true if the order is a market buy order. */ -export function isMarketBuy (ord) { +export function isMarketBuy (ord: Order) { return ord.type === Market && !ord.sell } @@ -48,7 +55,7 @@ export function isMarketBuy (ord) { * hasLiveMatches returns true if the order has matches that have not completed * settlement yet. */ -export function hasLiveMatches (order) { +export function hasLiveMatches (order: Order) { if (!order.matches) return false for (const match of order.matches) { if (!match.revoked && match.status < MakerRedeemed) return true @@ -57,7 +64,7 @@ export function hasLiveMatches (order) { } /* statusString converts the order status to a string */ -export function statusString (order) { +export function statusString (order: Order): string { const isLive = hasLiveMatches(order) switch (order.status) { case StatusUnknown: return intl.prep(intl.ID_UNKNOWN) @@ -73,12 +80,13 @@ export function statusString (order) { case StatusRevoked: return isLive ? `${intl.prep(intl.ID_REVOKED)}/${intl.prep(intl.ID_SETTLING)}` : intl.prep(intl.ID_REVOKED) } + return '' } /* settled sums the quantities of the matches that have completed. */ -export function settled (order) { +export function settled (order: Order) { if (!order.matches) return 0 - const qty = isMarketBuy(order) ? m => m.qty * m.rate / RateEncodingFactor : m => m.qty + const qty = isMarketBuy(order) ? (m: Match) => m.qty * m.rate / RateEncodingFactor : (m: Match) => m.qty return order.matches.reduce((settled, match) => { if (match.isCancel) return settled const redeemed = (match.side === Maker && match.status >= MakerRedeemed) || @@ -91,7 +99,7 @@ export function settled (order) { * matchStatusString is a string used to create a displayable string describing * describing the match status. */ -export function matchStatusString (status, side) { +export function matchStatusString (status: number, side: number) { switch (status) { case NewlyMatched: return '(0 / 4) Newly Matched' @@ -112,24 +120,34 @@ export function matchStatusString (status, side) { // Having the caller set these vars on load using an exported function makes // life easier. -let orderOptTmpl, booleanOptTmpl, rangeOptTmpl +let orderOptTmpl: HTMLElement, booleanOptTmpl: HTMLElement, rangeOptTmpl: HTMLElement // setOptionTemplates sets the package vars for the templates and application. -export function setOptionTemplates (page) { +export function setOptionTemplates (page: Record) { [booleanOptTmpl, rangeOptTmpl, orderOptTmpl] = [page.booleanOptTmpl, page.rangeOptTmpl, page.orderOptTmpl] } +interface OptionsReporters { + enable: () => void + disable: () => void +} + /* * OrderOption is a base class for option elements. OrderOptions stores some * common parameters and monitors the toggle switch, calling the child class's * enable/disable methods when the user manually turns the option on or off. */ class OrderOption { - constructor (opt, order, isSwapOption, changed) { + opt: OrderOpt + order: TradeForm + node: HTMLElement + tmpl: Record + on: boolean + + constructor (opt: OrderOpt, order: TradeForm, isSwapOption: boolean, report: OptionsReporters) { this.opt = opt this.order = order - this.changed = changed - const node = this.node = orderOptTmpl.cloneNode(true) + const node = this.node = orderOptTmpl.cloneNode(true) as HTMLElement const tmpl = this.tmpl = Doc.parseTemplate(node) tmpl.optName.textContent = opt.displayname tmpl.tooltip.dataset.tooltip = opt.description @@ -143,14 +161,14 @@ class OrderOption { if (this.on) return this.on = true node.classList.add('selected') - this.enable() + report.enable() }) Doc.bind(tmpl.toggle, 'click', e => { if (!this.on) return e.stopPropagation() this.on = false node.classList.remove('selected') - this.disable() + report.disable() }) } @@ -169,10 +187,17 @@ class OrderOption { * client/asset. */ class BooleanOrderOption extends OrderOption { - constructor (opt, order, changed, isSwapOption) { - super(opt, order, isSwapOption, changed) + control: HTMLElement + changed: () => void + + constructor (opt: OrderOpt, order: TradeForm, changed: () => void, isSwapOption: boolean) { + super(opt, order, isSwapOption, { + enable: () => this.enable(), + disable: () => this.disable() + }) + this.changed = () => changed() const cfg = opt.boolean - const control = this.control = booleanOptTmpl.cloneNode(true) + const control = this.control = booleanOptTmpl.cloneNode(true) as HTMLElement // Append to parent's options div. this.tmpl.controls.appendChild(control) const tmpl = Doc.parseTemplate(control) @@ -202,9 +227,17 @@ class BooleanOrderOption extends OrderOption { * The user can also manually enter values for x or y. */ class XYRangeOrderOption extends OrderOption { - constructor (opt, order, changed, isSwapOption) { - super(opt, order, isSwapOption, changed) - const control = this.control = rangeOptTmpl.cloneNode(true) + control: HTMLElement + x: number + changed: () => void + + constructor (opt: OrderOpt, order: TradeForm, changed: () => void, isSwapOption: boolean) { + super(opt, order, isSwapOption, { + enable: () => this.enable(), + disable: () => this.disable() + }) + this.changed = changed + const control = this.control = rangeOptTmpl.cloneNode(true) as HTMLElement const tmpl = Doc.parseTemplate(control) const cfg = opt.xyRange const { slider, handle } = tmpl @@ -213,7 +246,7 @@ class XYRangeOrderOption extends OrderOption { const rangeX = cfg.end.x - cfg.start.x const rangeY = cfg.end.y - cfg.start.y - const normalizeX = x => (x - cfg.start.x) / rangeX + const normalizeX = (x: number) => (x - cfg.start.x) / rangeX // r, x, and y will be updated by the various input event handlers. r is // x (or y) normalized on its range, e.g. [x_min, x_max] -> [0, 1] @@ -221,13 +254,13 @@ class XYRangeOrderOption extends OrderOption { let x = this.x = opt.default let y = r * rangeY + cfg.start.y - const number = new Intl.NumberFormat(navigator.languages, { + const number = new Intl.NumberFormat((navigator.languages as string[]), { minimumSignificantDigits: 3, maximumSignificantDigits: 3 }) // accept needs to be called anytime a handler updates x, y, and r. - const accept = (skipStore) => { + const accept = (skipStore?: boolean) => { tmpl.x.textContent = number.format(x) tmpl.y.textContent = number.format(y) handle.style.left = `calc(${r * 100}% - ${r * 14}px)` @@ -236,7 +269,7 @@ class XYRangeOrderOption extends OrderOption { } // Set up the handlers for the x and y text input fields. - const clickOutX = e => { + const clickOutX = (e: MouseEvent) => { if (e.type !== 'change' && e.target === tmpl.xInput) return const s = tmpl.xInput.value if (s) { @@ -265,7 +298,7 @@ class XYRangeOrderOption extends OrderOption { Doc.bind(tmpl.xInput, 'change', clickOutX) - const clickOutY = e => { + const clickOutY = (e: MouseEvent) => { if (e.type !== 'change' && e.target === tmpl.yInput) return const s = tmpl.yInput.value if (s) { @@ -295,22 +328,22 @@ class XYRangeOrderOption extends OrderOption { Doc.bind(tmpl.yInput, 'change', clickOutY) // Read the slider. - Doc.bind(handle, 'mousedown', e => { + Doc.bind(handle, 'mousedown', (e: MouseEvent) => { if (e.button !== 0) return e.preventDefault() this.node.classList.add('selected') const startX = e.pageX const w = slider.clientWidth - handle.offsetWidth const startLeft = normalizeX(x) * w - const left = ee => Math.max(Math.min(startLeft + (ee.pageX - startX), w), 0) - const trackMouse = ee => { + const left = (ee: MouseEvent) => Math.max(Math.min(startLeft + (ee.pageX - startX), w), 0) + const trackMouse = (ee: MouseEvent) => { ee.preventDefault() r = left(ee) / w x = r * rangeX + cfg.start.x y = r * rangeY + cfg.start.y accept() } - const mouseUp = ee => { + const mouseUp = (ee: MouseEvent) => { trackMouse(ee) Doc.unbind(document, 'mousemove', trackMouse) Doc.unbind(document, 'mouseup', mouseUp) @@ -354,7 +387,7 @@ class XYRangeOrderOption extends OrderOption { * client/asset. change is a function with no arguments that is called when the * returned option's value has changed. */ -export function optionElement (opt, order, change, isSwap) { +export function optionElement (opt: OrderOpt, order: TradeForm, change: () => void, isSwap: boolean): HTMLElement { switch (true) { case !!opt.boolean: return new BooleanOrderOption(opt, order, change, isSwap).node @@ -363,10 +396,12 @@ export function optionElement (opt, order, change, isSwap) { default: console.error('no option type specified', opt) } + console.error('unknown option type', opt) + return document.createElement('div') } -function dexAssetSymbol (host, assetID) { +function dexAssetSymbol (host: string, assetID: number) { return app().exchanges[host].assets[assetID].symbol } -const clamp = (v, min, max) => v < min ? min : v > max ? max : v +const clamp = (v: number, min: number, max: number) => v < min ? min : v > max ? max : v diff --git a/client/webserver/site/src/js/register.js b/client/webserver/site/src/js/register.ts similarity index 84% rename from client/webserver/site/src/js/register.js rename to client/webserver/site/src/js/register.ts index f57a5bc662..8875e5b278 100644 --- a/client/webserver/site/src/js/register.js +++ b/client/webserver/site/src/js/register.ts @@ -1,4 +1,3 @@ -import { app } from './registry' import Doc from './doc' import BasePage from './basepage' import { postJSON } from './http' @@ -13,13 +12,31 @@ import { bind as bindForm } from './forms' import * as intl from './locales' +import { + app, + PasswordCache, + Exchange, + PageElement, + WalletStateNote, + BalanceNote +} from './registry' export default class RegistrationPage extends BasePage { - constructor (body) { + body: HTMLElement + pwCache: PasswordCache + currentDEX: Exchange + page: Record + loginForm: LoginForm + dexAddrForm: DEXAddressForm + newWalletForm: NewWalletForm + regAssetForm: FeeAssetSelectionForm + walletWaitForm: WalletWaitForm + confirmRegisterForm: ConfirmRegistrationForm + + constructor (body: HTMLElement) { super() this.body = body - this.pwCache = {} - this.currentDEX = null + this.pwCache = { pw: '' } const page = this.page = Doc.idDescendants(body) // Hide the form closers for the registration process. @@ -88,7 +105,7 @@ export default class RegistrationPage extends BasePage { this.animateRegAsset(page.confirmRegForm) }, this.pwCache) - const currentForm = page.forms.querySelector(':scope > form.selected') + const currentForm = Doc.safeSelector(page.forms, ':scope > form.selected') currentForm.classList.remove('selected') switch (currentForm) { case page.loginForm: @@ -102,13 +119,13 @@ export default class RegistrationPage extends BasePage { // Attempt to load the dcrwallet configuration from the default location. if (app().user.authed) this.auth() this.notifiers = { - walletstate: note => this.walletWaitForm.reportWalletState(note.wallet), - balance: note => this.walletWaitForm.reportBalance(note.balance, note.assetID) + walletstate: (note: WalletStateNote) => this.walletWaitForm.reportWalletState(note.wallet), + balance: (note: BalanceNote) => this.walletWaitForm.reportBalance(note.balance, note.assetID) } } unload () { - delete this.pwCache.pw + this.pwCache.pw = '' } // auth should be called once user is known to be authed with the server. @@ -117,21 +134,21 @@ export default class RegistrationPage extends BasePage { } /* Swap in the asset selection form and run the animation. */ - async animateRegAsset (oldForm) { + async animateRegAsset (oldForm: HTMLElement) { Doc.hide(oldForm) this.regAssetForm.animate() Doc.show(this.page.regAssetForm) } /* Swap in the confirmation form and run the animation. */ - async animateConfirmForm (oldForm) { + async animateConfirmForm (oldForm: HTMLElement) { this.confirmRegisterForm.animate() Doc.hide(oldForm) Doc.show(this.page.confirmRegForm) } // Retrieve an estimate for the tx fee needed to pay the registration fee. - async getRegistrationTxFeeEstimate (assetID, form) { + async getRegistrationTxFeeEstimate (assetID: number, form: HTMLElement) { const cert = await this.getCertFile() const loaded = app().loading(form) const res = await postJSON('/api/regtxfee', { @@ -150,7 +167,7 @@ export default class RegistrationPage extends BasePage { async setAppPass () { const page = this.page Doc.hide(page.appPWErrMsg) - const pw = page.appPW.value + const pw = page.appPW.value || '' const pwAgain = page.appPWAgain.value if (pw === '') { page.appPWErrMsg.textContent = intl.prep(intl.ID_NO_PASS_ERROR_MSG) @@ -196,7 +213,8 @@ export default class RegistrationPage extends BasePage { async getCertFile () { let cert = '' if (this.dexAddrForm.page.certFile.value) { - cert = await this.dexAddrForm.page.certFile.files[0].text() + const files = this.dexAddrForm.page.certFile.files + if (files && files.length) cert = await files[0].text() } return cert } @@ -212,9 +230,10 @@ export default class RegistrationPage extends BasePage { app().loadPage('markets') } - async newWalletCreated (assetID) { + async newWalletCreated (assetID: number) { this.regAssetForm.refresh() const user = await app().fetchUser() + if (!user) return const page = this.page const asset = user.assets[assetID] const wallet = asset.wallet diff --git a/client/webserver/site/src/js/registry.js b/client/webserver/site/src/js/registry.js deleted file mode 100644 index d9f100c7ed..0000000000 --- a/client/webserver/site/src/js/registry.js +++ /dev/null @@ -1,9 +0,0 @@ -let application - -export function registerApplication (a) { - application = a -} - -export function app () { - return application -} diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts new file mode 100644 index 0000000000..8fd005d05c --- /dev/null +++ b/client/webserver/site/src/js/registry.ts @@ -0,0 +1,504 @@ +declare global { + interface Window { + log: (...args: any) => void + enableLogger: (loggerID: string, enable: boolean) => void + recordLogger: (loggerID: string, enable: boolean) => void + dumpLogger: (loggerID: string) => void + localeDiscrepancies: () => void + } +} + +export interface Exchange { + host: string + acctID: string + markets: Record + assets: Record + connected: boolean + feeAsset: FeeAsset // DEPRECATED. DCR. + regFees: Record + pendingFee: PendingFeeState | null + candleDurs: string[] +} + +export interface Candle { + startStamp: number + endStamp: number + matchVolume: number + quoteVolume: number + highRate: number + lowRate: number + startRate: number + endRate: number +} + +export interface CandlesPayload { + dur: string + ms: number + candles: Candle[] +} + +export interface Market { + name: string + baseid: number + basesymbol: string + quoteid: number + quotesymbol: string + lotsize: number + ratestep: number + epochlen: number + startepoch: number + buybuffer: number + orders: Order[] + spot: Spot +} + +export interface Order { + host: string + baseID: number + baseSymbol: string + quoteID: number + quoteSymbol: string + market: string + type: number + id: string + stamp: number + sig: string + status: number + epoch: number + qty: number + sell: boolean + filled: number + matches: Match[] + cancelling: boolean + canceled: boolean + feesPaid: FeeBreakdown + fundingCoins: Coin[] + lockedamt: number + rate: number // limit only + tif: number // limit only + targetOrderID: string // cancel only +} + +export interface Match { + matchID: string + status: number + active: boolean + revoked: boolean + rate: number + qty: number + side: number + feeRate: number + swap: Coin + counterSwap: Coin + redeem: Coin + counterRedeem: Coin + refund: Coin + stamp: number + isCancel: boolean +} + +export interface Spot { + stamp: number + baseID: number + quoteID: number + rate: number + bookVolume: number + change24: number + vol24: number +} + +export interface Asset { + id: number + symbol: string + version: number + maxFeeRate: number + swapSize: number + swapSizeBase: number + redeemSize: number + swapConf: number + unitInfo: UnitInfo +} + +export interface FeeAsset { + id: number + confs: number + amount: number +} + +export interface PendingFeeState { + symbol: string + assetID: number + confs: number +} + +export interface FeeBreakdown { + swap: number + redemption: number +} + +export interface SupportedAsset { + id: number + symbol: string + wallet: WalletState + info: WalletInfo +} + +export interface WalletState { + symbol: string + assetID: number + version: number + type: string + traits: number + open: boolean + running: boolean + balance: WalletBalance + address: string + units: string + encrypted: boolean + peerCount: number + synced: boolean + syncProgress: number +} + +export interface WalletInfo { + name: string + version: number + availablewallets: WalletDefinition[] + emptyidx: number + unitinfo: UnitInfo +} + +export interface WalletBalance { + available: number + immature: number + locked: number + stamp: string // time.Time + orderlocked: number + contractlocked: number +} + +export interface WalletDefinition { + seeded: boolean + type: string + tab: string + description: string + configpath: string + configopts: ConfigOption[] +} + +export interface ConfigOption { + key: string + displayname: string + description: string + default: any + max: any + min: any + noecho: boolean + isboolean: boolean + isdate: boolean + disablewhenactive: boolean + isBirthdayConfig: boolean +} + +export interface Coin { + id: string + stringID: string + assetID: number + symbol: string + confs: Confirmations +} + +export interface Confirmations { + required: number + count: number +} + +export interface UnitInfo { + atomicUnit: string + conventional: Denomination + denominations: Denomination[] +} + +export interface Denomination { + unit: string + conversionFactor: number +} + +export interface User { + exchanges: Record + inited: boolean + seedgentime: number + assets: Record + authed: boolean // added by webserver + ok: boolean // added by webserver +} + +export interface CoreNote { + type: string + topic: string + subject: string + details: string + severity: number + stamp: number + acked: boolean + id: string +} + +export interface FeePaymentNote extends CoreNote { + asset: number + confirmations: number + dex: string +} + +export interface BalanceNote extends CoreNote { + assetID: number + balance: WalletBalance +} + +export interface WalletConfigNote extends CoreNote { + wallet: WalletState +} + +export type WalletStateNote = WalletConfigNote + +export interface SpotPriceNote extends CoreNote { + host: string + spots: Record +} + +export interface MatchNote extends CoreNote { + orderID: string + match: Match + host: string + marketID: string +} + +export interface ConnEventNote extends CoreNote { + host: string + connected: boolean +} + +export interface OrderNote extends CoreNote { + order: Order +} + +export interface EpochNote extends CoreNote { + host: string + marketID: string + epoch: number +} + +export interface APIResponse { + requestSuccessful: boolean + ok: boolean + msg: string + err?: string +} + +export interface LogMessage { + time: string + msg: string +} + +export interface NoteElement extends HTMLElement { + note: CoreNote +} + +export interface BalanceResponse extends APIResponse { + balance: WalletBalance +} + +export interface LayoutMetrics { + bodyTop: number + bodyLeft: number + width: number + height: number + centerX: number + centerY: number +} + +export interface PasswordCache { + pw: string +} + +export interface PageElement extends HTMLElement { + value?: string + src?: string + files?: FileList + checked?: boolean + href?: string + htmlFor?: string +} + +export interface BooleanConfig { + reason: string +} + +export interface XYRangePoint { + label: string + x: number + y: number +} + +export interface XYRange { + start: XYRangePoint + end: XYRangePoint + xUnit: string + yUnit: string +} + +export interface OrderOption extends ConfigOption { + boolean: BooleanConfig + xyRange: XYRange +} + +export interface SwapEstimate { + lots: number + value: number + maxFees: number + realisticWorstCase: number + realisticBestCase: number +} + +export interface RedeemEstimate { + realisticBestCase: number + realisticWorstCase: number +} + +export interface PreSwap { + estimate: SwapEstimate + options: OrderOption[] +} + +export interface PreRedeem { + estimate: RedeemEstimate + options: OrderOption[] +} + +export interface OrderEstimate { + swap: PreSwap + redeem: PreRedeem +} + +export interface MaxOrderEstimate { + swap: SwapEstimate + redeem: RedeemEstimate +} + +export interface MaxSell { + maxSell: MaxOrderEstimate +} + +export interface MaxBuy { + maxBuy: MaxOrderEstimate +} + +export interface TradeForm { + host: string + isLimit: boolean + sell: boolean + base: number + quote: number + qty: number + rate: number + tifnow: boolean + options: Record +} + +export interface BookUpdate { + action: string + host: string + marketID: string + payload: any +} + +export interface MiniOrder { + qty: number + qtyAtomic: number + rate: number + msgRate: number + epoch: number + sell: boolean + token: string +} + +export interface CoreOrderBook { + sells: MiniOrder[] + buys: MiniOrder[] + epoch: MiniOrder[] +} + +export interface MarketOrderBook { + base: number + quote: number + book: CoreOrderBook +} + +export interface RemainderUpdate { + token: string + qty: number + qtyAtomic: number +} + +export interface OrderFilter { + n?: number + offset?: string + hosts: string[] + assets: number[] + statuses: number[] +} + +export interface Application { + assets: Record + seedGenTime: number + user: User + header: HTMLElement + walletMap: Record + exchanges: Record + showPopups: boolean + commitHash: string + start (): Promise + reconnected (): void + fetchUser (): Promise + loadPage (page: string, data?: any, skipPush?: boolean): Promise + attach (data: any): void + bindTooltips (ancestor: HTMLElement): void + attachHeader (): void + showDropdown (icon: HTMLElement, dialog: HTMLElement): void + ackNotes (): void + setNoteTimes (noteList: HTMLElement): void + bindInternalNavigation (ancestor: HTMLElement): void + storeNotes (): void + updateMenuItemsDisplay (): void + attachCommon (node: HTMLElement): void + updateExchangeRegistration (dexAddr: string, confs: number, assetID: number): void + handleFeePaymentNote (note: FeePaymentNote): void + setNotes (notes: CoreNote[]): void + notify (note: CoreNote): void + log (loggerID: string, ...msg: any): void + prependPokeElement (note: CoreNote): void + prependNoteElement (note: CoreNote, skipSave?: boolean): void + prependListElement (noteList: HTMLElement, note: CoreNote, el: NoteElement): void + loading (el: HTMLElement): () => void + orders (host: string, mktID: string): Order[] + haveAssetOrders (assetID: number): boolean + order (oid: string): Order | null + unitInfo (assetID: number, xc?: Exchange): UnitInfo + conventionalRate (baseID: number, quoteID: number, encRate: number): number + walletDefinition (assetID: number, walletType: string): WalletDefinition + currentWalletDefinition (assetID: number): WalletDefinition + fetchBalance (assetID: number): Promise + checkResponse (resp: APIResponse, skipNote?: boolean): boolean + signOut (): Promise +} + +// TODO: Define an interface for Application? +let application: Application + +export function registerApplication (a: Application) { + application = a +} + +export function app (): Application { + return application +} diff --git a/client/webserver/site/src/js/settings.js b/client/webserver/site/src/js/settings.ts similarity index 86% rename from client/webserver/site/src/js/settings.js rename to client/webserver/site/src/js/settings.ts index 32b9af6c2a..205101357e 100644 --- a/client/webserver/site/src/js/settings.js +++ b/client/webserver/site/src/js/settings.ts @@ -1,24 +1,45 @@ -import { app } from './registry' import Doc from './doc' import BasePage from './basepage' import State from './state' import { postJSON } from './http' import * as forms from './forms' import * as intl from './locales' +import { + app, + Exchange, + PageElement, + PasswordCache, + WalletStateNote, + BalanceNote +} from './registry' const animationLength = 300 export default class SettingsPage extends BasePage { - constructor (body) { + body: HTMLElement + currentDEX: Exchange + page: Record + forms: PageElement[] + regAssetForm: forms.FeeAssetSelectionForm + confirmRegisterForm: forms.ConfirmRegistrationForm + newWalletForm: forms.NewWalletForm + walletWaitForm: forms.WalletWaitForm + dexAddrForm: forms.DEXAddressForm + currentForm: PageElement + pwCache: PasswordCache + defaultTLSText: string + keyup: (e: KeyboardEvent) => void + + constructor (body: HTMLElement) { super() this.body = body - this.currentDEX = null + this.defaultTLSText = 'none selected' const page = this.page = Doc.idDescendants(body) - this.forms = page.forms.querySelectorAll(':scope > form') + this.forms = Doc.applySelector(page.forms, ':scope > form') Doc.bind(page.darkMode, 'click', () => { - State.dark(page.darkMode.checked) + State.dark(page.darkMode.checked || false) if (page.darkMode.checked) { document.body.classList.add('dark') } else { @@ -27,7 +48,7 @@ export default class SettingsPage extends BasePage { }) Doc.bind(page.showPokes, 'click', () => { - const show = page.showPokes.checked + const show = page.showPokes.checked || false State.setCookie('popups', show ? '1' : '0') app().showPopups = show }) @@ -82,7 +103,7 @@ export default class SettingsPage extends BasePage { }, () => { this.animateRegAsset(page.walletWait) }) // Enter an address for a new DEX - this.dexAddrForm = new forms.DEXAddressForm(page.dexAddrForm, async (xc, certFile) => { + this.dexAddrForm = new forms.DEXAddressForm(page.dexAddrForm, async (xc: Exchange, certFile: string) => { this.currentDEX = xc this.confirmRegisterForm.setExchange(xc, certFile) this.walletWaitForm.setExchange(xc) @@ -124,11 +145,11 @@ export default class SettingsPage extends BasePage { page.seedDiv.textContent = '' } - Doc.bind(page.forms, 'mousedown', e => { + Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { if (!Doc.mouseInElement(e, this.currentForm)) { closePopups() } }) - this.keyup = e => { + this.keyup = (e: KeyboardEvent) => { if (e.key === 'Escape') { closePopups() } @@ -140,13 +161,13 @@ export default class SettingsPage extends BasePage { }) this.notifiers = { - walletstate: note => this.walletWaitForm.reportWalletState(note.wallet), - balance: note => this.walletWaitForm.reportBalance(note.balance, note.assetID) + walletstate: (note: WalletStateNote) => this.walletWaitForm.reportWalletState(note.wallet), + balance: (note: BalanceNote) => this.walletWaitForm.reportBalance(note.balance, note.assetID) } } // Retrieve an estimate for the tx fee needed to pay the registration fee. - async getRegistrationTxFeeEstimate (assetID, form) { + async getRegistrationTxFeeEstimate (assetID: number, form: HTMLElement) { const cert = await this.getCertFile() const loaded = app().loading(form) const res = await postJSON('/api/regtxfee', { @@ -161,8 +182,9 @@ export default class SettingsPage extends BasePage { return res.txfee } - async newWalletCreated (assetID) { + async newWalletCreated (assetID: number) { const user = await app().fetchUser() + if (!user) return const page = this.page const asset = user.assets[assetID] const wallet = asset.wallet @@ -179,7 +201,7 @@ export default class SettingsPage extends BasePage { await forms.slideSwap(page.newWalletForm, page.walletWait) } - async prepareAccountExport (host, authorizeAccountExportForm) { + async prepareAccountExport (host: string, authorizeAccountExportForm: HTMLElement) { const page = this.page page.exportAccountHost.textContent = host page.exportAccountErr.textContent = '' @@ -190,7 +212,7 @@ export default class SettingsPage extends BasePage { } } - async prepareAccountDisable (host, disableAccountForm) { + async prepareAccountDisable (host: string, disableAccountForm: HTMLElement) { const page = this.page page.disableAccountHost.textContent = host page.disableAccountErr.textContent = '' @@ -249,7 +271,7 @@ export default class SettingsPage extends BasePage { async onAccountFileChange () { const page = this.page const files = page.accountFile.files - if (!files.length) return + if (!files || !files.length) return page.selectedAccount.textContent = files[0].name Doc.show(page.removeAccount) Doc.hide(page.addAccount) @@ -264,7 +286,7 @@ export default class SettingsPage extends BasePage { Doc.show(page.addAccount) } - async prepareAccountImport (authorizeAccountImportForm) { + async prepareAccountImport (authorizeAccountImportForm: HTMLElement) { const page = this.page page.importAccountErr.textContent = '' this.showForm(authorizeAccountImportForm) @@ -277,7 +299,12 @@ export default class SettingsPage extends BasePage { page.importAccountAppPass.value = '' let accountString = '' if (page.accountFile.value) { - accountString = await page.accountFile.files[0].text() + const files = page.accountFile.files + if (!files || !files.length) { + console.error('importAccount: no file specified') + return + } + accountString = await files[0].text() } let account try { @@ -333,7 +360,7 @@ export default class SettingsPage extends BasePage { } /* showForm shows a modal form with a little animation. */ - async showForm (form) { + async showForm (form: HTMLElement) { const page = this.page this.currentForm = form this.forms.forEach(form => Doc.hide(form)) @@ -359,7 +386,8 @@ export default class SettingsPage extends BasePage { async getCertFile () { let cert = '' if (this.dexAddrForm.page.certFile.value) { - cert = await this.dexAddrForm.page.certFile.files[0].text() + const files = this.dexAddrForm.page.certFile.files + if (files && files.length) cert = await files[0].text() } return cert } @@ -430,7 +458,7 @@ export default class SettingsPage extends BasePage { } /* Swap in the asset selection form and run the animation. */ - async animateRegAsset (oldForm) { + async animateRegAsset (oldForm: HTMLElement) { Doc.hide(oldForm) const form = this.page.regAssetForm this.currentForm = form @@ -439,7 +467,7 @@ export default class SettingsPage extends BasePage { } /* Swap in the confirmation form and run the animation. */ - async animateConfirmForm (oldForm) { + async animateConfirmForm (oldForm: HTMLElement) { this.confirmRegisterForm.animate() const form = this.page.confirmRegForm this.currentForm = form diff --git a/client/webserver/site/src/js/state.js b/client/webserver/site/src/js/state.ts similarity index 92% rename from client/webserver/site/src/js/state.js rename to client/webserver/site/src/js/state.ts index 420ef09e5d..562749c954 100644 --- a/client/webserver/site/src/js/state.js +++ b/client/webserver/site/src/js/state.ts @@ -7,7 +7,7 @@ const pwKeyCK = 'sessionkey' // utilities for setting and retrieving cookies and storing user configuration // to localStorage. export default class State { - static setCookie (cname, cvalue) { + static setCookie (cname: string, cvalue: string) { const d = new Date() // Set cookie to expire in ten years. d.setTime(d.getTime() + (86400 * 365 * 10 * 1000)) @@ -18,7 +18,7 @@ export default class State { /* * getCookie returns the value at the specified cookie name, otherwise null. */ - static getCookie (cname) { + static getCookie (cname: string) { for (const cstr of document.cookie.split(';')) { const [k, v] = cstr.split('=') if (k.trim() === cname) return v @@ -27,7 +27,7 @@ export default class State { } /* dark sets the dark-mode cookie. */ - static dark (dark) { + static dark (dark: boolean) { this.setCookie(darkModeCK, dark ? '1' : '0') if (dark) { document.body.classList.add('dark') @@ -49,7 +49,7 @@ export default class State { } /* store puts the key-value pair into Window.localStorage. */ - static store (k, v) { + static store (k: string, v: any) { window.localStorage.setItem(k, JSON.stringify(v)) } @@ -66,7 +66,7 @@ export default class State { * fetch fetches the value associated with the key in Window.localStorage, or * null if the no value exists for the key. */ - static fetch (k) { + static fetch (k: string) { const v = window.localStorage.getItem(k) if (v !== null) { return JSON.parse(v) diff --git a/client/webserver/site/src/js/wallets.js b/client/webserver/site/src/js/wallets.ts similarity index 79% rename from client/webserver/site/src/js/wallets.js rename to client/webserver/site/src/js/wallets.ts index aa4d0e4fbe..2d612df4c2 100644 --- a/client/webserver/site/src/js/wallets.js +++ b/client/webserver/site/src/js/wallets.ts @@ -1,4 +1,3 @@ -import { app } from './registry' import Doc, { WalletIcons } from './doc' import BasePage from './basepage' import { postJSON } from './http' @@ -6,42 +5,99 @@ import { NewWalletForm, WalletConfigForm, UnlockWalletForm, bind as bindForm } f import * as ntfn from './notifications' import State from './state' import * as intl from './locales' +import { + app, + PageElement, + SupportedAsset, + WalletDefinition, + BalanceNote, + WalletStateNote, + Market +} from './registry' const bind = Doc.bind const animationLength = 300 const traitNewAddresser = 1 << 1 const traitLogFiler = 1 << 2 +interface Actions { + connect: HTMLElement + unlock: HTMLElement + withdraw: HTMLElement + deposit: HTMLElement + create: HTMLElement + rescan: HTMLElement + lock: HTMLElement + settings: HTMLElement +} + +interface RowInfo { + assetID: number + tr: HTMLElement + symbol: string + name: string + stateIcons: WalletIcons + actions: Actions +} + +interface ReconfigRequest { + assetID: number + walletType: string + config: Record + newWalletPW?: string + appPW: string +} + export default class WalletsPage extends BasePage { - constructor (body) { + body: HTMLElement + page: Record + rowInfos: Record + withdrawAsset: SupportedAsset + newWalletForm: NewWalletForm + reconfigForm: WalletConfigForm + unlockForm: UnlockWalletForm + lastFormAsset: number + keyup: (e: KeyboardEvent) => void + changeWalletPW: boolean + depositAsset: number + // Methods to switch the item displayed on the right side, with a little + // fade-in animation. + displayed: HTMLElement + animation: Promise + openAsset: number + walletAsset: number + reconfigAsset: number + + constructor (body: HTMLElement) { super() this.body = body const page = this.page = Doc.idDescendants(body) // Read the document, storing some info about each asset's row. - const getAction = (row, name) => row.querySelector(`[data-action=${name}]`) - const rowInfos = this.rowInfos = {} - const rows = page.walletTable.querySelectorAll('tr') + const getAction = (row: HTMLElement, name: string) => row.querySelector(`[data-action=${name}]`) as HTMLElement + const rowInfos: Record = this.rowInfos = {} + const rows = Doc.applySelector(page.walletTable, 'tr') let firstRow for (const tr of rows) { - const assetID = parseInt(tr.dataset.assetID) - const rowInfo = rowInfos[assetID] = {} - if (!firstRow) firstRow = rowInfo - rowInfo.assetID = assetID - rowInfo.tr = tr - rowInfo.symbol = tr.dataset.symbol - rowInfo.name = tr.dataset.name - rowInfo.stateIcons = new WalletIcons(tr) - rowInfo.actions = { - connect: getAction(tr, 'connect'), - unlock: getAction(tr, 'unlock'), - withdraw: getAction(tr, 'withdraw'), - deposit: getAction(tr, 'deposit'), - create: getAction(tr, 'create'), - rescan: getAction(tr, 'rescan'), - lock: getAction(tr, 'lock'), - settings: getAction(tr, 'settings') + const assetID = parseInt(tr.dataset.assetID || '') + rowInfos[assetID] = { + assetID: assetID, + tr: tr, + symbol: tr.dataset.symbol || '', + name: tr.dataset.name || '', + stateIcons: new WalletIcons(tr), + actions: { + connect: getAction(tr, 'connect'), + unlock: getAction(tr, 'unlock'), + withdraw: getAction(tr, 'withdraw'), + deposit: getAction(tr, 'deposit'), + create: getAction(tr, 'create'), + rescan: getAction(tr, 'rescan'), + lock: getAction(tr, 'lock'), + settings: getAction(tr, 'settings') + } } + if (!firstRow) firstRow = rowInfos[assetID] } // Prepare templates @@ -50,16 +106,6 @@ export default class WalletsPage extends BasePage { page.oneMarket.removeAttribute('id') page.oneMarket.remove() - // Methods to switch the item displayed on the right side, with a little - // fade-in animation. - this.displayed = null // The currently displayed right-side element. - this.animation = null // Store Promise of currently running animation. - - this.openAsset = null - this.walletAsset = null - this.reconfigAsset = null - this.withdrawAsset = null - // Bind the new wallet form. this.newWalletForm = new NewWalletForm(page.newWalletForm, () => { this.createWalletSuccess() }) @@ -88,7 +134,7 @@ export default class WalletsPage extends BasePage { }) }) - this.keyup = e => { + this.keyup = (e: KeyboardEvent) => { if (e.key === 'Escape') { this.showMarkets(this.lastFormAsset) } @@ -101,7 +147,7 @@ export default class WalletsPage extends BasePage { for (const [k, asset] of Object.entries(rowInfos)) { const assetID = parseInt(k) // keys are string asset ID. const a = asset.actions - const run = (e, f) => { + const run = (e: Event, f: (assetID: number, asset: RowInfo) => void) => { e.stopPropagation() f(assetID, asset) } @@ -122,7 +168,7 @@ export default class WalletsPage extends BasePage { // amount field. bind(page.withdrawAvail, 'click', () => { const asset = this.withdrawAsset - page.withdrawAmt.value = asset.wallet.balance.available / asset.info.unitinfo.conventional.conversionFactor + page.withdrawAmt.value = String(asset.wallet.balance.available / asset.info.unitinfo.conventional.conversionFactor) }) // A link on the wallet reconfiguration form to show/hide the password field. @@ -147,16 +193,16 @@ export default class WalletsPage extends BasePage { this.showMarkets(firstRow.assetID) this.notifiers = { - balance: note => { this.handleBalanceNote(note) }, - walletstate: note => { this.handleWalletStateNote(note) }, - walletconfig: note => { this.handleWalletStateNote(note) } + balance: (note: BalanceNote) => { this.handleBalanceNote(note) }, + walletstate: (note: WalletStateNote) => { this.handleWalletStateNote(note) }, + walletconfig: (note: WalletStateNote) => { this.handleWalletStateNote(note) } } } /* * setPWSettingViz sets the visibility of the password field section. */ - setPWSettingViz (visible) { + setPWSettingViz (visible: boolean) { if (visible) { Doc.hide(this.page.showIcon) Doc.show(this.page.hideIcon, this.page.changePW) @@ -181,7 +227,7 @@ export default class WalletsPage extends BasePage { /* * showBox shows the box with a fade-in animation. */ - async showBox (box, focuser) { + async showBox (box: HTMLElement, focuser?: PageElement) { box.style.opacity = '0' Doc.show(box) if (focuser) focuser.focus() @@ -196,7 +242,7 @@ export default class WalletsPage extends BasePage { * Show the markets box, which lists the markets available for a selected * asset. */ - async showMarkets (assetID) { + async showMarkets (assetID: number) { const page = this.page const box = page.marketsBox const card = page.marketsCard @@ -211,18 +257,18 @@ export default class WalletsPage extends BasePage { if (market.baseid === assetID || market.quoteid === assetID) count++ } if (count === 0) continue - const marketBox = page.marketCard.cloneNode(true) + const marketBox = page.marketCard.cloneNode(true) as HTMLElement const tmpl = Doc.parseTemplate(marketBox) tmpl.dexTitle.textContent = host card.appendChild(marketBox) for (const market of Object.values(xc.markets)) { // Only show markets where this is the base or quote asset. if (market.baseid !== assetID && market.quoteid !== assetID) continue - const mBox = page.oneMarket.cloneNode(true) - mBox.querySelector('span').textContent = prettyMarketName(market) + const mBox = page.oneMarket.cloneNode(true) as HTMLElement + Doc.safeSelector(mBox, 'span').textContent = prettyMarketName(market) let counterSymbol = market.basesymbol if (market.baseid === assetID) counterSymbol = market.quotesymbol - mBox.querySelector('img').src = Doc.logoPath(counterSymbol) + Doc.safeSelector(mBox, 'img').src = Doc.logoPath(counterSymbol) // Bind the click to a load of the markets page. const pageData = { host: host, base: market.baseid, quote: market.quoteid } bind(mBox, 'click', () => { app().loadPage('markets', pageData) }) @@ -233,7 +279,7 @@ export default class WalletsPage extends BasePage { } /* Show the new wallet form. */ - async showNewWallet (assetID) { + async showNewWallet (assetID: number) { const page = this.page const box = page.newWalletForm await this.hideBox() @@ -243,7 +289,7 @@ export default class WalletsPage extends BasePage { await this.newWalletForm.loadDefaults() } - async rescanWallet (assetID) { + async rescanWallet (assetID: number) { const loaded = app().loading(this.body) const res = await postJSON('/api/rescanwallet', { assetID: assetID, @@ -256,13 +302,13 @@ export default class WalletsPage extends BasePage { /* Show the open wallet form if the password is not cached, and otherwise * attempt to open the wallet. */ - async openWallet (assetID) { + async openWallet (assetID: number) { if (!State.passwordIsCached()) { this.showOpen(assetID) } else { this.openAsset = assetID const open = { - assetID: parseInt(assetID) + assetID: assetID } const res = await postJSON('/api/openwallet', open) if (app().checkResponse(res)) { @@ -274,7 +320,7 @@ export default class WalletsPage extends BasePage { } /* Show the form used to unlock a wallet. */ - async showOpen (assetID, errorMsg) { + async showOpen (assetID: number, errorMsg?: string) { const page = this.page this.openAsset = this.lastFormAsset = assetID await this.hideBox() @@ -284,7 +330,7 @@ export default class WalletsPage extends BasePage { } /* Show the form used to change wallet configuration settings. */ - async showReconfig (assetID) { + async showReconfig (assetID: number) { const page = this.page Doc.hide(page.changeWalletType, page.changeTypeHideIcon, page.reconfigErr, page.showChangeType, page.changeTypeHideIcon) Doc.hide(page.reconfigErr) @@ -301,8 +347,8 @@ export default class WalletsPage extends BasePage { Doc.show(page.showChangeType, page.changeTypeShowIcon) page.changeTypeMsg.textContent = intl.prep(intl.ID_CHANGE_WALLET_TYPE) for (const wDef of asset.info.availablewallets) { - const option = document.createElement('option') - if (wDef.type === currentDef.type) option.selected = '1' + const option = document.createElement('option') as HTMLOptionElement + if (wDef.type === currentDef.type) option.selected = true option.value = option.textContent = wDef.type page.changeWalletTypeSelect.appendChild(option) } @@ -336,13 +382,13 @@ export default class WalletsPage extends BasePage { changeWalletType () { const page = this.page - const walletType = page.changeWalletTypeSelect.value + const walletType = page.changeWalletTypeSelect.value || '' const walletDef = app().walletDefinition(this.reconfigAsset, walletType) this.reconfigForm.update(walletDef.configopts || []) this.updateDisplayedReconfigFields(walletDef) } - updateDisplayedReconfigFields (walletDef) { + updateDisplayedReconfigFields (walletDef: WalletDefinition) { if (walletDef.seeded) { Doc.hide(this.page.showChangePW) this.changeWalletPW = false @@ -351,7 +397,7 @@ export default class WalletsPage extends BasePage { } /* Display a deposit address. */ - async showDeposit (assetID) { + async showDeposit (assetID: number) { const page = this.page Doc.hide(page.depositErr) const box = page.deposit @@ -391,10 +437,11 @@ export default class WalletsPage extends BasePage { } /* Show the form to withdraw funds. */ - async showWithdraw (assetID) { + async showWithdraw (assetID: number) { const page = this.page const box = page.withdrawForm const asset = this.withdrawAsset = app().assets[assetID] + this.lastFormAsset = assetID const wallet = app().walletMap[assetID] if (!wallet) { app().notify(ntfn.make('Cannot withdraw.', `No wallet found for ${asset.info.name}`, ntfn.ERROR)) @@ -409,12 +456,12 @@ export default class WalletsPage extends BasePage { page.withdrawName.textContent = asset.info.name // page.withdrawFee.textContent = wallet.feerate // page.withdrawUnit.textContent = wallet.units - box.dataset.assetID = this.lastFormAsset = assetID + box.dataset.assetID = String(assetID) this.animation = this.showBox(box, page.walletPass) } /* doConnect connects to a wallet via the connectwallet API route. */ - async doConnect (assetID) { + async doConnect (assetID: number) { const loaded = app().loading(this.body) const res = await postJSON('/api/connectwallet', { assetID: assetID @@ -454,12 +501,12 @@ export default class WalletsPage extends BasePage { async withdraw () { const page = this.page Doc.hide(page.withdrawErr) - const assetID = parseInt(page.withdrawForm.dataset.assetID) + const assetID = parseInt(page.withdrawForm.dataset.assetID || '') const conversionFactor = app().unitInfo(assetID).conventional.conversionFactor const open = { assetID: assetID, address: page.withdrawAddr.value, - value: parseInt(Math.round(page.withdrawAmt.value * conversionFactor)), + value: Math.round(parseFloat(page.withdrawAmt.value || '') * conversionFactor), pw: page.withdrawPW.value } const loaded = app().loading(page.withdrawForm) @@ -485,14 +532,14 @@ export default class WalletsPage extends BasePage { let walletType = app().currentWalletDefinition(this.reconfigAsset).type if (!Doc.isHidden(page.changeWalletType)) { - walletType = page.changeWalletTypeSelect.value + walletType = page.changeWalletTypeSelect.value || '' } const loaded = app().loading(page.reconfigForm) - const req = { + const req: ReconfigRequest = { assetID: this.reconfigAsset, config: this.reconfigForm.map(), - appPW: page.appPW.value, + appPW: page.appPW.value || '', walletType: walletType } if (this.changeWalletPW) req.newWalletPW = page.newPW.value @@ -509,7 +556,7 @@ export default class WalletsPage extends BasePage { } /* lock instructs the API to lock the wallet. */ - async lock (assetID, asset) { + async lock (assetID: number, asset: RowInfo) { const page = this.page const loaded = app().loading(page.newWalletForm) const res = await postJSON('/api/closewallet', { assetID: assetID }) @@ -523,15 +570,15 @@ export default class WalletsPage extends BasePage { async downloadLogs () { const search = new URLSearchParams('') search.append('assetid', `${this.reconfigAsset}`) - const url = new URL(window.location) + const url = new URL(window.location.href) url.search = search.toString() url.pathname = '/wallets/logfile' window.open(url.toString()) } /* handleBalance handles notifications updating a wallet's balance. */ - handleBalanceNote (note) { - const td = this.page.walletTable.querySelector(`[data-balance-target="${note.assetID}"]`) + handleBalanceNote (note: BalanceNote) { + const td = Doc.safeSelector(this.page.walletTable, `[data-balance-target="${note.assetID}"]`) td.textContent = Doc.formatFullPrecision(note.balance.available, app().unitInfo(note.assetID)) } @@ -539,7 +586,7 @@ export default class WalletsPage extends BasePage { * handleWalletStateNote is a handler for both the 'walletstate' and * 'walletconfig' notifications. */ - handleWalletStateNote (note) { + handleWalletStateNote (note: WalletStateNote) { this.rowInfos[note.wallet.assetID].stateIcons.readWallet(note.wallet) } @@ -557,6 +604,6 @@ export default class WalletsPage extends BasePage { * create a string ABC-XYZ, where ABC and XYZ are the upper-case ticker symbols * for the base and quote assets respectively. */ -function prettyMarketName (market) { +function prettyMarketName (market: Market) { return `${market.basesymbol.toUpperCase()}-${market.quotesymbol.toUpperCase()}` } diff --git a/client/webserver/site/src/js/ws.js b/client/webserver/site/src/js/ws.ts similarity index 80% rename from client/webserver/site/src/js/ws.js rename to client/webserver/site/src/js/ws.ts index 44a22c46ef..467aeca97f 100644 --- a/client/webserver/site/src/js/ws.js +++ b/client/webserver/site/src/js/ws.ts @@ -19,10 +19,9 @@ // // Based on messagesocket_service.js by Jonathan Chappelow @ dcrdata, which is // based on ws_events_dispatcher.js by Ismael Celis - const typeRequest = 1 -function forward (route, payload, handlers) { +function forward (route: string, payload: any, handlers: Record void)[]>) { if (!route && payload.error) { const err = payload.error console.error(`websocket error (code ${err.code}): ${err.message}`) @@ -40,26 +39,33 @@ function forward (route, payload, handlers) { let id = 0 +type NoteReceiver = (payload: any) => void + class MessageSocket { + uri: string + connection: WebSocket | null + handlers: Record + queue: [string, any][] + maxQlength: number + reloader: () => void // appears unused + constructor () { - this.uri = undefined - this.connection = undefined this.handlers = {} this.queue = [] this.maxQlength = 5 } - registerRoute (route, handler) { + registerRoute (route: string, handler: NoteReceiver) { this.handlers[route] = this.handlers[route] || [] this.handlers[route].push(handler) } - deregisterRoute (route) { + deregisterRoute (route: string) { this.handlers[route] = [] } // request sends a request-type message to the server - request (route, payload) { + request (route: string, payload: any) { if (!this.connection || this.connection.readyState !== window.WebSocket.OPEN) { while (this.queue.length > this.maxQlength - 1) this.queue.shift() this.queue.push([route, payload]) @@ -77,32 +83,33 @@ class MessageSocket { this.connection.send(message) } - close (reason) { + close (reason: string) { window.log('ws', 'close, reason:', reason, this.handlers) this.handlers = {} - this.connection.close() + if (this.connection) this.connection.close() } - connect (uri, reloader) { + connect (uri: string, reloader: () => void) { this.uri = uri this.reloader = reloader let retrys = 0 const go = () => { window.log('ws', `connecting to ${uri}`) - let conn = this.connection = new window.WebSocket(uri) + let conn: WebSocket | null = this.connection = new window.WebSocket(uri) + if (!conn) return const timeout = setTimeout(() => { // readyState is still WebSocket.CONNECTING. Cancel and trigger onclose. - conn.close() + if (conn) conn.close() }, 500) // unmarshal message, and forward the message to registered handlers - conn.onmessage = (evt) => { + conn.onmessage = (evt: MessageEvent) => { const message = JSON.parse(evt.data) forward(message.route, message.payload, this.handlers) } // Stub out standard functions - conn.onclose = (evt) => { + conn.onclose = (evt: CloseEvent) => { window.log('ws', 'onclose') clearTimeout(timeout) conn = this.connection = null @@ -131,7 +138,7 @@ class MessageSocket { } } - conn.onerror = (evt) => { + conn.onerror = (evt: Event) => { window.log('ws', 'onerror:', evt) forward('error', evt, this.handlers) } diff --git a/client/webserver/site/tsconfig.json b/client/webserver/site/tsconfig.json new file mode 100644 index 0000000000..4a5c12f243 --- /dev/null +++ b/client/webserver/site/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "outDir": "./dist/", + "noImplicitAny": true, + "module": "es6", + "target": "es6", + "moduleResolution": "node", + "esModuleInterop": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strictNullChecks": true + } +} diff --git a/client/webserver/site/webpack/common.js b/client/webserver/site/webpack/common.js index caaed87988..a73ce0f585 100644 --- a/client/webserver/site/webpack/common.js +++ b/client/webserver/site/webpack/common.js @@ -3,6 +3,7 @@ const webpack = require('webpack') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const StyleLintPlugin = require('stylelint-webpack-plugin') +const ESLintPlugin = require('eslint-webpack-plugin') const child_process = require('child_process') function git(command) { @@ -46,6 +47,10 @@ module.exports = { }), new StyleLintPlugin({ threads: true, + }), + new ESLintPlugin({ + extensions: ['ts'], + formatter: 'stylish' }) ], output: { @@ -53,6 +58,9 @@ module.exports = { path: path.resolve(__dirname, '../dist'), publicPath: '/dist/' }, + resolve: { + extensions: ['.ts'], + }, // Fixes weird issue with watch script. See // https://github.com/webpack/webpack/issues/2297#issuecomment-289291324 watchOptions: { diff --git a/client/webserver/site/webpack/dev.js b/client/webserver/site/webpack/dev.js index 8733148c69..dbfa29db19 100644 --- a/client/webserver/site/webpack/dev.js +++ b/client/webserver/site/webpack/dev.js @@ -1,11 +1,14 @@ const { merge } = require('webpack-merge') const common = require('./common.js') -const ESLintPlugin = require('eslint-webpack-plugin') module.exports = merge(common, { mode: 'development', - plugins: [new ESLintPlugin({ - formatter: 'stylish' - })], + module: { + rules: [{ + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + }] + }, devtool: 'inline-source-map' }) diff --git a/client/webserver/site/webpack/prod.js b/client/webserver/site/webpack/prod.js index 2f277f6671..e64ddd5137 100644 --- a/client/webserver/site/webpack/prod.js +++ b/client/webserver/site/webpack/prod.js @@ -1,14 +1,10 @@ const { merge } = require('webpack-merge') const common = require('./common.js') const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') -const ESLintPlugin = require('eslint-webpack-plugin') module.exports = merge(common, { mode: 'production', devtool: 'source-map', - plugins: [new ESLintPlugin({ - formatter: 'stylish' - })], optimization: { usedExports: true, minimize: true, @@ -20,16 +16,18 @@ module.exports = merge(common, { module: { rules: [ { - test: /\.js$/, + test: /\.ts$/, exclude: /node_modules/, use: { + // babel-loader does not fail on type errors. ts-loader does, but we + // probably still want to transpile (right?). loader: 'babel-loader', options: { presets: [ [ - "@babel/preset-env", + "@babel/preset-typescript", { - "exclude": ["@babel/plugin-transform-regenerator"] + "exclude": ["@babel/plugin-transform-typescript"] } ] ]