diff --git a/.eslintrc.json b/.eslintrc.json index 91bad378..4636c068 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,12 +5,14 @@ "jest/globals": true }, "parser": "babel-eslint", - "extends": ["airbnb"], + "extends": [ + "airbnb", + "plugin:jsdoc/recommended" + ], "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, - "parserOptions": { "ecmaFeatures": { "jsx": true @@ -18,7 +20,11 @@ "ecmaVersion": 2018, "sourceType": "module" }, - "plugins": ["react", "jest"], + "plugins": [ + "react", + "jsdoc", + "jest" + ], "rules": { "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], "no-underscore-dangle":"off", diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..486a2325 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.zip filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index a0ab4d28..68355866 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ node_modules/ docs/ build/ -dist/ +dist/linux-unpacked/ +dist/builder-effective-config.yaml +dist/zero_one-app_1.0.0_amd64.deb +dist/config.json +dist/contracts/ +dist/wallets/ +dist/data/ +dist/win-unpacked/ coverage/ +src/data/ diff --git a/dist/linux.zip b/dist/linux.zip new file mode 100644 index 00000000..a4a7bbd5 --- /dev/null +++ b/dist/linux.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d30c428aa3bedd28e1d6247f2acd8814592f51f31dc94842d15bc2086debaca +size 109840162 diff --git a/dist/mac.zip b/dist/mac.zip new file mode 100644 index 00000000..3e1445a5 --- /dev/null +++ b/dist/mac.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3bed43c90a1892552c5f0107ea51b19e4464fba90aadb8042cb209299191fd1 +size 107905159 diff --git a/dist/win.zip b/dist/win.zip new file mode 100644 index 00000000..2628c3ca --- /dev/null +++ b/dist/win.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07fa61020ed1a61fe721045a684d81c5a07c3f0707f2e836be14904245022562 +size 119072240 diff --git a/package-lock.json b/package-lock.json index 5b262551..1840793e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,35 +14,146 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, "requires": { "@babel/highlight": "^7.0.0" } }, "@babel/core": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.2.tgz", - "integrity": "sha512-eeD7VEZKfhK1KUXGiyPFettgF3m513f8FoBSWiQ1xTvl1RAopLs42Wp9+Ze911I6H0N9lNqJMDgoZT7gHsipeQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.2", - "@babel/helpers": "^7.7.0", - "@babel/parser": "^7.7.2", - "@babel/template": "^7.7.0", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.7.2", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", + "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.7", + "@babel/helpers": "^7.8.4", + "@babel/parser": "^7.8.7", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", "json5": "^2.1.0", "lodash": "^4.17.13", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.8.tgz", + "integrity": "sha512-HKyUVu69cZoclptr8t8U5b6sx6zoWjh8jiUhnuj3MpZuKT2dJ8zPTuiy31luq32swhI0SpwItCIlU8XW7BZeJg==", + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helpers": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", + "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.8.tgz", + "integrity": "sha512-mO5GWzBPsPf6865iIbzNE0AvkKF3NE+2S3eRUpE+FE07BOAkXh6G+GW/Pj01hhXjve1WScbaIO4UlY1JKeqCcA==" + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/generator": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.2.tgz", "integrity": "sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ==", + "dev": true, "requires": { "@babel/types": "^7.7.2", "jsesc": "^2.5.1", @@ -138,6 +249,7 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz", "integrity": "sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q==", + "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.7.0", "@babel/template": "^7.7.0", @@ -148,6 +260,7 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz", "integrity": "sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw==", + "dev": true, "requires": { "@babel/types": "^7.7.0" } @@ -255,6 +368,7 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz", "integrity": "sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA==", + "dev": true, "requires": { "@babel/types": "^7.7.0" } @@ -275,6 +389,7 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.0.tgz", "integrity": "sha512-VnNwL4YOhbejHb7x/b5F39Zdg5vIQpUUNzJwx0ww1EcVRt41bbGRZWhAURrfY32T5zTT3qwNOQFWpn+P0i0a2g==", + "dev": true, "requires": { "@babel/template": "^7.7.0", "@babel/traverse": "^7.7.0", @@ -285,6 +400,7 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -294,7 +410,8 @@ "@babel/parser": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.3.tgz", - "integrity": "sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A==" + "integrity": "sha512-bqv+iCo9i+uLVbI0ILzKkvMorqxouI+GbV13ivcARXn9NNEabi2IEz912IgNpT/60BNXac5dgcfjb94NjsF33A==", + "dev": true }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.7.0", @@ -978,6 +1095,7 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.0.tgz", "integrity": "sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ==", + "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", @@ -988,6 +1106,7 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.2.tgz", "integrity": "sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw==", + "dev": true, "requires": { "@babel/code-frame": "^7.5.5", "@babel/generator": "^7.7.2", @@ -1156,9 +1275,9 @@ "dev": true }, "@hapi/hoek": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.0.tgz", - "integrity": "sha512-7XYT10CZfPsH7j9F1Jmg1+d0ezOux2oM2GfArAzLwWe4mE2Dr3hVjsAL6+TFY49RRJlCdJDMw3nJsLFroTc8Kw==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", "dev": true }, "@hapi/joi": { @@ -2164,6 +2283,14 @@ "@types/babel-types": "*" } }, + "@types/bignumber.js": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/bignumber.js/-/bignumber.js-5.0.0.tgz", + "integrity": "sha512-0DH7aPGCClywOFaxxjE6UwpN2kQYe9LwuDQMv+zYA97j5GkOMo8e66LYT+a8JYU7jfmUFRZLa9KycxHDsKXJCA==", + "requires": { + "bignumber.js": "*" + } + }, "@types/bn.js": { "version": "4.11.5", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.5.tgz", @@ -2457,6 +2584,54 @@ } } }, + "@web3-js/scrypt-shim": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@web3-js/scrypt-shim/-/scrypt-shim-0.1.0.tgz", + "integrity": "sha512-ZtZeWCc/s0nMcdx/+rZwY1EcuRdemOK9ag21ty9UsHkFxsNb/AaoucUz0iPuyGe0Ku+PFuRmWZG7Z7462p9xPw==", + "requires": { + "scryptsy": "^2.1.0", + "semver": "^6.3.0" + }, + "dependencies": { + "scryptsy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz", + "integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "@web3-js/websocket": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/@web3-js/websocket/-/websocket-1.0.30.tgz", + "integrity": "sha512-fDwrD47MiDrzcJdSeTLF75aCcxVVt8B1N74rA+vh2XCAvFy4tEWJjtnUtj2QG7/zlQ6g9cQ88bZFBxwd9/FmtA==", + "requires": { + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "nan": "^2.14.0", + "typedarray-to-buffer": "^3.1.5", + "yaeti": "^0.0.6" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -2637,6 +2812,14 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "abstract-leveldown": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz", + "integrity": "sha512-2++wDf/DYqkPR3o5tbfdhF96EfMApo1GpPfzOsR/ZYXdkSmELlvOOEAl9iKkRsktMPHdGjO4rtkBpf2I7TiTeA==", + "requires": { + "xtend": "~4.0.0" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -2647,9 +2830,9 @@ } }, "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" }, "acorn-globals": { "version": "3.1.0", @@ -2900,6 +3083,11 @@ } } }, + "app-module-path": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz", + "integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU=" + }, "app-root-dir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", @@ -3169,6 +3357,14 @@ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" }, + "async-eventemitter": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz", + "integrity": "sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==", + "requires": { + "async": "^2.4.0" + } + }, "async-exit-hook": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", @@ -3376,6 +3572,17 @@ "loader-utils": "^1.0.2", "mkdirp": "^0.5.1", "pify": "^4.0.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } } }, "babel-plugin-add-react-displayname": { @@ -4067,6 +4274,11 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", @@ -4322,6 +4534,11 @@ "resolved": "https://registry.npmjs.org/browser-solc/-/browser-solc-1.0.0.tgz", "integrity": "sha512-0qiaQc65DqOukrNCew7Olg3EUXWOh54h7nUBeYyebhxwl4n5fXT/D5M3AllgKJ1msVOV9L5XIp2uAZrGSRF9iw==" }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==" + }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -4365,15 +4582,6 @@ "randombytes": "^2.0.1" } }, - "browserify-sha3": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/browserify-sha3/-/browserify-sha3-0.0.4.tgz", - "integrity": "sha1-CGxHuMgjFsnUcCLCYYWVRXbdjiY=", - "requires": { - "js-sha3": "^0.6.1", - "safe-buffer": "^5.1.1" - } - }, "browserify-sign": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", @@ -4549,6 +4757,16 @@ "ssri": "^6.0.1", "unique-filename": "^1.1.1", "y18n": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "cache-base": { @@ -4747,6 +4965,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "checkpoint-store": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/checkpoint-store/-/checkpoint-store-1.1.0.tgz", + "integrity": "sha1-BOTLUWuRQziTWB5tRgGnjpVS6gY=", + "requires": { + "functional-red-black-tree": "^1.0.1" + } + }, "cheerio": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", @@ -4850,8 +5076,7 @@ "classnames": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "dev": true + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" }, "clean-css": { "version": "4.2.1", @@ -4985,6 +5210,11 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, "clone-deep": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", @@ -5112,6 +5342,11 @@ "integrity": "sha512-Jrx3xsP4pPv4AwJUDWY9wOXGtwPXARej6Xd99h4TUGotmf8APuquKMpK+dnD3UgyxK7OEWaisjZz+3b5jtL6xQ==", "dev": true }, + "command-exists": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.8.tgz", + "integrity": "sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw==" + }, "commander": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", @@ -5120,6 +5355,12 @@ "graceful-readlink": ">= 1.0.0" } }, + "comment-parser": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.7.0.tgz", + "integrity": "sha512-m0SGP0RFO4P3hIBlIor4sBFPe5Y4HUeGgo/UZK/1Zdea5eUiqxroQ3lFqBDDSfWo9z9Q6LLnt2u0JqwacVEd/A==", + "dev": true + }, "common-tags": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", @@ -5386,6 +5627,16 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "copy-descriptor": { @@ -5397,15 +5648,14 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz", "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==", - "dev": true, "requires": { "toggle-selection": "^1.0.6" } }, "copy-webpack-plugin": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.0.5.tgz", - "integrity": "sha512-7N68eIoQTyudAuxkfPT7HzGoQ+TsmArN/I3HFwG+lVE3FNzqvZKIiaxtYh4o3BIznioxUvx9j26+Rtsc9htQUQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz", + "integrity": "sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==", "dev": true, "requires": { "cacache": "^12.0.3", @@ -5418,7 +5668,7 @@ "normalize-path": "^3.0.0", "p-limit": "^2.2.1", "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.0", + "serialize-javascript": "^2.1.2", "webpack-log": "^2.0.0" }, "dependencies": { @@ -5443,9 +5693,9 @@ "dev": true }, "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -5463,12 +5713,6 @@ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true }, - "serialize-javascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.0.tgz", - "integrity": "sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==", - "dev": true - }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -5676,6 +5920,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -5852,6 +6101,67 @@ "type": "^1.0.1" } }, + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + }, + "d3-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz", + "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==" + }, + "d3-format": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.2.tgz", + "integrity": "sha512-gco1Ih54PgMsyIXgttLxEhNy/mXxq8+rLnCb5shQk+P5TsiySrwWU5gpB4zen626J4LIwBxHvDChyA8qDm57ww==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-scale": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.1.tgz", + "integrity": "sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA==", + "requires": { + "d3-array": "1.2.0 - 2", + "d3-format": "1", + "d3-interpolate": "^1.2.0", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.2.tgz", + "integrity": "sha512-pweL2Ri2wqMY+wlW/wpkl8T3CUzKAha8S9nmiQlMABab8r5MJN0PD1V4YyRNVaKQfeh4Z0+VO70TLw6ESVOYzw==", + "requires": { + "d3-time": "1" + } + }, "damerau-levenshtein": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", @@ -5930,6 +6240,11 @@ } } }, + "decimal.js-light": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.0.tgz", + "integrity": "sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg==" + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -6081,6 +6396,14 @@ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.0.tgz", "integrity": "sha512-WE2sZoctWm/v4smfCAdjYbrfS55JiMRdlY9ZubFhsYbteCK9+BvAx4YV7nPjYM6ZnX5BcoVKwfmyx9sIFTgQMQ==" }, + "deferred-leveldown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-1.2.2.tgz", + "integrity": "sha512-uukrWD2bguRtXilKt6cAWKyoXrTSMo5m7crUdLfWQmu8kIm88w3QZoUL+6nhpfKVmhHANER6Re3sKoNoZ3IKMA==", + "requires": { + "abstract-leveldown": "~2.6.0" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -6126,6 +6449,11 @@ } } }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, "del": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", @@ -6242,6 +6570,11 @@ } } }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, "diff-match-patch": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.4.tgz", @@ -6340,6 +6673,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -6440,6 +6781,14 @@ "dotenv-defaults": "^1.0.2" } }, + "dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "requires": { + "minimatch": "^3.0.4" + } + }, "drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", @@ -6844,7 +7193,6 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "dev": true, "requires": { "iconv-lite": "~0.4.13" } @@ -7006,6 +7354,7 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz", "integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==", + "dev": true, "requires": { "es-to-primitive": "^1.2.0", "function-bind": "^1.1.1", @@ -7030,12 +7379,12 @@ } }, "es5-ext": { - "version": "0.10.52", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.52.tgz", - "integrity": "sha512-bWCbE9fbpYQY4CU6hJbJ1vSz70EClMlDgJ7BmwI+zEJhxrwjesZRPglGJlsZhu0334U3hI+gaspwksH9IGD6ag==", + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "requires": { "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.2", + "es6-symbol": "~3.1.3", "next-tick": "~1.0.0" } }, @@ -7257,6 +7606,15 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -7551,6 +7909,30 @@ "@typescript-eslint/experimental-utils": "^2.5.0" } }, + "eslint-plugin-jsdoc": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-18.1.3.tgz", + "integrity": "sha512-QmdxsDzGG9eb20VD3V8hw4huehTnU6x1/we9tN56p8MOkJNEiHDFW59stcS2/sk+BSnsJy61qZ+6wCPya8B45w==", + "dev": true, + "requires": { + "comment-parser": "^0.7.0", + "debug": "^4.1.1", + "jsdoctypeparser": "^6.0.0", + "lodash": "^4.17.15", + "object.entries-ponyfill": "^1.0.1", + "regextras": "^0.6.1", + "semver": "^6.3.0", + "spdx-expression-parse": "^3.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "eslint-plugin-jsx-a11y": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", @@ -7637,9 +8019,9 @@ }, "dependencies": { "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true } } @@ -7698,13 +8080,12 @@ } }, "eth-lib": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.27.tgz", - "integrity": "sha512-B8czsfkJYzn2UIEMwjc7Mbj+Cy72V+/OXH/tb44LV8jhrjizQJJ325xMOMyk3+ETa6r6oi0jsUY14+om8mQMWA==", + "version": "0.1.29", + "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.29.tgz", + "integrity": "sha512-bfttrr3/7gG4E02HoWTDUcDDslN003OlOoBxk9virpAZQ1ja/jDgwkWB8QfJF7ojuEowrqy+lzp9VcJG7/k5bQ==", "requires": { "bn.js": "^4.11.6", "elliptic": "^6.4.0", - "keccakjs": "^0.2.1", "nano-json-stream-parser": "^0.1.2", "servify": "^0.1.12", "ws": "^3.0.0", @@ -7717,23 +8098,113 @@ "integrity": "sha512-dE9CGNzgOOsdh7msZirvv8qjHtnHpvBlKe2647kM8v+yeF71IRso55jpojemvHV+jMjr48irPWxMRaHuOWzAFA==", "requires": { "js-sha3": "^0.8.0" + } + }, + "ethereum-common": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.2.0.tgz", + "integrity": "sha512-XOnAR/3rntJgbCdGhqdaLIxDLWKLmsZOGhHdBKadEr6gEnJLH52k93Ou+TUdFaPN3hJc3isBZBal3U/XZ15abA==" + }, + "ethereumjs-account": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/ethereumjs-account/-/ethereumjs-account-2.0.5.tgz", + "integrity": "sha512-bgDojnXGjhMwo6eXQC0bY6UK2liSFUSMwwylOmQvZbSl/D7NXQ3+vrGO46ZeOgjGfxXmgIeVNDIiHw7fNZM4VA==", + "requires": { + "ethereumjs-util": "^5.0.0", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1" }, "dependencies": { - "js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "keccak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", + "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "requires": { + "bindings": "^1.2.1", + "inherits": "^2.0.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" + } } } }, - "ethereumjs-common": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.4.0.tgz", - "integrity": "sha512-ser2SAplX/YI5W2AnzU8wmSjKRy4KQd4uxInJ36BzjS3m18E/B9QedPUIresZN1CSEQb/RgNQ2gN7C/XbpTafA==" - }, - "ethereumjs-tx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-2.1.1.tgz", + "ethereumjs-block": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-1.7.1.tgz", + "integrity": "sha512-B+sSdtqm78fmKkBq78/QLKJbu/4Ts4P2KFISdgcuZUPDm9x+N7qgBPIIFUGbaakQh8bzuquiRVbdmvPKqbILRg==", + "requires": { + "async": "^2.0.1", + "ethereum-common": "0.2.0", + "ethereumjs-tx": "^1.2.2", + "ethereumjs-util": "^5.0.0", + "merkle-patricia-tree": "^2.1.2" + }, + "dependencies": { + "ethereumjs-tx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", + "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", + "requires": { + "ethereum-common": "^0.0.18", + "ethereumjs-util": "^5.0.0" + }, + "dependencies": { + "ethereum-common": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + } + } + }, + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "keccak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", + "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "requires": { + "bindings": "^1.2.1", + "inherits": "^2.0.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" + } + } + } + }, + "ethereumjs-common": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.0.tgz", + "integrity": "sha512-SZOjgK1356hIY7MRj3/ma5qtfr/4B5BL+G4rP/XSMYr2z1H5el4RX5GReYCKmQmYI/nSBmRnwrZ17IfHuG0viQ==" + }, + "ethereumjs-tx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-2.1.1.tgz", "integrity": "sha512-QtVriNqowCFA19X9BCRPMgdVNJ0/gMBS91TQb1DfrhsbR748g4STwxZptFAwfqehMyrF8rDwB23w87PQwru0wA==", "requires": { "ethereumjs-common": "^1.3.1", @@ -7754,6 +8225,65 @@ "secp256k1": "^3.0.1" } }, + "ethereumjs-vm": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/ethereumjs-vm/-/ethereumjs-vm-2.6.0.tgz", + "integrity": "sha512-r/XIUik/ynGbxS3y+mvGnbOKnuLo40V5Mj1J25+HEO63aWYREIqvWeRO/hnROlMBE5WoniQmPmhiaN0ctiHaXw==", + "requires": { + "async": "^2.1.2", + "async-eventemitter": "^0.2.2", + "ethereumjs-account": "^2.0.3", + "ethereumjs-block": "~2.2.0", + "ethereumjs-common": "^1.1.0", + "ethereumjs-util": "^6.0.0", + "fake-merkle-patricia-tree": "^1.0.1", + "functional-red-black-tree": "^1.0.1", + "merkle-patricia-tree": "^2.3.2", + "rustbn.js": "~0.2.0", + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "ethereumjs-block": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ethereumjs-block/-/ethereumjs-block-2.2.2.tgz", + "integrity": "sha512-2p49ifhek3h2zeg/+da6XpdFR3GlqY3BIEiqxGF8j9aSRIgkb7M1Ky+yULBKJOu8PAZxfhsYA+HxUk2aCQp3vg==", + "requires": { + "async": "^2.0.1", + "ethereumjs-common": "^1.5.0", + "ethereumjs-tx": "^2.1.1", + "ethereumjs-util": "^5.0.0", + "merkle-patricia-tree": "^2.1.2" + }, + "dependencies": { + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + } + } + }, + "keccak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", + "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "requires": { + "bindings": "^1.2.1", + "inherits": "^2.0.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" + } + } + } + }, "ethereumjs-wallet": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz", @@ -7788,9 +8318,9 @@ }, "dependencies": { "@types/node": { - "version": "10.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.5.tgz", - "integrity": "sha512-RElZIr/7JreF1eY6oD5RF3kpmdcreuQPjg5ri4oQ5g9sq7YWU8HkfB3eH8GwAwxf5OaCh0VPi7r4N/yoTGelrA==" + "version": "10.17.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.11.tgz", + "integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==" }, "aes-js": { "version": "3.0.0", @@ -8046,9 +8576,9 @@ } }, "ext": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.2.0.tgz", - "integrity": "sha512-0ccUQK/9e3NreLFg6K6np8aPyRgwycx+oFGtfx1dSp7Wj00Ozw9r05FgBRlzjf2XBM7LAzwgLyDscRrtSU91hA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", "requires": { "type": "^2.0.0" }, @@ -8184,6 +8714,21 @@ "pend": "~1.2.0" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -8206,6 +8751,14 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, + "fake-merkle-patricia-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fake-merkle-patricia-tree/-/fake-merkle-patricia-tree-1.0.1.tgz", + "integrity": "sha1-S4w6z7Ugr635hgsfFM2M40As3dM=", + "requires": { + "checkpoint-store": "^1.1.0" + } + }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", @@ -9246,6 +9799,17 @@ "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + } } }, "function-bind": { @@ -9268,8 +9832,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, "functions-have-names": { "version": "1.2.0", @@ -9345,6 +9908,11 @@ "globule": "^1.0.0" } }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -9493,13 +10061,13 @@ "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=" }, "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.1.tgz", + "integrity": "sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==", "dev": true, "requires": { "glob": "~7.1.1", - "lodash": "~4.17.10", + "lodash": "~4.17.12", "minimatch": "~3.0.2" } }, @@ -9556,6 +10124,11 @@ "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==" + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -9582,26 +10155,6 @@ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==" }, - "handlebars": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", - "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -9883,6 +10436,12 @@ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" }, + "html-escaper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "dev": true + }, "html-minifier": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", @@ -10100,6 +10659,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==" }, + "immediate": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", + "integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=" + }, "immer": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", @@ -10675,8 +11239,7 @@ "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" }, "is-whitespace-character": { "version": "1.0.3", @@ -10729,7 +11292,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "dev": true, "requires": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" @@ -10833,12 +11395,12 @@ } }, "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", "dev": true, "requires": { - "handlebars": "^4.1.2" + "html-escaper": "^2.0.0" } }, "isurl": { @@ -11431,6 +11993,15 @@ "semver": "^6.2.0" }, "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -11465,6 +12036,15 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11538,9 +12118,9 @@ } }, "js-base64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", + "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==", "dev": true }, "js-levenshtein": { @@ -11550,9 +12130,9 @@ "dev": true }, "js-sha3": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.6.1.tgz", - "integrity": "sha1-W4n3enR3Z5h39YxKB1JAk0sflcA=" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, "js-stringify": { "version": "1.0.2", @@ -11625,6 +12205,15 @@ "graceful-fs": "^4.1.9" } }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "strip-json-comments": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", @@ -11633,6 +12222,12 @@ } } }, + "jsdoctypeparser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-6.0.0.tgz", + "integrity": "sha512-61VtBXLkHfOFSIdp/VDVNMksxK0ID0cPTNvxDR92tPA6K7r2AX0OcJegYxhJIwtpWKU4p3D9L3U02hhlP1kQLQ==", + "dev": true + }, "jsdom": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", @@ -11668,9 +12263,9 @@ }, "dependencies": { "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", "dev": true }, "acorn-globals": { @@ -11684,9 +12279,9 @@ }, "dependencies": { "acorn": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", - "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true } } @@ -11807,15 +12402,6 @@ "safe-buffer": "^5.1.0" } }, - "keccakjs": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/keccakjs/-/keccakjs-0.2.3.tgz", - "integrity": "sha512-BjLkNDcfaZ6l8HBG9tH0tpmDv3sS2mA7FNQxFHpCdzP3Gb2MVruXBSuoM66SnVxKJpAr5dKGdkHD+bDokt8fTg==", - "requires": { - "browserify-sha3": "^0.0.4", - "sha3": "^1.2.2" - } - }, "keyboardevent-from-electron-accelerator": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-2.0.0.tgz", @@ -11840,15 +12426,14 @@ "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==" }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "klaw": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", - "dev": true, "requires": { "graceful-fs": "^4.1.9" } @@ -11920,6 +12505,109 @@ "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", "dev": true }, + "level-codec": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-7.0.1.tgz", + "integrity": "sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ==" + }, + "level-errors": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-1.0.5.tgz", + "integrity": "sha512-/cLUpQduF6bNrWuAC4pwtUKA5t669pCsCi2XbmojG2tFeOr9j6ShtdDCtFFQO1DRt+EVZhx9gPzP9G2bUaG4ig==", + "requires": { + "errno": "~0.1.1" + } + }, + "level-iterator-stream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-1.3.1.tgz", + "integrity": "sha1-5Dt4sagUPm+pek9IXrjqUwNS8u0=", + "requires": { + "inherits": "^2.0.1", + "level-errors": "^1.0.3", + "readable-stream": "^1.0.33", + "xtend": "^4.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "level-ws": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/level-ws/-/level-ws-0.0.0.tgz", + "integrity": "sha1-Ny5RIXeSSgBCSwtDrvK7QkltIos=", + "requires": { + "readable-stream": "~1.0.15", + "xtend": "~2.1.1" + }, + "dependencies": { + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "levelup": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-1.3.9.tgz", + "integrity": "sha512-VVGHfKIlmw8w1XqpGOAGwq6sZm2WwWLmlDcULkKWQXEA5EopA8OBNJ2Ck2v6bdk8HeEZSbCSEgzXadyQFm76sQ==", + "requires": { + "deferred-leveldown": "~1.2.1", + "level-codec": "~7.0.0", + "level-errors": "~1.0.3", + "level-iterator-stream": "~1.3.0", + "prr": "~1.0.1", + "semver": "~5.4.1", + "xtend": "~4.0.0" + }, + "dependencies": { + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + } + } + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -11944,6 +12632,11 @@ "uc.micro": "^1.0.1" } }, + "litepicker": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/litepicker/-/litepicker-1.0.29.tgz", + "integrity": "sha512-QbJhpfZf/2AziOjmWR9VUszHCwcTZAiK8SRyjHumq2QxQ9lCwzWYVAMBp05iqWCSXn0vbaQeLGS/ogkdpIMX+w==" + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -11993,6 +12686,21 @@ "pinkie-promise": "^2.0.0" } }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", @@ -12052,6 +12760,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -12175,6 +12893,11 @@ "yallist": "^3.0.2" } }, + "ltgt": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", + "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" + }, "make-dir": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", @@ -12281,6 +13004,11 @@ "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", "dev": true }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=" + }, "mathml-tag-names": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz", @@ -12331,6 +13059,29 @@ "p-is-promise": "^2.0.0" } }, + "memdown": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz", + "integrity": "sha1-tOThkhdGZP+65BNhqlAPMRnv4hU=", + "requires": { + "abstract-leveldown": "~2.7.1", + "functional-red-black-tree": "^1.0.1", + "immediate": "^3.2.3", + "inherits": "~2.0.1", + "ltgt": "~2.2.0", + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "abstract-leveldown": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz", + "integrity": "sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==", + "requires": { + "xtend": "~4.0.0" + } + } + } + }, "memoizerific": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", @@ -12378,6 +13129,11 @@ } } }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=" + }, "meow": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", @@ -12432,33 +13188,107 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==" }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "microevent.ts": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", - "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", + "merkle-patricia-tree": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/merkle-patricia-tree/-/merkle-patricia-tree-2.3.2.tgz", + "integrity": "sha512-81PW5m8oz/pz3GvsAwbauj7Y00rqm81Tzad77tHBwU7pIAtN+TJnMSOJhxBKflSVYhptMMb9RskhqHqrSm1V+g==", + "requires": { + "async": "^1.4.2", + "ethereumjs-util": "^5.0.0", + "level-ws": "0.0.0", + "levelup": "^1.2.1", + "memdown": "^1.0.0", + "readable-stream": "^2.0.0", + "rlp": "^2.0.0", + "semaphore": ">=1.0.1" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "keccak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", + "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "requires": { + "bindings": "^1.2.1", + "inherits": "^2.0.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", "snapdragon": "^0.8.1", "to-regex": "^3.0.2" } @@ -12579,9 +13409,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minimist-options": { "version": "3.0.2", @@ -12664,19 +13494,9 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", + "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==" }, "mkdirp-promise": { "version": "5.0.1", @@ -12717,10 +13537,92 @@ "resolved": "https://registry.npmjs.org/mobx-utils/-/mobx-utils-5.5.2.tgz", "integrity": "sha512-cOlFJDWU/NHyGKvdhWqPdHmhPfeKewElAIZp5XticWIsSLGAA+4Uou3+8ookhQ/yG7qZXzvjAq90TZWXiR5+XA==" }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=" + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "mock-fs": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.10.3.tgz", - "integrity": "sha512-bcukePBvuA3qovmq0Qtqu9+1APCIGkFHnsozrPIVromt5XFGGgkQSfaN0H6RI8gStHkO/hRgimvS3tooNes4pQ==" + "version": "4.10.4", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.10.4.tgz", + "integrity": "sha512-gDfZDLaPIvtOusbusLinfx6YSe2YpQsDT8qdP41P47dQ/NQggtkHukz7hwqgt8QvMBmAv+Z6DGmXPyb5BWX2nQ==" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, "moo": { "version": "0.4.3", @@ -12739,6 +13641,16 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "ms": { @@ -12863,7 +13775,6 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "dev": true, "requires": { "encoding": "^0.1.11", "is-stream": "^1.0.1" @@ -12894,6 +13805,15 @@ "which": "1" }, "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", @@ -13035,9 +13955,9 @@ } }, "node-sass": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.0.tgz", - "integrity": "sha512-W1XBrvoJ1dy7VsvTAS5q1V45lREbTlZQqFbiHb3R3OTTCma0XBtuG6xZ6Z4506nR4lmHPTqVRwxT6KgtWC97CA==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.1.tgz", + "integrity": "sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -13182,6 +14102,15 @@ "trim-newlines": "^1.0.0" } }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -13499,7 +14428,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -13519,6 +14447,12 @@ "has": "^1.0.3" } }, + "object.entries-ponyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz", + "integrity": "sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY=", + "dev": true + }, "object.fromentries": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.1.tgz", @@ -13629,24 +14563,6 @@ "is-wsl": "^1.1.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -13669,6 +14585,11 @@ "url-parse": "^1.4.3" } }, + "original-require": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/original-require/-/original-require-1.0.1.tgz", + "integrity": "sha1-DxMEcVhM0zURxew4yNWSE/msXiA=" + }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -13693,8 +14614,7 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", @@ -13803,6 +14723,11 @@ } } }, + "paginator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/paginator/-/paginator-1.0.0.tgz", + "integrity": "sha1-dWVwKvmrlhbcph/CLHDroqQ1cmU=" + }, "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", @@ -13917,13 +14842,9 @@ } }, "parse-headers": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.2.tgz", - "integrity": "sha512-/LypJhzFmyBIDYP9aDVgeyEb5sQfbfY5mnDq4hVhlQ69js87wXfmEI5V3xI6vvXasqebp0oCytYFLxsBVfCzSg==", - "requires": { - "for-each": "^0.3.3", - "string.prototype.trim": "^1.1.2" - } + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", + "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==" }, "parse-json": { "version": "4.0.0", @@ -14181,6 +15102,14 @@ "requires": { "ms": "^2.1.1" } + }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } } } }, @@ -15060,13 +15989,20 @@ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.9.0.tgz", + "integrity": "sha512-KG4bhCFYapExLsUHrFt+kQVEegF2agm4cpF/VNc6pZVthIfCc/GK8t8VyNIE3nyXG9DK3Tf2EGkxjR6/uRdYsA==", "requires": { "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "dependencies": { + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + } } }, "querystring": { @@ -15093,11 +16029,15 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, "requires": { "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", + "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" + }, "railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -15208,6 +16148,20 @@ "@babel/runtime": "^7.0.0" } }, + "react-collapse": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-5.0.0.tgz", + "integrity": "sha512-VpNmFZIUhHcl2Xqt9+N9PpjK14J6DyF56mdFn9Ci+3WqR+duQrQ0U2cuaMXw0VrHWLy/pACFX/bGSMVe303gNQ==" + }, + "react-copy-to-clipboard": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", + "integrity": "sha512-/2t5mLMMPuN5GmdXo6TebFa8IoFxZ+KTDDqYhcDm0PhkgEzSxVvIX26G20s1EB02A4h2UZgwtfymZ3lGJm0OLg==", + "requires": { + "copy-to-clipboard": "^3", + "prop-types": "^15.5.8" + } + }, "react-dev-utils": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.1.0.tgz", @@ -15536,16 +16490,31 @@ "html-parse-stringify2": "2.0.1" } }, + "react-id-generator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-id-generator/-/react-id-generator-3.0.0.tgz", + "integrity": "sha512-egmdswpzA/z8UlignCZKmJqrO0EaqmW13M8756wE+V/6NtYf0CdvDjkGpat/Q/djXfg7F/C0267Gff1RHgpfIQ==" + }, "react-is": { "version": "16.11.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", "integrity": "sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==" }, + "react-js-pagination": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-js-pagination/-/react-js-pagination-3.0.2.tgz", + "integrity": "sha512-gsXaQVis1YDKhbeZC7VT9dsSWyZ8GZNEX06dy2HLl36LohY2Vt/iWJ5k6HAb0YOKW0mklCY7wjxJIfJeeP295g==", + "requires": { + "classnames": "^2.2.5", + "paginator": "^1.0.0", + "prop-types": "15.x.x - 16.x.x", + "react": "15.x.x - 16.x.x" + } + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "dev": true + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-popper": { "version": "1.3.6", @@ -15600,6 +16569,18 @@ "prop-types": "^15.5.8" } }, + "react-resize-detector": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-4.2.1.tgz", + "integrity": "sha512-ZfPMBPxXi0o3xox42MIEtz84tPSVMW9GgwLHYvjVXlFM+OkNzbeEtpVSV+mSTJmk4Znwomolzt35zHN9LNBQMQ==", + "requires": { + "lodash": "^4.17.15", + "lodash-es": "^4.17.15", + "prop-types": "^15.7.2", + "raf-schd": "^4.0.2", + "resize-observer-polyfill": "^1.5.1" + } + }, "react-router": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", @@ -15643,6 +16624,17 @@ "throttle-debounce": "^2.1.0" } }, + "react-smooth": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-1.0.5.tgz", + "integrity": "sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w==", + "requires": { + "lodash": "~4.17.4", + "prop-types": "^15.6.0", + "raf": "^3.4.0", + "react-transition-group": "^2.5.0" + } + }, "react-syntax-highlighter": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-8.1.0.tgz", @@ -15656,6 +16648,15 @@ "refractor": "^2.4.1" } }, + "react-tabs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-3.0.0.tgz", + "integrity": "sha512-z90cDIb+5V7MzjXFHq1VLxYiMH7dDQWan7mXSw6BWQtw+9pYAnq/fEDvsPaXNyevYitvLetdW87C61uu27JVMA==", + "requires": { + "classnames": "^2.2.0", + "prop-types": "^15.5.0" + } + }, "react-test-renderer": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.12.0.tgz", @@ -15690,6 +16691,17 @@ "prop-types": "^15.6.0" } }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "read-config-file": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-3.2.2.tgz", @@ -15839,6 +16851,39 @@ } } }, + "recharts": { + "version": "2.0.0-beta.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.0.0-beta.1.tgz", + "integrity": "sha512-awJH2DE6JRgp5ymzmH5dKh2Pu6prqZJCr3NRaYCcyub1fBa+fIG3ZlpLyl9hWizHPGEvfZLvcjIM+qgTsr9aSQ==", + "requires": { + "classnames": "^2.2.5", + "core-js": "^3.4.2", + "d3-interpolate": "^1.3.0", + "d3-scale": "^3.1.0", + "d3-shape": "^1.3.5", + "lodash": "^4.17.5", + "prop-types": "^15.6.0", + "react-resize-detector": "^4.2.1", + "react-smooth": "^1.0.5", + "recharts-scale": "^0.4.2", + "reduce-css-calc": "^1.3.0" + }, + "dependencies": { + "core-js": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.4.8.tgz", + "integrity": "sha512-b+BBmCZmVgho8KnBUOXpvlqEMguko+0P+kXCwD4vIprsXC6ht1qgPxtb1OK6XgSlrySF71wkwBQ0Hv695bk9gQ==" + } + } + }, + "recharts-scale": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.3.tgz", + "integrity": "sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA==", + "requires": { + "decimal.js-light": "^2.4.1" + } + }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -15866,6 +16911,31 @@ "strip-indent": "^2.0.0" } }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, "reflect.ownkeys": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", @@ -15949,6 +17019,12 @@ "unicode-match-property-value-ecmascript": "^1.1.0" } }, + "regextras": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.6.1.tgz", + "integrity": "sha512-EzIHww9xV2Kpqx+corS/I7OBmf2rZ0pKKJPsw5Dc+l6Zq1TslDmtRIP9maVn3UH+72MIXmn8zzDgP07ihQogUA==", + "dev": true + }, "registry-auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.0.0.tgz", @@ -16168,6 +17244,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -16190,8 +17271,7 @@ "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "resolve": { "version": "1.12.0", @@ -16285,9 +17365,17 @@ "signal-exit": "^3.0.2" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "requires": { + "through": "~2.3.4" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, "retry": { @@ -16362,6 +17450,11 @@ "aproba": "^1.1.1" } }, + "rustbn.js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/rustbn.js/-/rustbn.js-0.2.0.tgz", + "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==" + }, "rx": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", @@ -16731,26 +17824,6 @@ "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-2.0.3.tgz", "integrity": "sha1-uwBAvgMEPamgEqLOqfyfhSz8h9Q=" }, - "scrypt-shim": { - "version": "github:web3-js/scrypt-shim#be5e616323a8b5e568788bf94d03c1b8410eac54", - "from": "github:web3-js/scrypt-shim", - "requires": { - "scryptsy": "^2.1.0", - "semver": "^6.3.0" - }, - "dependencies": { - "scryptsy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/scryptsy/-/scryptsy-2.1.0.tgz", - "integrity": "sha512-1CdSqHQowJBnMAFyPEBRfqag/YP9OF394FV+4YREIJX4ljD7OxvQRDayyoyyCk+senRjSkP6VnUNQmVQqB6g7w==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, "scrypt.js": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/scrypt.js/-/scrypt.js-0.3.0.tgz", @@ -16832,6 +17905,11 @@ "node-forge": "0.9.0" } }, + "semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -16889,9 +17967,9 @@ } }, "serialize-javascript": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", - "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==" }, "serve-favicon": { "version": "2.5.0", @@ -17038,21 +18116,6 @@ "safe-buffer": "^5.0.1" } }, - "sha3": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/sha3/-/sha3-1.2.3.tgz", - "integrity": "sha512-sOWDZi8cDBRkLfWOw18wvJyNblXDHzwMGnRWut8zNNeIeLnmMRO17bjpLc7OzMuj1ASUgx2IyohzUCAl+Kx5vA==", - "requires": { - "nan": "2.13.2" - }, - "dependencies": { - "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==" - } - } - }, "shallow-clone": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", @@ -17391,6 +18454,56 @@ } } }, + "solc": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.6.1.tgz", + "integrity": "sha512-iKqNYps2p++x8L9sBg7JeAJb7EmW8VJ/2asAzwlLYcUhj86AzuWLe94UTSQHv1SSCCj/x6lya8twvXkZtlTbIQ==", + "requires": { + "command-exists": "^1.2.8", + "commander": "3.0.2", + "fs-extra": "^0.30.0", + "js-sha3": "0.8.0", + "memorystream": "^0.3.1", + "require-from-string": "^2.0.0", + "semver": "^5.5.0", + "tmp": "0.0.33" + }, + "dependencies": { + "commander": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", + "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" + }, + "fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "^4.1.6" + } + } + } + }, + "solidity-bytes-utils": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/solidity-bytes-utils/-/solidity-bytes-utils-0.0.6.tgz", + "integrity": "sha512-kOQ1uRvorR6q0PT0mEnIDDhrSRSyDxL5XYCpyQ6GYXWPZRTGB4vzkhuIHxZRsVKvHFIFVtft6C3HTq3r0Sfosw==", + "requires": { + "truffle-hdwallet-provider": "0.0.3" + } + }, "sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -17519,6 +18632,11 @@ "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", "dev": true }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -17619,9 +18737,9 @@ "dev": true }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -17830,6 +18948,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz", "integrity": "sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg==", + "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.13.0", @@ -17840,6 +18959,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -17849,6 +18969,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -18129,6 +19250,15 @@ "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.2.1.tgz", "integrity": "sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw==", "dev": true + }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } } } }, @@ -18235,6 +19365,115 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, + "tape": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/tape/-/tape-4.13.2.tgz", + "integrity": "sha512-waWwC/OqYVE9TS6r1IynlP2sEdk4Lfo6jazlgkuNkPTHIbuG2BTABIaKdlQWwPeB6Oo4ksZ1j33Yt0NTOAlYMQ==", + "requires": { + "deep-equal": "~1.1.1", + "defined": "~1.0.0", + "dotignore": "~0.1.2", + "for-each": "~0.3.3", + "function-bind": "~1.1.1", + "glob": "~7.1.6", + "has": "~1.0.3", + "inherits": "~2.0.4", + "is-regex": "~1.0.5", + "minimist": "~1.2.0", + "object-inspect": "~1.7.0", + "resolve": "~1.15.1", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.2.1", + "through": "~2.3.8" + }, + "dependencies": { + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "string.prototype.trim": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", + "integrity": "sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + } + } + }, "tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", @@ -18247,6 +19486,16 @@ "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", "yallist": "^3.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "tar-stream": { @@ -18415,9 +19664,9 @@ } }, "terser": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.0.tgz", - "integrity": "sha512-oDG16n2WKm27JO8h4y/w3iqBGAOSCtq7k8dRmrn4Wf9NouL0b2WpMHGChFGZq4nFAQy1FsNJrVQHfurXOSTmOA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.4.3.tgz", + "integrity": "sha512-0ikKraVtRDKGzHrzkCv5rUNDzqlhmhowOBqC0XqUHFpW+vJ45+20/IFBcebwKfiS2Z9fJin6Eo+F1zLZsxi8RA==", "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -18437,15 +19686,15 @@ } }, "terser-webpack-plugin": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz", - "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", + "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^1.7.0", + "serialize-javascript": "^2.1.2", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -18638,7 +19887,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "requires": { "os-tmpdir": "~1.0.2" } @@ -18710,8 +19958,7 @@ "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", - "dev": true + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, "toidentifier": { "version": "1.0.0", @@ -18750,9 +19997,9 @@ } }, "tree-kill": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", - "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, "trim": { @@ -18784,6 +20031,62 @@ "glob": "^7.1.2" } }, + "truffle": { + "version": "5.1.17", + "resolved": "https://registry.npmjs.org/truffle/-/truffle-5.1.17.tgz", + "integrity": "sha512-x8CNCjtsX3B34Q6TI467+kgxqzjCRMvLsGIqf/MHo1NpntP1tIOySGJ+idEg857X0hJz8zfOVz9QKbvrrsKUGQ==", + "requires": { + "app-module-path": "^2.2.0", + "mocha": "5.2.0", + "original-require": "1.0.1" + } + }, + "truffle-hdwallet-provider": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/truffle-hdwallet-provider/-/truffle-hdwallet-provider-0.0.3.tgz", + "integrity": "sha1-Dh3gIQS3PTh14c9wkzBbTqii2EM=", + "requires": { + "bip39": "^2.2.0", + "ethereumjs-wallet": "^0.6.0", + "web3": "^0.18.2", + "web3-provider-engine": "^8.4.0" + }, + "dependencies": { + "bignumber.js": { + "version": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2", + "from": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2" + }, + "bip39": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-2.6.0.tgz", + "integrity": "sha512-RrnQRG2EgEoqO24ea+Q/fftuPUZLmrEM3qNhhGsA3PbaXaCW791LTzPuVyx/VprXQcTbPJ3K3UeTna8ZnVl2sg==", + "requires": { + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1", + "safe-buffer": "^5.0.1", + "unorm": "^1.3.3" + } + }, + "utf8": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", + "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=" + }, + "web3": { + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/web3/-/web3-0.18.4.tgz", + "integrity": "sha1-gewXhBRUkfLqqJVbMcBgSeB8Xn0=", + "requires": { + "bignumber.js": "git+https://github.com/debris/bignumber.js.git#94d7146671b9719e00a09c29b01a691bc85048c2", + "crypto-js": "^3.1.4", + "utf8": "^2.1.1", + "xhr2": "*", + "xmlhttprequest": "*" + } + } + } + }, "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", @@ -19101,6 +20404,11 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, + "unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -19577,31 +20885,31 @@ } }, "web3": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.2.2.tgz", - "integrity": "sha512-/ChbmB6qZpfGx6eNpczt5YSUBHEA5V2+iUCbn85EVb3Zv6FVxrOo5Tv7Lw0gE2tW7EEjASbCyp3mZeiZaCCngg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.2.4.tgz", + "integrity": "sha512-xPXGe+w0x0t88Wj+s/dmAdASr3O9wmA9mpZRtixGZxmBexAF0MjfqYM+MS4tVl5s11hMTN3AZb8cDD4VLfC57A==", "requires": { "@types/node": "^12.6.1", - "web3-bzz": "1.2.2", - "web3-core": "1.2.2", - "web3-eth": "1.2.2", - "web3-eth-personal": "1.2.2", - "web3-net": "1.2.2", - "web3-shh": "1.2.2", - "web3-utils": "1.2.2" + "web3-bzz": "1.2.4", + "web3-core": "1.2.4", + "web3-eth": "1.2.4", + "web3-eth-personal": "1.2.4", + "web3-net": "1.2.4", + "web3-shh": "1.2.4", + "web3-utils": "1.2.4" }, "dependencies": { "@types/node": { - "version": "12.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz", - "integrity": "sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==" + "version": "12.12.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.20.tgz", + "integrity": "sha512-VAe+DiwpnC/g448uN+/3gRl4th0BTdrR9gSLIOHA+SUQskaYZQDOHG7xmjiE7JUhjbXnbXytf6Ih+/pA6CtMFQ==" } } }, "web3-bzz": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.2.2.tgz", - "integrity": "sha512-b1O2ObsqUN1lJxmFSjvnEC4TsaCbmh7Owj3IAIWTKqL9qhVgx7Qsu5O9cD13pBiSPNZJ68uJPaKq380QB4NWeA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.2.4.tgz", + "integrity": "sha512-MqhAo/+0iQSMBtt3/QI1rU83uvF08sYq8r25+OUZ+4VtihnYsmkkca+rdU0QbRyrXY2/yGIpI46PFdh0khD53A==", "requires": { "@types/node": "^10.12.18", "got": "9.6.0", @@ -19610,132 +20918,133 @@ }, "dependencies": { "@types/node": { - "version": "10.17.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.5.tgz", - "integrity": "sha512-RElZIr/7JreF1eY6oD5RF3kpmdcreuQPjg5ri4oQ5g9sq7YWU8HkfB3eH8GwAwxf5OaCh0VPi7r4N/yoTGelrA==" + "version": "10.17.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.11.tgz", + "integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==" } } }, "web3-core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.2.2.tgz", - "integrity": "sha512-miHAX3qUgxV+KYfaOY93Hlc3kLW2j5fH8FJy6kSxAv+d4d5aH0wwrU2IIoJylQdT+FeenQ38sgsCnFu9iZ1hCQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.2.4.tgz", + "integrity": "sha512-CHc27sMuET2cs1IKrkz7xzmTdMfZpYswe7f0HcuyneTwS1yTlTnHyqjAaTy0ZygAb/x4iaVox+Gvr4oSAqSI+A==", "requires": { + "@types/bignumber.js": "^5.0.0", "@types/bn.js": "^4.11.4", "@types/node": "^12.6.1", - "web3-core-helpers": "1.2.2", - "web3-core-method": "1.2.2", - "web3-core-requestmanager": "1.2.2", - "web3-utils": "1.2.2" + "web3-core-helpers": "1.2.4", + "web3-core-method": "1.2.4", + "web3-core-requestmanager": "1.2.4", + "web3-utils": "1.2.4" }, "dependencies": { "@types/node": { - "version": "12.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz", - "integrity": "sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==" + "version": "12.12.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.20.tgz", + "integrity": "sha512-VAe+DiwpnC/g448uN+/3gRl4th0BTdrR9gSLIOHA+SUQskaYZQDOHG7xmjiE7JUhjbXnbXytf6Ih+/pA6CtMFQ==" } } }, "web3-core-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.2.2.tgz", - "integrity": "sha512-HJrRsIGgZa1jGUIhvGz4S5Yh6wtOIo/TMIsSLe+Xay+KVnbseJpPprDI5W3s7H2ODhMQTbogmmUFquZweW2ImQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.2.4.tgz", + "integrity": "sha512-U7wbsK8IbZvF3B7S+QMSNP0tni/6VipnJkB0tZVEpHEIV2WWeBHYmZDnULWcsS/x/jn9yKhJlXIxWGsEAMkjiw==", "requires": { "underscore": "1.9.1", - "web3-eth-iban": "1.2.2", - "web3-utils": "1.2.2" + "web3-eth-iban": "1.2.4", + "web3-utils": "1.2.4" } }, "web3-core-method": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.2.2.tgz", - "integrity": "sha512-szR4fDSBxNHaF1DFqE+j6sFR/afv9Aa36OW93saHZnrh+iXSrYeUUDfugeNcRlugEKeUCkd4CZylfgbK2SKYJA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.2.4.tgz", + "integrity": "sha512-8p9kpL7di2qOVPWgcM08kb+yKom0rxRCMv6m/K+H+yLSxev9TgMbCgMSbPWAHlyiF3SJHw7APFKahK5Z+8XT5A==", "requires": { "underscore": "1.9.1", - "web3-core-helpers": "1.2.2", - "web3-core-promievent": "1.2.2", - "web3-core-subscriptions": "1.2.2", - "web3-utils": "1.2.2" + "web3-core-helpers": "1.2.4", + "web3-core-promievent": "1.2.4", + "web3-core-subscriptions": "1.2.4", + "web3-utils": "1.2.4" } }, "web3-core-promievent": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.2.2.tgz", - "integrity": "sha512-tKvYeT8bkUfKABcQswK6/X79blKTKYGk949urZKcLvLDEaWrM3uuzDwdQT3BNKzQ3vIvTggFPX9BwYh0F1WwqQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.2.4.tgz", + "integrity": "sha512-gEUlm27DewUsfUgC3T8AxkKi8Ecx+e+ZCaunB7X4Qk3i9F4C+5PSMGguolrShZ7Zb6717k79Y86f3A00O0VAZw==", "requires": { "any-promise": "1.3.0", "eventemitter3": "3.1.2" } }, "web3-core-requestmanager": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.2.2.tgz", - "integrity": "sha512-a+gSbiBRHtHvkp78U2bsntMGYGF2eCb6219aMufuZWeAZGXJ63Wc2321PCbA8hF9cQrZI4EoZ4kVLRI4OF15Hw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.2.4.tgz", + "integrity": "sha512-eZJDjyNTDtmSmzd3S488nR/SMJtNnn/GuwxnMh3AzYCqG3ZMfOylqTad2eYJPvc2PM5/Gj1wAMQcRpwOjjLuPg==", "requires": { "underscore": "1.9.1", - "web3-core-helpers": "1.2.2", - "web3-providers-http": "1.2.2", - "web3-providers-ipc": "1.2.2", - "web3-providers-ws": "1.2.2" + "web3-core-helpers": "1.2.4", + "web3-providers-http": "1.2.4", + "web3-providers-ipc": "1.2.4", + "web3-providers-ws": "1.2.4" } }, "web3-core-subscriptions": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.2.2.tgz", - "integrity": "sha512-QbTgigNuT4eicAWWr7ahVpJyM8GbICsR1Ys9mJqzBEwpqS+RXTRVSkwZ2IsxO+iqv6liMNwGregbJLq4urMFcQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.2.4.tgz", + "integrity": "sha512-3D607J2M8ymY9V+/WZq4MLlBulwCkwEjjC2U+cXqgVO1rCyVqbxZNCmHyNYHjDDCxSEbks9Ju5xqJxDSxnyXEw==", "requires": { "eventemitter3": "3.1.2", "underscore": "1.9.1", - "web3-core-helpers": "1.2.2" + "web3-core-helpers": "1.2.4" } }, "web3-eth": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.2.2.tgz", - "integrity": "sha512-UXpC74mBQvZzd4b+baD4Ocp7g+BlwxhBHumy9seyE/LMIcMlePXwCKzxve9yReNpjaU16Mmyya6ZYlyiKKV8UA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.2.4.tgz", + "integrity": "sha512-+j+kbfmZsbc3+KJpvHM16j1xRFHe2jBAniMo1BHKc3lho6A8Sn9Buyut6odubguX2AxoRArCdIDCkT9hjUERpA==", "requires": { "underscore": "1.9.1", - "web3-core": "1.2.2", - "web3-core-helpers": "1.2.2", - "web3-core-method": "1.2.2", - "web3-core-subscriptions": "1.2.2", - "web3-eth-abi": "1.2.2", - "web3-eth-accounts": "1.2.2", - "web3-eth-contract": "1.2.2", - "web3-eth-ens": "1.2.2", - "web3-eth-iban": "1.2.2", - "web3-eth-personal": "1.2.2", - "web3-net": "1.2.2", - "web3-utils": "1.2.2" + "web3-core": "1.2.4", + "web3-core-helpers": "1.2.4", + "web3-core-method": "1.2.4", + "web3-core-subscriptions": "1.2.4", + "web3-eth-abi": "1.2.4", + "web3-eth-accounts": "1.2.4", + "web3-eth-contract": "1.2.4", + "web3-eth-ens": "1.2.4", + "web3-eth-iban": "1.2.4", + "web3-eth-personal": "1.2.4", + "web3-net": "1.2.4", + "web3-utils": "1.2.4" } }, "web3-eth-abi": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.2.2.tgz", - "integrity": "sha512-Yn/ZMgoOLxhTVxIYtPJ0eS6pnAnkTAaJgUJh1JhZS4ekzgswMfEYXOwpMaD5eiqPJLpuxmZFnXnBZlnQ1JMXsw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.2.4.tgz", + "integrity": "sha512-8eLIY4xZKoU3DSVu1pORluAw9Ru0/v4CGdw5so31nn+7fR8zgHMgwbFe0aOqWQ5VU42PzMMXeIJwt4AEi2buFg==", "requires": { "ethers": "4.0.0-beta.3", "underscore": "1.9.1", - "web3-utils": "1.2.2" + "web3-utils": "1.2.4" } }, "web3-eth-accounts": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.2.2.tgz", - "integrity": "sha512-KzHOEyXOEZ13ZOkWN3skZKqSo5f4Z1ogPFNn9uZbKCz+kSp+gCAEKxyfbOsB/JMAp5h7o7pb6eYsPCUBJmFFiA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.2.4.tgz", + "integrity": "sha512-04LzT/UtWmRFmi4hHRewP5Zz43fWhuHiK5XimP86sUQodk/ByOkXQ3RoXyGXFMNoRxdcAeRNxSfA2DpIBc9xUw==", "requires": { + "@web3-js/scrypt-shim": "^0.1.0", "any-promise": "1.3.0", "crypto-browserify": "3.12.0", "eth-lib": "0.2.7", "ethereumjs-common": "^1.3.2", "ethereumjs-tx": "^2.1.1", - "scrypt-shim": "github:web3-js/scrypt-shim", "underscore": "1.9.1", "uuid": "3.3.2", - "web3-core": "1.2.2", - "web3-core-helpers": "1.2.2", - "web3-core-method": "1.2.2", - "web3-utils": "1.2.2" + "web3-core": "1.2.4", + "web3-core-helpers": "1.2.4", + "web3-core-method": "1.2.4", + "web3-utils": "1.2.4" }, "dependencies": { "eth-lib": { @@ -19756,119 +21065,429 @@ } }, "web3-eth-contract": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.2.2.tgz", - "integrity": "sha512-EKT2yVFws3FEdotDQoNsXTYL798+ogJqR2//CaGwx3p0/RvQIgfzEwp8nbgA6dMxCsn9KOQi7OtklzpnJMkjtA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.2.4.tgz", + "integrity": "sha512-b/9zC0qjVetEYnzRA1oZ8gF1OSSUkwSYi5LGr4GeckLkzXP7osEnp9lkO/AQcE4GpG+l+STnKPnASXJGZPgBRQ==", "requires": { "@types/bn.js": "^4.11.4", "underscore": "1.9.1", - "web3-core": "1.2.2", - "web3-core-helpers": "1.2.2", - "web3-core-method": "1.2.2", - "web3-core-promievent": "1.2.2", - "web3-core-subscriptions": "1.2.2", - "web3-eth-abi": "1.2.2", - "web3-utils": "1.2.2" + "web3-core": "1.2.4", + "web3-core-helpers": "1.2.4", + "web3-core-method": "1.2.4", + "web3-core-promievent": "1.2.4", + "web3-core-subscriptions": "1.2.4", + "web3-eth-abi": "1.2.4", + "web3-utils": "1.2.4" } }, "web3-eth-ens": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.2.2.tgz", - "integrity": "sha512-CFjkr2HnuyMoMFBoNUWojyguD4Ef+NkyovcnUc/iAb9GP4LHohKrODG4pl76R5u61TkJGobC2ij6TyibtsyVYg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.2.4.tgz", + "integrity": "sha512-g8+JxnZlhdsCzCS38Zm6R/ngXhXzvc3h7bXlxgKU4coTzLLoMpgOAEz71GxyIJinWTFbLXk/WjNY0dazi9NwVw==", "requires": { "eth-ens-namehash": "2.0.8", "underscore": "1.9.1", - "web3-core": "1.2.2", - "web3-core-helpers": "1.2.2", - "web3-core-promievent": "1.2.2", - "web3-eth-abi": "1.2.2", - "web3-eth-contract": "1.2.2", - "web3-utils": "1.2.2" + "web3-core": "1.2.4", + "web3-core-helpers": "1.2.4", + "web3-core-promievent": "1.2.4", + "web3-eth-abi": "1.2.4", + "web3-eth-contract": "1.2.4", + "web3-utils": "1.2.4" } }, "web3-eth-iban": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.2.2.tgz", - "integrity": "sha512-gxKXBoUhaTFHr0vJB/5sd4i8ejF/7gIsbM/VvemHT3tF5smnmY6hcwSMmn7sl5Gs+83XVb/BngnnGkf+I/rsrQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.2.4.tgz", + "integrity": "sha512-D9HIyctru/FLRpXakRwmwdjb5bWU2O6UE/3AXvRm6DCOf2e+7Ve11qQrPtaubHfpdW3KWjDKvlxV9iaFv/oTMQ==", "requires": { "bn.js": "4.11.8", - "web3-utils": "1.2.2" + "web3-utils": "1.2.4" } }, "web3-eth-personal": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.2.2.tgz", - "integrity": "sha512-4w+GLvTlFqW3+q4xDUXvCEMU7kRZ+xm/iJC8gm1Li1nXxwwFbs+Y+KBK6ZYtoN1qqAnHR+plYpIoVo27ixI5Rg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.2.4.tgz", + "integrity": "sha512-5Russ7ZECwHaZXcN3DLuLS7390Vzgrzepl4D87SD6Sn1DHsCZtvfdPIYwoTmKNp69LG3mORl7U23Ga5YxqkICw==", "requires": { "@types/node": "^12.6.1", - "web3-core": "1.2.2", - "web3-core-helpers": "1.2.2", - "web3-core-method": "1.2.2", - "web3-net": "1.2.2", - "web3-utils": "1.2.2" + "web3-core": "1.2.4", + "web3-core-helpers": "1.2.4", + "web3-core-method": "1.2.4", + "web3-net": "1.2.4", + "web3-utils": "1.2.4" }, "dependencies": { "@types/node": { - "version": "12.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.7.tgz", - "integrity": "sha512-E6Zn0rffhgd130zbCbAr/JdXfXkoOUFAKNs/rF8qnafSJ8KYaA/j3oz7dcwal+lYjLA7xvdd5J4wdYpCTlP8+w==" + "version": "12.12.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.20.tgz", + "integrity": "sha512-VAe+DiwpnC/g448uN+/3gRl4th0BTdrR9gSLIOHA+SUQskaYZQDOHG7xmjiE7JUhjbXnbXytf6Ih+/pA6CtMFQ==" } } }, "web3-net": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.2.2.tgz", - "integrity": "sha512-K07j2DXq0x4UOJgae65rWZKraOznhk8v5EGSTdFqASTx7vWE/m+NqBijBYGEsQY1lSMlVaAY9UEQlcXK5HzXTw==", - "requires": { - "web3-core": "1.2.2", - "web3-core-method": "1.2.2", - "web3-utils": "1.2.2" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.2.4.tgz", + "integrity": "sha512-wKOsqhyXWPSYTGbp7ofVvni17yfRptpqoUdp3SC8RAhDmGkX6irsiT9pON79m6b3HUHfLoBilFQyt/fTUZOf7A==", + "requires": { + "web3-core": "1.2.4", + "web3-core-method": "1.2.4", + "web3-utils": "1.2.4" + } + }, + "web3-provider-engine": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/web3-provider-engine/-/web3-provider-engine-8.6.1.tgz", + "integrity": "sha1-TYbhnjDKr5ffNRUR7A9gE25bMOs=", + "requires": { + "async": "^2.1.2", + "clone": "^2.0.0", + "ethereumjs-block": "^1.2.2", + "ethereumjs-tx": "^1.2.0", + "ethereumjs-util": "^5.0.1", + "ethereumjs-vm": "^2.0.2", + "isomorphic-fetch": "^2.2.0", + "request": "^2.67.0", + "semaphore": "^1.0.3", + "solc": "^0.4.2", + "tape": "^4.4.0", + "web3": "^0.16.0", + "xhr": "^2.2.0", + "xtend": "^4.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "bignumber.js": { + "version": "git+https://github.com/debris/bignumber.js.git#c7a38de919ed75e6fb6ba38051986e294b328df9", + "from": "git+https://github.com/debris/bignumber.js.git#master" + }, + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "ethereum-common": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz", + "integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8=" + }, + "ethereumjs-tx": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.7.tgz", + "integrity": "sha512-wvLMxzt1RPhAQ9Yi3/HKZTn0FZYpnsmQdbKYfUUpi4j1SEIcbkd9tndVjcPrufY3V7j2IebOpC00Zp2P/Ay2kA==", + "requires": { + "ethereum-common": "^0.0.18", + "ethereumjs-util": "^5.0.0" + } + }, + "ethereumjs-util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", + "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "requires": { + "bn.js": "^4.11.0", + "create-hash": "^1.1.2", + "ethjs-util": "^0.1.3", + "keccak": "^1.0.2", + "rlp": "^2.0.0", + "safe-buffer": "^5.1.1", + "secp256k1": "^3.0.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "fs-extra": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "keccak": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", + "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "requires": { + "bindings": "^1.2.1", + "inherits": "^2.0.3", + "nan": "^2.2.1", + "safe-buffer": "^5.1.0" + } + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "require-from-string": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", + "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" + }, + "solc": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.4.26.tgz", + "integrity": "sha512-o+c6FpkiHd+HPjmjEVpQgH7fqZ14tJpXhho+/bQXlXbliLIS/xjXb42Vxh+qQY1WCSTMQ0+a5vR9vi0MfhU6mA==", + "requires": { + "fs-extra": "^0.30.0", + "memorystream": "^0.3.1", + "require-from-string": "^1.1.0", + "semver": "^5.3.0", + "yargs": "^4.7.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, + "utf8": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", + "integrity": "sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=" + }, + "web3": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/web3/-/web3-0.16.0.tgz", + "integrity": "sha1-pFVBdc1GKUMDWx8dOUMvdBxrYBk=", + "requires": { + "bignumber.js": "git+https://github.com/debris/bignumber.js.git#master", + "crypto-js": "^3.1.4", + "utf8": "^2.1.1", + "xmlhttprequest": "*" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "window-size": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", + "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=", + "requires": { + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "lodash.assign": "^4.0.3", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.1", + "which-module": "^1.0.0", + "window-size": "^0.2.0", + "y18n": "^3.2.1", + "yargs-parser": "^2.4.1" + } + }, + "yargs-parser": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", + "requires": { + "camelcase": "^3.0.0", + "lodash.assign": "^4.0.6" + } + } } }, "web3-providers-http": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.2.2.tgz", - "integrity": "sha512-BNZ7Hguy3eBszsarH5gqr9SIZNvqk9eKwqwmGH1LQS1FL3NdoOn7tgPPdddrXec4fL94CwgNk4rCU+OjjZRNDg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.2.4.tgz", + "integrity": "sha512-dzVCkRrR/cqlIrcrWNiPt9gyt0AZTE0J+MfAu9rR6CyIgtnm1wFUVVGaxYRxuTGQRO4Dlo49gtoGwaGcyxqiTw==", "requires": { - "web3-core-helpers": "1.2.2", + "web3-core-helpers": "1.2.4", "xhr2-cookies": "1.1.0" } }, "web3-providers-ipc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.2.2.tgz", - "integrity": "sha512-t97w3zi5Kn/LEWGA6D9qxoO0LBOG+lK2FjlEdCwDQatffB/+vYrzZ/CLYVQSoyFZAlsDoBasVoYSWZK1n39aHA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.2.4.tgz", + "integrity": "sha512-8J3Dguffin51gckTaNrO3oMBo7g+j0UNk6hXmdmQMMNEtrYqw4ctT6t06YOf9GgtOMjSAc1YEh3LPrvgIsR7og==", "requires": { "oboe": "2.1.4", "underscore": "1.9.1", - "web3-core-helpers": "1.2.2" + "web3-core-helpers": "1.2.4" } }, "web3-providers-ws": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.2.2.tgz", - "integrity": "sha512-Wb1mrWTGMTXOpJkL0yGvL/WYLt8fUIXx8k/l52QB2IiKzvyd42dTWn4+j8IKXGSYYzOm7NMqv6nhA5VDk12VfA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.2.4.tgz", + "integrity": "sha512-F/vQpDzeK+++oeeNROl1IVTufFCwCR2hpWe5yRXN0ApLwHqXrMI7UwQNdJ9iyibcWjJf/ECbauEEQ8CHgE+MYQ==", "requires": { + "@web3-js/websocket": "^1.0.29", "underscore": "1.9.1", - "web3-core-helpers": "1.2.2", - "websocket": "github:web3-js/WebSocket-Node#polyfill/globalThis" + "web3-core-helpers": "1.2.4" } }, "web3-shh": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.2.2.tgz", - "integrity": "sha512-og258NPhlBn8yYrDWjoWBBb6zo1OlBgoWGT+LL5/LPqRbjPe09hlOYHgscAAr9zZGtohTOty7RrxYw6Z6oDWCg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.2.4.tgz", + "integrity": "sha512-z+9SCw0dE+69Z/Hv8809XDbLj7lTfEv9Sgu8eKEIdGntZf4v7ewj5rzN5bZZSz8aCvfK7Y6ovz1PBAu4QzS4IQ==", "requires": { - "web3-core": "1.2.2", - "web3-core-method": "1.2.2", - "web3-core-subscriptions": "1.2.2", - "web3-net": "1.2.2" + "web3-core": "1.2.4", + "web3-core-method": "1.2.4", + "web3-core-subscriptions": "1.2.4", + "web3-net": "1.2.4" } }, "web3-utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.2.2.tgz", - "integrity": "sha512-joF+s3243TY5cL7Z7y4h1JsJpUCf/kmFmj+eJar7Y2yNIGVcW961VyrAms75tjUysSuHaUQ3eQXjBEUJueT52A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.2.4.tgz", + "integrity": "sha512-+S86Ip+jqfIPQWvw2N/xBQq5JNqCO0dyvukGdJm8fEWHZbckT4WxSpHbx+9KLEWY4H4x9pUwnoRkK87pYyHfgQ==", "requires": { "bn.js": "4.11.8", "eth-lib": "0.2.7", @@ -19899,9 +21518,9 @@ "dev": true }, "webpack": { - "version": "4.41.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.2.tgz", - "integrity": "sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==", + "version": "4.41.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.3.tgz", + "integrity": "sha512-EcNzP9jGoxpQAXq1VOoTet0ik7/VVU1MovIfcUSAjLowc7GhcQku/sOXALvq5nPpSei2HF6VRhibeJSC3i/Law==", "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-module-context": "1.8.5", @@ -19923,9 +21542,19 @@ "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.1", + "terser-webpack-plugin": "^1.4.3", "watchpack": "^1.6.0", "webpack-sources": "^1.4.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "webpack-cli": { @@ -20103,6 +21732,14 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } } } }, @@ -20236,32 +21873,6 @@ } } }, - "websocket": { - "version": "github:web3-js/WebSocket-Node#905deb4812572b344f5801f8c9ce8bb02799d82e", - "from": "github:web3-js/WebSocket-Node#polyfill/globalThis", - "requires": { - "debug": "^2.2.0", - "es5-ext": "^0.10.50", - "nan": "^2.14.0", - "typedarray-to-buffer": "^3.1.5", - "yaeti": "^0.0.6" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, "websocket-driver": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", @@ -20289,8 +21900,7 @@ "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", - "dev": true + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" }, "whatwg-mimetype": { "version": "2.3.0", @@ -20522,6 +22132,16 @@ "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", "requires": { "mkdirp": "^0.5.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "requires": { + "minimist": "^1.2.5" + } + } } }, "write-file-atomic": { @@ -20579,6 +22199,18 @@ "timed-out": "^4.0.1", "url-set-query": "^1.0.0", "xhr": "^2.0.4" + }, + "dependencies": { + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + } } }, "xhr-request-promise": { @@ -20589,6 +22221,11 @@ "xhr-request": "^1.0.1" } }, + "xhr2": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.0.tgz", + "integrity": "sha512-BDtiD0i2iKPK/S8OAZfpk6tyzEDnKKSjxWHcMBVmh+LuqJ8A32qXTyOx+TVOg2dKvq6zGBq2sgKPkEeRs1qTRA==" + }, "xhr2-cookies": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xhr2-cookies/-/xhr2-cookies-1.1.0.tgz", @@ -20757,6 +22394,36 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "zeroone-contracts": { + "version": "github:Neos1/zeroone-contracts#b3bf7308f9b96dc3faa1563693afb3a74e3712fd", + "from": "github:Neos1/zeroone-contracts#zeroone", + "requires": { + "solc": "^0.6.1", + "solidity-bytes-utils": "0.0.6", + "truffle": "^5.1.8", + "zeroone-translator": "github:Neos1/zeroone-translator#master", + "zeroone-voting-vm": "github:Neos1/zeroone-voting-vm#master" + }, + "dependencies": { + "zeroone-translator": { + "version": "github:Neos1/zeroone-translator#ddf70025bd83f05e5546240b027e583f7074f794", + "from": "github:Neos1/zeroone-translator#master" + } + } + }, + "zeroone-translator": { + "version": "github:Neos1/zeroone-translator#ddf70025bd83f05e5546240b027e583f7074f794", + "from": "github:Neos1/zeroone-translator" + }, + "zeroone-voting-vm": { + "version": "github:Neos1/zeroone-voting-vm#abaa67309763c0f1ce3921cd69998508ea7ce83e", + "from": "github:Neos1/zeroone-voting-vm#master", + "requires": { + "solc": "^0.6.1", + "solidity-bytes-utils": "0.0.6", + "truffle": "^5.1.14" + } } } } diff --git a/package.json b/package.json index cf708e48..62825652 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,16 @@ }, "scripts": { "test": "jest", - "dev": "webpack-dev-server --hot --config webpack.dev.js", "build": "webpack --mode=production --config webpack.dev.js && electron-builder", - "electron-dev": "concurrently \"npm run dev\" \"wait-on http://localhost:3000 && electron ./src/electron.js\"", - "electron-build": "electron-builder", + "react-dev": "webpack-dev-server --hot --config webpack.dev.js", + "electron": "electron ./src/electron.js", + "electron-dev": "concurrently \"webpack-dev-server --hot --config webpack.dev.js\" \"wait-on http://localhost:3000 && electron ./src/electron.js\"", "storybook": "start-storybook", "docs": "jsdoc -c ./jsdoc.json" }, "license": "ISC", "dependencies": { - "@babel/core": "^7.6.4", + "@babel/core": "^7.8.7", "@babel/preset-react": "^7.6.3", "@babel/preset-stage-0": "^7.0.0", "@namics/stylelint-bem": "^6.1.0", @@ -27,27 +27,42 @@ "electron-is-dev": "^1.1.0", "electron-localshortcut": "^3.1.0", "eslint-plugin-jest": "^23.1.1", + "ethereumjs-common": "^1.5.0", "ethereumjs-tx": "^2.1.1", "ethereumjs-util": "^6.2.0", "ethereumjs-wallet": "^0.6.3", "i18next": "^19.0.0", "i18next-xhr-backend": "^3.2.2", + "litepicker": "^1.0.29", "mobx": "^5.15.0", "mobx-react": "^6.1.4", "mobx-react-form": "^2.0.8", "mobx-utils": "^5.5.1", + "moment": "^2.24.0", "prop-types": "^15.7.2", + "query-string": "^6.9.0", "react": "^16.10.2", + "react-collapse": "^5.0.0", + "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.10.2", "react-i18next": "^11.1.0", + "react-id-generator": "^3.0.0", + "react-js-pagination": "^3.0.2", "react-portal": "^4.2.0", "react-router-dom": "^5.1.2", + "react-tabs": "^3.0.0", + "recharts": "^2.0.0-beta.1", + "solc": "^0.6.1", "stylelint-config-rational-order": "^0.1.2", "stylelint-config-standard": "^19.0.0", "validatorjs": "^3.17.1", - "web3": "^1.0.0-beta.34", + "web3": "^1.2.4", + "web3-utils": "^1.2.4", "webpack": "^4.41.2", - "webpack-dev-server": "^3.8.2" + "webpack-dev-server": "^3.8.2", + "zeroone-contracts": "github:Neos1/zeroone-contracts#zeroone", + "zeroone-translator": "github:Neos1/zeroone-translator", + "zeroone-voting-vm": "github:Neos1/zeroone-voting-vm#master" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.5.5", @@ -60,7 +75,7 @@ "better-docs": "^1.3.3", "clean-webpack-plugin": "^3.0.0", "concurrently": "^5.0.0", - "copy-webpack-plugin": "^5.0.4", + "copy-webpack-plugin": "^5.1.1", "cross-env": "^6.0.3", "css-loader": "^3.2.0", "electron": "^3.0.11", @@ -71,13 +86,14 @@ "eslint-config-airbnb": "^18.0.1", "eslint-loader": "^3.0.2", "eslint-plugin-import": "^2.18.2", + "eslint-plugin-jsdoc": "^18.1.3", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-react": "^7.16.0", "eslint-plugin-react-hooks": "^1.7.0", "jest": "^24.9.0", "jest-css-modules": "^2.1.0", "jsdoc": "^3.6.3", - "node-sass": "^4.12.0", + "node-sass": "^4.13.1", "sass-loader": "^8.0.0", "style-loader": "^1.0.0", "wait-on": "^3.3.0", @@ -109,15 +125,19 @@ "appId": "ZeroOne", "files": [ "build/**/*", - "src/wallets/**/*", - "src/contracts/**/*", - "src/config.json", - "src/electron.js" + "src/electron.js", + "src/splash.html" + ], + "extraResources": [ + "build/wallets/**/*", + "build/contracts/**/*", + "build/config.json", + "node_modules/zeroone-contracts/**/*", + "node_modules/zeroone-voting-vm/**/*", + "node_modules/zeroone-translator**/*" ], "win": { - "target": [ - "portable" - ] + "target": "dir" }, "linux": { "target": "deb" @@ -131,8 +151,7 @@ "portable": { "artifactName": "voter_portable--win.exe" }, - "compression": "store", - "asar": false + "compression": "store" }, "jest": { "moduleNameMapper": { diff --git a/src/assets/images/activeTab.svg b/src/assets/images/activeTab.svg new file mode 100644 index 00000000..be16684f --- /dev/null +++ b/src/assets/images/activeTab.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/assets/images/activeVoting.svg b/src/assets/images/activeVoting.svg new file mode 100644 index 00000000..5c86abe4 --- /dev/null +++ b/src/assets/images/activeVoting.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/assets/styles/includes/_fonts.scss b/src/assets/styles/includes/_fonts.scss index b4c0eb13..ffecfc70 100644 --- a/src/assets/styles/includes/_fonts.scss +++ b/src/assets/styles/includes/_fonts.scss @@ -1,17 +1,3 @@ @import url('https://fonts.googleapis.com/css?family=Roboto+Mono:700&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap&subset=cyrillic'); -@font-face { - font-weight: 300; - font-family: "Grotesk"; - src: url("../fonts/AktivGroteskCorp-Light.ttf"); -} -@font-face { - font-weight: 400; - font-family: "Grotesk"; - src: url("../fonts/AktivGroteskCorp-Regular.ttf"); -} -@font-face { - font-weight: 700; - font-family: "Grotesk"; - src: url("../fonts/AktivGroteskCorp-Bold.ttf"); -} diff --git a/src/assets/styles/includes/_mixin.scss b/src/assets/styles/includes/_mixin.scss new file mode 100644 index 00000000..ea0778c4 --- /dev/null +++ b/src/assets/styles/includes/_mixin.scss @@ -0,0 +1,59 @@ +@import '../partials/variables'; + +@mixin scrollbar { + &::-webkit-scrollbar { + width: 20px; + } + + /* Track */ + &::-webkit-scrollbar-track { + background: #fff; + border: 2px solid #f1f1f1; + } + + /* Handle */ + &::-webkit-scrollbar-thumb { + background-image: url(../../assets/images/thumb.png); + background-repeat: no-repeat; + background-position: center; + border: 1px solid $border; + } + + &::-webkit-scrollbar-button:vertical:decrement{ + width: 20px; + height: 20px; + background-image: url(../../assets/images/scrollbarBtn.svg); + background-repeat: no-repeat; + background-position: center; + background-size: cover; + } + + &::-webkit-scrollbar-button:vertical:increment{ + position: relative; + width: 20px; + height: 20px; + background-image: url(../../assets/images/scrollbarBtnDown.svg); + background-repeat: no-repeat; + background-position: center; + background-size: cover; + } +} + +@mixin title { + margin-top: 18px; + margin-bottom: 8px; + color: #000; + font-weight: 700; + font-size: 24px; + font-family: "Roboto"; + line-height: 28px; + text-align: center; +} + +@mixin clearfix { + &::after { + display: block; + clear: both; + content: ""; + } +} \ No newline at end of file diff --git a/src/assets/styles/partials/_variables.scss b/src/assets/styles/partials/_variables.scss index 354989c4..ef5edd4b 100644 --- a/src/assets/styles/partials/_variables.scss +++ b/src/assets/styles/partials/_variables.scss @@ -1,6 +1,7 @@ $primary: #000; $secondary: #4d4d4d; $white: #fff; +$gray: #c8c9ca; $grey: rgba( $color: $primary, diff --git a/src/assets/styles/style.scss b/src/assets/styles/style.scss index c6318625..091b06d5 100644 --- a/src/assets/styles/style.scss +++ b/src/assets/styles/style.scss @@ -6,7 +6,13 @@ margin: 0; padding: 0; font-weight: 400; - font-family: "Grotesk"; + font-family: "Roboto"; +} + +html, +body, +#root { + height: 100%; } body { @@ -43,6 +49,223 @@ a { } svg { - width: 18px; - height: 18px; + width: 20px; + height: 20px; +} + +.step-indicator { + position: absolute; + bottom: 100%; + left: 50%; + color: #808080; + font-size: 12px; + text-align: center; + transform: translate(-50%, 30%); +} + +.progress-block{ + position: relative; + display: inline-block; + width: 80px; + height: 80px; + transition: .3s linear; + + .progress-line{ + position: relative; + top: 50%; + left: -96px; + width: 96px; + height: 4px; + overflow: hidden; + border: unset; + transform: translateY(-50%); + opacity: 0; + transition: .3s ease-in; + + &::after { + position: absolute; + top: 2px; + left: -100%; + z-index: 1; + width: 200%; + height: 2px; + background-image: linear-gradient(90deg, #000, #000 75%, transparent 75%, transparent 100%); + background-size: 4px 2px; + animation: ticker-animation 6s linear; + animation-play-state: running; + animation-delay: 0s; + animation-iteration-count: infinite; + content: ''; + } + } + + &__icon { + position: absolute; + top: 50%; + left: 50%; + display: inline-block; + transform: translate(-50%, -50%); + + & > svg { + width: 42px; + height: 42px; + + path { + transition: .2s; + fill: rgba($color: $primary, $alpha: .5); + } + } + } + + &>svg { + position: absolute; + top: 0%; + left: 50%; + width: 80px; + height: 80px; + transform: translate(-50%, 0%) scale(1); + } + + .stroke-still { + stroke-dasharray: 2; + stroke-width: 4; + transition: .2s; + stroke: rgba($color: $primary, $alpha: .5); + } + + & > img { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: .5; + } + + &>p{ + width: 100%; + margin: 90px 0 0; + color: #37474F; + font-size: 12px; + opacity: .5; + + span { + display: block; + margin-bottom: 10px; + } + + &+strong { + display: inline-block; + width: 165%; + margin: 5px 0 0 -35%; + } + } + + &.success{ + .progress-block__icon { + &>svg { + path { + fill: rgba($color: $primary, $alpha: 1); + } + } + } + + .stroke-still { + stroke-dasharray: 0; + stroke: rgba($color: $primary, $alpha: 1); + } + + & > img { + opacity: 1; + } + + .progress-line{ + width: 96px; + border: unset; + opacity: 1; + + &::after { + background-color: $primary; + background-image: unset; + } + } + + &>p{ + color: $primary; + opacity: 1; + } + } + &.active{ + & > img { + opacity: 1; + } + + .progress-block__icon { + svg { + path { + fill: $primary + } + } + } + + .progress-line { + opacity: 1; + } + + .stroke-still { + stroke: transparent; + } + + .stroke-animation { + stroke-width: 4; + transform-origin: center center; + animation: stroke-spacing 6s linear, stroke-color 6s linear; + animation-play-state: running; + animation-delay: 0s; + animation-iteration-count: infinite; + } + + &>p{ + color: $primary; + opacity: 1; + } + } +} + +.ReactCollapse--collapse { + transition: height 500ms; +} + +@keyframes stroke-spacing { + 0% { + stroke-dasharray: 2; + } + 100% { + stroke-dashoffset: -108; + stroke-dasharray: 2; + } +} + + +@keyframes stroke-color { + 0% { stroke: $primary; } + 24% { stroke: $primary; } + 25% { stroke: $primary; } + 49% { stroke: $primary; } + 50% { stroke: $primary; } + 74% { stroke: $primary; } + 75% { stroke: $primary; } + 99% { stroke: $primary; } +} + +@keyframes ticker-animation { + 0% { + left: -100%; + } + 100% { + left: 0; + } +} + +::-webkit-scrollbar-thumb { + border: 1px solid $border; } \ No newline at end of file diff --git a/src/components/AddExisitingProject/index.js b/src/components/AddExisitingProject/index.js index baa72b36..e0263d91 100644 --- a/src/components/AddExisitingProject/index.js +++ b/src/components/AddExisitingProject/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import propTypes from 'prop-types'; -import { NavLink } from 'react-router-dom'; +import { NavLink, Redirect } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; import { withTranslation } from 'react-i18next'; import Button from '../Button/Button'; @@ -23,9 +23,7 @@ import styles from '../Login/Login.scss'; class AddExistingProject extends Component { connectForm = new ConnectProjectForm({ hooks: { - onSuccess: (form) => { - this.connectProject(form); - }, + onSuccess: (form) => this.connectProject(form), onError: () => { this.showError(); }, @@ -36,6 +34,7 @@ class AddExistingProject extends Component { default: 0, loading: 1, success: 2, + redirect: 3, } // eslint-disable-next-line react/static-property-placement @@ -44,6 +43,9 @@ class AddExistingProject extends Component { checkProject: propTypes.func.isRequired, addProjectToList: propTypes.func.isRequired, displayAlert: propTypes.func.isRequired, + checkIsQuestionsUploaded: propTypes.func.isRequired, + gotoProject: propTypes.func.isRequired, + setProjectAddress: propTypes.func.isRequired, }).isRequired, t: propTypes.func.isRequired, }; @@ -52,29 +54,36 @@ class AddExistingProject extends Component { super(props); this.state = { currentStep: this.steps.default, + projectName: '', + projectAddress: '', }; } connectProject = (form) => { const { steps } = this; const { appStore, t } = this.props; - const { name, address } = form.values(); + const { name, address: rawAddress } = form.values(); + const address = rawAddress.trim(); this.setState({ currentStep: steps.loading, }); return new Promise((resolve, reject) => { appStore.checkProject(address) .then(() => { - this.setState({ currentStep: steps.success }); + this.setState({ + currentStep: steps.success, + projectAddress: address, + projectName: name, + }); appStore.addProjectToList({ name, address }); - resolve(); + return resolve(); }) .catch(() => { appStore.displayAlert(t('errors:tryAgain'), 3000); - this.state = { + this.setState({ currentStep: steps.default, - }; - reject(); + }); + return reject(); }); }); } @@ -84,8 +93,27 @@ class AddExistingProject extends Component { appStore.displayAlert(t('errors:validationError'), 3000); } + startUploading = (address) => { + const { appStore } = this.props; + appStore.setProjectAddress(address); + this.setState({ currentStep: this.steps.redirect }); + } + + checkProject = async ({ + address, + name, + }) => { + const { appStore } = this.props; + const isQuestionsUploaded = await appStore.checkIsQuestionsUploaded(address); + // eslint-disable-next-line no-unused-expressions + isQuestionsUploaded + ? appStore.gotoProject({ address, name }) + : this.startUploading(address); + } + renderSwitch(step) { const { steps } = this; + const { projectAddress, projectName } = this.state; const { t } = this.props; switch (step) { case steps.default: @@ -100,7 +128,15 @@ class AddExistingProject extends Component { ); case steps.success: - return ; + return ( + + ); + case steps.redirect: + return ; default: return ''; } @@ -121,8 +157,8 @@ class AddExistingProject extends Component { const InputBlock = withTranslation()(({ t, form }) => ( - {t('headings:сonnectProject.heading')} - {t('headings:сonnectProject.subheading')} + {t('headings:connectProject.heading')} + {t('headings:connectProject.subheading')} @@ -157,13 +193,15 @@ const InputBlock = withTranslation()(({ t, form }) => ( )); -const MessageBlock = withTranslation()(({ t }) => ( +const MessageBlock = withTranslation()(observer(({ + t, onClick, address, name, +}) => ( {t('headings:projectConnected.heading')} {t('headings:projectConnected.subheading')} - } type="submit"> + } onClick={() => { onClick({ address, name }); }}> {t('buttons:toConnectedProject')} @@ -172,7 +210,7 @@ const MessageBlock = withTranslation()(({ t }) => ( -)); +))); InputBlock.propTypes = { form: propTypes.shape({ diff --git a/src/components/Button/Button.js b/src/components/Button/Button.js index 522fa169..4afb07ee 100644 --- a/src/components/Button/Button.js +++ b/src/components/Button/Button.js @@ -1,5 +1,5 @@ import React from 'react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; import styles from './Button.scss'; @@ -12,16 +12,33 @@ const Button = ({ icon, iconPosition, onClick, + hint, + className, }) => ( // eslint-disable-next-line react/button-has-type + { + hint + ? ( + + + {hint} + + + ) + : null + } {icon} @@ -34,17 +51,26 @@ const Button = ({ ); Button.propTypes = { - children: propTypes.oneOfType([ - propTypes.string, - propTypes.shape({}), + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + PropTypes.shape({}), ]).isRequired, - icon: propTypes.node, - iconPosition: propTypes.bool, - type: propTypes.string, - disabled: propTypes.bool, - onClick: propTypes.func.isRequired, - theme: propTypes.string, - size: propTypes.string, + icon: PropTypes.node, + iconPosition: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + type: PropTypes.string, + disabled: PropTypes.bool, + onClick: PropTypes.func, + theme: PropTypes.string, + size: PropTypes.string, + hint: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + className: PropTypes.string, }; Button.defaultProps = { @@ -54,6 +80,9 @@ Button.defaultProps = { icon: null, iconPosition: false, disabled: false, + onClick: () => {}, + hint: null, + className: '', }; export default Button; diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss index bfb36fb5..c1fe4c52 100644 --- a/src/components/Button/Button.scss +++ b/src/components/Button/Button.scss @@ -10,6 +10,49 @@ cursor: pointer; transition: 0.2s linear; + &__hint { + position: absolute; + bottom: calc(100% + 10px); + left: 50%; + z-index: 999; + text-align: left; + transform: translateX(-50%); + visibility: hidden; + opacity: 0; + transition: opacity 0.5s; + + &-content { + display: inline-block; + padding: 14px 16px 16px; + color: #4d4d4d; + font-size: 11px; + line-height: 13px; + white-space: nowrap; + text-align: left; + background: #fff; + border: 1px solid #000; + border-radius: 2px; + } + } + + &--with-hint { + position: relative; + + &:hover { + .btn__hint { + visibility: visible; + opacity: 1; + } + } + + &:active, + &:focus { + .btn__hint { + color: #000; + } + } + } + svg, &__icon, &__text { @@ -38,8 +81,11 @@ &[disabled]{ cursor: default; - opacity: .5; - } + + .btn__content { + opacity: .5; + } + } &--black { color: $white; @@ -61,6 +107,22 @@ } } + &--gray-bordered { + color: $gray; + font-size: 14px; + line-height: 107.5%; + background-color: transparent; + border: 1px solid $gray; + border-radius: 2px; + transition: color 0.2s, border-color 0.2s; + + &:hover, + &:active { + color: $primary; + border-color: $primary; + } + } + &--white { color: $primary; background-color: $white; @@ -152,11 +214,35 @@ } &:hover { color: $primary; - } + } + &:active { + color: $white; + } } &--showseed { @extend .btn--white; + svg{ + path{ + fill: $white; + } + } + &:hover { + svg { + path { + fill: $white; + stroke: $primary; + } + } + } + &:active { + svg { + path { + fill: $primary; + stroke: $white; + } + } + } } &--back { @@ -176,5 +262,94 @@ &--310 { width: 310px; } + + &--with-play-icon { + width: 100%; + padding: 26px 96px 26px 100px; + background: #fff; + border: 1px solid #e1e4e8; + + .btn__text { + color: #000; + font-weight: 300; + font-size: 18px; + font-family: "Roboto"; + line-height: 21px; + } + + svg { + width: auto; + height: auto; + } + } + + &--question-start { + display: inline-block; + background: #fff; + + .btn__text { + display: inline-block; + width: 65%; + color: $primary; + font-size: 11px; + } + + svg { + width: 30px; + height: 30px; + } + } + + &--toggle-user { + padding: 10px 16px 9px; + background: #fff; + border: 1px solid #e1e4e8; + border-radius: 2px; + + .btn__text { + color: #000; + font-weight: 500; + font-size: 14px; + font-family: "Roboto"; + line-height: 107.5%; + } + } + + &--voting-decision { + width: 50%; + padding: 28px 30px 23px; + background-color: #fff; + + .btn__text { + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + svg { + width: auto; + height: auto; + } + + &:hover { + border-color: $primary; + } + + &:active { + background-color: $primary; + + .btn__text { + color: $white; + } + + svg { + path { + stroke: $white; + } + } + } + } + } diff --git a/src/components/Button/index.js b/src/components/Button/index.js new file mode 100644 index 00000000..c49322d0 --- /dev/null +++ b/src/components/Button/index.js @@ -0,0 +1,7 @@ +import Button from './Button'; + +export default Button; + +export { + Button, +}; diff --git a/src/components/Container/Container.scss b/src/components/Container/Container.scss index 632cd7be..f4c86800 100644 --- a/src/components/Container/Container.scss +++ b/src/components/Container/Container.scss @@ -1,7 +1,14 @@ .container { position: relative; + top: 55px; width: 100%; max-width: 1120px; - height: 100vh; + // height: calc(100vh - 110px); + min-height: calc(100% - 108px); margin: 0 auto; + padding-bottom: 50px; + + &--small { + max-width: 845px; + } } diff --git a/src/components/Container/index.js b/src/components/Container/index.js index e1739d91..c666382b 100644 --- a/src/components/Container/index.js +++ b/src/components/Container/index.js @@ -3,13 +3,17 @@ import propTypes from 'prop-types'; import styles from './Container.scss'; -const Container = ({ children }) => ( - +const Container = ({ children, className }) => ( + {children} ); Container.propTypes = { children: propTypes.node.isRequired, + className: propTypes.string, +}; +Container.defaultProps = { + className: '', }; export default Container; diff --git a/src/components/CreateGroupQuestions/CreateGroupQuestions.js b/src/components/CreateGroupQuestions/CreateGroupQuestions.js new file mode 100644 index 00000000..78b6ba3b --- /dev/null +++ b/src/components/CreateGroupQuestions/CreateGroupQuestions.js @@ -0,0 +1,114 @@ +import React from 'react'; +import { withTranslation, Trans } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { Hint } from '../Hint'; +import CreateGroupQuestionsForm from '../../stores/FormsStore/CreateGroupQuestionsForm'; +import Input from '../Input'; +import { TokenName } from '../Icons'; +import Button from '../Button/Button'; +// import InputTextarea from '../Input/InputTextarea'; +import { systemQuestionsId } from '../../constants'; + +import styles from './CreateGroupQuestions.scss'; + +@withTranslation() +@inject('dialogStore', 'projectStore') +@observer +class CreateGroupQuestions extends React.PureComponent { + form = new CreateGroupQuestionsForm({ + hooks: { + onSuccess: (form) => { + const { + projectStore: { + rootStore: { + Web3Service, + }, + questionStore, + }, + projectStore, + dialogStore, + } = this.props; + const questionId = systemQuestionsId.connectGroupQuestions; + const { name } = form.values(); + const [question] = questionStore.getQuestionById(questionId); + const { paramTypes, groupId } = question; + const encodedParams = Web3Service.web3.eth.abi.encodeParameters(['tuple(uint256,uint256,uint256,uint256,uint256)', `tuple(${paramTypes.join(',')})`], [[0, 0, 0, 0, 0], [name]]); + // const votingData = encodedParams.replace('0x', methodSelector); + // TODO groupId fix + projectStore.setVotingData(questionId, groupId, encodedParams); + dialogStore.toggle('password_form_questions'); + return Promise.resolve(); + }, + onError: () => { + /* eslint-disable-next-line */ + console.error('error'); + }, + }, + }); + + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.shape({ + toggle: PropTypes.func.isRequired, + }).isRequired, + projectStore: PropTypes.shape().isRequired, + }; + + render() { + const { props, form } = this; + const { t, projectStore: { historyStore } } = props; + return ( + + + {t('dialogs:createAGroupOfQuestions')} + + {t('other:createGroupQuestionsDescription')} + + + + {t('other:createNameForTheGroupQuestions')} + + + + + + {/* */} + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:create')} + + + + {t('other:voteLaunchAdminDescription')} + + + ); + } +} + +export default CreateGroupQuestions; diff --git a/src/components/CreateGroupQuestions/CreateGroupQuestions.scss b/src/components/CreateGroupQuestions/CreateGroupQuestions.scss new file mode 100644 index 00000000..6f90303a --- /dev/null +++ b/src/components/CreateGroupQuestions/CreateGroupQuestions.scss @@ -0,0 +1,54 @@ +@import '../../assets/styles/includes/mixin'; + +.create-group-questions { + padding: 61px 30px 33px; + + &__title { + @include title; + + .hint { + margin-left: 16px; + vertical-align: middle; + } + } + + &__subtitle { + max-width: 297px; + margin: 0 auto; + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + text-align: center; + } + + &__subtext { + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + } + + form { + margin-top: 48px; + } + + button { + width: 100%; + margin-bottom: 8px; + } + + .field { + width: 100%; + margin-bottom: 32px; + + &__input--textarea { + width: 100%; + min-width: 100%; + max-height: 100px; + } + + &--textarea { + margin-bottom: 48px; + } + } +} \ No newline at end of file diff --git a/src/components/CreateGroupQuestions/CreateGroupQuestions.test.js b/src/components/CreateGroupQuestions/CreateGroupQuestions.test.js new file mode 100644 index 00000000..5ff49367 --- /dev/null +++ b/src/components/CreateGroupQuestions/CreateGroupQuestions.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CreateGroupQuestions from './CreateGroupQuestions'; + +jest.mock('../../utils/Validator'); + +describe('CreateGroupQuestions', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/CreateGroupQuestions/index.js b/src/components/CreateGroupQuestions/index.js new file mode 100644 index 00000000..2388dc76 --- /dev/null +++ b/src/components/CreateGroupQuestions/index.js @@ -0,0 +1,3 @@ +import CreateGroupQuestions from './CreateGroupQuestions'; + +export default CreateGroupQuestions; diff --git a/src/components/CreateNewProjectWithTokens/InputProjectData.js b/src/components/CreateNewProjectWithTokens/InputProjectData.js new file mode 100644 index 00000000..dfcd288a --- /dev/null +++ b/src/components/CreateNewProjectWithTokens/InputProjectData.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import FormBlock from '../FormBlock'; +import Heading from '../Heading'; +import Input from '../Input'; +import { TokenName, Password, BackIcon } from '../Icons'; +import Button from '../Button/Button'; +import Explanation from '../Explanation'; +import CreateProjectForm from '../../stores/FormsStore/CreateProject'; + +import styles from '../Login/Login.scss'; + +@withTranslation() +@observer +class InputProjectData extends React.Component { + static propTypes = { + form: PropTypes.instanceOf(CreateProjectForm).isRequired, + onClick: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, + }; + + render() { + const { props } = this; + const { onClick, form, t } = props; + return ( + + + {t('headings:projectCreating.heading')} + + {t('headings:projectCreating.subheading.0')} + + {t('headings:projectCreating.subheading.1')} + + + + + + + + + + + + {t('buttons:continue')} + + + + + + {t('explanations:project.name')} + + + + + {t('explanations:freeze')} + + + + } onClick={onClick} disabled={form.loading}> + {t('buttons:back')} + + + + ); + } +} + +export default InputProjectData; diff --git a/src/components/CreateNewProjectWithTokens/index.js b/src/components/CreateNewProjectWithTokens/index.js index 1bdf36f5..2c361f3c 100644 --- a/src/components/CreateNewProjectWithTokens/index.js +++ b/src/components/CreateNewProjectWithTokens/index.js @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import { observable } from 'mobx'; import { inject, observer } from 'mobx-react'; import propTypes from 'prop-types'; import { NavLink, Redirect } from 'react-router-dom'; @@ -10,14 +11,14 @@ import Container from '../Container'; import LoadingBlock from '../LoadingBlock'; import Input from '../Input'; import StepIndicator from '../StepIndicator'; -import Explanation from '../Explanation'; import { - BackIcon, Address, TokenName, Password, + BackIcon, Address, } from '../Icons'; import ConnectTokenForm from '../../stores/FormsStore/ConnectToken'; import CreateProjectForm from '../../stores/FormsStore/CreateProject'; import styles from '../Login/Login.scss'; +import InputProjectData from './InputProjectData'; @withTranslation() @inject('userStore', 'appStore') @@ -30,7 +31,7 @@ class CreateNewProjectWithTokens extends Component { }, }); - createProject = new CreateProjectForm({ + @observable createProject = new CreateProjectForm({ hooks: { onSuccess: (form) => this.gotoUploading(form), onError: () => {}, @@ -63,7 +64,8 @@ class CreateNewProjectWithTokens extends Component { checkToken = (form) => { const { steps } = this; - const { address } = form.values(); + const { address: rawAddress } = form.values(); + const address = rawAddress.trim(); const { appStore } = this.props; this.setState({ currentStep: steps.check, @@ -149,7 +151,7 @@ class CreateNewProjectWithTokens extends Component { render() { const { steps } = this; const { currentStep, indicatorStep } = this.state; - if (currentStep === steps.uploading) return ; + if (currentStep === steps.uploading) return ; return ( @@ -209,44 +211,6 @@ const ContractConfirmation = inject('appStore')(observer(withTranslation()(({ t, )))); -const InputProjectData = withTranslation()(({ - t, form, onClick, -}) => ( - - - {t('headings:projectCreating.heading')} - - {t('headings:projectCreating.subheading.0')} - - {t('headings:projectCreating.subheading.1')} - - - - - - - - - - - - {t('buttons:continue')} - - - - - - {t('explanations:project.name')} - - - - } onClick={onClick}> - {t('buttons:back')} - - - -)); - CreateNewProjectWithTokens.propTypes = { appStore: propTypes.shape({ checkErc: propTypes.func.isRequired, @@ -275,12 +239,5 @@ InputTokenAddress.propTypes = { ContractConfirmation.propTypes = { onSubmit: propTypes.func.isRequired, }; -InputProjectData.propTypes = { - form: propTypes.shape({ - $: propTypes.func.isRequired, - onSubmit: propTypes.func.isRequired, - }).isRequired, - onClick: propTypes.func.isRequired, -}; export default CreateNewProjectWithTokens; diff --git a/src/components/CreateNewProjectWithoutTokens/index.js b/src/components/CreateNewProjectWithoutTokens/index.js index cc2286a7..8f15194c 100644 --- a/src/components/CreateNewProjectWithoutTokens/index.js +++ b/src/components/CreateNewProjectWithoutTokens/index.js @@ -1,3 +1,4 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import propTypes from 'prop-types'; @@ -16,6 +17,8 @@ import { } from '../Icons'; import CreateTokenForm from '../../stores/FormsStore/CreateToken'; import CreateProjectForm from '../../stores/FormsStore/CreateProject'; +import AppStore from '../../stores/AppStore/AppStore'; +import UserStore from '../../stores/UserStore'; import styles from '../Login/Login.scss'; @@ -23,6 +26,12 @@ import styles from '../Login/Login.scss'; @inject('userStore', 'appStore') @observer class CreateNewProjectWithoutTokens extends Component { + static propTypes = { + appStore: propTypes.instanceOf(AppStore).isRequired, + userStore: propTypes.instanceOf(UserStore).isRequired, + t: propTypes.func.isRequired, + }; + form = new CreateTokenForm({ hooks: { onSuccess: (form) => this.createToken(form), @@ -42,6 +51,7 @@ class CreateNewProjectWithoutTokens extends Component { creation: 2, tokenCreated: 3, projectInfo: 4, + uploading: 5, } constructor(props) { @@ -180,8 +190,9 @@ class CreateNewProjectWithoutTokens extends Component { } render() { + const { steps } = this; const { currentStep, indicatorStep } = this.state; - if (currentStep === 'uploading') return ; + if (currentStep === steps.uploading) return ; return ( @@ -196,7 +207,9 @@ class CreateNewProjectWithoutTokens extends Component { const CreateTokenData = withTranslation()(inject('userStore', 'appStore')(observer((({ t, userStore: { address }, appStore: { balances }, form, }) => ( - + {t('headings:newTokens.heading')} {t('headings:newTokens.subheading')} @@ -328,6 +341,7 @@ const InputProjectData = withTranslation()(({ theme="back" icon={} onClick={onClick} + disabled={form.loading} > {t('buttons:back')} @@ -335,24 +349,6 @@ const InputProjectData = withTranslation()(({ )); -CreateNewProjectWithoutTokens.propTypes = { - appStore: propTypes.shape({ - deployContract: propTypes.func.isRequired, - checkReceipt: propTypes.func.isRequired, - deployArgs: propTypes.arrayOf(propTypes.any).isRequired, - displayAlert: propTypes.func.isRequired, - setProjectName: propTypes.func.isRequired, - password: propTypes.string.isRequired, - setDeployArgs: propTypes.func.isRequired, - }).isRequired, - userStore: propTypes.shape({ - readWallet: propTypes.func.isRequired, - checkBalance: propTypes.func.isRequired, - address: propTypes.string.isRequired, - setPassword: propTypes.func.isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; CreateTokenData.propTypes = { form: propTypes.shape({ onSubmit: propTypes.func.isRequired, @@ -360,9 +356,11 @@ CreateTokenData.propTypes = { loading: propTypes.bool.isRequired, }).isRequired, }; + TokenCreationAlert.propTypes = { onSubmit: propTypes.func.isRequired, }; + InputProjectData.propTypes = { form: propTypes.shape({ $: propTypes.func.isRequired, diff --git a/src/components/CreateNewQuestion/CreateNewQuestion.js b/src/components/CreateNewQuestion/CreateNewQuestion.js new file mode 100644 index 00000000..4258cf51 --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestion.js @@ -0,0 +1,126 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import SimpleDropdown from '../SimpleDropdown'; +import { QuestionIcon } from '../Icons'; +import CreateNewQuestionForm from './CreateNewQuestionForm'; +import StepIndicator from '../StepIndicator'; + +import styles from './CreateNewQuestion.scss'; + +/** + * Component for creating a new question + * + * @param selected + */ +@withTranslation() +@inject('projectStore') +@observer +class CreateNewQuestion extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + projectStore: PropTypes.shape().isRequired, + }; + + constructor() { + super(); + this.state = { + isSelected: false, + activeTab: 0, + selectedGroup: 0, + }; + } + + handleDropdownSelect = (selected) => { + this.setState({ isSelected: true, selectedGroup: selected.value }); + } + + /** + * Method for toggle active tab + * + * @param {number} number index active tab + */ + toggleActiveTab = (number) => { + this.setState({ activeTab: number }); + } + + render() { + const { isSelected, activeTab, selectedGroup } = this.state; + const { props } = this; + const { t, projectStore: { questionStore } } = props; + return ( + + + + + {t('other:createANewQuestion')} + + { + isSelected + ? ( + <> + + {t('other:basicInfo')} + + + + + > + ) + : null + } + + + + + + + + { + isSelected + ? ( + + {/* TODO change to description for selected group questions */} + Description text + + ) + : null + } + + + + { + isSelected + ? ( + this.toggleActiveTab(0)} + /> + ) + : ( + + + {t('other:selectQuestionGroup')} + + + ) + } + + + ); + } +} + +export default CreateNewQuestion; diff --git a/src/components/CreateNewQuestion/CreateNewQuestion.scss b/src/components/CreateNewQuestion/CreateNewQuestion.scss new file mode 100644 index 00000000..fef3e663 --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestion.scss @@ -0,0 +1,212 @@ +@import "../../assets/styles/partials/variables"; +@import "../../assets/styles/includes/mixin"; + +.create-question { + width: 100%; + padding: 40px 60px 0; + + &__top { + display: inline-block; + width: 100%; + margin: 0 -15px; + + .dropdown { + width: 100%; + } + + &-left, + &-right { + display: inline-block; + width: 50%; + padding: 0 15px; + text-align: left; + vertical-align: top; + } + } + + &__title { + color: $primary; + font-weight: 700; + font-size: 36px; + line-height: 42px; + } + + &__sub-title { + margin-top: 8px; + margin-bottom: 13px; + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + } + + &__content { + min-height: 324px; + padding: 10px 0; + + &--empty { + height: 100%; + max-height: 324px; + text-align: center; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + &-text { + display: inline-block; + max-width: 261px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + vertical-align: middle; + } + } + } + + &__description { + margin-top: 9px; + margin-bottom: 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + } + + &__step-progress { + .step-indicator { + position: relative; + left: unset; + text-align: left; + transform: unset; + } + + p { + &:first-child { + display: inline-block; + margin: 8px 13px; + } + + &:last-child { + float: left; + } + } + } + + &__field-remove { + position: absolute; + top: 50%; + padding: 15px 8px; + background-color: transparent; + border: unset; + outline: none; + transform: translateY(-50%); + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + + svg { + width: 10px; + height: 10px; + } + } + + &__field-description { + margin-top: 8px; + padding: 11px 18px 10px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 11px; + line-height: 13px; + background: #fff; + border: 1px dashed #000; + } + + &__form { + &--basic { + padding-top: 49px; + padding-bottom: 20px; + } + + &--dynamic { + padding-top: 46px; + padding-bottom: 29px; + } + + .field { + width: 100%; + + &__input--textarea { + width: 100%; + min-width: 100%; + } + } + + .dropdown { + width: 100%; + } + + &-row { + position: relative; + margin: 0 -15px; + padding: 15px 0; + text-align: left; + + &:hover { + .create-question__field-remove { + opacity: 1; + } + } + } + + &-col { + display: inline-block; + width: 50%; + padding: 0 15px; + vertical-align: top; + + &--full { + width: 100%; + } + + button { + width: 100%; + } + } + + &-text { + margin-top: 8px; + padding: 0 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + } + } + + &__button-new-param { + margin-bottom: 26px; + padding: 8px 0; + color: $placeholderColor; + font-size: 14px; + line-height: 16px; + text-align: left; + background-color: transparent; + border: unset; + border-bottom: 1px solid $placeholderColor; + outline: none; + cursor: pointer; + transition: color 0.2s, border-color 0.2s; + + &:hover, + &:active { + color: $primary; + border-bottom: 1px solid $primary; + } + } +} +.extra-padding { + padding-top: 20px; +} diff --git a/src/components/CreateNewQuestion/CreateNewQuestion.test.js b/src/components/CreateNewQuestion/CreateNewQuestion.test.js new file mode 100644 index 00000000..f9ac077a --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestion.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CreateNewQuestion from './CreateNewQuestion'; + +describe('CreateNewQuestion', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('handleDropdownSelect should change isSelected to true', () => { + expect(wrapperInstance.state.isSelected).toEqual(false); + wrapperInstance.handleDropdownSelect(); + expect(wrapperInstance.state.isSelected).toEqual(true); + }); + + it('toggleActiveTab with (1) should change activeTab to 1', () => { + expect(wrapperInstance.state.activeTab).toEqual(0); + wrapperInstance.toggleActiveTab(1); + expect(wrapperInstance.state.activeTab).toEqual(1); + }); +}); diff --git a/src/components/CreateNewQuestion/CreateNewQuestionForm.js b/src/components/CreateNewQuestion/CreateNewQuestionForm.js new file mode 100644 index 00000000..6f21b7bd --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestionForm.js @@ -0,0 +1,214 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import CreateQuestionBasicForm from '../../stores/FormsStore/CreateQuestionBasicForm'; +import CreateQuestionDynamicForm from '../../stores/FormsStore/CreateQuestionDynamicForm'; +import FormBasic from './FormBasic'; +import Question from '../../services/ContractService/entities/Question'; +import FormDynamic from './FormDynamic'; +import { systemQuestionsId } from '../../constants'; + +import styles from './CreateNewQuestion.scss'; + +@withTranslation() +@inject('dialogStore', 'projectStore') +@observer +class CreateNewQuestionForm extends React.PureComponent { + /** Form with basic info for new question */ + + formBasic = new CreateQuestionBasicForm({ + hooks: { + onSuccess: (form) => { + this.onBasicSubmit(form); + const { data: { groupId } } = this; + return Promise.resolve(); + }, + onError: () => { + console.log('error'); + }, + }, + }) + + /** Form with additional info for new question */ + formDynamic = new CreateQuestionDynamicForm({ + hooks: { + onSuccess: (form) => { + this.onDynamicSubmit(form); + return Promise.resolve(); + }, + onError: () => { + console.log('error'); + }, + }, + }); + + static propTypes = { + /** Current active tab */ + activeTab: PropTypes.number.isRequired, + /** Method called on toggle tab */ + onToggle: PropTypes.func.isRequired, + /** Method called on success fill all data */ + onComplete: PropTypes.func.isRequired, + dialogStore: PropTypes.shape({ + toggle: PropTypes.func.isRequired, + }).isRequired, + projectStore: PropTypes.shape().isRequired, + selectedGroup: PropTypes.number.isRequired, + }; + + constructor(props) { + super(props); + this.data = { + name: '', + description: '', + groupId: '', + time: 0, + formula: '', + target: '', + methodSelector: '', + }; + } + + /** + * Action on basic form submit + * + * @param form + */ + onBasicSubmit = (form) => { + const { props, data } = this; + const { selectedGroup } = props; + const { onToggle } = props; + const { + question_title: Name, + question_life_time: time, + description, + target, + methodSelector, + voting_formula: formula, + } = form.values(); + data.name = Name.trim(); + data.time = time; + data.formula = formula.trim(); + data.target = target.trim(); + data.description = description.trim(); + data.methodSelector = methodSelector.trim() || '0x00000000'; + data.groupId = selectedGroup; + onToggle(1); + } + + /** + * Method for getting uniq key for + * similar fields (input & select) + * + * @param {string} key name field + * @returns {string} uniq key for + * select & input + */ + getUniqKey = (key) => { + if (!key || !key.split) return ''; + return key.split('--')[1] || ''; + } + + /** + * Method for getting parameters array + * from form with dynamic fields + * + * @param {object} form form + * @returns {Array} array parameters + */ + getParametersFromForm = (form) => { + let values; + if (form.values()) { + values = form.values(); + } else { + values = {}; + } + const paramTypes = []; + const paramNames = []; + Object.keys(values).forEach((key, index) => { + if (Number.isInteger(index / 2) === false) return; + const uniqKey = this.getUniqKey(key); + const selectValue = values[`select--${uniqKey}`]; + const inputValue = values[`input--${uniqKey}`]; + paramTypes.push(selectValue); + paramNames.push(inputValue); + }); + // parameters = parameters.filter((e) => e !== ''); + return { paramTypes, paramNames }; + } + + /** + * Action on dynamic form submit + * + * @param form + */ + onDynamicSubmit = (form) => { + const { data } = this; + const { + dialogStore, + projectStore: { questionStore, rootStore: { Web3Service, contractService } }, + projectStore, + onComplete, + } = this.props; + const futureQuestionId = questionStore.questions.length + 1; + const { paramTypes, paramNames } = this.getParametersFromForm(form); + const question = new Question({ + id: futureQuestionId, + group: data.groupId, + name: data.name, + caption: data.description, + time: Number(data.time), + method: data.methodSelector, + formula: data.formula, + paramTypes, + paramNames, + }); + const rawVotingData = question.getUploadingParams(data.target); + const votingData = Web3Service.web3.eth.abi.encodeParameters( + ['tuple(uint, uint, uint, uint, uint)', 'tuple(bool, string, string, uint, uint, string[], string[], address, bytes4, string, bytes)'], + [[0, 0, 0, 0, 0], rawVotingData], + ); + projectStore.setVotingData(systemQuestionsId.addingNewQuestion, 0, votingData); + dialogStore.toggle('password_form_questions'); + this.formBasic.clear(); + form.clear(); + onComplete(); + } + + renderStep = () => { + const { props, formBasic, formDynamic } = this; + const { activeTab, onToggle } = props; + switch (activeTab) { + case 0: + return ( + + ); + case 1: + return ( + + ); + default: + return ( + + ); + } + } + + render() { + return ( + + {this.renderStep()} + + ); + } +} + +export default CreateNewQuestionForm; diff --git a/src/components/CreateNewQuestion/CreateNewQuestionForm.test.js b/src/components/CreateNewQuestion/CreateNewQuestionForm.test.js new file mode 100644 index 00000000..81ccb782 --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestionForm.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CreateNewQuestionForm from './CreateNewQuestionForm'; +import FormBasic from './FormBasic'; +import FormDynamic from './FormDynamic'; + +jest.mock('../../utils/Validator'); + +describe('CreateNewQuestionForm', () => { + describe('With activeTab 0', () => { + let wrapper; + let mockOnToggle; + let wrapperInstance; + + beforeEach(() => { + mockOnToggle = jest.fn(); + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error with correct form', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(FormBasic).length).toEqual(1); + expect(wrapper.find(FormDynamic).length).toEqual(0); + }); + + it('onBasicSubmit should call mockOnToggle with 1', () => { + wrapperInstance.onBasicSubmit(); + expect(mockOnToggle).toHaveBeenCalledWith(1); + }); + }); + + describe('With activeTab 1', () => { + let wrapper; + let mockOnToggle; + let wrapperInstance; + + beforeEach(() => { + mockOnToggle = jest.fn(); + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error with correct form', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(FormBasic).length).toEqual(0); + expect(wrapper.find(FormDynamic).length).toEqual(1); + }); + + it('onBasicSubmit should call mockOnToggle with 1', () => { + wrapperInstance.onBasicSubmit(); + expect(mockOnToggle).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/src/components/CreateNewQuestion/FormBasic.js b/src/components/CreateNewQuestion/FormBasic.js new file mode 100644 index 00000000..de926072 --- /dev/null +++ b/src/components/CreateNewQuestion/FormBasic.js @@ -0,0 +1,128 @@ +import React from 'react'; +import { withTranslation, Trans } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import Input from '../Input'; +// import { SquareHint } from '../Hint'; +import { TokenName, DateIcon, Address } from '../Icons'; +import InputTextarea from '../Input/InputTextarea'; +import Button from '../Button/Button'; + +import styles from './CreateNewQuestion.scss'; +import { FormulaHint, SelectorHint } from '../Hint'; + +@withTranslation() +@inject('projectStore') +@observer +class FormBasic extends React.Component { + static propTypes = { + formBasic: PropTypes.shape({ + onSubmit: PropTypes.func.isRequired, + $: PropTypes.func.isRequired, + }).isRequired, + t: PropTypes.func.isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.shape({ + isVotingActive: PropTypes.bool.isRequired, + }), + }).isRequired, + }; + + render() { + const { props } = this; + const { formBasic, t, projectStore: { historyStore } } = props; + return ( + + + + + + + + + + + + + + + + } + > + + + { + !formBasic.$('methodSelector').error + ? ( + + {t('other:selectorNonexistentFunctionDescription')} + + ) + : null + } + + + + + + + + + + } + /> + + + + + + + + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:nextStep')} + + + + + ); + } +} + +export default FormBasic; diff --git a/src/components/CreateNewQuestion/FormBasic.test.js b/src/components/CreateNewQuestion/FormBasic.test.js new file mode 100644 index 00000000..39c2e674 --- /dev/null +++ b/src/components/CreateNewQuestion/FormBasic.test.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import FormBasic from './FormBasic'; +import CreateQuestionBasicForm from '../../stores/FormsStore/CreateQuestionBasicForm'; + +describe('FormBasic', () => { + let wrapper; + let formBasic; + + beforeEach(() => { + formBasic = new CreateQuestionBasicForm({ + hooks: { + onSuccess: () => (Promise.resolve()), + }, + }); + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/CreateNewQuestion/FormDynamic.js b/src/components/CreateNewQuestion/FormDynamic.js new file mode 100644 index 00000000..5a2c0e08 --- /dev/null +++ b/src/components/CreateNewQuestion/FormDynamic.js @@ -0,0 +1,210 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import uniqKey from 'react-id-generator'; +import Input from '../Input'; +import { TokenName, CloseIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './CreateNewQuestion.scss'; +import SimpleDropdown from '../SimpleDropdown'; + +@withTranslation() +@inject('projectStore') +@observer +class FormDynamic extends React.Component { + static propTypes = { + formDynamic: PropTypes.shape({ + onSubmit: PropTypes.func.isRequired, + $: PropTypes.func.isRequired, + map: PropTypes.func.isRequired, + add: PropTypes.func.isRequired, + del: PropTypes.func.isRequired, + }).isRequired, + t: PropTypes.func.isRequired, + onToggle: PropTypes.func.isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.shape({ + isVotingActive: PropTypes.bool.isRequired, + }), + }).isRequired, + }; + + /** + * Method for adding new fields (input & select) + * to dynamic form + */ + addDynamicFields = () => { + const { props } = this; + const { t, formDynamic } = props; + const key = uniqKey(); + // Add input field + formDynamic.add({ + // this format important!!! + // @see getFieldKey + // @see removeRowFields + name: `input--${key}`, + type: 'text', + label: 'parameter', + placeholder: t('fields:enterNewParameterName'), + rules: 'required', + }); + // Add select field + formDynamic.add({ + // this format important!!! + // @see getFieldKey + // @see removeRowFields + name: `select--${key}`, + type: 'text', + label: 'parameter', + placeholder: t('fields:selectParameterType'), + rules: 'required', + }); + } + + /** + * Method for getting uniq key for + * similar fields (input & select) + * + * @param {string} name name field + * @returns {string} uniq key for + * select & input + */ + getFieldKey = (name) => { + if (!name || !name.split) return ''; + return name.split('--')[1] || ''; + } + + /** + * Method for removing fields + * with similar uniq key (input & select) + * + * @param {string} name name field + */ + removeRowFields = (name) => { + const key = this.getFieldKey(name); + const { props } = this; + const { formDynamic } = props; + formDynamic.del(`input--${key}`); + formDynamic.del(`select--${key}`); + } + + render() { + const { props } = this; + const { + formDynamic, t, onToggle, projectStore: { historyStore }, + } = props; + const options = [{ + label: 'uint', + value: 'uint', + }, { + label: 'String', + value: 'string', + }, { + label: 'Address', + value: 'address', + }, { + label: 'bytes4', + value: 'bytes4', + }, { + label: 'bytes32', + value: 'bytes32', + }]; + return ( + + {/* Render dynamic fields start */} + { + formDynamic.map((field, index) => { + const key = this.getFieldKey(field.name); + // Since two fields are added at a time, + // duplicates need to be excluded + // @see addDynamicFields method + if (Number.isInteger(index / 2) === false) return null; + return ( + + + + + + + + {}} + > + + + + this.removeRowFields(field.name) + } + className={styles['create-question__field-remove']} + > + + + + ); + }) + } + {/* Render dynamic fields end */} + + + + {t('buttons:addParameter')} + + + + + + { + onToggle(0); + }} + > + {t('buttons:back')} + + + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:create')} + + + {t('other:voteLaunchDescription')} + + + + + ); + } +} + +export default FormDynamic; diff --git a/src/components/CreateNewQuestion/FormDynamic.test.js b/src/components/CreateNewQuestion/FormDynamic.test.js new file mode 100644 index 00000000..073d9ea3 --- /dev/null +++ b/src/components/CreateNewQuestion/FormDynamic.test.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import FormDynamic from './FormDynamic'; +import CreateQuestionDynamicForm from '../../stores/FormsStore/CreateQuestionDynamicForm'; + +jest.mock('../../utils/Validator'); + +describe('FormDynamic', () => { + let wrapper; + let wrapperInstance; + let formDynamic; + + beforeEach(() => { + formDynamic = new CreateQuestionDynamicForm({ + hooks: { + onSuccess: () => (Promise.resolve()), + }, + }); + wrapper = shallow( + {}} + />, + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('addDynamicFields should add fields', () => { + // Ignore error inside mobx (test work correctly!) + console.error = jest.fn(); + expect(formDynamic.fields.size).toEqual(2); + wrapperInstance.addDynamicFields(); + expect(formDynamic.fields.size).toEqual(4); + }); + + it('getFieldKey with some params should be correct', () => { + let result = wrapperInstance.getFieldKey('select--id1'); + expect(result).toEqual('id1'); + result = wrapperInstance.getFieldKey('select-id1'); + expect(result).toEqual(''); + result = wrapperInstance.getFieldKey(undefined); + expect(result).toEqual(''); + result = wrapperInstance.getFieldKey({}); + expect(result).toEqual(''); + result = wrapperInstance.getFieldKey(null); + expect(result).toEqual(''); + }); + + it('removeRowFields should remove fields', () => { + // Ignore error inside mobx (test work correctly!) + console.error = jest.fn(); + expect(formDynamic.fields.size).toEqual(2); + wrapperInstance.removeRowFields('input--0'); + expect(formDynamic.fields.size).toEqual(0); + }); +}); diff --git a/src/components/CreateNewQuestion/index.js b/src/components/CreateNewQuestion/index.js new file mode 100644 index 00000000..0728c1b7 --- /dev/null +++ b/src/components/CreateNewQuestion/index.js @@ -0,0 +1,3 @@ +import CreateNewQuestion from './CreateNewQuestion'; + +export default CreateNewQuestion; diff --git a/src/components/CreateWallet/PasswordForm.js b/src/components/CreateWallet/PasswordForm.js index 36c35e91..8c0464e7 100644 --- a/src/components/CreateWallet/PasswordForm.js +++ b/src/components/CreateWallet/PasswordForm.js @@ -15,6 +15,16 @@ import styles from '../Login/Login.scss'; @withTranslation() class PasswordForm extends Component { + static propTypes = { + state: propTypes.bool.isRequired, + form: propTypes.shape({ + $: propTypes.func.isRequired, + onSubmit: propTypes.func.isRequired, + loading: propTypes.bool.isRequired, + }).isRequired, + t: propTypes.func.isRequired, + }; + constructor(props) { super(props); this.state = { @@ -22,6 +32,15 @@ class PasswordForm extends Component { }; } + componentDidMount() { + const { form } = this.props; + if (form.$('password').value !== '') { + const { value } = form.$('password'); + const validity = passwordValidation(value); + this.setState({ validity }); + } + } + handleInput = (value) => { const validity = passwordValidation(value); this.setState({ validity }); @@ -30,7 +49,6 @@ class PasswordForm extends Component { render() { const { state, form, t } = this.props; const { validity } = this.state; - return ( @@ -65,26 +83,26 @@ class PasswordForm extends Component { { t('explanations:passwordCreating.1')} - + - + { t('explanations:passwordRules.numeric')} - + { t('explanations:passwordRules.upperCase')} - + { t('explanations:passwordRules.symbol')} - + { t('explanations:passwordRules.length')} - + @@ -101,13 +119,4 @@ class PasswordForm extends Component { } } -PasswordForm.propTypes = { - state: propTypes.bool.isRequired, - form: propTypes.shape({ - $: propTypes.func.isRequired, - onSubmit: propTypes.func.isRequired, - loading: propTypes.bool.isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; export default PasswordForm; diff --git a/src/components/CreateWallet/index.js b/src/components/CreateWallet/index.js index 38b36b31..eb270ac1 100644 --- a/src/components/CreateWallet/index.js +++ b/src/components/CreateWallet/index.js @@ -1,3 +1,4 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import propTypes from 'prop-types'; import { observer, inject } from 'mobx-react'; @@ -17,6 +18,19 @@ import styles from '../Login/Login.scss'; @inject('userStore', 'appStore') @observer class CreateWallet extends Component { + static propTypes = { + userStore: propTypes.shape({ + recoverWallet: propTypes.func.isRequired, + saveWalletToFile: propTypes.func.isRequired, + createWallet: propTypes.func.isRequired, + }).isRequired, + recover: propTypes.bool, + }; + + static defaultProps = { + recover: false, + }; + createForm = new CreateWalletForm({ hooks: { onSuccess: (form) => this.createWallet(form), @@ -95,13 +109,4 @@ const CreationLoader = withTranslation(['headings'])(({ t }) => ( )); -CreateWallet.propTypes = { - userStore: propTypes.shape({ - recoverWallet: propTypes.func.isRequired, - saveWalletToFile: propTypes.func.isRequired, - createWallet: propTypes.func.isRequired, - }).isRequired, - recover: propTypes.bool.isRequired, -}; - export default CreateWallet; diff --git a/src/components/DatePicker/DatePicker.js b/src/components/DatePicker/DatePicker.js new file mode 100644 index 00000000..3455f269 --- /dev/null +++ b/src/components/DatePicker/DatePicker.js @@ -0,0 +1,273 @@ +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import PropTypes from 'prop-types'; +import Litepicker from 'litepicker'; +import moment from 'moment'; +import { observer } from 'mobx-react'; +import { computed, observable, action } from 'mobx'; +import { withTranslation } from 'react-i18next'; +import { getCorrectPickerLocale } from '../../utils/Date'; +import i18n from '../../i18n'; +import { + ThinArrow, + Arrow, + DateIcon, + CloseIcon, +} from '../Icons'; + +import styles from './DatePicker.scss'; + +@withTranslation() +@observer +class DateTest extends React.Component { + @observable start = null; + + @observable end = null; + + ref; + + picker; + + static propTypes = { + t: PropTypes.func.isRequired, + onDatesSet: PropTypes.func.isRequired, + onDatesClear: PropTypes.func.isRequired, + init: PropTypes.shape({ + startDate: PropTypes.instanceOf(moment), + endDate: PropTypes.instanceOf(moment), + }), + }; + + static defaultProps = { + init: { + startDate: null, + endDate: null, + }, + } + + componentDidMount() { + const { props } = this; + const { + init: { + startDate, + endDate, + }, + } = props; + this.picker = new Litepicker({ + element: this.minRef, + elementEnd: this.maxRef, + format: 'DD.MM.YYYY', + firstDay: 1, + numberOfMonths: 2, + numberOfColumns: 2, + minDate: null, + maxDate: null, + minDays: null, + maxDays: null, + singleMode: false, + autoApply: true, + scrollToDate: true, + showWeekNumbers: false, + showTooltip: true, + disableWeekends: false, + splitView: true, + onSelect: this.handleSelect, + buttonText: { + previousMonth: ReactDOMServer.renderToStaticMarkup( + , + ), + nextMonth: ReactDOMServer.renderToStaticMarkup( + , + ), + }, + startDate, + endDate, + }); + this.updateLanguage(); + this.start = startDate; + this.end = endDate; + window.ipcRenderer.on('change-language:confirm', () => { + this.updateLanguage(); + }); + } + + componentWillUnmount() { + window.ipcRenderer.removeListener('change-language:confirm', () => { + this.updateLanguage(); + }); + } + + @computed + get startDate() { + return this.start; + } + + @computed + get endDate() { + return this.end; + } + + /** + * Method for handle date select + * + * @param {Date} startDate start date + * @param {Date} endDate end date + */ + @action + handleSelect = (startDate, endDate) => { + const { props } = this; + const { onDatesSet } = props; + const start = moment(startDate); + // To include the maximum date in the range + const end = moment(endDate) + .add('hours', 23) + .add('minutes', 59) + .add('seconds', 59); + this.start = start; + this.end = end; + onDatesSet({ startDate: start, endDate: end }); + } + + /** + * Method for clearing selected date + */ + @action + handleClear = () => { + const { props } = this; + const { onDatesClear } = props; + if (this.picker) { + this.picker.clearSelection(); + } + this.start = null; + this.end = null; + onDatesClear(); + } + + /** + * Method for getting correct plural + * text for tooltip + * + * @param {string} language actual language + * @returns {object} correct tooltip text + */ + getTooltipText = (language) => { + switch (language) { + case 'ru-RU': + return { + one: 'день', + many: 'дней', + few: 'дня', + }; + case 'en-US': + return { + one: 'day', + other: 'days', + }; + default: + return { + one: 'day', + other: 'days', + }; + } + } + + /** + * Method for update options in picker + * on change language event + */ + updateLanguage = () => { + const lang = getCorrectPickerLocale(i18n.language); + const tooltipText = this.getTooltipText(lang); + if (this.picker) { + this.picker.setOptions({ + lang: getCorrectPickerLocale(i18n.language), + tooltipText, + }); + } + } + + render() { + const { start, end, props } = this; + const { t } = props; + const filled = Boolean(start && end); + return ( + + { /* eslint-disable-next-line */} + + + + + { + this.minRef = el; + } + } + /> + + + {t('fields:dateFrom')} + + + + + + + + { /* eslint-disable-next-line */} + + { + this.maxRef = el; + } + } + /> + + + {t('fields:dateTo')} + + + + + + + + + ); + } +} + +export default DateTest; diff --git a/src/components/DatePicker/DatePicker.scss b/src/components/DatePicker/DatePicker.scss new file mode 100644 index 00000000..73a223b1 --- /dev/null +++ b/src/components/DatePicker/DatePicker.scss @@ -0,0 +1,293 @@ +.date-picker { + position: relative; + display: inline-block; + max-width: 240px; + + &__base { + position: relative; + } + + &__input { + position: relative; + display: inline-block; + width: 73px; + padding: 8px 0; + color: #181818; + font-size: 14px; + line-height: 16px; + background: transparent; + border: unset; + outline: none; + transition: border-bottom-color 0.3s; + + &-line { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 1px; + background-color: rgba(0, 0, 0, 0.1); + transition: background-color 0.3s; + } + + &-placeholder { + position: absolute; + top: 50%; + color: rgba(24, 24, 24, 0.5); + font-size: 14px; + line-height: 16px; + transform: translateY(-50%); + cursor: text; + transition: all 0.3s; + } + + &::-webkit-input-placeholder { + color: transparent; + } + + &:focus { + & + .date-picker__input-after { + .date-picker__input-line { + background-color: rgba(0, 0, 0, 1); + } + } + } + + &:focus, + &:not(:placeholder-shown) { + & + .date-picker__input-after { + .date-picker__input-placeholder { + top: 0; + font-size: 9px; + line-height: 11px; + } + } + } + } + + &__min { + margin-left: 22px; + + & + .date-picker__input-after { + .date-picker__input-placeholder { + left: 38px; + } + } + } + + label { + position: relative; + display: inline-block; + vertical-align: middle; + } + + &__arrow { + &--basic { + display: inline-block; + margin-right: 7px; + margin-left: 9px; + vertical-align: middle; + } + + svg { + width: auto; + height: auto; + } + } + + &__icon { + position: relative; + display: inline-block; + vertical-align: middle; + + &::after { + position: absolute; + top: 50%; + right: -10px; + width: 1px; + height: 13px; + background-color: rgba(0, 0, 0, 0.1); + transform: translate(-50%, -50%); + content: ""; + } + } + + &__clear { + position: absolute; + top: 50%; + right: -28px; + padding: 5px; + color: #e1e4e8; + background-color: #fff; + border: 1px solid #e1e4e8; + outline: none; + transform: translateY(-50%); + visibility: hidden; + cursor: pointer; + opacity: 0; + transition: color 0.3s, border-color 0.3s, opacity 0.3s; + + &:hover, + &:active { + color: #000; + border-color: #000; + } + + &--visible { + visibility: visible; + opacity: 1; + } + + svg { + width: 7px; + height: 7px; + } + } +} + +$month-padding: 21px; + +.litepicker { + .container__months { + &.columns-2 { + width: calc((var(--litepickerMonthWidth) * 2) + #{$month-padding * 4}) !important; + margin-top: 12px; + border: 1px solid #e1e4e8; + border-radius: 0; + box-shadow: unset; + } + + .month-item { + position: relative; + padding: 5px $month-padding !important; + + &::after { + position: absolute; + top: 0; + right: 0; + width: 1px; + height: 100%; + background-color: #e1e4e8; + content: ''; + } + + &:last-child { + &::after { + content: none; + } + } + + &-weekdays-row { + position: relative; + + &::after { + position: absolute; + bottom: 0; + left: -$month-padding; + width: calc(100% + #{$month-padding * 2}); + height: 1px; + background-color: #e1e4e8; + content: ''; + } + + & > div { + font-size: 14px; + line-height: 16px; + text-transform: lowercase; + } + } + + .button-previous-month, + .button-next-month { + position: absolute; + top: 9px; + cursor: pointer; + + svg { + width: auto; + height: auto; + } + } + + .button-previous-month { + left: -16px; + } + + .button-next-month { + right: -16px; + margin-top: 1px; + } + } + + .month-item-header { + position: relative; + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + } + } + + .container__days { + .day-item { + font-size: 14px; + line-height: 16px; + border-color: transparent; + border-radius: 0px !important; + box-shadow: unset !important; + + &.is-today { + font-weight: 700; + } + + &:hover { + background-color: rgba(128, 128, 128, 0.8) !important; + } + } + } + + .container__tooltip { + margin-top: -12px; + padding: 5px 8px; + color: #fff; + font-size: 14px; + line-height: 16px; + background-color: #000; + border-radius: 0; + + &::after { + bottom: -10px; + left: calc(50% - 10px); + border-top: 10px solid #000; + border-right: 10px solid transparent; + border-left: 10px solid transparent; + } + + &::before { + content: none; + } + } +} + +:root { + --litepickerBgColor: #fff !important; + --litepickerMonthHeaderTextColor: #000 !important; + --litepickerMonthButton: rgba(0, 0, 0, 0.5) !important; + --litepickerMonthButtonHover: rgba(0, 0, 0, 0.5) !important; + --litepickerMonthWidth: calc(var(--litepickerDayWidth) * 7) !important; + --litepickerMonthWeekdayColor: #c8c9ca !important; + --litepickerDayColor: #000 !important; + --litepickerDayColorHover: #000 !important; + --litepickerDayIsTodayColor: #000 !important; + --litepickerDayIsInRange: rgba(230, 230, 230, 0.8) !important; + --litepickerDayIsLockedColor: rgba(230, 230, 230, 0.2) !important; + --litepickerDayIsBookedColor: #9e9e9e !important; + --litepickerDayIsStartColor: #000 !important; + --litepickerDayIsStartBg: rgba(128, 128, 128, 0.8) !important; + --litepickerDayIsEndColor: #000 !important; + --litepickerDayIsEndBg: rgba(128, 128, 128, 0.8) !important; + --litepickerDayWidth: 32px !important; + --litepickerButtonCancelColor: #000 !important; + --litepickerButtonCancelBg: rgba(128, 128, 128, 0.8) !important; + --litepickerButtonApplyColor: #000 !important; + --litepickerButtonApplyBg: rgba(128, 128, 128, 0.8) !important; +} diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js new file mode 100644 index 00000000..949a9240 --- /dev/null +++ b/src/components/DatePicker/index.js @@ -0,0 +1,3 @@ +import DatePicker from './DatePicker'; + +export default DatePicker; diff --git a/src/components/Decision/Decision.js b/src/components/Decision/Decision.js new file mode 100644 index 00000000..4a8efae5 --- /dev/null +++ b/src/components/Decision/Decision.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; + +import styles from './Decision.scss'; + +@withTranslation() +class Decision extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + icon: PropTypes.oneOfType([ + () => null, + PropTypes.node, + ]), + title: PropTypes.string.isRequired, + buttonText: PropTypes.string.isRequired, + }; + + static defaultProps = { + icon: null, + } + + render() { + const { props } = this; + const { + t, icon, title, form, buttonText, + } = props; + return ( + + + {icon} + + + {title} + + + {t('other:enterPassForConfirm')} + + + + ); + } +} + +export default Decision; diff --git a/src/components/Decision/Decision.scss b/src/components/Decision/Decision.scss new file mode 100644 index 00000000..48143e0b --- /dev/null +++ b/src/components/Decision/Decision.scss @@ -0,0 +1,46 @@ +@import '../../assets/styles/includes/mixin'; + +.decision { + width: 100%; + text-align: center; + + &__title { + @include title; + } + + &__subtext { + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + text-align: center; + } + + &__icon { + svg { + width: auto; + height: auto; + margin-top: 47px; + } + } + + &__token-form { + width: 100%; + padding: 0 40px; + .field { + width: 100%; + margin-bottom: 20px; + } + &__group{ + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + .field{ + width: 45%; + &__input{ + width: 60%; + } + } + } + } +} \ No newline at end of file diff --git a/src/components/Decision/Decision.test.js b/src/components/Decision/Decision.test.js new file mode 100644 index 00000000..0b78456f --- /dev/null +++ b/src/components/Decision/Decision.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DecisionReject, DecisionAgree } from '.'; + +describe('DecisionReject', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); + +describe('DecisionAgree', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Decision/DecisionAgree.js b/src/components/Decision/DecisionAgree.js new file mode 100644 index 00000000..e293ef35 --- /dev/null +++ b/src/components/Decision/DecisionAgree.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Decision from './Decision'; +import { VerifyIcon } from '../Icons'; + +@withTranslation() +@inject('dialogStore') +@observer +class DecisionAgree extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + render() { + const { props } = this; + const { t, form } = props; + return ( + <> + )} + form={form} + buttonText={t('buttons:vote')} + /> + > + ); + } +} + +export default DecisionAgree; diff --git a/src/components/Decision/DecisionClose.js b/src/components/Decision/DecisionClose.js new file mode 100644 index 00000000..6a087097 --- /dev/null +++ b/src/components/Decision/DecisionClose.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Decision from './Decision'; + +@withTranslation() +@inject('dialogStore') +@observer +class DecisionClose extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + render() { + const { props } = this; + // eslint-disable-next-line no-unused-vars + const { t, form } = props; + return ( + <> + + > + ); + } +} + +export default DecisionClose; diff --git a/src/components/Decision/DecisionReject.js b/src/components/Decision/DecisionReject.js new file mode 100644 index 00000000..b3e63429 --- /dev/null +++ b/src/components/Decision/DecisionReject.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Decision from './Decision'; +import { RejectIcon } from '../Icons'; + +@withTranslation() +@inject('dialogStore') +@observer +class DecisionReject extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + render() { + const { props } = this; + const { t, form } = props; + return ( + <> + )} + form={form} + buttonText={t('buttons:vote')} + /> + > + ); + } +} + +export default DecisionReject; diff --git a/src/components/Decision/index.js b/src/components/Decision/index.js new file mode 100644 index 00000000..9077e969 --- /dev/null +++ b/src/components/Decision/index.js @@ -0,0 +1,11 @@ +import Decision from './Decision'; +import DecisionReject from './DecisionReject'; +import DecisionAgree from './DecisionAgree'; + +export default Decision; + +export { + Decision, + DecisionReject, + DecisionAgree, +}; diff --git a/src/components/Dialog/Dialog.js b/src/components/Dialog/Dialog.js index c6fd776c..885413c6 100644 --- a/src/components/Dialog/Dialog.js +++ b/src/components/Dialog/Dialog.js @@ -18,6 +18,7 @@ class Dialog extends React.Component { 'sm', 'md', 'lg', + 'xlg', ]), name: PropTypes.string.isRequired, header: PropTypes.string, diff --git a/src/components/Dialog/Dialog.scss b/src/components/Dialog/Dialog.scss index 80c3a289..c34ba23f 100644 --- a/src/components/Dialog/Dialog.scss +++ b/src/components/Dialog/Dialog.scss @@ -13,7 +13,7 @@ &__inner { position: relative; z-index: 1; - min-height: 325px; + min-height: 309px; } } @@ -42,7 +42,7 @@ padding: 0; font-weight: 700; font-size: 24px; - font-family: "Grotesk"; + font-family: "Roboto"; line-height: 28px; } @@ -57,11 +57,17 @@ z-index: 5; box-sizing: border-box; width: 100%; - padding: 25px 10px; + padding: 0 10px 25px; &--default { padding-top: 55px; } + + .text { + padding: 0 40px; + color: #c8c9ca; + font-size: 14px; + } } &--open { @@ -110,8 +116,38 @@ &--lg { .content { - width: 740px; - min-width: 740px; + width: 754px; + min-width: 754px; + } + } + + &--xlg { + .content { + width: 845px; + min-width: 845px; + } + } + + dialog-success { + &_modal, + &_modal_voting_info_wrapper, + &_modal_contract_uploading, + &_modal_questions, + &_modal_return_tokens, + &_modal_voting { + .dialog { + &__header { + padding: 20px; + padding-top: 55px; + } + &__body { + padding-bottom: 10px; + } + } + .content__inner { + height: auto; + min-height: 255px; + } } } } @@ -152,6 +188,8 @@ } } + + @keyframes anim-open { 0% { transform: translate(0, -800px); diff --git a/src/components/Dialog/Dialog.test.js b/src/components/Dialog/Dialog.test.js index d1772409..1ed75529 100644 --- a/src/components/Dialog/Dialog.test.js +++ b/src/components/Dialog/Dialog.test.js @@ -203,11 +203,11 @@ describe('Dialog', () => { ).dive().dive(); }); - it('should has dialog--close class', () => { + it('should have dialog--close class', () => { expect(wrapper.find('.dialog').hasClass('dialog--close')).toEqual(true); }); - it('should has dialog--open class', () => { + it('should have dialog--open class', () => { expect(wrapper.find('.dialog').hasClass('dialog--open')).toEqual(true); }); }); diff --git a/src/components/Dialog/index.js b/src/components/Dialog/index.js index 16772f4f..4a07e93e 100644 --- a/src/components/Dialog/index.js +++ b/src/components/Dialog/index.js @@ -1,3 +1,8 @@ import Dialog from './Dialog'; +import DefaultDialogFooter from './DefaultDialogFooter'; export default { Dialog }; + +export { + DefaultDialogFooter, +}; diff --git a/src/components/Dropdown/Dropdown.scss b/src/components/Dropdown/Dropdown.scss index 90dba5cc..bd307ccc 100644 --- a/src/components/Dropdown/Dropdown.scss +++ b/src/components/Dropdown/Dropdown.scss @@ -2,11 +2,31 @@ .dropdown { position: relative; display: inline-block; - + button { background: transparent; outline: none; } + + &--simple { + &.dropdown--opened { + .dropdown__options { + height: 160px; + } + } + .dropdown__options { + width: 100%; + max-height: 160px; + } + .dropdown__option { + display: block; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-align: left; + text-overflow: ellipsis; + } + } &__head { position: relative; @@ -39,6 +59,27 @@ } } + &__error-text { + position: absolute; + bottom: -15px; + width: 100%; + font-size: 11px; + text-align: center; + visibility: hidden; + opacity: 0; + } + + &--error { + .dropdown__error-text { + visibility: visible; + opacity: 1; + } + + .dropdown__head { + border-bottom: 1px dashed rgba(0, 0, 0, 1); + } + } + &__arrow { position: absolute; top: 50%; @@ -47,10 +88,11 @@ transform: translateY(-50%) rotate(0deg); transition: 0.2s; svg { + width: 12px; path { opacity: 1; transition: 0.2s; - stroke: $lightGrey; + stroke: $border; } } } @@ -78,18 +120,26 @@ display: inline-block; max-width: 80%; overflow: hidden; + white-space: nowrap; text-overflow: ellipsis; vertical-align: middle; + &-label { + position: absolute; + bottom: 90%; + left: 45px; + color: rgba(0, 0, 0, 0.7); + font-size: 10px; + } } &__options { position: absolute; top: 100%; z-index: 1; - width: 155%; + width: 156%; height: 0; max-height: 150px; - padding: 5px 10px; + padding: 5px 0; overflow-x: hidden; overflow-y: auto; background-color: $white; @@ -97,6 +147,8 @@ opacity: 0; transition: .3s ease-in-out; &::-webkit-scrollbar { + position: relative; + right: -30px; width: 20px; } /* Track */ @@ -131,16 +183,32 @@ } &__option { - display: block; - padding: 10px 0; + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + width: 100%; + padding: 10px; + text-align: left; border: none; cursor: pointer; + transition: .2s; + + &:hover { + background-color: rgba($color: #E6E6E6, $alpha: .8); + } + + &-label { + display: inline-block; + width: 320px; + } } &__suboption { - margin-left: 30px; + margin-left: 20px; color: $linkColor; font-weight: 700; + font-size: 14px; + text-align: right; } &--opened { diff --git a/src/components/Dropdown/index.js b/src/components/Dropdown/index.js index 2e346f3d..d4b26bdb 100644 --- a/src/components/Dropdown/index.js +++ b/src/components/Dropdown/index.js @@ -1,18 +1,45 @@ import React, { Component } from 'react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; import DropdownOption from '../DropdownOption'; import { DropdownArrowIcon } from '../Icons'; +import i18n from '../../i18n'; + import styles from './Dropdown.scss'; class Dropdown extends Component { + static propTypes = { + children: PropTypes.element, + options: PropTypes.arrayOf(PropTypes.object).isRequired, + subOptions: PropTypes.shape({}), + onSelect: PropTypes.func.isRequired, + field: PropTypes.shape({ + set: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + placeholder: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]).isRequired, + validate: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, + error: PropTypes.string, + }).isRequired, + }; + + static defaultProps = { + children: '', + subOptions: {}, + }; + constructor(props) { super(props); this.state = { selectedValue: '', }; - this.setWrapperRef = this.setWrapperRef.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); + window.ipcRenderer.on('change-language:confirm', () => { + this.updateLanguage(); + }); } componentDidMount() { @@ -21,12 +48,20 @@ class Dropdown extends Component { componentWillUnmount() { document.addEventListener('mousedown', this.handleClickOutside); + window.ipcRenderer.removeListener('change-language:confirm', () => { + this.updateLanguage(); + }); } setWrapperRef(node) { this.wrapperRef = node; } + updateLanguage = () => { + const { field } = this.props; + field.set('placeholder', i18n.t(`fields:${field.label}`)); + } + toggleOptions = () => { const { opened } = this.state; this.setState({ opened: !opened }); @@ -36,12 +71,19 @@ class Dropdown extends Component { this.setState({ opened: false }); } + calculateHeight = () => { + const optionsLength = document.querySelectorAll('.dropdown__option').length; + const height = optionsLength * 40; + return height > 150 ? 150 : height; + } + handleSelect = (selected) => { const { onSelect, field } = this.props; this.setState({ selectedValue: selected, }); field.set(selected); + field.validate(); onSelect(selected); this.toggleOptions(); } @@ -69,7 +111,14 @@ class Dropdown extends Component { )); return ( - + {children ? {children} : ''} @@ -81,30 +130,20 @@ class Dropdown extends Component { - + {getOptions} + + {field.error} + ); } } -Dropdown.propTypes = { - children: propTypes.element, - options: propTypes.arrayOf(propTypes.object).isRequired, - subOptions: propTypes.shape({}), - onSelect: propTypes.func.isRequired, - field: propTypes.shape({ - set: propTypes.func.isRequired, - value: propTypes.string.isRequired, - placeholder: propTypes.string.isRequired, - }).isRequired, -}; - -Dropdown.defaultProps = { - children: '', - subOptions: {}, -}; - - export default Dropdown; diff --git a/src/components/DropdownOption/index.js b/src/components/DropdownOption/index.js index 69a6cf81..e183d6e7 100644 --- a/src/components/DropdownOption/index.js +++ b/src/components/DropdownOption/index.js @@ -11,7 +11,9 @@ const DropdownOption = ({ className={styles.dropdown__option} onClick={() => { select(value); }} > - {label} + + {label} + {subOption !== '' ? ( @@ -27,10 +29,11 @@ DropdownOption.propTypes = { value: propTypes.string.isRequired, label: propTypes.string.isRequired, select: propTypes.func.isRequired, - subOption: propTypes.string.isRequired, + subOption: propTypes.string, }; DropdownOption.defaultProps = { + subOption: '', }; export default DropdownOption; diff --git a/src/components/Explanation/Explanation.scss b/src/components/Explanation/Explanation.scss index 1d03db9b..d73449d0 100644 --- a/src/components/Explanation/Explanation.scss +++ b/src/components/Explanation/Explanation.scss @@ -17,4 +17,13 @@ } } } + &--bold { + color: $primary; + .explanation__string { + border-left: 2px solid $primary; + } + p { + font-weight: 700; + } + } } \ No newline at end of file diff --git a/src/components/Explanation/index.js b/src/components/Explanation/index.js index 61f3208b..cc3e26b5 100644 --- a/src/components/Explanation/index.js +++ b/src/components/Explanation/index.js @@ -2,17 +2,19 @@ import React from 'react'; import propTypes from 'prop-types'; import styles from './Explanation.scss'; -const Explanation = ({ children }) => ( - - {children} - +const Explanation = ({ children, bold }) => ( + + {children} + ); Explanation.defaultProps = { children: '', + bold: false, }; Explanation.propTypes = { children: propTypes.node, + bold: propTypes.bool, }; export default Explanation; diff --git a/src/components/FinPassFormWrapper/FinPassFormWrapper.js b/src/components/FinPassFormWrapper/FinPassFormWrapper.js new file mode 100644 index 00000000..3cec825c --- /dev/null +++ b/src/components/FinPassFormWrapper/FinPassFormWrapper.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import Input from '../Input'; +import { Password } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './FinPassFormWrapper.scss'; + +@withTranslation() +class FinPassFormWrapper extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape({ + onSubmit: PropTypes.func.isRequired, + $: PropTypes.func.isRequired, + }).isRequired, + buttonText: PropTypes.string.isRequired, + }; + + render() { + const { props } = this; + const { t, form, buttonText } = props; + return ( + + + + + + + + + + {buttonText || t('buttons:startNewVoting')} + + + + + ); + } +} + +export default FinPassFormWrapper; diff --git a/src/components/FinPassFormWrapper/FinPassFormWrapper.scss b/src/components/FinPassFormWrapper/FinPassFormWrapper.scss new file mode 100644 index 00000000..9e1ed67d --- /dev/null +++ b/src/components/FinPassFormWrapper/FinPassFormWrapper.scss @@ -0,0 +1,21 @@ +.form-fin-pass { + margin-top: 56px; + + .input__wrapper { + .field { + width: 100%; + max-width: 309px; + margin-bottom: 0px; + } + } + + .button__wrapper { + margin-top: 48px; + margin-bottom: 59px; + + button { + width: 100%; + max-width: 309px; + } + } +} diff --git a/src/components/FinPassFormWrapper/FinPassFormWrapper.stories.js b/src/components/FinPassFormWrapper/FinPassFormWrapper.stories.js new file mode 100644 index 00000000..6df33427 --- /dev/null +++ b/src/components/FinPassFormWrapper/FinPassFormWrapper.stories.js @@ -0,0 +1,24 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import FinPassFormWrapper from './FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; + +const form = new FinPassForm({ + hooks: { + onSuccess() { + return Promise.resolve(); + }, + onError() { + /* eslint-disable-next-line */ + console.error('error'); + }, + }, +}); + +storiesOf('FinPassFormWrapper', module) + .add('Default', () => ( + + )); diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss new file mode 100644 index 00000000..ef7e038e --- /dev/null +++ b/src/components/Footer/Footer.scss @@ -0,0 +1,39 @@ +@import '../../assets/styles/partials/variables'; + +.footer { + position: relative; + z-index: 1; + padding: 15px 0; + text-align: center; + + a { + flex-flow: row nowrap; + align-items: center; + justify-content: center; + color: $border; + font-size: 11px; + text-align: center; + svg { + vertical-align: middle; + path { + transition: .2s; + } + } + span { + margin-left: 10px; + vertical-align: middle; + transition: .2s; + } + &:hover{ + svg { + path { + opacity: 1; + fill: $primary; + } + } + span { + color: $primary; + } + } + } +} \ No newline at end of file diff --git a/src/components/Footer/index.js b/src/components/Footer/index.js new file mode 100644 index 00000000..10d5f276 --- /dev/null +++ b/src/components/Footer/index.js @@ -0,0 +1,16 @@ + +import React from 'react'; +import { GithubIcon } from '../Icons'; + +import styles from './Footer.scss'; + +const Footer = () => ( + +); + +export default Footer; diff --git a/src/components/Forms/ProjectInputForm.js b/src/components/Forms/ProjectInputForm.js new file mode 100644 index 00000000..aa41e54b --- /dev/null +++ b/src/components/Forms/ProjectInputForm.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import Input from '../Input'; +import { + TokenName, Password, Address, +} from '../Icons'; +import Button from '../Button/Button'; + +import styles from '../Decision/Decision.scss'; + +@withTranslation() +@observer +class ProjectInputForm extends Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { props } = this; + const { + t, form, + } = props; + return ( + + + + + + + + + + + + + + {t('buttons:create')} + + + + + ); + } +} + +export default ProjectInputForm; diff --git a/src/components/Forms/TokenInputForm.js b/src/components/Forms/TokenInputForm.js new file mode 100644 index 00000000..d67a62ea --- /dev/null +++ b/src/components/Forms/TokenInputForm.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import Input from '../Input'; +import { + TokenName, TokenSymbol, TokenCount, Password, +} from '../Icons'; +import Button from '../Button/Button'; + +import styles from '../Decision/Decision.scss'; + +@withTranslation() +@observer +class TokenInputForm extends Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { props } = this; + const { + t, form, + } = props; + return ( + + + + + + + + + + + + + + + + + + + {t('buttons:create')} + + + + + ); + } +} + +export default TokenInputForm; diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index 392cf70d..8f2ac892 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -1,7 +1,7 @@ @import '../../assets/styles/partials/variables'; .header { - position: absolute; + position: relative; top: 30px; left: 50%; z-index: 2; @@ -27,14 +27,31 @@ background-color: $lightGrey; border: none; transform: translate(-50%, -50%); + &--bold { + height: 2px; + background-color: $primary; + } } &__link { position: relative; - margin: 0 30px; + display: inline-block; + width: 105px; + margin: 0 20px; + text-align: center; transition: .2s linear; + svg { + path { + fill: $white; + } + } &.active{ font-weight: bold; + svg { + path { + fill: $primary; + } + } &:before, &:after { position: absolute; left: 50%; @@ -48,17 +65,54 @@ top: 100%; transform: translateX(-50%) rotate(180deg); } - } + } + } + + &__settings { + svg { + path { + fill: $white; + } + } + &.active{ + font-weight: bold; + svg { + path { + fill: $primary; + } + } + } } + &__right { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + background-color: #FAFBFC; + .user { + &:hover { + position: absolute; + right: 17px; + } + } + + } + + .lang { padding: 10px; background-color: #fafbfc; } - + .user { + margin-right: 10px; margin-left: 10px; vertical-align: middle; - background-color: #FAFBFC; + background-color: $white; } + +} +.is-logged { + width: 260px; } \ No newline at end of file diff --git a/src/components/Header/HeaderNav/index.js b/src/components/Header/HeaderNav/index.js index 169a1b5c..e4a93544 100644 --- a/src/components/Header/HeaderNav/index.js +++ b/src/components/Header/HeaderNav/index.js @@ -1,16 +1,24 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; +import { withTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; import styles from '../Header.scss'; -const HeaderNav = () => ( +const HeaderNav = ({ + t, +}) => ( - Голосования + {t('other:voting')} / - Вопросы + {t('other:questions')} / - Участники + {t('other:members')} ); -export default HeaderNav; +HeaderNav.propTypes = { + t: PropTypes.func.isRequired, +}; + +export default withTranslation()(HeaderNav); diff --git a/src/components/Header/index.js b/src/components/Header/index.js index 4cb974f4..44b53744 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -1,20 +1,31 @@ import React from 'react'; import { inject, observer } from 'mobx-react'; +import { NavLink } from 'react-router-dom'; import Logo from '../Logo'; import HeaderNav from './HeaderNav'; import LangSwitcher from '../LangSwitcher'; import User from '../User'; import styles from './Header.scss'; +import { SettingsIcon } from '../Icons'; const Header = inject('userStore', 'appStore')(observer(({ appStore: { inProject }, userStore: { authorized, address } }) => ( {inProject ? : ''} - - + + {authorized ? {address} : ''} + { + authorized + ? ( + + + + ) + : null + } ))); diff --git a/src/components/Heading/index.js b/src/components/Heading/index.js index fca314fa..f47ff389 100644 --- a/src/components/Heading/index.js +++ b/src/components/Heading/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; import styles from './Heading.scss'; const Heading = ({ children }) => ( @@ -10,7 +10,10 @@ const Heading = ({ children }) => ( ); Heading.propTypes = { - children: propTypes.arrayOf(propTypes.string).isRequired, + children: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.string, + ]).isRequired, }; export default Heading; diff --git a/src/components/Hint/Hint.scss b/src/components/Hint/Hint.scss index e1c23c5c..7047564e 100644 --- a/src/components/Hint/Hint.scss +++ b/src/components/Hint/Hint.scss @@ -17,11 +17,12 @@ line-height: 15px; text-align: center; transition: 0.2s; + &::before { position: absolute; top: 50%; left: 50%; - z-index: -1; + z-index: 2; width: 100%; height: 100%; background-color: $white; @@ -30,31 +31,72 @@ transition: 0.2s; content: ''; } + + &::after { + position: relative; + top: 1px; + z-index: 3; + font-weight: 400; + content: ' ? '; + } + &:hover { color: $white; + &:before { background-color: $primary; border-color: $primary; } + & + .hint__text { visibility: visible; opacity: 1; } } } - + &__text { position: absolute; - top:50%; + top:50%; left: 50%; - z-index: -2; - width: 185px; - padding: 24px; + z-index: 1; + width: 292px; + padding: 21px; font-size: 11px; + line-height: 13px; + white-space: pre-wrap; + text-align: left; + background-color: #fff; border: 1px solid $border; - transform: translate(-1px, 2px); + transform: translateZ(0); visibility: hidden; opacity: 0; transition: 0.2s; + strong { + font-weight: 700; + } + } + + &--square { + .hint__icon { + &::before{ + transform: translate(-50%, -50%); + } + } + } + &--formula { + .hint__text { + top: unset; + bottom: 50%; + width: 570px; + &>p { + margin: 5px 0; + font-size: 11px; + line-height: 13px; + &:first-child, &:last-child { + margin: 10px 0; + } + } + } } } \ No newline at end of file diff --git a/src/components/Hint/index.js b/src/components/Hint/index.js index e55184fa..11279efd 100644 --- a/src/components/Hint/index.js +++ b/src/components/Hint/index.js @@ -1,19 +1,109 @@ import React from 'react'; import propTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; import styles from './Hint.scss'; const Hint = ({ children }) => ( - ? + {children} ); +const SquareHint = ({ children }) => ( + + + + {children} + + +); + +const FormulaHint = withTranslation()(() => ( + + + + + Формула голосования записывается в примерном виде: + + {'erc20{0x123...EF}->exclude{0x234...FE}->conditions{quorum>50%, positive>50% of all}, где:'} + + + + {'1) erc20{0x123...EF} / custom{0x123...EF}'} + {' '} + - тип токенов и адрес токенов необходимой группы + + + {'2) exclude{0x234...FE}'} + {' '} + – пользователи, которые не должны голосовать (опционально) + + + {'3) conditions{quorum>50%, positive>50% of all}'} + {' '} + - условия для принятия решения по голосованию + + + {'3.1) quorum>50%'} + {' '} + min% голосов в общем + + + {'3.2) positive>50%'} + {' '} + min% голосов «ЗА» + + + 3.3 of quorum / of all + {' '} + – модификатор, от какого числа считать условие positive - от числа токенов, + которые учавствовали в голосовании, или от всех токенов из контракта группы + + + Вы можете связывать несколько групп пользователей, объединяя их формулы операторами + and + или + or + .Например: + «Формула 1» + or + «Формула 2» + and + «Формула 3» + + + +)); + + +const SelectorHint = () => ( + + + + Выглядит как 4 байта Keccak хэша от сигнатуры функции в ASCII кодировке + Пример: + + bytes4(keccak256(baz(uint32,bool))) = + {' '} + 0xcdcd77c0 + + + +); + Hint.propTypes = { - children: propTypes.arrayOf(propTypes.string).isRequired, + children: propTypes.string.isRequired, }; -export default Hint; +SquareHint.propTypes = { + children: propTypes.string.isRequired, +}; + + +export { + Hint, SquareHint, FormulaHint, SelectorHint, +}; diff --git a/src/components/Icons/entities/AdminIcon.js b/src/components/Icons/entities/AdminIcon.js new file mode 100644 index 00000000..b587a17e --- /dev/null +++ b/src/components/Icons/entities/AdminIcon.js @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const AdminIcon = ({ + width, + height, + color, +}) => ( + + + + + + + + +); + +AdminIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +AdminIcon.defaultProps = { + width: 16, + height: 16, + color: '#000', +}; + +export default AdminIcon; diff --git a/src/components/Icons/entities/Arrow.js b/src/components/Icons/entities/Arrow.js new file mode 100644 index 00000000..5d1123dc --- /dev/null +++ b/src/components/Icons/entities/Arrow.js @@ -0,0 +1,12 @@ +import React from 'react'; + +const Arrow = () => ( + + + +); + +export default Arrow; diff --git a/src/components/Icons/entities/BinaryIcon.js b/src/components/Icons/entities/BinaryIcon.js new file mode 100644 index 00000000..16bc2923 --- /dev/null +++ b/src/components/Icons/entities/BinaryIcon.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const BinaryIcon = ({ + width, + height, + color, +}) => ( + + + + +); + +BinaryIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +BinaryIcon.defaultProps = { + width: 16, + height: 16, + color: '#4D4D4D', +}; + +export default BinaryIcon; diff --git a/src/components/Icons/entities/BorderArrowIcon.js b/src/components/Icons/entities/BorderArrowIcon.js new file mode 100644 index 00000000..188de531 --- /dev/null +++ b/src/components/Icons/entities/BorderArrowIcon.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const BorderArrowIcon = ({ + width, + height, + color, +}) => ( + + + + +); + +BorderArrowIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +BorderArrowIcon.defaultProps = { + width: 25, + height: 25, + color: '#000', +}; + +export default BorderArrowIcon; diff --git a/src/components/Icons/entities/DateIcon.js b/src/components/Icons/entities/DateIcon.js new file mode 100644 index 00000000..4658fc83 --- /dev/null +++ b/src/components/Icons/entities/DateIcon.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const DateIcon = ({ + width, + height, + color, +}) => ( + + + + + + +); + +DateIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +DateIcon.defaultProps = { + width: 16, + height: 17, + color: '#4D4D4D', +}; +export default DateIcon; diff --git a/src/components/Icons/entities/DescisionIcon.js b/src/components/Icons/entities/DescisionIcon.js new file mode 100644 index 00000000..b750f54d --- /dev/null +++ b/src/components/Icons/entities/DescisionIcon.js @@ -0,0 +1,10 @@ +import React from 'react'; + +const DescisionIcon = () => ( + + + + +); + +export default DescisionIcon; diff --git a/src/components/Icons/entities/GithubIcon.js b/src/components/Icons/entities/GithubIcon.js new file mode 100644 index 00000000..f6f0d017 --- /dev/null +++ b/src/components/Icons/entities/GithubIcon.js @@ -0,0 +1,8 @@ +import React from 'react'; + +const GithubIcon = () => ( + + + +); +export default GithubIcon; diff --git a/src/components/Icons/entities/NoQuorum.js b/src/components/Icons/entities/NoQuorum.js new file mode 100644 index 00000000..ab485355 --- /dev/null +++ b/src/components/Icons/entities/NoQuorum.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const NoQuorum = ({ + width, + height, +}) => ( + + + + +); + +NoQuorum.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +NoQuorum.defaultProps = { + width: 32, + height: 32, +}; + +export default NoQuorum; diff --git a/src/components/Icons/entities/PlayCircleIcon.js b/src/components/Icons/entities/PlayCircleIcon.js new file mode 100644 index 00000000..0570e037 --- /dev/null +++ b/src/components/Icons/entities/PlayCircleIcon.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const PlayCircleIcon = ({ + width, + height, + color, + opacity, +}) => ( + + + + + + +); + +PlayCircleIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + opacity: PropTypes.number, + color: PropTypes.string, +}; + +PlayCircleIcon.defaultProps = { + width: 32, + height: 32, + opacity: 0.7, + color: '#000', +}; + +export default PlayCircleIcon; diff --git a/src/components/Icons/entities/Pudding.js b/src/components/Icons/entities/Pudding.js new file mode 100644 index 00000000..0a377b4c --- /dev/null +++ b/src/components/Icons/entities/Pudding.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Pudding = ({ + width, + height, + color, +}) => ( + + + +); + +Pudding.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +Pudding.defaultProps = { + width: 19, + height: 17, + color: '#000', +}; + +export default Pudding; diff --git a/src/components/Icons/entities/QuestionIcon.js b/src/components/Icons/entities/QuestionIcon.js new file mode 100644 index 00000000..dbfe2cfd --- /dev/null +++ b/src/components/Icons/entities/QuestionIcon.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const QuestionIcon = ({ + width, + height, + opacity, + color, +}) => ( + + + + + + + +); + +QuestionIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + opacity: PropTypes.number, + color: PropTypes.string, +}; + +QuestionIcon.defaultProps = { + width: 16, + height: 16, + opacity: 0.7, + color: '#000', +}; + +export default QuestionIcon; diff --git a/src/components/Icons/entities/SettingsIcon.js b/src/components/Icons/entities/SettingsIcon.js new file mode 100644 index 00000000..5c6a6217 --- /dev/null +++ b/src/components/Icons/entities/SettingsIcon.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const SettingsIcon = () => ( + + + +); + +export default SettingsIcon; diff --git a/src/components/Icons/entities/SigningIcon.js b/src/components/Icons/entities/SigningIcon.js new file mode 100644 index 00000000..328b9831 --- /dev/null +++ b/src/components/Icons/entities/SigningIcon.js @@ -0,0 +1,15 @@ +import React from 'react'; + +const SigningIcon = () => ( + + + + + + + + + +); + +export default SigningIcon; diff --git a/src/components/Icons/entities/StartIcon.js b/src/components/Icons/entities/StartIcon.js new file mode 100644 index 00000000..699eba1b --- /dev/null +++ b/src/components/Icons/entities/StartIcon.js @@ -0,0 +1,12 @@ +import React from 'react'; + +const StartIcon = () => ( + + + + + + +); + +export default StartIcon; diff --git a/src/components/Icons/entities/StatsIcon.js b/src/components/Icons/entities/StatsIcon.js index 22cbe455..0f69a1ae 100644 --- a/src/components/Icons/entities/StatsIcon.js +++ b/src/components/Icons/entities/StatsIcon.js @@ -3,8 +3,18 @@ import React from 'react'; const Stats = () => ( - - + + diff --git a/src/components/Icons/entities/ThinArrow.js b/src/components/Icons/entities/ThinArrow.js new file mode 100644 index 00000000..f491cc06 --- /dev/null +++ b/src/components/Icons/entities/ThinArrow.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ThinArrow = ({ + width, + height, + color, + reverse, +}) => ( + + + +); + +ThinArrow.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, + reverse: PropTypes.bool, +}; + +ThinArrow.defaultProps = { + width: 5, + height: 9, + color: 'currentColor', + reverse: false, +}; + +export default ThinArrow; diff --git a/src/components/Icons/index.js b/src/components/Icons/index.js index fa86a243..73c10f18 100644 --- a/src/components/Icons/index.js +++ b/src/components/Icons/index.js @@ -13,6 +13,8 @@ import EyeIcon from './entities/EyeIcon'; import IconInfo from './entities/InfoIcon'; import Login from './entities/LoginIcon'; import Password from './entities/PasswordIcon'; +import PlayCircleIcon from './entities/PlayCircleIcon'; +import Pudding from './entities/Pudding'; import QuestionUploadingIcon from './entities/QuestionUploadingIcon'; import SendingIcon from './entities/SendingIcon'; import Stats from './entities/StatsIcon'; @@ -22,7 +24,19 @@ import TokenName from './entities/TokenNameIcon'; import TxHashIcon from './entities/TxHashIcon'; import TxRecieptIcon from './entities/TxRecieptIcon'; import VerifyIcon from './entities/VerifyIcon'; +import StartIcon from './entities/StartIcon'; +import GithubIcon from './entities/GithubIcon'; import RejectIcon from './entities/RejectIcon'; +import BorderArrowIcon from './entities/BorderArrowIcon'; +import AdminIcon from './entities/AdminIcon'; +import QuestionIcon from './entities/QuestionIcon'; +import ThinArrow from './entities/ThinArrow'; +import DateIcon from './entities/DateIcon'; +import SettingsIcon from './entities/SettingsIcon'; +import NoQuorum from './entities/NoQuorum'; +import DescisionIcon from './entities/DescisionIcon'; +import Arrow from './entities/Arrow'; +import SigningIcon from './entities/SigningIcon'; export { AddIcon, @@ -40,6 +54,8 @@ export { IconInfo, Login, Password, + PlayCircleIcon, + Pudding, QuestionUploadingIcon, SendingIcon, Stats, @@ -49,5 +65,17 @@ export { TxHashIcon, TxRecieptIcon, VerifyIcon, + StartIcon, + GithubIcon, RejectIcon, + BorderArrowIcon, + AdminIcon, + QuestionIcon, + ThinArrow, + DateIcon, + SettingsIcon, + NoQuorum, + DescisionIcon, + Arrow, + SigningIcon, }; diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss index f07437e7..f164c17d 100644 --- a/src/components/Input/Input.scss +++ b/src/components/Input/Input.scss @@ -12,7 +12,7 @@ &__input { width: 85%; - margin-left: 20px; + margin-left: 17px; padding: 8px 0; vertical-align: middle; background: transparent; @@ -32,6 +32,26 @@ font-size: 9px; } } + + &--textarea { + max-width: 100%; + min-height: 80px; + padding: 10px; + background-color: transparent; + border: 1px solid #e1e4e8; + border-radius: 2px; + outline: none; + transition: .2s; + &::-webkit-input-placeholder { + opacity: 0; + } + &:focus { + border-color: $primary; + & + .field__label--textarea { + color: $primary; + } + } + } } &__label { @@ -43,6 +63,18 @@ font-size: 14px; transform: translateY(-50%); transition: 0.2s; + + &--textarea { + position: absolute; + bottom: 100%; + margin-bottom: 4px; + &>span { + color: $placeholderColor; + font-size: 14px; + line-height: 16px; + text-align: left; + } + } } &__error-text { @@ -54,7 +86,7 @@ visibility: hidden; opacity: 0; } - + &__line { position: absolute; bottom: -1px; @@ -62,6 +94,7 @@ width: 0; height: 1px; background-color: $primary; + border-bottom: 1px solid $primary; transition: 0.3s ease-in; } @@ -82,6 +115,10 @@ style: dashed; color: $primary; } + &.field--textarea { + padding-bottom: 1px; + border-bottom: none; + } .field__error-text { visibility: visible; opacity: 1; @@ -100,9 +137,37 @@ } &:focus { & ~ .field__line { - width: 0; + width: 100%; } } + + &--textarea { + border-color: $primary; + border-style: dashed; + } + } + + } + + &--textarea { + position: relative; + .field__error-text { + top: unset; + bottom: -17px; } + .hint { + position: relative; + z-index: 1; + margin-left: 10px; + transform: translate(0,0); + } + } + + .hint { + position: absolute; + top: 50%; + right: 0; + z-index: 1; + transform: translateY(-50%); } } diff --git a/src/components/Input/InputTextarea.js b/src/components/Input/InputTextarea.js new file mode 100644 index 00000000..f458a9d8 --- /dev/null +++ b/src/components/Input/InputTextarea.js @@ -0,0 +1,66 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React, { Component } from 'react'; +import propTypes from 'prop-types'; +import { observer } from 'mobx-react'; + +import styles from './Input.scss'; + +@observer +class InputTextarea extends Component { + handleOnChange = (e) => { + const { field, onInput } = this.props; + field.onChange(e); + onInput(field.value); + } + + render() { + const { + field, className, hint, + } = this.props; + return ( + + + + {field.placeholder} + {hint} + + + {field.error} + + + ); + } +} + +InputTextarea.propTypes = { + className: propTypes.string, + field: propTypes.shape({ + error: propTypes.string, + value: propTypes.string.isRequired, + placeholder: propTypes.string, + label: propTypes.string, + bind: propTypes.func.isRequired, + onChange: propTypes.func.isRequired, + }).isRequired, + onInput: propTypes.func, + hint: propTypes.element, +}; + +InputTextarea.defaultProps = { + className: '', + onInput: () => null, + hint: null, +}; + +export default InputTextarea; diff --git a/src/components/Input/index.js b/src/components/Input/index.js index d073daff..9b54c81c 100644 --- a/src/components/Input/index.js +++ b/src/components/Input/index.js @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable react/jsx-props-no-spreading */ import React, { Component } from 'react'; import propTypes from 'prop-types'; @@ -7,6 +9,15 @@ import styles from './Input.scss'; @observer class Input extends Component { + constructor(props) { + super(props); + + const { field } = props; + if (props.defaultValue) { + field.set(props.defaultValue); + } + } + handleOnChange = (e) => { const { field, onInput } = this.props; field.onChange(e); @@ -15,7 +26,7 @@ class Input extends Component { render() { const { - children, field, className, + children, field, className, hint, } = this.props; return ( @@ -26,7 +37,13 @@ class Input extends Component { value={field.value} onChange={this.handleOnChange} /> - {field.placeholder} + { field.focus(); }} + > + {field.placeholder} + + {hint} {field.error} @@ -37,21 +54,34 @@ class Input extends Component { } Input.propTypes = { - children: propTypes.element.isRequired, + children: propTypes.element, className: propTypes.string, field: propTypes.shape({ - error: propTypes.string.isRequired, - value: propTypes.string.isRequired, - placeholder: propTypes.string.isRequired, + error: propTypes.string, + value: propTypes.oneOfType([ + propTypes.string, + propTypes.number, + ]).isRequired, + placeholder: propTypes.oneOfType([ + propTypes.string, + propTypes.shape({}), + ]).isRequired, + set: propTypes.func.isRequired, + focus: propTypes.func.isRequired, bind: propTypes.func.isRequired, onChange: propTypes.func.isRequired, }).isRequired, + defaultValue: propTypes.oneOfType([propTypes.string, propTypes.number]), onInput: propTypes.func, + hint: propTypes.element, }; Input.defaultProps = { + children: null, className: '', onInput: () => null, + defaultValue: '', + hint: null, }; export default Input; diff --git a/src/components/InputSeed/SeedForm.js b/src/components/InputSeed/SeedForm.js index f390f8c2..bc64a6b6 100644 --- a/src/components/InputSeed/SeedForm.js +++ b/src/components/InputSeed/SeedForm.js @@ -8,6 +8,16 @@ import styles from '../Login/Login.scss'; @withTranslation() class SeedInput extends Component { + static propTypes = { + form: propTypes.shape({ + onSubmit: propTypes.func.isRequired, + loading: propTypes.bool.isRequired, + $: propTypes.func.isRequired, + }).isRequired, + seed: propTypes.arrayOf(propTypes.string).isRequired, + t: propTypes.func.isRequired, + }; + render() { const { seed, form, t, @@ -36,14 +46,4 @@ class SeedInput extends Component { } } -SeedInput.propTypes = { - form: propTypes.shape({ - onSubmit: propTypes.func.isRequired, - loading: propTypes.bool.isRequired, - $: propTypes.func.isRequired, - }).isRequired, - seed: propTypes.arrayOf(propTypes.string).isRequired, - t: propTypes.func.isRequired, -}; - export default SeedInput; diff --git a/src/components/InputSeed/index.js b/src/components/InputSeed/index.js index 2b1b3690..eced8159 100644 --- a/src/components/InputSeed/index.js +++ b/src/components/InputSeed/index.js @@ -1,3 +1,4 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import propTypes from 'prop-types'; @@ -11,6 +12,8 @@ import { BackIcon } from '../Icons'; import Loader from '../Loader'; import SeedForm from '../../stores/FormsStore/SeedForm'; import SeedInput from './SeedForm'; +import UserStore from '../../stores/UserStore/UserStore'; +import AppStore from '../../stores/AppStore/AppStore'; import styles from '../Login/Login.scss'; @@ -18,6 +21,13 @@ import styles from '../Login/Login.scss'; @inject('appStore', 'userStore') @observer class InputSeed extends Component { + static propTypes = { + userStore: propTypes.instanceOf(UserStore).isRequired, + appStore: propTypes.instanceOf(AppStore).isRequired, + recover: propTypes.bool.isRequired, + t: propTypes.func.isRequired, + }; + seedForm = new SeedForm({ hooks: { onSuccess: (form) => this.submitForm(form), @@ -108,21 +118,4 @@ class InputSeed extends Component { } } -InputSeed.propTypes = { - userStore: propTypes.shape({ - setMnemonicRepeat: propTypes.func.isRequired, - isSeedValid: propTypes.func.isRequired, - recoverWallet: propTypes.func.isRequired, - setEncryptedWallet: propTypes.func.isRequired, - getEthBalance: propTypes.func.isRequired, - saveWalletToFile: propTypes.func.isRequired, - mnemonic: propTypes.arrayOf(propTypes.string).isRequired, - }).isRequired, - appStore: propTypes.shape({ - displayAlert: propTypes.func.isRequired, - }).isRequired, - recover: propTypes.bool.isRequired, - t: propTypes.func.isRequired, -}; - export default InputSeed; diff --git a/src/components/LangSwitcher/LangSwitcher.scss b/src/components/LangSwitcher/LangSwitcher.scss index 046b9ecf..65394fe0 100644 --- a/src/components/LangSwitcher/LangSwitcher.scss +++ b/src/components/LangSwitcher/LangSwitcher.scss @@ -1,6 +1,7 @@ @import '../../assets/styles/partials/variables'; .lang { + position: relative; display: inline-block; &--opened { @@ -40,10 +41,12 @@ &__options { position: absolute; + left: 10px; display: inline-block; width: max-content; - padding: 5px 10px; - border: 1px solid $border; + padding: 5px 15px; + background-color: $white; + border: 1px solid #E1E4E8; visibility: hidden; opacity: 0; transition: 0.2s linear; @@ -51,10 +54,18 @@ &__option { display: block; padding: 5px 0; - font-size: 16px; + font-size: 14px; background-color: $white; border: none; outline: none; cursor: pointer; + + &:first-child { + padding: 5px 0 10px; + } + + &:last-child { + padding: 10px 0 5px; + } } } \ No newline at end of file diff --git a/src/components/LangSwitcher/index.js b/src/components/LangSwitcher/index.js index 5dc1669f..30f635d9 100644 --- a/src/components/LangSwitcher/index.js +++ b/src/components/LangSwitcher/index.js @@ -2,20 +2,39 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import React, { Component } from 'react'; import { withTranslation } from 'react-i18next'; +import moment from 'moment'; import propTypes from 'prop-types'; +import nextId from 'react-id-generator'; import i18n from '../../i18n'; import styles from './LangSwitcher.scss'; +import { getCorrectMomentLocale } from '../../utils/Date'; @withTranslation() class LangSwitcher extends Component { + static propTypes = { + t: propTypes.func.isRequired, + disabled: propTypes.bool, + onSelect: propTypes.func, + } + + static defaultProps = { + disabled: false, + onSelect: () => {}, + }; + constructor(props) { super(props); this.state = { opened: false, + language: null, }; this.setWrapperRef = this.setWrapperRef.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); + + window.ipcRenderer.on('change-language:confirm', (event, value) => { + this.changeLanguage(value); + }); } componentDidMount() { @@ -24,6 +43,9 @@ class LangSwitcher extends Component { componentWillUnmount() { document.addEventListener('mousedown', this.handleClickOutside); + window.ipcRenderer.removeListener('change-language:confirm', (event, value) => { + this.changeLanguage(value); + }); } setWrapperRef(node) { @@ -37,10 +59,21 @@ class LangSwitcher extends Component { }); } + changeLanguage = (value) => { + i18n.changeLanguage(value); + moment.locale(getCorrectMomentLocale(i18n.language)); + } + selectOption = (e) => { + const { props: { disabled, onSelect } } = this; const value = e.target.getAttribute('data-value'); this.toggleOptions(); - i18n.changeLanguage(value); + this.setState({ language: value }); + if (disabled) { + onSelect(value); + } else { + window.ipcRenderer.send('change-language:request', value); + } } closeOptions = () => { @@ -56,13 +89,13 @@ class LangSwitcher extends Component { } render() { - const { opened } = this.state; + const { opened, language: stateLanguage } = this.state; const { t } = this.props; const { language } = i18n; return ( - {language} + {stateLanguage !== null ? stateLanguage : language} { @@ -72,6 +105,7 @@ class LangSwitcher extends Component { className={styles.lang__option} data-value={item} onClick={this.selectOption} + key={nextId('lang_switcher_option')} > {`${t(`other:${item}`)} (${item})`} @@ -83,8 +117,5 @@ class LangSwitcher extends Component { } } -LangSwitcher.propTypes = { - t: propTypes.func.isRequired, -}; export default LangSwitcher; diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss index 1ad9986d..ebb56c1f 100644 --- a/src/components/Loader/Loader.scss +++ b/src/components/Loader/Loader.scss @@ -7,7 +7,7 @@ height: 14px; margin: 20px; background-color: $primary; - + &:before { position: absolute; top: 50%; @@ -20,6 +20,7 @@ animation: loaderSpin 2s ease-in infinite; content: ''; } + &:after { position: absolute; top: 50%; @@ -28,11 +29,23 @@ color: $white; font-weight: bolder; font-size: 102px; - line-height: 26px; + line-height: 26px; transform: translate(-50%, -50%); animation: loaderSpin 2s ease-in infinite; content: "+"; } + + &--white { + &::after { + color: $white; + } + } + + &--gray { + &::after { + color: #fafbfc; + } + } } @keyframes loaderSpin { diff --git a/src/components/Loader/index.js b/src/components/Loader/index.js index 70ceea1e..956659d3 100644 --- a/src/components/Loader/index.js +++ b/src/components/Loader/index.js @@ -1,9 +1,25 @@ import React from 'react'; +import PropTypes from 'prop-types'; import styles from './Loader.scss'; -const Loader = () => ( - +const Loader = ({ + theme, +}) => ( + ); +Loader.propTypes = { + theme: PropTypes.oneOf(['white', 'gray']), +}; + +Loader.defaultProps = { + theme: 'white', +}; + export default Loader; diff --git a/src/components/Login/Login.scss b/src/components/Login/Login.scss index bd4d362a..f1b10563 100644 --- a/src/components/Login/Login.scss +++ b/src/components/Login/Login.scss @@ -46,6 +46,12 @@ form, .add-project { padding: 0 50px; } + + &.create-token-data { + .btn { + margin: 38px auto 16px; + } + } } &__submit { @@ -147,10 +153,11 @@ text-align: left; &-id { display: inline-block; + width: 20px; margin-right: 10px; color: #181818; + text-align: right; opacity: .5; - } &-text { font-weight: bold; @@ -235,7 +242,7 @@ } } } -} +} .create { .btn--white { @@ -254,7 +261,7 @@ margin: 5px auto 0; } } - + } &__label { @@ -275,7 +282,7 @@ display: inline-block; width: 80px; height: 80px; - transition: .3s linear; + transition: .3s linear; &__icon { position: absolute; top: 50%; @@ -351,7 +358,7 @@ opacity: 1; } } - + &.active{ & > img { opacity: 1; diff --git a/src/components/Login/index.js b/src/components/Login/index.js index 52095573..2dfff961 100644 --- a/src/components/Login/index.js +++ b/src/components/Login/index.js @@ -1,6 +1,7 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import { observer, inject } from 'mobx-react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; import { NavLink, Redirect } from 'react-router-dom'; import { withTranslation } from 'react-i18next'; import Container from '../Container'; @@ -12,6 +13,8 @@ import Input from '../Input'; import Button from '../Button/Button'; import LoadingBlock from '../LoadingBlock'; import LoginForm from '../../stores/FormsStore/LoginForm'; +import AppStore from '../../stores/AppStore/AppStore'; +import UserStore from '../../stores/UserStore/UserStore'; import styles from './Login.scss'; @@ -19,6 +22,12 @@ import styles from './Login.scss'; @inject('userStore', 'appStore') @observer class Login extends Component { + static propTypes = { + appStore: PropTypes.instanceOf(AppStore).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + t: PropTypes.func.isRequired, + }; + loginForm = new LoginForm({ hooks: { onSuccess: (form) => this.login(form), @@ -87,7 +96,10 @@ const InputForm = withTranslation()(({ - + @@ -109,27 +121,14 @@ const InputForm = withTranslation()(({ )); -Login.propTypes = { - appStore: propTypes.shape({ - displayAlert: propTypes.func.isRequired, - readWalletList: propTypes.func.isRequired, - }).isRequired, - userStore: propTypes.shape({ - logging: propTypes.bool.isRequired, - login: propTypes.func.isRequired, - authorized: propTypes.bool.isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; - InputForm.propTypes = { - appStore: propTypes.shape({ - wallets: propTypes.arrayOf(propTypes.object).isRequired, + appStore: PropTypes.shape({ + wallets: PropTypes.arrayOf(PropTypes.object).isRequired, }).isRequired, - form: propTypes.shape({ - $: propTypes.func.isRequired, - onSubmit: propTypes.func.isRequired, - loading: propTypes.bool.isRequired, + form: PropTypes.shape({ + $: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, }).isRequired, }; diff --git a/src/components/Logo/Logo.scss b/src/components/Logo/Logo.scss index 8ed4a7ce..31893530 100644 --- a/src/components/Logo/Logo.scss +++ b/src/components/Logo/Logo.scss @@ -35,7 +35,7 @@ span { font-weight: 700; font-size: 14px; - font-family: "Grotesk"; + font-family: "Roboto"; } } diff --git a/src/components/Logo/index.js b/src/components/Logo/index.js index ac442623..c1839dab 100644 --- a/src/components/Logo/index.js +++ b/src/components/Logo/index.js @@ -1,9 +1,11 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import { inject, observer } from 'mobx-react'; + import styles from './Logo.scss'; -const Logo = () => ( - +const Logo = inject('appStore')(observer(({ appStore: { inProject } }) => ( + 01 @@ -11,6 +13,6 @@ const Logo = () => ( ZeroOne -); +))); export default Logo; diff --git a/src/components/Members/MemberItem.js b/src/components/Members/MemberItem.js new file mode 100644 index 00000000..198d9d31 --- /dev/null +++ b/src/components/Members/MemberItem.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const MemberItem = ({ + name, +}) => ( + + {name} + +); + +MemberItem.propTypes = { + name: PropTypes.string.isRequired, +}; + +export default MemberItem; diff --git a/src/components/Members/Members.scss b/src/components/Members/Members.scss new file mode 100644 index 00000000..11168ac1 --- /dev/null +++ b/src/components/Members/Members.scss @@ -0,0 +1,272 @@ +.members { + &__page { + margin-bottom: 40px; + text-align: center; + + &-loader { + text-align: center; + } + + .loader { + margin-top: 50px; + } + } + + &__top { + width: 100%; + margin-bottom: 15px; + + &-button { + display: inline-block; + width: 100%; + padding: 26px 96px 26px 100px; + background: #fff; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + + &-icon, + &-text { + display: inline-block; + vertical-align: middle; + } + + &-icon { + margin-right: 16px; + + svg { + width: auto; + height: auto; + } + } + + &-text { + color: #000; + font-weight: 300; + font-size: 18px; + font-family: "Roboto"; + line-height: 21px; + } + } + } + + &__group { + margin-bottom: 8px; + + &-button { + width: 100%; + padding: 8px 0; + text-align: left; + background: #fff; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + } + + &-id { + margin-bottom: 4px; + color: rgba(0, 0, 0, 0.2); + font-weight: 300; + font-size: 11px; + font-family: "Roboto"; + line-height: 13px; + } + + &-main, + &-extra, + &-divider { + display: inline-block; + vertical-align: middle; + } + + &-main { + width: 75%; + } + + &-id, + &-main, + &-wallet { + padding-left: 29px; + } + + &-extra { + position: relative; + width: calc(25% - 1px); + padding: 10px 20px; + text-align: center; + } + + &-divider { + width: 1px; + height: 100%; + max-height: 112px; + margin-top: -6px; + margin-bottom: -6px; + background-color: rgba(0, 0, 0, 0.1); + } + + &-name { + margin-bottom: 8px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + &-description { + min-height: 65px; + margin-bottom: 8px; + padding-right: 15px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 11px; + line-height: 13px; + } + + &-wallet, + &-token { + color: rgba(200, 201, 202, 0.7); + font-size: 11px; + line-height: 13px; + } + + &-balance { + margin-bottom: 8px; + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 14px; + line-height: 16px; + } + + &-no-data { + box-sizing: border-box; + width: 100%; + padding: 18px 32px; + background: #fff; + border: 1px solid #e1e4e8; + border-top: unset; + + &-icon, + &-text { + display: inline-block; + vertical-align: middle; + } + + &-icon { + margin-right: 10px; + } + + &-text { + color: #4d4d4d; + font-size: 11px; + line-height: 13px; + white-space: pre-line; + } + } + + &-table { + width: 100%; + padding-top: 8px; + background: #fff; + border: 1px solid #e1e4e8; + border-top: unset; + border-collapse: collapse; + + &-th { + padding: 8px; + color: #808080; + font-size: 11px; + line-height: 13px; + text-align: left; + + &--weight, + &--balance { + padding-right: 40px; + text-align: right; + } + } + + &-td { + padding: 10px; + + button { + width: 100%; + height: 100%; + background-color: transparent; + border: none; + outline: unset; + cursor: pointer; + } + + &--is { + min-width: 20px; + padding: 0; + text-align: center; + } + + &--img { + width: 24px; + padding: 0; + } + + &--wallet { + padding: 10px 8px; + color: #000; + font-size: 14px; + line-height: 16px; + } + + &--weight, + &--balance { + padding-right: 40px; + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-align: right; + + button { + text-align: right; + } + } + + &--balance { + position: relative; + + span { + position: absolute; + top: 50%; + right: -14px; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.5s; + } + + svg { + width: auto; + height: auto; + } + } + } + + &-tr { + background-color: #fff; + transition: background-color 0.5s; + + &:hover { + background: #e1e4e8; + + .members__group-table-td--balance { + span { + opacity: 1; + } + } + } + } + } + } +} +.text { + &--left { + text-align: left; + } +} \ No newline at end of file diff --git a/src/components/Members/Members.stories.js b/src/components/Members/Members.stories.js new file mode 100644 index 00000000..960305ce --- /dev/null +++ b/src/components/Members/Members.stories.js @@ -0,0 +1,80 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import MembersTop from './MembersTop'; +import MembersGroupComponent from './MembersGroupComponent'; +import MembersStore from '../../stores/MembersStore/MembersStore'; + +const memberStore = new MembersStore([]); + +memberStore.addToGroups({ + name: 'Менеджеры', + description: 'Могут голосовать только по вопросам из групп: “Дизайн”, “Верстка”, “Бэкэнд”', + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + balance: '0,000654', + customTokenName: 'TKN', + tokenName: 'Кастомные токены', + textForEmptyState: 'other:noDataAdmins', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 25, + balance: '0,000004', + customTokenName: 'TKN', + isAdmin: true, + }, + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 25, + balance: '0,000004', + customTokenName: 'TKN', + }, + ], +}); + +memberStore.addToGroups({ + name: 'Администраторы', + description: 'Могут голосовать по любым вопросам.', + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + balance: '11,510156', + customTokenName: 'TKN', + tokenName: 'ERC20', + textForEmptyState: 'other:noDataAdmins', + list: [], +}); + +const groupWithList = memberStore.groups[0]; +const groupWithEmptyList = memberStore.groups[1]; + +storiesOf('Members', module) + .add('MembersTop', () => ( + + )) + .add('MembersGroupComponent with list', () => ( + + )) + .add('MembersGroupComponent with empty list', () => ( + + )); diff --git a/src/components/Members/MembersGroupComponent.js b/src/components/Members/MembersGroupComponent.js new file mode 100644 index 00000000..26b4cbba --- /dev/null +++ b/src/components/Members/MembersGroupComponent.js @@ -0,0 +1,323 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { computed } from 'mobx'; +import { Collapse } from 'react-collapse'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import MemberItem from '../../stores/MembersStore/MemberItem'; +import DialogStore from '../../stores/DialogStore'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import UserStore from '../../stores/UserStore'; +import AppStore from '../../stores/AppStore'; +import { Pudding } from '../Icons'; +import MembersGroupTable from './MembersGroupTable'; +import Dialog from '../Dialog/Dialog'; +import TokenTransfer from '../TokenTransfer/TokenTransfer'; +import TransferTokenForm from '../../stores/FormsStore/TransferTokenForm'; +import { + TokenInProgressMessage, + TransferSuccessMessage, + TransferErrorMessage, +} from '../Message'; +import { tokenTypes } from '../../constants'; + +import styles from './Members.scss'; + +/** + * Group members component + * + * @param selectedWallet selected wallet + * @param item + */ +@withTranslation() +@inject('dialogStore', 'membersStore', 'userStore', 'appStore') +@observer +class MembersGroupComponent extends React.Component { + transferSteps = { + input: 0, + transfering: 1, + success: 2, + error: 3, + } + + transferForm = new TransferTokenForm({ + hooks: { + onSuccess: (form) => { + const { + id, + membersStore, + userStore, + } = this.props; + const { selectedWallet } = this.state; + const groupId = id; + const { address: rawAddress, count, password } = form.values(); + const address = rawAddress.trim(); + userStore.setPassword(password); + membersStore.setTransferStatus('transfering'); + return membersStore.transferTokens(groupId, selectedWallet, address, count) + .then(() => { + membersStore.setTransferStatus('success'); + membersStore.list[groupId].updateMemberBalanceAndWeight(selectedWallet); + membersStore.list[groupId].updateMemberBalanceAndWeight(address); + }) + .catch(() => { + membersStore.setTransferStatus('error'); + }); + }, + onError: () => { + /* eslint-disable-next-line */ + console.error('form error'); + }, + }, + }) + + static propTypes = { + /** id group */ + id: PropTypes.number.isRequired, + /** name group */ + name: PropTypes.string.isRequired, + /** group token type */ + groupType: PropTypes.string.isRequired, + /** balance with token */ + fullBalance: PropTypes.string.isRequired, + /** info about group */ + description: PropTypes.string.isRequired, + /** wallet group */ + wallet: PropTypes.string.isRequired, + /** token group */ + token: PropTypes.string.isRequired, + /** member list */ + list: PropTypes.arrayOf(PropTypes.instanceOf(MemberItem)).isRequired, + /** text when list is empty */ + textForEmptyState: PropTypes.string.isRequired, + /** translate method */ + t: PropTypes.func.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + appStore: PropTypes.instanceOf(AppStore).isRequired, + admin: PropTypes.arrayOf( + PropTypes.shape({}), + ).isRequired, + } + + constructor() { + super(); + this.state = { + isOpen: false, + selectedWallet: '', + }; + } + + /** + * Return actual modal props state + * + * @returns {object} actual modal props + */ + @computed + get modalPropsSwitch() { + const { + membersStore: { transferStatus }, + } = this.props; + const { transferSteps } = this; + switch (transferStatus) { + case (transferSteps.input): + return { header: null, footer: null }; + case (transferSteps.transfering): + return { header: null, footer: null, closeable: false }; + case (transferSteps.success): + return { header: null, footer: null }; + case (transferSteps.error): + return { header: null, footer: null }; + default: + return null; + } + } + + /** + * Method for return actual modal content + * + * @returns {Node} actual modal content + */ + modalContentSwitch = () => { + const { + id, + wallet, + groupType, + membersStore, + dialogStore, + t, + } = this.props; + const { transferStatus } = membersStore; + const { selectedWallet } = this.state; + const { transferSteps } = this; + switch (transferStatus) { + case (transferSteps.input): + return ( + + ); + case (transferSteps.transfering): + return ; + case (transferSteps.success): + return dialogStore.hide()} />; + case (transferSteps.error): + return ( + { membersStore.setTransferStatus('input'); }} + buttonText={t('buttons:retry')} + /> + ); + default: + return null; + } + } + + /** + * Method for change isOpen state + */ + toggleOpen = () => { + const { membersStore } = this.props; + this.setState((prevState) => ({ + isOpen: !prevState.isOpen, + })); + membersStore.setTransferStatus('input'); + } + + handleClick = ({ selectedWallet }) => { + const { + membersStore, + admin: administrator, + userStore: { address }, + appStore, + groupType, + t, + } = this.props; + let isAdmininstrator = false; + const isCustomToken = groupType === tokenTypes.Custom; + const isIdentical = selectedWallet.toUpperCase() === address.toUpperCase(); + if (isCustomToken) { + const [groupAdmin] = administrator; + isAdmininstrator = (address.toUpperCase() === groupAdmin.wallet.toUpperCase()); + } + if (isCustomToken && !isAdmininstrator) { + appStore.displayAlert(t('errors:transferLocked')); + return; + } + if (isIdentical || isAdmininstrator) { + membersStore.setTransferStatus('input'); + const { dialogStore, id } = this.props; + this.setState({ selectedWallet }); + dialogStore.show(`transfer-token-${id}`); + } else { + // eslint-disable-next-line react/prop-types + appStore.displayAlert(t('errors:transferIfNotAdmin')); + } + } + + render() { + const { + id, + name, + fullBalance, + description, + wallet, + token, + list, + groupType, + textForEmptyState, + t, + membersStore: { transferStatus }, + } = this.props; + const { transferSteps } = this; + const { isOpen } = this.state; + console.log('list', list); + return ( + + + {`#${id}`} + + + {name} + + + {description} + + + + + + {fullBalance} + + + {token} + + + {wallet} + + + { + list && list.length + ? ( + + + + ) + : ( + + + + + + {t(textForEmptyState)} + + + ) + } + { + groupType === tokenTypes.ERC20 + ? ( + + + + + + {t('other:noDataAdmins')} + + + ) + : null + + } + + + {this.modalContentSwitch()} + + + ); + } +} + +export default MembersGroupComponent; diff --git a/src/components/Members/MembersGroupComponent.test.js b/src/components/Members/MembersGroupComponent.test.js new file mode 100644 index 00000000..3e9a6f0c --- /dev/null +++ b/src/components/Members/MembersGroupComponent.test.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import MembersGroupComponent from './MembersGroupComponent'; +import MemberItem from '../../stores/MembersStore/MemberItem'; +import MembersGroupTable from './MembersGroupTable'; + +describe('MembersGroupComponent', () => { + const defaultProps = { + id: 0, + name: 'Admins', + fullBalance: '0.1201 TKN', + description: 'description text', + wallet: '0xA234FA767ASD7F67HH34HF7DF7S', + token: 'ERC 20', + textForEmptyState: 'textForEmptyState', + list: [], + }; + + describe('With correct data, list is empty', () => { + let wrapper; + let instance; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + instance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.members__group-id').text()).toEqual('#0'); + expect(wrapper.find('.members__group-name').text()).toEqual('Admins'); + expect(wrapper.find('.members__group-description').text()).toEqual('description text'); + expect(wrapper.find('.members__group-wallet').text()).toEqual('0xA234FA767ASD7F67HH34HF7DF7S'); + expect(wrapper.find('.members__group-token').text()).toEqual('ERC 20'); + expect(wrapper.find('.members__group-no-data-text').text()).toEqual('textForEmptyState'); + expect(wrapper.find('.members__group-button').prop('onClick')).toEqual(instance.toggleOpen); + }); + + it('toggleOpen should change isOpen state', () => { + expect(instance.state.isOpen).toEqual(false); + instance.toggleOpen(); + expect(instance.state.isOpen).toEqual(true); + instance.toggleOpen(); + expect(instance.state.isOpen).toEqual(false); + }); + }); + + describe('With correct data, list have items', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render correct with correct prop data', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(MembersGroupTable).prop('list')).toEqual([ + new MemberItem({ + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + isAdmin: true, + }), + new MemberItem({ + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + isAdmin: false, + }), + ]); + }); + }); +}); diff --git a/src/components/Members/MembersGroupTable.js b/src/components/Members/MembersGroupTable.js new file mode 100644 index 00000000..c0e4acd4 --- /dev/null +++ b/src/components/Members/MembersGroupTable.js @@ -0,0 +1,132 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import MemberItem from '../../stores/MembersStore/MemberItem'; +import { AdminIcon, BorderArrowIcon } from '../Icons'; + +import styles from './Members.scss'; + +/** + * Component for displaying table with members + */ +@withTranslation('other') +@observer +class MembersGroupTable extends React.PureComponent { + static propTypes = { + /** List members */ + list: PropTypes.arrayOf(PropTypes.instanceOf(MemberItem)).isRequired, + /** Method for translate */ + t: PropTypes.func.isRequired, + /** Method for handle table row click */ + onRowClick: PropTypes.func.isRequired, + } + + render() { + const { + list, + t, + onRowClick, + } = this.props; + if (!list || !list.length) return null; + return ( + + + + {/* eslint-disable-next-line */} + + {/* eslint-disable-next-line */} + + + {t('other:walletAddress')} + + + {t('other:weightVote')} + + + {t('other:balance')} + + + { + list.map((item, index) => ( + + + {item.isAdmin ? : ''} + + + + + + {item.wallet} + + + {`${item.weight}%`} + + + { + onRowClick({ selectedWallet: item.wallet }); + }} + > + {item.fullBalance} + + + + + + + )) + } + + + ); + } +} + +export default MembersGroupTable; diff --git a/src/components/Members/MembersGroupTable.test.js b/src/components/Members/MembersGroupTable.test.js new file mode 100644 index 00000000..674c878a --- /dev/null +++ b/src/components/Members/MembersGroupTable.test.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { observable } from 'mobx'; +import MembersGroupTable from './MembersGroupTable'; +import MemberItem from '../../stores/MembersStore/MemberItem'; + +describe('MembersGroupTable', () => { + describe('List is empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + {}} + list={observable([])} + />, + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('List is not empty', () => { + let wrapper; + let mockRowClick; + + beforeEach(() => { + mockRowClick = jest.fn(); + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('should have correct text', () => { + expect( + wrapper.find('.members__group-table-tr').text(), + ).toEqual('0xA234FA767ASD7F67HH34HF7DF7S10%120 TKN'); + }); + + it('row onClick prop should call mockRowClick', () => { + wrapper.find('.members__group-table-tr').prop('onClick')(); + expect(mockRowClick).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/Members/MembersPage.js b/src/components/Members/MembersPage.js new file mode 100644 index 00000000..4cb00e61 --- /dev/null +++ b/src/components/Members/MembersPage.js @@ -0,0 +1,132 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import { observable, computed } from 'mobx'; +import Container from '../Container'; +import MembersTop from './MembersTop'; +import MembersGroupComponent from './MembersGroupComponent'; +import Dialog from '../Dialog/Dialog'; +import Loader from '../Loader'; +import Footer from '../Footer'; +import Notification from '../Notification/Notification'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import DialogStore from '../../stores/DialogStore'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; + +import styles from './Members.scss'; + +/** + * Component for page with members + */ +@withTranslation() +@inject('membersStore', 'projectStore', 'dialogStore') +@observer +class MembersPage extends React.Component { + @observable votingIsActive = false; + + static propTypes = { + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + t: PropTypes.func.isRequired, + } + + @computed + get loading() { + const { membersStore } = this.props; + return membersStore.loading; + } + + render() { + const { loading } = this; + const { + membersStore: { list }, projectStore, dialogStore, t, + } = this.props; + const { historyStore } = projectStore; + return ( + <> + + {/* FIXME remove comment */} + + { + !loading + ? ( + <> + + + { + list && list.length + ? ( + list.map((group, index) => ( + + )) + ) + : null + } + + > + ) + : ( + + + + ) + } + + + + + { dialogStore.hide(); }} /> + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + + > + ); + } +} + +export default MembersPage; diff --git a/src/components/Members/MembersPage.test.js b/src/components/Members/MembersPage.test.js new file mode 100644 index 00000000..da0fa06f --- /dev/null +++ b/src/components/Members/MembersPage.test.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { observable } from 'mobx'; +import { MembersPage } from '.'; +import MembersGroup from '../../stores/MembersStore/MembersGroup'; + +describe('MembersPage', () => { + describe('List is empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + {}, + list: observable([]), + }} + />, + ).dive().dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + }); + + describe('List not empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + {}, + list: observable([ + new MembersGroup({ + name: 'Admins', + description: 'short description for group', + customTokenName: 'TKN', + tokenName: 'ERC20', + wallet: '0xB210af05Bf82eF6C6BA034B22D18c89B5D23Cc90', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + }, + ], + }), + ]), + }} + />, + ).dive().dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + }); +}); diff --git a/src/components/Members/MembersTop.js b/src/components/Members/MembersTop.js new file mode 100644 index 00000000..9e23a383 --- /dev/null +++ b/src/components/Members/MembersTop.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { withTranslation, Trans } from 'react-i18next'; +import { PlayCircleIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './Members.scss'; +import { systemQuestionsId } from '../../constants'; + +/** + * Component in top members page + * + * @returns {Node} component + */ +const MembersTop = ({ + projectName, + votingIsActive, + history, +}) => ( + + )} + theme="with-play-icon" + onClick={ + () => history.push(`/votings?modal=start_new_vote&option=${systemQuestionsId.connectGroupUsers}`) + } + disabled={votingIsActive} + hint={ + votingIsActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + + + +); + +MembersTop.propTypes = { + projectName: PropTypes.string.isRequired, + votingIsActive: PropTypes.bool.isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, +}; + +export default withRouter(withTranslation('other')(MembersTop)); diff --git a/src/components/Members/MembersTop.test.js b/src/components/Members/MembersTop.test.js new file mode 100644 index 00000000..7141d08e --- /dev/null +++ b/src/components/Members/MembersTop.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Trans } from 'react-i18next'; +import { MembersTop } from '.'; + +describe('MembersTop', () => { + it('should render correct without onClick props', () => { + const wrapper = shallow().dive(); + expect(wrapper.length).toEqual(1); + expect(wrapper.find(Trans).props().values).toEqual({ project: 'test' }); + }); + + it('button onClick should call mockClick with onClick prop', () => { + const mockClick = jest.fn(); + const wrapper = shallow( + , + ).dive(); + expect(wrapper.length).toEqual(1); + expect(wrapper.find(Trans).props().values).toEqual({ project: 'test project' }); + const button = wrapper.find('button'); + button.prop('onClick')(); + expect(mockClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Members/index.js b/src/components/Members/index.js new file mode 100644 index 00000000..859b0564 --- /dev/null +++ b/src/components/Members/index.js @@ -0,0 +1,11 @@ +import MembersPage from './MembersPage'; +import MembersTop from './MembersTop'; +import MembersGroupComponent from './MembersGroupComponent'; + +export default MembersPage; + +export { + MembersPage, + MembersTop, + MembersGroupComponent, +}; diff --git a/src/components/Message/ERC20TokensUsed.js b/src/components/Message/ERC20TokensUsed.js new file mode 100644 index 00000000..57c363f9 --- /dev/null +++ b/src/components/Message/ERC20TokensUsed.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Message about agreed decision + */ +@withTranslation(['dialogs']) +class ERC20TokensUsed extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func, + buttonText: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + }; + + static defaultProps = { + onButtonClick: null, + buttonText: , + } + + render() { + const { + props: { + onButtonClick, + t, + buttonText, + }, + } = this; + return ( + + + + {t('other:erc20ListIsNotViewable')} + + + { + onButtonClick + ? ( + + + {buttonText} + + + ) + : null + } + + ); + } +} + +export default ERC20TokensUsed; diff --git a/src/components/Message/ErrorMessage.js b/src/components/Message/ErrorMessage.js new file mode 100644 index 00000000..510981bd --- /dev/null +++ b/src/components/Message/ErrorMessage.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other', 'headings']) +class ErrorMessage extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func, + buttonText: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + } + + static defaultProps = { + onButtonClick: null, + buttonText: , + } + + render() { + const { props: { t, onButtonClick, buttonText } } = this; + return ( + + + {t('headings:failedTransaction.subheading')} + + { + onButtonClick + ? ( + + + {buttonText} + + + ) + : null + } + + ); + } +} + +export default ErrorMessage; diff --git a/src/components/Message/Message.scss b/src/components/Message/Message.scss index e74b429a..d8b29553 100644 --- a/src/components/Message/Message.scss +++ b/src/components/Message/Message.scss @@ -9,7 +9,7 @@ color: #000; font-weight: 700; font-size: 24px; - font-family: "Grotesk"; + font-family: "Roboto"; line-height: 28px; text-align: center; } @@ -58,6 +58,58 @@ } } + &--transfer-error { + .subtext { + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + } + + .message { + &__title { + margin-top: 57px; + margin-bottom: 24px; + } + } + + .footer { + padding-top: 73px; + padding-bottom: 51px; + } + } + + &--transfer-progress { + display: flex; + flex-flow: row nowrap; + &::after { + display: inline-block; + min-height: 200px; + vertical-align: middle; + content: ''; + } + } + + &--erc20 { + .message { + &__title { + margin-top: 58px; + white-space: pre-wrap; + } + } + + .subtext { + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 14px; + line-height: 16px; + white-space: pre-wrap; + } + + .footer { + padding-top: 40px; + } + } + &--progress { .message { &__title { @@ -78,4 +130,15 @@ text-align: center; } } + + &--agreed, + &--reject, + &--transfer-success, + &--transfer-error, + &--erc20 { + button { + width: 100%; + max-width: 309px; + } + } } diff --git a/src/components/Message/SuccessMessage.js b/src/components/Message/SuccessMessage.js new file mode 100644 index 00000000..946e370c --- /dev/null +++ b/src/components/Message/SuccessMessage.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other', 'headings']) +class SuccessMessage extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func.isRequired, + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + ]), + } + + static defaultProps = { + children: null, + }; + + render() { + const { props: { t, onButtonClick, children } } = this; + return ( + + + {children} + + + + {t('buttons:continue')} + + + + ); + } +} + +export default SuccessMessage; diff --git a/src/components/Message/TokenInProgressMessage.js b/src/components/Message/TokenInProgressMessage.js index ee5a02f4..815422e3 100644 --- a/src/components/Message/TokenInProgressMessage.js +++ b/src/components/Message/TokenInProgressMessage.js @@ -1,8 +1,8 @@ import React from 'react'; import { withTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; -import Loader from '../Loader'; import DefaultMessage from './DefaultMessage'; +import { TransactionLoader } from '../Progress'; import styles from './Message.scss'; @@ -27,7 +27,7 @@ class TokenInProgressMessage extends React.Component { {t('dialogs:someTimeText')} - + diff --git a/src/components/Message/TransactionProgress.js b/src/components/Message/TransactionProgress.js new file mode 100644 index 00000000..06eb95af --- /dev/null +++ b/src/components/Message/TransactionProgress.js @@ -0,0 +1,51 @@ +/* eslint-disable no-unused-vars */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { EMPTY_DATA_STRING } from '../../constants'; +import DefaultMessage from './DefaultMessage'; +import { TransactionLoader, DeployingProgress } from '../Progress'; +// import Loader from '../Loader'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other']) +class TransactionProgress extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + value: PropTypes.string, + deploy: PropTypes.bool, + type: PropTypes.string, + } + + static defaultProps = { + value: EMPTY_DATA_STRING, + deploy: false, + type: '', + } + + render() { + const { + props: { + t, + value, + deploy, + type, + }, + } = this; + return ( + + { + deploy + ? + : + } + + ); + } +} + +export default TransactionProgress; diff --git a/src/components/Message/TransferErrorMessage.js b/src/components/Message/TransferErrorMessage.js new file mode 100644 index 00000000..b9fd7357 --- /dev/null +++ b/src/components/Message/TransferErrorMessage.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other']) +class TransferErrorMessage extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func, + buttonText: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + } + + static defaultProps = { + onButtonClick: null, + buttonText: , + } + + render() { + const { props: { t, onButtonClick, buttonText } } = this; + return ( + + + {t('other:notEnoughTokens')} + + { + onButtonClick + ? ( + + + {buttonText} + + + ) + : null + } + + ); + } +} + +export default TransferErrorMessage; diff --git a/src/components/Message/TransferErrorMessage.test.js b/src/components/Message/TransferErrorMessage.test.js new file mode 100644 index 00000000..3dd15293 --- /dev/null +++ b/src/components/Message/TransferErrorMessage.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import TransferErrorMessage from './TransferErrorMessage'; +import Button from '../Button/Button'; + +describe('TransferErrorMessage', () => { + let wrapper; + let mockOnClick; + + beforeEach(() => { + mockOnClick = jest.fn(); + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('should call mockOnClick on button onClick', () => { + const button = wrapper.find(Button); + expect(button.length).toEqual(1); + button.prop('onClick')(); + expect(mockOnClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Message/TransferSuccessMessage.js b/src/components/Message/TransferSuccessMessage.js index 936a2b1f..213905a9 100644 --- a/src/components/Message/TransferSuccessMessage.js +++ b/src/components/Message/TransferSuccessMessage.js @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withTranslation, Trans } from 'react-i18next'; -import { EMPTY_DATA_STRING } from '../../constants'; import DefaultMessage from './DefaultMessage'; import Button from '../Button/Button'; import styles from './Message.scss'; + /** * Dialog with message about success token transfer */ @@ -14,7 +14,6 @@ class TransferSuccessMessage extends React.Component { static propTypes = { t: PropTypes.func.isRequired, onButtonClick: PropTypes.func, - value: PropTypes.string, buttonText: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({}), @@ -26,7 +25,6 @@ class TransferSuccessMessage extends React.Component { } static defaultProps = { - value: EMPTY_DATA_STRING, buttonText: , } @@ -35,7 +33,6 @@ class TransferSuccessMessage extends React.Component { props: { onButtonClick, t, - value, buttonText, }, } = this; @@ -43,10 +40,7 @@ class TransferSuccessMessage extends React.Component { - {t('other:yourBalance')} - {value} - + /> { onButtonClick ? ( diff --git a/src/components/Message/index.js b/src/components/Message/index.js index 4e724aae..bc3ae3cb 100644 --- a/src/components/Message/index.js +++ b/src/components/Message/index.js @@ -3,6 +3,8 @@ import AgreedMessage from './AgreedMessage'; import RejectMessage from './RejectMessage'; import TransferSuccessMessage from './TransferSuccessMessage'; import TokenInProgressMessage from './TokenInProgressMessage'; +import TransferErrorMessage from './TransferErrorMessage'; +import ERC20TokensUsed from './ERC20TokensUsed'; export default DefaultMessage; @@ -11,4 +13,6 @@ export { RejectMessage, TransferSuccessMessage, TokenInProgressMessage, + TransferErrorMessage, + ERC20TokensUsed, }; diff --git a/src/components/Notification/Notification.js b/src/components/Notification/Notification.js new file mode 100644 index 00000000..a8e4589e --- /dev/null +++ b/src/components/Notification/Notification.js @@ -0,0 +1,137 @@ +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import NotificationStore from '../../stores/NotificationStore'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import NotificationItem from './NotificationItem'; +import TokensWithoutActiveVoting from '../Notifications/TokensWithoutActiveVoting'; +import TokensWithActiveVoting from '../Notifications/TokensWithActiveVoting'; + +import styles from './Notification.scss'; + +/** + * Class for render notification + */ +@inject('notificationStore', 'projectStore') +@observer +class Notification extends React.Component { + idTimer = null; + + static propTypes = { + notificationStore: PropTypes.instanceOf(NotificationStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + }; + + componentDidMount() { + const { projectStore: { rootStore } } = this.props; + const { configStore: { UPDATE_INTERVAL } } = rootStore; + this.updateReturnTokensNotification(); + this.idTimer = setInterval(() => { + this.updateReturnTokensNotification(); + }, UPDATE_INTERVAL); + } + + componentWillUnmount() { + clearInterval(this.idTimer); + } + + /** + * Method for remove notification + * + * @param {string} id id notification + */ + removeNotification = (id) => { + const { props } = this; + const { + notificationStore, + } = props; + notificationStore.remove(id); + } + + resetNotification = () => { + const { props } = this; + const { + notificationStore, + } = props; + notificationStore.reset(); + } + + updateReturnTokensNotification = async () => { + const { props } = this; + const { + projectStore: { + historyStore, + }, + notificationStore, + } = props; + const hasActiveVoting = await historyStore.isVotingActive; + + const userTokenReturns = await historyStore.fetchUserReturnTokens(); + const lastUserVoting = await historyStore.lastUserVoting(); + const countOfVoting = await historyStore.fetchVotingsCount(); + const lastVoteIndex = countOfVoting - 1; + + if (userTokenReturns === true) { + this.resetNotification(); + return; + } + if (Number(lastVoteIndex) !== Number(lastUserVoting) && userTokenReturns === false) { + this.resetNotification(); + // TODO maybe make other notification description? + notificationStore.add({ + isOpen: true, + content: , + }); + return; + } + if (hasActiveVoting === true && userTokenReturns === false) { + this.resetNotification(); + notificationStore.add({ + isOpen: true, + content: , + status: 'important', + }); + } + if (hasActiveVoting === false && userTokenReturns === false) { + this.resetNotification(); + notificationStore.add({ + isOpen: true, + content: , + }); + } + } + + render() { + const { props } = this; + const { + notificationStore: { + list, + }, + } = props; + return ( + <> + { + list && list.length + ? ( + + { + list.map((notification) => ( + this.removeNotification(notification.id)} + /> + )) + } + + ) + : null + } + > + ); + } +} + +export default Notification; diff --git a/src/components/Notification/Notification.scss b/src/components/Notification/Notification.scss new file mode 100644 index 00000000..1b0bf188 --- /dev/null +++ b/src/components/Notification/Notification.scss @@ -0,0 +1,65 @@ +.notification { + &__container { + padding-top: 40px; + padding-bottom: 40px; + } + + &__item { + position: relative; + width: 100%; + margin-bottom: 20px; + padding: 20px 40px; + color: #000; + font-weight: 700; + font-size: 13px; + line-height: 14px; + text-align: center; + background: #fff; + border: 1px solid #fff; + + &-close { + position: absolute; + top: 50%; + right: 15px; + display: none; + color: #c8c9ca; + background: transparent; + border: unset; + outline: none; + transform: translate(0, -50%); + cursor: pointer; + } + + .btn { + &__text { + font-weight: 700; + font-size: 12px; + line-height: 14px; + } + } + + &--info { + color: #000; + background: #fff; + border: 1px solid #000; + + .btn { + margin-left: 5px; + color: #000; + border-bottom-color: #000; + } + } + + &--important { + color: #fff; + background: #000; + border: 1px solid #000; + + .btn { + margin-left: 5px; + color: #fff; + border-bottom-color: #fff; + } + } + } +} \ No newline at end of file diff --git a/src/components/Notification/NotificationItem.js b/src/components/Notification/NotificationItem.js new file mode 100644 index 00000000..ec3a7661 --- /dev/null +++ b/src/components/Notification/NotificationItem.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { CloseIcon } from '../Icons'; + +import styles from './Notification.scss'; + +/** + * Class for render notification item + */ +class NotificationItem extends React.PureComponent { + static propTypes = { + isOpen: PropTypes.bool.isRequired, + content: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + ]).isRequired, + status: PropTypes.oneOf([ + 'info', + 'important', + ]).isRequired, + handleRemove: PropTypes.func.isRequired, + }; + + render() { + const { props } = this; + const { + isOpen, + content, + status, + handleRemove, + } = props; + return ( + <> + { + isOpen + ? ( + + {content} + + + + + ) + : null + } + > + ); + } +} + +export default NotificationItem; diff --git a/src/components/Notification/index.js b/src/components/Notification/index.js new file mode 100644 index 00000000..29a08c89 --- /dev/null +++ b/src/components/Notification/index.js @@ -0,0 +1,3 @@ +import Notification from './Notification'; + +export default Notification; diff --git a/src/components/Notifications/TokensWithActiveVoting.js b/src/components/Notifications/TokensWithActiveVoting.js new file mode 100644 index 00000000..d0a77da5 --- /dev/null +++ b/src/components/Notifications/TokensWithActiveVoting.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Button from '../Button/Button'; +import DialogStore from '../../stores/DialogStore'; + +@withTranslation() +@inject('dialogStore') +class TokensWithActiveVoting extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + }; + + handleClick = () => { + const { props } = this; + const { dialogStore } = props; + dialogStore.show('return_tokens'); + } + + render() { + const { props } = this; + const { t } = props; + return ( + <> + {t('other:youVotedAndTokensInContract')} + + {t('buttons:pickUpTokens')} + + > + ); + } +} + +export default TokensWithActiveVoting; diff --git a/src/components/Notifications/TokensWithoutActiveVoting.js b/src/components/Notifications/TokensWithoutActiveVoting.js new file mode 100644 index 00000000..321f5836 --- /dev/null +++ b/src/components/Notifications/TokensWithoutActiveVoting.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Button from '../Button/Button'; +import DialogStore from '../../stores/DialogStore'; + +@withTranslation() +@inject('dialogStore') +class TokensWithoutActiveVoting extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + }; + + handleClick = () => { + const { props } = this; + const { dialogStore } = props; + dialogStore.show('return_tokens'); + } + + render() { + const { props } = this; + const { t } = props; + return ( + <> + {t('other:votingCompletedButTokensInContract')} + + {t('buttons:pickUpTokensCapital')} + + > + ); + } +} + +export default TokensWithoutActiveVoting; diff --git a/src/components/Pagination/Pagination.js b/src/components/Pagination/Pagination.js new file mode 100644 index 00000000..122334b0 --- /dev/null +++ b/src/components/Pagination/Pagination.js @@ -0,0 +1,172 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import ReactJsPagination from 'react-js-pagination'; +import { withTranslation } from 'react-i18next'; +import { ThinArrow } from '../Icons'; + +import './Pagination.scss'; + +/** + * Component for pagination + */ +@withTranslation() +@observer +class Pagination extends React.Component { + static propTypes = { + activePage: PropTypes.number.isRequired, + lastPage: PropTypes.number.isRequired, + handlePageChange: PropTypes.func.isRequired, + itemsCountPerPage: PropTypes.number.isRequired, + totalItemsCount: PropTypes.number.isRequired, + pageRangeDisplayed: PropTypes.number.isRequired, + t: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + this.state = { + value: props.activePage, + }; + } + + static getDerivedStateFromProps(props, state) { + if (props.activePage !== state.value) { + return { + value: props.activePage, + }; + } + return null; + } + + /** + * Handle input change + * + * @param {event} event event called on input + */ + handleChange = (event) => { + const { + lastPage, + } = this.props; + const minPage = 1; + let newValue = parseInt(event.target.value, 10) || 0; + // do not let us enter a value greater than the maximum + if (newValue >= lastPage) newValue = lastPage; + if (newValue <= minPage) newValue = minPage; + this.setPaginationValue(newValue); + event.target.select(); + } + + /** + * Method for setting input value + * + * @param {number} newValue new value input + */ + setPaginationValue = (newValue) => { + this.setState({ value: newValue }); + } + + /** + * Handle click on button chained to input + * + * @param {number} page page from input + */ + handleButtonClick = (page) => { + this.onPageChange(page); + } + + /** + * Method called onChange event in + * ReactJsPagination component + * + * @param {number} page page from input + */ + onPageChange = (page) => { + const { + handlePageChange, + } = this.props; + handlePageChange(page); + this.setPaginationValue(page); + } + + /** + * Handle focus on input element + * + * @param {event} event event called on input + */ + handleFocus = (event) => { + event.target.select(); + } + + render() { + const { + activePage, + lastPage, + itemsCountPerPage, + totalItemsCount, + pageRangeDisplayed, + t, + } = this.props; + const { + value, + } = this.state; + return ( + <> + { + totalItemsCount === 0 + ? null + : ( + <> + )} + nextPageText={()} + firstPageText={( + <> + + + > + )} + lastPageText={( + <> + + + > + )} + itemClass="pagination__item" + itemClassFirst="pagination__item--first" + itemClassLast="pagination__item--last" + itemClassPrev="pagination__item--prev" + itemClassNext="pagination__item--next" + /> + + {t('other:page')} + + {`${t('other:outOf')} ${lastPage}`} + this.handleButtonClick(value)} + > + {t('other:goTo')} + + + > + ) + } + > + ); + } +} + +export default Pagination; diff --git a/src/components/Pagination/Pagination.scss b/src/components/Pagination/Pagination.scss new file mode 100644 index 00000000..616506d9 --- /dev/null +++ b/src/components/Pagination/Pagination.scss @@ -0,0 +1,122 @@ +.pagination { + margin-top: 27px; + margin-bottom: 18px; + padding: 0; + text-align: center; + list-style: none; + + &__item { + display: inline-block; + margin: 0 10px; + color: #808080; + font-size: 14px; + line-height: 16px; + vertical-align: middle; + + a { + text-decoration: unset; + } + + &.active { + a { + color: #000; + font-weight: 700; + } + } + + &--first, + &--last, + &--next, + &--prev { + margin: 0 4px; + + a { + display: inline-block; + width: 21px; + height: 21px; + color: #808080; + background-color: transparent; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0); + transition: background-color 0.3s, box-shadow 0.3s; + + svg { + width: 6px; + height: 18px; + } + } + } + + &--prev { + margin-right: 20px; + } + + &--next { + margin-left: 20px; + } + + &--last, + &--next { + a { + padding-right: 2px; + transform: rotate(180deg); + } + } + } + + &__footer { + margin-bottom: 65px; + text-align: center; + + span { + color: rgba(128, 128, 128, 0.4); + font-size: 11px; + line-height: 16px; + } + + input { + max-width: 50px; + margin: 0 9px; + padding: 3px; + color: #000; + font-size: 11px; + line-height: 16px; + text-align: center; + background: transparent; + border: unset; + border-bottom: 1px solid #c8c9ca; + outline: none; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0); + transition: background-color 0.3s, box-shadow 0.3s; + + &[type=number]::-webkit-inner-spin-button, + &[type=number]::-webkit-outer-spin-button { + margin: 0; + -webkit-appearance: none; + } + } + + button { + margin-left: 10px; + padding: 5px 15px; + color: rgba(128, 128, 128, 0.4); + font-size: 11px; + line-height: 16px; + background: #fff; + border: 1px solid #e1e4e8; + border-radius: 2px; + outline: none; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0); + cursor: pointer; + transition: background-color 0.3s, box-shadow 0.3s; + + &:hover, + &:active { + background-color: #f4fbfc; + } + + &:active { + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1); + } + } + } +} diff --git a/src/components/Pagination/Pagination.stories.js b/src/components/Pagination/Pagination.stories.js new file mode 100644 index 00000000..9d8b16af --- /dev/null +++ b/src/components/Pagination/Pagination.stories.js @@ -0,0 +1,26 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import Pagination from '.'; + +storiesOf('Pagination', module) + .add('Default', () => ( + {}} + itemsCountPerPage={10} + totalItemsCount={100} + pageRangeDisplayed={5} + /> + )) + .add('Short', () => ( + {}} + itemsCountPerPage={10} + totalItemsCount={21} + pageRangeDisplayed={5} + /> + )); diff --git a/src/components/Pagination/Pagination.test.js b/src/components/Pagination/Pagination.test.js new file mode 100644 index 00000000..716cf9cb --- /dev/null +++ b/src/components/Pagination/Pagination.test.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Pagination from '.'; + +describe('Pagination', () => { + let wrapper; + let mockHandlePageChange; + let wrapperInstance; + + beforeEach(() => { + mockHandlePageChange = jest.fn(); + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error & elements have correct prop', () => { + expect(wrapper.length).toEqual(1); + const inputElement = wrapper.find('input'); + expect(inputElement.prop('onChange')).toEqual(wrapperInstance.handleChange); + expect(inputElement.prop('onFocus')).toEqual(wrapperInstance.handleFocus); + }); + + it('handleChange with value 11 should set value to 10', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.handleChange({ target: { value: 11, select: () => {} } }); + expect(wrapperInstance.state.value).toEqual(10); + }); + + it('handleChange with value 5 should set value to 5', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.handleChange({ target: { value: 5, select: () => {} } }); + expect(wrapperInstance.state.value).toEqual(5); + }); + + it('handleChange with value -5 should set value to 1', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.handleChange({ target: { value: -5, select: () => {} } }); + expect(wrapperInstance.state.value).toEqual(1); + }); + + it('handleButtonClick with 5 should call mockHandlePageChange with 5', () => { + wrapperInstance.handleButtonClick(5); + expect(mockHandlePageChange).toHaveBeenCalledWith(5); + }); + + it('onPageChange with 6 should call mockHandlePageChange with 6 & set value to 6', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.onPageChange(6); + expect(mockHandlePageChange).toHaveBeenCalledWith(6); + expect(wrapperInstance.state.value).toEqual(6); + }); + + it('handleFocus should call mockFocus', () => { + const mockSelect = jest.fn(); + wrapperInstance.handleFocus({ target: { select: mockSelect } }); + expect(mockSelect).toHaveBeenCalled(); + }); + + it('button onClick should call mockHandlePageChange with 1', () => { + wrapper.find('button').prop('onClick')(); + expect(mockHandlePageChange).toHaveBeenCalledWith(1); + }); +}); diff --git a/src/components/Pagination/index.js b/src/components/Pagination/index.js new file mode 100644 index 00000000..9ed530b1 --- /dev/null +++ b/src/components/Pagination/index.js @@ -0,0 +1,3 @@ +import Pagination from './Pagination'; + +export default Pagination; diff --git a/src/components/Progress/DeployingProgress.js b/src/components/Progress/DeployingProgress.js new file mode 100644 index 00000000..49467685 --- /dev/null +++ b/src/components/Progress/DeployingProgress.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import ProgressBlock from '../ProjectUploading/ProgressBlock'; +import { + SendingIcon, TxHashIcon, TxRecieptIcon, CompilingIcon, QuestionUploadingIcon, +} from '../Icons'; + +import styles from './progress.scss'; + +const DeployingProgress = withTranslation()(inject('appStore')(observer(({ appStore, type, t }) => { + const stagesToken = [ + [t('other:compiling'), ], + [t('other:sending'), ], + [t('other:txHash'), ], + [t('other:txReceipt'), ], + ]; + + const stagesZeroOne = [ + [t('other:compiling'), ], + [t('other:sending'), ], + [t('other:txHash'), ], + [t('other:txReceipt'), ], + [t('other:questionsUploading'), [ + , + + {appStore.uploadedQuestion} + {'/'} + {appStore.countOfQuestions} + ], + ], + ]; + + return ( + + { + type === 'ZeroOne' + ? stagesZeroOne.map((stage, index) => ( + + {stage[1]} + + )) + : stagesToken.map((stage, index) => ( + + {stage[1]} + + )) + } + + ); +}))); + +export default DeployingProgress; diff --git a/src/components/Progress/TransactionLoader.js b/src/components/Progress/TransactionLoader.js new file mode 100644 index 00000000..d084e479 --- /dev/null +++ b/src/components/Progress/TransactionLoader.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { withTranslation } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import ProgressBlock from '../ProjectUploading/ProgressBlock'; +import { + SigningIcon, SendingIcon, TxHashIcon, TxRecieptIcon, +} from '../Icons'; + +import styles from './progress.scss'; + +const TransactionLoader = withTranslation()(inject('appStore')(observer(({ appStore: { transactionStep }, t }) => { + const stages = [ + [t('other:txSigning'), ], + [t('other:sending'), ], + [t('other:txHash'), ], + [t('other:txReceipt'), ], + ]; + + return ( + + { + stages.map((stage, index) => ( + + {stage[1]} + + )) + } + + ); +}))); + +export default TransactionLoader; diff --git a/src/components/Progress/index.js b/src/components/Progress/index.js new file mode 100644 index 00000000..af3dec47 --- /dev/null +++ b/src/components/Progress/index.js @@ -0,0 +1,7 @@ +import TransactionLoader from './TransactionLoader'; +import DeployingProgress from './DeployingProgress'; + +export { + TransactionLoader, + DeployingProgress, +}; diff --git a/src/components/Progress/progress.scss b/src/components/Progress/progress.scss new file mode 100644 index 00000000..99311250 --- /dev/null +++ b/src/components/Progress/progress.scss @@ -0,0 +1,24 @@ +.transaction-progress { + display: flex; + justify-content: space-between; + width: 602px; + margin: 0 auto; + padding-top: 40px; + text-align: center; + &--zeroone { + .progress { + &-block { + .progress-line { + left: -51px; + width: 52px; + } + &.success { + .progress-line { + left: -51px; + width: 52px; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/components/ProgressBar/ProgressBar.js b/src/components/ProgressBar/ProgressBar.js new file mode 100644 index 00000000..411da328 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import styles from './ProgressBar.scss'; + +class ProgressBar extends React.PureComponent { + static propTypes = { + progress: PropTypes.number.isRequired, + countIndicator: PropTypes.number, + className: PropTypes.string, + }; + + static defaultProps = { + countIndicator: 10, + className: '', + }; + + /** + * Method for getting dimension scale + * for indicator + * + * @returns {number} dimension value + */ + getDimensionIndicator = () => { + const { props } = this; + const { countIndicator } = props; + return 100 / countIndicator; + } + + /** + * Method for detect filling indicator + * + * @param {number} index index indicator + * @returns {boolean} indicator filled state + */ + indicatorIsFilled = (index) => { + const { props } = this; + const { progress } = props; + const dimension = this.getDimensionIndicator(); + return (dimension * (index + 1)) <= progress; + } + + render() { + const { props } = this; + const { countIndicator, className } = props; + const arrIndicator = new Array(countIndicator).fill(''); + return ( + + { + arrIndicator.map((item, index) => ( + + )) + } + + ); + } +} + +export default ProgressBar; diff --git a/src/components/ProgressBar/ProgressBar.scss b/src/components/ProgressBar/ProgressBar.scss new file mode 100644 index 00000000..b0c90361 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.scss @@ -0,0 +1,15 @@ +.progress-bar { + &__indicator { + display: inline-block; + width: 8px; + height: 8px; + margin-right: 1px; + margin-bottom: 1px; + background: #fff; + border: 1px solid #000; + + &--filled { + background-color: #000; + } + } +} \ No newline at end of file diff --git a/src/components/ProgressBar/ProgressBar.test.js b/src/components/ProgressBar/ProgressBar.test.js new file mode 100644 index 00000000..991aa708 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.test.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ProgressBar from '.'; + +describe('ProgressBar', () => { + describe('countIndicator 10, progress 21', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + , + ); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.progress-bar__indicator').length).toEqual(10); + expect(wrapper.find('.progress-bar__indicator--filled').length).toEqual(2); + }); + + it('getDimensionIndicator should be equal 10', () => { + expect(wrapperInstance.getDimensionIndicator()).toEqual(10); + }); + + it('indicatorIsFilled should be equal correct state', () => { + // do not forget that the index counts from 0 + expect(wrapperInstance.indicatorIsFilled(0)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(1)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(2)).toEqual(false); + }); + }); + + describe('countIndicator 20, progress 49', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + , + ); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.progress-bar__indicator').length).toEqual(20); + expect(wrapper.find('.progress-bar__indicator--filled').length).toEqual(9); + }); + + it('getDimensionIndicator should be equal 5', () => { + expect(wrapperInstance.getDimensionIndicator()).toEqual(5); + }); + + it('indicatorIsFilled should be equal correct state', () => { + // do not forget that the index counts from 0 + expect(wrapperInstance.indicatorIsFilled(0)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(8)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(9)).toEqual(false); + }); + }); +}); diff --git a/src/components/ProgressBar/index.js b/src/components/ProgressBar/index.js new file mode 100644 index 00000000..26a10f67 --- /dev/null +++ b/src/components/ProgressBar/index.js @@ -0,0 +1,3 @@ +import ProgressBar from './ProgressBar'; + +export default ProgressBar; diff --git a/src/components/ProjectList/index.js b/src/components/ProjectList/index.js index 08f606f2..dd2de92b 100644 --- a/src/components/ProjectList/index.js +++ b/src/components/ProjectList/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import propTypes from 'prop-types'; -import { NavLink } from 'react-router-dom'; +import { NavLink, Redirect } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; import { withTranslation } from 'react-i18next'; import Button from '../Button/Button'; @@ -15,9 +15,23 @@ import styles from '../Login/Login.scss'; @inject('appStore') @observer class ProjectList extends Component { + static propTypes = { + appStore: propTypes.shape({ + readProjectList: propTypes.func.isRequired, + projectList: propTypes.arrayOf(propTypes.object).isRequired, + checkIsQuestionsUploaded: propTypes.func.isRequired, + gotoProject: propTypes.func.isRequired, + setProjectAddress: propTypes.func.isRequired, + }).isRequired, + t: propTypes.func.isRequired, + }; + constructor(props) { super(props); - this.state = {}; + this.state = { + redirectToProject: false, + redirectToUploading: false, + }; } componentDidMount() { @@ -25,16 +39,51 @@ class ProjectList extends Component { appStore.readProjectList(); } + gotoProject = ({ address, name }) => { + const { appStore } = this.props; + appStore.gotoProject({ address, name }); + this.setState({ redirectToProject: true }); + } + + startUploading = (address) => { + const { appStore } = this.props; + appStore.setProjectAddress(address); + this.setState({ redirectToUploading: true }); + } + + checkProject = async ({ + address, + name, + }) => { + const { appStore } = this.props; + // eslint-disable-next-line no-unused-vars + const isQuestionsUploaded = await appStore.checkIsQuestionsUploaded(address); + // eslint-disable-next-line no-unused-expressions + isQuestionsUploaded + ? this.gotoProject({ address, name }) + : this.startUploading(address); + } + render() { const { appStore: { projectList }, t } = this.props; + const { redirectToProject, redirectToUploading } = this.state; const projects = projectList.map((project, index) => ( { + this.checkProject({ + address: project.address, + name: project.name, + }); + }} > {project.name.replace(/([!@#$%^&*()_+\-=])+/g, ' ')} )); + + if (redirectToProject) return ; + if (redirectToUploading) return ; return ( @@ -58,12 +107,4 @@ class ProjectList extends Component { } } -ProjectList.propTypes = { - appStore: propTypes.shape({ - readProjectList: propTypes.func.isRequired, - projectList: propTypes.arrayOf(propTypes.object).isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; - export default ProjectList; diff --git a/src/components/ProjectUploading/ProgressBlock/index.js b/src/components/ProjectUploading/ProgressBlock/index.js index e1426c9b..a85e76ea 100644 --- a/src/components/ProjectUploading/ProgressBlock/index.js +++ b/src/components/ProjectUploading/ProgressBlock/index.js @@ -6,10 +6,11 @@ import styles from '../../Login/Login.scss'; const ProgressBlock = ({ children, text, index, state, noline, }) => ( - index ? 'success' : ''}`} > + {!noline ? : ''} @@ -23,7 +24,6 @@ const ProgressBlock = ({ {text} {children[1] ? children[1] : ''} - {!noline ? : ''} ); diff --git a/src/components/ProjectUploading/index.js b/src/components/ProjectUploading/index.js index c323c6e6..4170262f 100644 --- a/src/components/ProjectUploading/index.js +++ b/src/components/ProjectUploading/index.js @@ -31,19 +31,44 @@ class ProjectUploading extends Component { this.state = { step: this.steps.compiling, uploading: true, + contractAddress: '', + projectName: '', }; } componentDidMount() { const { steps } = this; const { - appStore, appStore: { deployArgs, name }, userStore: { password }, t, + appStore, appStore: { deployArgs, projectAddress }, userStore: { password }, type, } = this.props; - this.setState({ - step: steps.sending, - }); - appStore.deployContract('project', deployArgs, password) + switch (type) { + case ('project'): + this.setState({ + step: steps.sending, + }); + this.deployProject(deployArgs, password); + break; + case ('question'): + this.setState({ + step: steps.questions, + }); + appStore.deployQuestions(projectAddress).then(() => { + this.setState({ + uploading: false, + }); + }); + break; + default: + break; + } + } + + deployProject(deployArgs, password) { + const { steps } = this; + const { appStore, appStore: { name }, t } = this.props; + + appStore.deployContract('ZeroOne', deployArgs, password) .then((txHash) => { this.setState({ step: steps.receipt, @@ -55,23 +80,33 @@ class ProjectUploading extends Component { this.setState({ step: steps.questions, }); + appStore.setProjectAddress(receipt.contractAddress); appStore.addProjectToList({ name, address: receipt.contractAddress }); appStore.deployQuestions(receipt.contractAddress).then(() => { this.setState({ uploading: false, + contractAddress: receipt.contractAddress, + projectName: name, }); }); } - }).catch(() => { appStore.displayAlert(t('errors:hostUnreachable'), 3000); }); + }).catch((err) => { + alert(err); + appStore.displayAlert(t('errors:hostUnreachable'), 3000); + }); } render() { - const { step, uploading } = this.state; + const { + step, uploading, contractAddress, projectName, + } = this.state; return ( { - uploading ? : + uploading + ? + : } @@ -106,7 +141,7 @@ const Progress = withTranslation()(inject('appStore')(observer(({ t, appStore, s text={item[0]} index={index} state={step} - noline={index === 4} + noline={index === 0} > {item[1]} @@ -116,7 +151,9 @@ const Progress = withTranslation()(inject('appStore')(observer(({ t, appStore, s ); }))); -const AlertBlock = withTranslation()(({ t }) => ( +const AlertBlock = withTranslation()(inject('appStore')(observer(({ + t, appStore, address, name, +}) => ( {t('headings:projectCreated.heading')} @@ -126,16 +163,24 @@ const AlertBlock = withTranslation()(({ t }) => ( {t('headings:projectCreated.subheading.1')} - } type="submit"> - {t('buttons:toCreatedProject')} - + + } + type="button" + onClick={() => { appStore.gotoProject({ address, name }); }} + > + {t('buttons:toCreatedProject')} + + - + {t('buttons:otherProject')} -)); +)))); ProjectUploading.propTypes = { appStore: propTypes.shape({ @@ -147,11 +192,14 @@ ProjectUploading.propTypes = { addProjectToList: propTypes.func.isRequired, deployQuestions: propTypes.func.isRequired, displayAlert: propTypes.func.isRequired, + projectAddress: propTypes.func.isRequired, + setProjectAddress: propTypes.func.isRequired, }).isRequired, userStore: propTypes.shape({ password: propTypes.string.isRequired, }).isRequired, t: propTypes.func.isRequired, + type: propTypes.string.isRequired, }; Progress.propTypes = { diff --git a/src/components/Questions/FullQuestion/index.js b/src/components/Questions/FullQuestion/index.js new file mode 100644 index 00000000..2edc0e23 --- /dev/null +++ b/src/components/Questions/FullQuestion/index.js @@ -0,0 +1,44 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import Container from '../../Container'; +import Question from '../Question'; +import Button from '../../Button/Button'; + +import styles from '../Questions.scss'; + +const FullQuestion = withTranslation()(inject( + 'projectStore', +)(observer(({ + t, + projectStore, +}) => { + const { id: pageid } = useParams(); + const { goBack } = useHistory(); + const { questionStore, historyStore } = projectStore; + const [question] = questionStore.getQuestionById(pageid); + return ( + + + + + + {t('buttons:back')} + + + + + + + + + ); +}))); + +export default FullQuestion; diff --git a/src/components/Questions/Question/Question.scss b/src/components/Questions/Question/Question.scss new file mode 100644 index 00000000..5c38fd8e --- /dev/null +++ b/src/components/Questions/Question/Question.scss @@ -0,0 +1,90 @@ +@import '../../../assets/styles/partials/variables'; +.question { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: start; + width: 100%; + height: 125px; + background-color: $white; + border: 1px solid $lightGrey; + &--extended { + align-items: flex-start; + height: auto; + .question__left { + width: 70%; + border: none; + } + .question__right { + padding-top: 28px; + } + } + &--short-name { + .question__left { + width: 50%; + } + .question__right { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + width: 50%; + .question__parameter { + &-heading { + display: block; + width: 100%; + margin-bottom: 10px; + } + width: 50%; + } + } + } + &__left { + align-self: flex-start; + width: 80%; + height: 100%; + padding: 10px 30px; + border-right: 1px solid $lightGrey; + cursor: pointer; + } + &__right { + width: 20%; + text-align: center; + } + &__id { + color: $lightGrey; + font-size: 11px; + } + &__caption { + max-width: 70%; + margin: 5px 0 10px; + font-weight: 700; + font-size: 18px; + } + &__description { + max-width: 70%; + font-size: 11px; + } + &__parameter { + margin: 10px 0; + text-align: left; + &-heading { + font-weight: 700; + font-size: 14px; + text-align: left; + } + &-label { + margin-bottom: 5px; + color: $primary; + font-size: 11px; + } + &-text { + color: $border; + font-size: 11px; + } + } + &__formula { + padding: 10px 30px 30px; + color: $border; + font-size: 11px; + } +} \ No newline at end of file diff --git a/src/components/Questions/Question/index.js b/src/components/Questions/Question/index.js new file mode 100644 index 00000000..f35e5231 --- /dev/null +++ b/src/components/Questions/Question/index.js @@ -0,0 +1,187 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import propTypes from 'prop-types'; +import { NavLink, withRouter } from 'react-router-dom'; +import { withTranslation, Trans } from 'react-i18next'; +import uniqKey from 'react-id-generator'; +import { inject, observer } from 'mobx-react'; +import { StartIcon } from '../../Icons'; +import Button from '../../Button/Button'; + +import styles from './Question.scss'; + +/** + * Component for render start button + * + * @param {object} param0 data + * @param {Function} param0.t method for translate text + * @param {*} param0.id id question + * @param {*} param0.history id question + * @returns {Node} component start button + */ +const startBlock = ({ + t, + id, + history, + votingIsActive, +}) => ( + + )} + onClick={() => history.push(`/votings?modal=start_new_vote&option=${id}`)} + disabled={votingIsActive} + hint={ + votingIsActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:startNewVote')} + + +); + +startBlock.propTypes = { + t: propTypes.func.isRequired, + id: propTypes.oneOfType([ + propTypes.number, + propTypes.string, + ]).isRequired, + history: propTypes.shape({ + push: propTypes.func.isRequired, + }).isRequired, + votingIsActive: propTypes.bool.isRequired, +}; + +const startBlockWithRouter = withRouter(startBlock); + +const ParametersBlock = (paramNames, paramTypes, t) => ( + + {t('other:parameters')} + {paramNames.map((param, index) => ( + + {param} + {paramTypes[index]} + + ))} + +); + +const ShortDescription = (text) => ( + + {text.slice(0, 250)} + +); + +const FullDescription = (text) => ( + + {text} + +); + +const FormulaBlock = (formula, t) => ( + + {`${t('other:votingFormula')}: ${formula}`} + +); + +const Content = inject('projectStore')(observer(({ + projectStore: { questionStore }, + id, + caption, + text, + extended, + groupId, +}) => { + const [group] = questionStore.getQuestionGroupById(groupId); + return ( + + {`#${id} - ${group.name}`} + {caption} + {extended ? FullDescription(text) : ShortDescription(text)} + + ); +})); + +// eslint-disable-next-line no-unused-vars +const Question = withTranslation()(({ + t, + extended, id, + name, + description, + formula, + paramNames, + paramTypes, + votingIsActive, + groupId, +}) => ( + 3 && extended) ? styles['question--short-name'] : ''} + `} + > + { + !extended + ? ( + + + + ) + : ( + + + + ) + } + { + extended + ? ParametersBlock(paramNames, paramTypes, t) + : startBlockWithRouter({ t, id, votingIsActive }) + } + + {extended ? FormulaBlock(formula, t) : null} + +)); + +Question.propTypes = { + id: propTypes.number.isRequired, + extended: propTypes.bool, + votingIsActive: propTypes.bool.isRequired, + name: propTypes.string.isRequired, + description: propTypes.string.isRequired, + formula: propTypes.string.isRequired, + paramNames: propTypes.arrayOf(propTypes.string).isRequired, + paramTypes: propTypes.arrayOf(propTypes.string).isRequired, +}; + +Question.defaultProps = { + extended: false, +}; + +export default Question; diff --git a/src/components/Questions/Questions.scss b/src/components/Questions/Questions.scss new file mode 100644 index 00000000..a7bef217 --- /dev/null +++ b/src/components/Questions/Questions.scss @@ -0,0 +1,83 @@ +@import '../../assets/styles/partials/variables'; +.questions { + margin: 0 auto; + + &__head { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + margin-bottom: 30px; + &-create { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + width: 45%; + .btn { + svg { + path { + fill: $white; + } + } + &:active { + path { + stroke: $white; + } + } + } + } + &-filters { + display: flex; + flex-flow: row nowrap; + align-items: center; + width: 25%; + .dropdown { + width: 100%; + } + } + } + + &__loader { + text-align: center; + } + + &__wrapper { + display: grid; + grid-template-columns:1fr; + row-gap: 10px; + justify-items: center; + margin-bottom: 30px; + } + + &__list-empty { + position: relative; + height: 40vh; + min-height: 200px; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + p { + display: inline-block; + margin: 0; + padding: 30px 0; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + text-align: center; + vertical-align: middle; + } + } +} + +.question { + &--back { + width: 105px; + padding: 7px 25px; + } +} \ No newline at end of file diff --git a/src/components/Questions/Questions.stories.js b/src/components/Questions/Questions.stories.js new file mode 100644 index 00000000..f6c1f71d --- /dev/null +++ b/src/components/Questions/Questions.stories.js @@ -0,0 +1,8 @@ +import React from 'react'; +import Questions from '.'; + +export default ({ title: 'Questions' }); + +export const Wrapper = () => ( + +); diff --git a/src/components/Questions/QuestionsHead.js b/src/components/Questions/QuestionsHead.js new file mode 100644 index 00000000..e5f2b628 --- /dev/null +++ b/src/components/Questions/QuestionsHead.js @@ -0,0 +1,111 @@ +import React from 'react'; +import { Trans, withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import uniqKey from 'react-id-generator'; +import ProjectStore from '../../stores/ProjectStore'; +import DialogStore from '../../stores/DialogStore'; +import { CreateToken, QuestionIcon } from '../Icons'; +import Button from '../Button/Button'; +import SimpleDropdown from '../SimpleDropdown'; + +import styles from './Questions.scss'; + +@withTranslation() +@inject('projectStore', 'dialogStore') +@observer +class QuestionsHead extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + }; + + /** + * Method for handle sort + * + * @param {object} selected new sort data + * @param {string|number} selected.value new sort value + * @param {string} selected.label new sort label + */ + handleSortSelect = (selected) => { + const { projectStore } = this.props; + const { + questionStore: { + addFilterRule, + }, + } = projectStore; + addFilterRule({ groupId: selected.value }); + } + + render() { + const { t, projectStore, dialogStore } = this.props; + const { + questionStore: { + questionGroups, + }, + historyStore, + } = projectStore; + return ( + + + } + onClick={() => { dialogStore.show('create_group_question'); }} + disabled={historyStore.isVotingActive} + hint={ + historyStore.isVotingActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:createQuestionGroup')} + + } + onClick={() => { dialogStore.show('create_question'); }} + disabled={historyStore.isVotingActive} + hint={ + historyStore.isVotingActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:createQuestion')} + + + + + + + + + ); + } +} + +export default QuestionsHead; diff --git a/src/components/Questions/QuestionsList.js b/src/components/Questions/QuestionsList.js new file mode 100644 index 00000000..40efb79f --- /dev/null +++ b/src/components/Questions/QuestionsList.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { computed } from 'mobx'; +import { inject, observer } from 'mobx-react'; +import { Trans } from 'react-i18next'; +import Question from './Question'; +import ProjectStore from '../../stores/ProjectStore'; + +import styles from './Questions.scss'; + +@inject('projectStore') +@observer +class QuestionsList extends React.Component { + static propTypes = { + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + }; + + @computed + get paginatedQuestions() { + const { props } = this; + const { + projectStore: { questionStore }, + } = props; + return questionStore.paginatedList; + } + + render() { + const { paginatedQuestions, props } = this; + const { + projectStore: { historyStore }, + } = props; + return ( + <> + { + paginatedQuestions + && paginatedQuestions.length + ? ( + paginatedQuestions.map((question) => ( + + )) + ) + : ( + + + + No questions have been + + created in this group yet + + + + ) + } + > + ); + } +} + +export default QuestionsList; diff --git a/src/components/Questions/QuestionsRoute.js b/src/components/Questions/QuestionsRoute.js new file mode 100644 index 00000000..3db31039 --- /dev/null +++ b/src/components/Questions/QuestionsRoute.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { + Route, Switch, +} from 'react-router-dom'; +import { inject } from 'mobx-react'; +import PropTypes from 'prop-types'; +import Questions from '.'; +import FullQuestion from './FullQuestion'; + +@inject('projectStore') +class QuestionsRoute extends React.Component { + static propTypes = { + projectStore: PropTypes.shape({ + questionStore: PropTypes.shape({ + resetFilter: PropTypes.func.isRequired, + }), + }).isRequired, + }; + + componentDidMount() { + const { projectStore } = this.props; + const { + questionStore: { + resetFilter, + }, + } = projectStore; + resetFilter(); + } + + render() { + return ( + + + + + ); + } +} + +export default QuestionsRoute; diff --git a/src/components/Questions/index.js b/src/components/Questions/index.js new file mode 100644 index 00000000..f5282c24 --- /dev/null +++ b/src/components/Questions/index.js @@ -0,0 +1,244 @@ +/* eslint-disable no-unused-vars */ +import React, { Component } from 'react'; +import propTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import { observable, computed } from 'mobx'; +import { withRouter } from 'react-router-dom'; +import ProjectStore from '../../stores/ProjectStore'; +import DialogStore from '../../stores/DialogStore'; +import UserStore from '../../stores/UserStore/UserStore'; +import Container from '../Container'; +import Footer from '../Footer'; +import Dialog from '../Dialog/Dialog'; +import CreateGroupQuestions from '../CreateGroupQuestions/CreateGroupQuestions'; +import CreateNewQuestion from '../CreateNewQuestion/CreateNewQuestion'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import Pagination from '../Pagination'; +import Loader from '../Loader'; +import Notification from '../Notification/Notification'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; +import QuestionsList from './QuestionsList'; +import QuestionsHead from './QuestionsHead'; + +import styles from './Questions.scss'; + +@withRouter +@withTranslation() +@inject('projectStore', 'dialogStore', 'userStore') +@observer +class Questions extends Component { + @observable votingIsActive = false; + + @observable _loading = false; + + passwordForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { props } = this; + const { + history, + dialogStore, + projectStore: { + historyStore, + rootStore: { Web3Service, contractService, appStore }, + votingData, + votingQuestion, + votingGroupId, + }, + userStore, + } = props; + dialogStore.show('progress_modal_questions'); + const { password } = form.values(); + userStore.setPassword(password); + appStore.setTransactionStep('compileOrSign'); + return userStore.readWallet(password) + .then(() => { + // eslint-disable-next-line max-len + const transaction = contractService.createVotingData(Number(votingQuestion), Number(votingGroupId), votingData); + return transaction; + }) + .then((tx) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + appStore.setTransactionStep('success'); + userStore.getEthBalance(); + dialogStore.hide(); + history.push('/votings'); + historyStore.getActualState(); + }) + .catch((error) => { + dialogStore.show('error_modal_questions'); + console.error(error); + })); + }, + onError: () => Promise.reject(), + }, + }); + + static propTypes = { + t: propTypes.func.isRequired, + projectStore: propTypes.instanceOf(ProjectStore).isRequired, + dialogStore: propTypes.instanceOf(DialogStore).isRequired, + userStore: propTypes.instanceOf(UserStore).isRequired, + history: propTypes.shape({ + push: propTypes.func.isRequired, + }).isRequired, + }; + + constructor(props) { + super(props); + this.state = {}; + } + + async componentDidMount() { + const { projectStore: { historyStore, questionStore } } = this.props; + this._loading = true; + this.votingIsActive = historyStore.isVotingActive; + questionStore.fetchActualQuestionGroups(); + this._loading = false; + } + + @computed + get loading() { + const { projectStore: { questionStore } } = this.props; + if (this._loading === true) return true; + return questionStore.loading; + } + + /** + * Method for getting init index + * for dropdown sort option + * + * @returns {number} index number + */ + get initIndex() { + const { projectStore } = this.props; + const { + questionStore: { + questionGroups, + filter: { rules }, + }, + } = projectStore; + let initIndex = 0; + questionGroups.forEach((option, index) => { + if (option.value === rules.groupId) { + initIndex = index; + } + }); + return initIndex; + } + + render() { + const { loading } = this; + const { + t, + projectStore, + dialogStore, + } = this.props; + const { + questionStore: { + pagination, + }, + rootStore: { contractService: { transactionStep } }, + } = projectStore; + return ( + <> + + {/* FIXME remove comment */} + + + { + !loading + ? ( + <> + + + + + { + pagination + ? ( + + ) + : null + } + > + ) + : ( + + + + ) + } + + + + + + + + + + + + + + + { dialogStore.hide(); }} /> + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + + > + ); + } +} + +export default Questions; diff --git a/src/components/ReturnTokens/ReturnTokens.js b/src/components/ReturnTokens/ReturnTokens.js new file mode 100644 index 00000000..60c84e46 --- /dev/null +++ b/src/components/ReturnTokens/ReturnTokens.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject } from 'mobx-react'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import UserStore from '../../stores/UserStore'; +import DialogStore from '../../stores/DialogStore'; +import Dialog from '../Dialog/Dialog'; +import TransactionProgress from '../Message/TransactionProgress'; +import HistoryStore from '../../stores/HistoryStore'; +import NotificationStore from '../../stores/NotificationStore'; +import ErrorMessage from '../Message/ErrorMessage'; +import SuccessMessage from '../Message/SuccessMessage'; + +@withTranslation() +@inject('userStore', 'dialogStore', 'projectStore', 'notificationStore') +class ReturnTokens extends React.Component { + form = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { + userStore, + dialogStore, + notificationStore, + projectStore: { + historyStore, + }, + } = this.props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.toggle('progress_modal_return_tokens'); + return historyStore.returnTokens() + .then(() => { + const notificationId = notificationStore.list[0].id; + notificationStore.remove(notificationId); + dialogStore.toggle('success_modal_return_tokens'); + historyStore.getActualState(); + }) + .catch((error) => { + console.error(error); + dialogStore.toggle('error_modal_return_tokens'); + }); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + notificationStore: PropTypes.instanceOf(NotificationStore).isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.instanceOf(HistoryStore), + }).isRequired, + }; + + render() { + const { props, form } = this; + const { t, dialogStore } = props; + return ( + <> + + + + + + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + { dialogStore.hide(); }} /> + + > + ); + } +} + +export default ReturnTokens; diff --git a/src/components/ReturnTokens/index.js b/src/components/ReturnTokens/index.js new file mode 100644 index 00000000..ccb7b6fe --- /dev/null +++ b/src/components/ReturnTokens/index.js @@ -0,0 +1,3 @@ +import ReturnTokens from './ReturnTokens'; + +export default ReturnTokens; diff --git a/src/components/Router/SimpleRouter.js b/src/components/Router/SimpleRouter.js index 92eea738..22361425 100644 --- a/src/components/Router/SimpleRouter.js +++ b/src/components/Router/SimpleRouter.js @@ -16,10 +16,16 @@ import ProjectUploading from '../ProjectUploading'; import CreationAlert from '../CreationAlert'; import DisplayUserInfo from '../DisplayUserInfo'; import Header from '../Header'; +import Members from '../Members'; +import Settings from '../Settings'; +import VotingRoute from '../Voting/VotingRoute'; +import QuestionsRoute from '../Questions/QuestionsRoute'; +import ReturnTokens from '../ReturnTokens/ReturnTokens'; const SimpleRouter = () => ( + @@ -36,8 +42,14 @@ const SimpleRouter = () => ( + ()} /> + ()} /> + + ()} /> ()} /> + + ); diff --git a/src/components/Settings/Settings.scss b/src/components/Settings/Settings.scss new file mode 100644 index 00000000..6edbbac0 --- /dev/null +++ b/src/components/Settings/Settings.scss @@ -0,0 +1,109 @@ +@import '../../assets/styles/partials/variables'; + +.settings { + display: flex; + flex-flow: row nowrap; + align-self: center; + justify-content: space-around; + padding-top: 50px; + + &__container { + display: block; + } + + &__block { + display: flex; + flex-flow: column nowrap; + justify-content: space-between; + width: 38%; + padding: 45px 40px; + text-align: center; + background-color: transparent; + //border: 1px solid $lightGrey; + &-heading { + margin-bottom: 35px; + font-weight: 700; + font-size: 18px; + } + &-content { + display: flex; + flex-flow: row wrap; + justify-content: space-around; + form { + width: 100%; + } + .field { + width: 100%; + margin-bottom: 35px; + &__label{ + z-index: 0; + } + } + .btn--white { + width: 45%; + margin: 5px 0; + } + } + &--contracts { + width: 22%; + padding: 45px 15px 25px; + .settings__block-content{ + .btn { + width: 80%; + } + } + } + &--settings { + .settings__block-group { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + .field { + width: 45%; + } + } + .field { + &::before { + content: unset; + } + &__input{ + width: 100%; + margin: 0; + } + &__label{ + left: 10px; + font-size: 12px; + } + } + } + } +} +.modal-buttons { + margin-top: 20px; + .btn { + &:first-child { + margin-bottom: 15px; + } + } + & > p { + margin-top: 5px; + color: #000000; + font-weight: 300; + font-size: 11px; + text-align: center; + opacity: 0.7; + } +} + +#dialog-project_modal_contract_uploading, +#dialog-token_modal_contract_uploading { + .dialog { + &__header { + padding: 20px; + padding-top: 55px; + } + &__body { + padding-bottom: 50px; + } + } +} diff --git a/src/components/Settings/entities/ContractUploading.js b/src/components/Settings/entities/ContractUploading.js new file mode 100644 index 00000000..410cacca --- /dev/null +++ b/src/components/Settings/entities/ContractUploading.js @@ -0,0 +1,203 @@ +/* eslint-disable react/no-unused-state */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import Button from '../../Button/Button'; +import CreateTokenForm from '../../../stores/FormsStore/CreateToken'; +import Dialog from '../../Dialog/Dialog'; +import TokenInputForm from '../../Forms/TokenInputForm'; +import TransactionProgress from '../../Message/TransactionProgress'; +import SuccessMessage from '../../Message/SuccessMessage'; +import ErrorMessage from '../../Message/ErrorMessage'; +import CreateProjectInSettings from '../../../stores/FormsStore/CreateProjectInSettings'; +import ProjectInputForm from '../../Forms/ProjectInputForm'; + +import styles from '../Settings.scss'; + +@withTranslation() +@inject('appStore', 'userStore', 'dialogStore') +@observer +class ContractUploading extends Component { + tokenForm = new CreateTokenForm({ + hooks: { + onSuccess: (form) => { + const { contractType } = this.state; + const { + appStore, userStore, dialogStore, appStore: { rootStore: { Web3Service } }, + } = this.props; + const { + name, count, password, symbol, + } = form.values(); + userStore.setPassword(password); + const deployArgs = [name, symbol, Number(count)]; + dialogStore.toggle('progress_modal_contract_uploading'); + return appStore.deployContract(contractType, deployArgs, password) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then((receipt) => { + appStore.setTransactionStep('success'); + dialogStore.toggle('success_modal_contract_uploading'); + this.setState({ address: receipt.contractAddress }); + form.clear(); + }) + .catch(() => { + dialogStore.toggle('error_modal_contract_uploading'); + }); + }, + onError: () => {}, + }, + }) + + projectForm = new CreateProjectInSettings({ + hooks: { + onSuccess: (form) => { + const { + appStore, userStore, dialogStore, appStore: { rootStore: { Web3Service } }, + } = this.props; + const { + name, address, password, + } = form.values(); + userStore.setPassword(password); + const deployArgs = [address]; + dialogStore.toggle('progress_modal_contract_uploading'); + return appStore.deployContract('ZeroOne', deployArgs, password) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then((receipt) => { + appStore.addProjectToList({ name, address: receipt.contractAddress }); + this.setState({ address: receipt.contractAddress }); + form.clear(); + return receipt.contractAddress; + }) + .then((contractAddress) => { + appStore.setTransactionStep('questionsUploading'); + return appStore.deployQuestions(contractAddress).then(() => { + }); + }) + .then(() => { + appStore.setTransactionStep('success'); + dialogStore.toggle('success_modal_contract_uploading'); + }) + .catch(() => { + dialogStore.toggle('error_modal_contract_uploading'); + }); + }, + onError: () => {}, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.shape().isRequired, + appStore: PropTypes.shape().isRequired, + userStore: PropTypes.shape().isRequired, + } + + constructor() { + super(); + this.state = { + contractType: '', + address: '', + }; + } + + // componentDidMount() { + // const { dialogStore } = this.props; + // this.setState({ contractType: 'token' }); + // dialogStore.toggle('progress_modal_contract_uploading'); + // } + + + triggerModal = async (contractType) => { + const { dialogStore } = this.props; + this.setState({ contractType }); + // eslint-disable-next-line react/destructuring-assignment + dialogStore.show('token_modal_contract_uploading'); + } + + triggerProjectModal = () => { + const { dialogStore } = this.props; + this.setState({ contractType: 'ZeroOne' }); + dialogStore.show('project_modal_contract_uploading'); + } + + changeFormLang =() => { + this.projectForm.fireHook('onLangChangeHook'); + } + + render() { + const { address, contractType } = this.state; + const { t, dialogStore } = this.props; + const modalFooter = () => ( + + + {t('explanations:freeze')} + + + ); + return ( + + {t('headings:creatingAndUpload')} + + { this.triggerModal('ERC20'); }}>ERC20 + { this.triggerModal('CustomToken'); }}>Custom tokens + { this.triggerProjectModal('ZeroOne'); }}>Project + + + + + + + + + + + + + { dialogStore.hide(); }}> + {`Contract address = ${address}`} + + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + ); + } +} + +export default ContractUploading; diff --git a/src/components/Settings/entities/LangChanger.js b/src/components/Settings/entities/LangChanger.js new file mode 100644 index 00000000..8754958e --- /dev/null +++ b/src/components/Settings/entities/LangChanger.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { withTranslation } from 'react-i18next'; +import i18n from '../../../i18n'; +import Button from '../../Button/Button'; +import LangSwitcher from '../../LangSwitcher'; +import { getCorrectMomentLocale } from '../../../utils/Date'; + + +import styles from '../Settings.scss'; + +@withTranslation() +class LangChanger extends Component { + static propTypes = { + t: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + this.state = { + language: i18n.language, + }; + window.ipcRenderer.on('change-language:confirm', (event, language) => { + this.changeLanguage(language); + }); + } + + componentWillUnmount() { + window.ipcRenderer.removeListener('change-language:confirm', (event, language) => { + this.changeLanguage(language); + }); + } + + handleSelect = (language) => { + this.setState({ language }); + } + + changeLanguage = (language) => { + i18n.changeLanguage(language); + moment.locale(getCorrectMomentLocale(i18n.language)); + } + + setLanguage = () => { + const { language } = this.state; + window.ipcRenderer.send('change-language:request', language); + } + + render() { + const { props: { t } } = this; + return ( + + {t('headings:interfaceLanguage')} + + + {t('buttons:apply')} + + + ); + } +} + + +export default LangChanger; diff --git a/src/components/Settings/entities/NodeConnection.js b/src/components/Settings/entities/NodeConnection.js new file mode 100644 index 00000000..266c84d9 --- /dev/null +++ b/src/components/Settings/entities/NodeConnection.js @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import Web3 from 'web3'; +import Input from '../../Input'; +import { Address } from '../../Icons'; +import NodeChangeForm from '../../../stores/FormsStore/NodeChangeForm'; +import Button from '../../Button/Button'; + +import styles from '../Settings.scss'; + + +@withTranslation() +@inject('appStore') +@observer +class NodeConnection extends Component { + nodeChange = new NodeChangeForm({ + hooks: { + onSuccess: (form) => { + // eslint-disable-next-line no-unused-vars + const { appStore } = this.props; + const { url } = form.values(); + const web3 = new Web3(url); + return web3.eth.getNodeInfo() + .then(() => { + this.setState({ success: true }); + return appStore.nodeChange(url); + }) + // eslint-disable-next-line consistent-return + .catch(() => { + // eslint-disable-next-line no-restricted-globals + if (confirm('Node is unreachable, continue anyway?')) { + this.setState({ success: true }); + return appStore.nodeChange(url); + } + }); + }, + onError: () => {}, + }, + }); + + static propTypes = { + appStore: PropTypes.shape().isRequired, + t: PropTypes.func.isRequired, + } + + constructor() { + super(); + this.state = { + success: false, + }; + } + + + render() { + const { nodeChange, state, props } = this; + const { t } = props; + const { success } = state; + return ( + + {t('headings:nodeConnection')} + + + + + + {success ? t('buttons:success') : t('buttons:continue')} + + + + ); + } +} + + +export default NodeConnection; diff --git a/src/components/Settings/entities/SettingsBlock.js b/src/components/Settings/entities/SettingsBlock.js new file mode 100644 index 00000000..d41ef69f --- /dev/null +++ b/src/components/Settings/entities/SettingsBlock.js @@ -0,0 +1,118 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import Button from '../../Button/Button'; +import ConfigForm from '../../../stores/FormsStore/ConfigForm'; +import Input from '../../Input'; +import Dialog from '../../Dialog/Dialog'; + +import styles from '../Settings.scss'; + +@withTranslation() +@inject('configStore', 'dialogStore') +@observer +class SettingsBlock extends Component { + settingsForm = new ConfigForm({ + hooks: { + onSuccess: (form) => { + // eslint-disable-next-line no-unused-vars + const { configStore, dialogStore } = this.props; + configStore.updateValues(form.values()); + dialogStore.show('apply_notification'); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + configStore: PropTypes.shape().isRequired, + dialogStore: PropTypes.shape({ + show: PropTypes.func.isRequired, + hide: PropTypes.func.isRequired, + }).isRequired, + }; + + constructor(props) { + super(props); + this.state = { + isChanged: false, + }; + } + + handleInputChange = () => { + const { settingsForm, props } = this; + const { configStore: { config } } = props; + const formValues = settingsForm.values(); + const formKeys = Object.keys(settingsForm.values()); + let changed = []; + formKeys.forEach((key) => { + if (Number(formValues[key]) !== config[key]) { + changed.push(key); + } else { + changed = changed.filter((e) => e !== key); + } + }); + if (changed.length !== 0) { + this.setState({ isChanged: true }); + } else { + this.setState({ isChanged: false }); + } + } + + reloadApp = () => { + window.location.reload(); + } + + render() { + const { props, settingsForm, state: { isChanged } } = this; + const { t, dialogStore, configStore: { config } } = props; + return ( + + {t('headings:other')} + + + + + + + + { + isChanged + ? {t('buttons:apply')} + : null +} + + + + + { this.reloadApp(); }}>{t('buttons:saveAndReload')} + { dialogStore.hide(); }}>{t('buttons:saveWithoutReload')} + + {t('other:reloadNotificaion')} + + + + + ); + } +} + +export default SettingsBlock; diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js new file mode 100644 index 00000000..7224b28c --- /dev/null +++ b/src/components/Settings/index.js @@ -0,0 +1,25 @@ +import React from 'react'; +import Container from '../Container'; +import Footer from '../Footer'; +import Contractuploading from './entities/ContractUploading'; +import NodeConnection from './entities/NodeConnection'; +import SettingsBlock from './entities/SettingsBlock'; + +import styles from './Settings.scss'; + +const Settings = () => ( + <> + + + + + + + + + + + > +); + +export default Settings; diff --git a/src/components/ShowSeed/index.js b/src/components/ShowSeed/index.js index 2462075e..45cd04e7 100644 --- a/src/components/ShowSeed/index.js +++ b/src/components/ShowSeed/index.js @@ -10,6 +10,7 @@ import Heading from '../Heading'; import Explanation from '../Explanation'; import Button from '../Button/Button'; import { BackIcon, EyeIcon, CrossedEyeIcon } from '../Icons'; +import UserStore from '../../stores/UserStore/UserStore'; import styles from '../Login/Login.scss'; @@ -17,6 +18,11 @@ import styles from '../Login/Login.scss'; @inject('userStore', 'appStore') @observer class ShowSeed extends Component { + static propTypes = { + userStore: propTypes.instanceOf(UserStore).isRequired, + t: propTypes.func.isRequired, + }; + constructor(props) { super(props); this.state = { @@ -67,12 +73,15 @@ class ShowSeed extends Component { - + {t('explanations:seed.0')} {t('explanations:seed.1')} : } onClick={this.toggleWords} > @@ -92,13 +101,6 @@ const SeedWord = ({ word, id, visible }) => ( ); -ShowSeed.propTypes = { - userStore: propTypes.shape({ - mnemonic: propTypes.arrayOf(propTypes.string).isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; - SeedWord.propTypes = { id: propTypes.number.isRequired, word: propTypes.string.isRequired, diff --git a/src/components/SimpleDropdown/index.js b/src/components/SimpleDropdown/index.js new file mode 100644 index 00000000..a95ebb1c --- /dev/null +++ b/src/components/SimpleDropdown/index.js @@ -0,0 +1,164 @@ +import React, { Component } from 'react'; +import propTypes from 'prop-types'; +import nextId from 'react-id-generator'; +import { withTranslation } from 'react-i18next'; +import { DropdownArrowIcon } from '../Icons'; +import DropdownOption from '../SimpleDropdownOption'; + +import styles from '../Dropdown/Dropdown.scss'; + +@withTranslation() +class SimpleDropdown extends Component { + static propTypes = { + children: propTypes.oneOfType([ + propTypes.element, + propTypes.string, + ]), + options: propTypes.arrayOf(propTypes.shape({ + value: propTypes.oneOfType([ + propTypes.string, + propTypes.number, + ]), + label: propTypes.string, + })).isRequired, + onSelect: propTypes.func, + field: propTypes.shape({ + set: propTypes.func, + validate: propTypes.func, + error: propTypes.string, + }), + initIndex: propTypes.number, + t: propTypes.func.isRequired, + placeholder: propTypes.string, + isNewQuestion: propTypes.bool, + }; + + static defaultProps = { + children: '', + onSelect: () => false, + field: { + set: () => {}, + validate: () => {}, + error: null, + }, + initIndex: null, + placeholder: null, + isNewQuestion: false, + } + + constructor(props) { + super(props); + const { + initIndex, + options, + } = props; + const initOption = options[initIndex] || {}; + this.state = { + opened: false, + selectedValue: initOption.value || '', + selectedLabel: initOption.label || '', + }; + this.setWrapperRef = this.setWrapperRef.bind(this); + this.handleClickOutside = this.handleClickOutside.bind(this); + } + + componentDidMount() { + document.addEventListener('mousedown', this.handleClickOutside); + } + + componentWillUnmount() { + document.removeEventListener('mousedown', this.handleClickOutside); + } + + setWrapperRef(node) { + this.wrapperRef = node; + } + + + toggleOptions = () => { + const { opened } = this.state; + this.setState({ opened: !opened }); + } + + closeOptions = () => { + this.setState({ opened: false }); + } + + handleSelect = async (selected) => { + const { onSelect, field } = this.props; + field.set(selected.value); + field.validate(); + onSelect(selected); + this.setState({ + selectedLabel: selected.label, + selectedValue: selected.value, + }); + this.toggleOptions(); + } + + handleClickOutside(event) { + if (this.wrapperRef && !this.wrapperRef.contains(event.target)) { + this.closeOptions(); + } + } + + render() { + const { + children, + options, + t, + field, + placeholder, + isNewQuestion, + } = this.props; + const { opened, selectedLabel, selectedValue } = this.state; + const getOptions = options.map((option) => ( + + )); + return ( + + + + {children ? {children} : ''} + + {selectedLabel || placeholder || t('other:select') } + { + (isNewQuestion && selectedLabel !== '') + ? ( + + {placeholder} + + ) + : '' + } + + + + + + + + {getOptions} + + + {field.error} + + + ); + } +} + +export default SimpleDropdown; diff --git a/src/components/SimpleDropdownOption/index.js b/src/components/SimpleDropdownOption/index.js new file mode 100644 index 00000000..3151bd94 --- /dev/null +++ b/src/components/SimpleDropdownOption/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import propTypes from 'prop-types'; + +import styles from '../Dropdown/Dropdown.scss'; + +const DropdownOption = ({ + value, label, select, +}) => ( + { select({ value, label }); }} + > + {label} + +); + +DropdownOption.propTypes = { + value: propTypes.oneOfType([ + propTypes.string, + propTypes.number, + ]).isRequired, + label: propTypes.string.isRequired, + select: propTypes.func.isRequired, +}; + + +export default DropdownOption; diff --git a/src/components/StartNewVote/StartNewVote.js b/src/components/StartNewVote/StartNewVote.js new file mode 100644 index 00000000..845c5021 --- /dev/null +++ b/src/components/StartNewVote/StartNewVote.js @@ -0,0 +1,317 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import { observer, inject } from 'mobx-react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { withTranslation, Trans } from 'react-i18next'; +import { observable } from 'mobx'; +import nextId from 'react-id-generator'; +import SimpleDropdown from '../SimpleDropdown'; +import { Address, QuestionIcon } from '../Icons'; +import Input from '../Input'; +import Button from '../Button/Button'; +import StarNewVoteForm from '../../stores/FormsStore/StartNewVoteForm'; +import { systemQuestionsId } from '../../constants'; + +import styles from './StartNewVote.scss'; + +@withRouter +@withTranslation() +@inject('projectStore', 'dialogStore') +@observer +class StartNewVote extends React.Component { + @observable initIndex = null; + + votingData = ''; + + form = new StarNewVoteForm({ + hooks: { + onSuccess: (form) => { + const { + projectStore: { + rootStore: { + Web3Service, + }, + questionStore, + }, + projectStore, + dialogStore, + } = this.props; + const data = form.values(); + const keys = Object.keys(data); + keys.forEach((key) => { + const text = String(data[key]); + data[key] = text.trim(); + }); + const { question: questionId } = data; + delete data.question; + const values = Object.values(data); + const [question] = questionStore.getQuestionById(questionId); + const { paramTypes, groupId } = question; + const encodedParams = question.id === 1 + ? Web3Service.web3.eth.abi + .encodeParameters( + ['tuple(uint256,uint256,uint256,uint256,uint256)', `tuple(${paramTypes.join(',')})`], + [[0, 0, 0, 0, 0], values], + ) + : Web3Service.web3.eth.abi + .encodeParameters( + ['tuple(uint256,uint256,uint256,uint256,uint256)', `${paramTypes.join(',')}`], + [[0, 0, 0, 0, 0], values.join(',')], + ); + projectStore.setVotingData(questionId, groupId, encodedParams); + dialogStore.toggle('password_form'); + return Promise.resolve(); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }); + + static propTypes = { + t: PropTypes.func.isRequired, + projectStore: PropTypes.shape().isRequired, + dialogStore: PropTypes.shape().isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + }; + + constructor() { + super(); + this.state = { + isSelected: false, + description: '', + }; + } + + componentDidMount() { + const { props } = this; + const { projectStore: { rootStore: { eventEmitterService } } } = props; + eventEmitterService.subscribe('new_vote:toggle', this.handleNewVoteToggle); + eventEmitterService.subscribe('new_vote:closed', this.handleNewVoteClose); + } + + componentWillUnmount() { + const { props } = this; + const { projectStore: { rootStore: { eventEmitterService } } } = props; + eventEmitterService.off('new_vote:toggle'); + eventEmitterService.off('new_vote:closed'); + } + + handleNewVoteToggle = (selected) => { + const { form } = this; + this.initIndex = Number(selected.value); + form.$('question').set(selected.value); + this.handleSelect(selected); + } + + handleNewVoteClose = () => { + this.initIndex = null; + this.setState({ isSelected: false }); + } + + // eslint-disable-next-line consistent-return + handleSelect = (selected) => { + const { form } = this; + const { projectStore, dialogStore, history } = this.props; + const { questionStore } = projectStore; + const [question] = questionStore.getQuestionById(selected.value); + const { paramTypes, paramNames, text: description } = question; + this.initIndex = Number(selected.value); + this.setState({ description }); + // @ Clearing fields, except question selection dropdown + // eslint-disable-next-line array-callback-return + form.map((field) => { + if (field.name === 'question') return; + form.del(field.name); + }); + // @ If Question have dedicated modal, then toggle them, else create fields + switch (selected.value) { + case systemQuestionsId.addingNewQuestion: + history.push('/questions'); + dialogStore.toggle('create_question'); + break; + case systemQuestionsId.connectGroupQuestions: + history.push('/questions'); + dialogStore.toggle('create_group_question'); + break; + default: + this.createFields(paramTypes, paramNames); + } + this.setState({ isSelected: true }); + } + + // eslint-disable-next-line class-methods-use-this + createFields(paramTypes, paramNames) { + if ( + paramTypes + && paramNames + && paramTypes.length + && paramNames.length + && paramTypes.length === paramNames.length + ) { + paramNames.forEach((name, index) => { + this.form.add({ + name, + type: 'text', + label: 'parameter', + placeholder: name, + rules: `required|${paramTypes[index]}`, + }); + }); + } + } + + render() { + const { form, initIndex } = this; + const { isSelected, description } = this.state; + const { props } = this; + const { t, projectStore } = props; + const { + historyStore, + questionStore: { newVotingOptions }, + questionStore: { rootStore: { membersStore: { nonERC } } }, + } = projectStore; + const groupTypes = [ + { label: 'ERC20', value: 0 }, + { label: 'Custom', value: 1 }, + ]; + return ( + + + + {t('other:startANewVote')} + + + + + + { + isSelected + ? ( + + {/* TODO add correct description text */} + { + description + && description.length + && description.length > 150 + ? description.substr(0, 130) + : description + } + + ) + : null + } + + + + { + isSelected + ? ( + + + {form.map((field, index) => { + if (field.name === 'question') return null; + if (field.placeholder === 'Group' || field.placeholder === 'Group address') { + return ( + + + + + + ); + } if (field.placeholder === 'Type' && this.initIndex === 1) { + return ( + + + + + + ); + } + return ( + + + + ); + })} + + + + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:start')} + + + + + ) + : ( + + + {t('other:newVoteEmptyStateText')} + + + ) + } + + + ); + } +} + +export default StartNewVote; diff --git a/src/components/StartNewVote/StartNewVote.scss b/src/components/StartNewVote/StartNewVote.scss new file mode 100644 index 00000000..711e3d54 --- /dev/null +++ b/src/components/StartNewVote/StartNewVote.scss @@ -0,0 +1,99 @@ +.new-vote { + width: 100%; + padding: 40px 61px; + + &__top { + display: inline-block; + width: 100%; + margin: 0 -15px; + + .dropdown { + width: 100%; + } + } + + &__title, + &__dropdown { + display: inline-block; + width: 50%; + padding: 0 15px; + vertical-align: top; + } + + &__title { + color: #000; + font-weight: 700; + font-size: 36px; + line-height: 42px; + text-align: left; + } + + &__description { + margin-top: 15px; + margin-bottom: 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: left; + } + + &__content { + min-height: 324px; + padding: 10px 0; + + &--empty { + height: 100%; + max-height: 324px; + text-align: center; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + &-text { + display: inline-block; + max-width: 261px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + vertical-align: middle; + } + } + } + + &__form { + padding-top: 45px; + padding-bottom: 15px; + + &-row { + display: inline-block; + width: 100%; + margin: 0 -15px; + text-align: left; + } + + &-col { + display: inline-block; + width: 50%; + padding: 0 15px; + } + + .field { + width: 100%; + margin-bottom: 30px; + } + .dropdown { + width: 100%; + margin-bottom: 30px; + } + + button { + width: 100%; + margin-top: 15px; + } + } +} diff --git a/src/components/StartNewVote/StartNewVote.test.js b/src/components/StartNewVote/StartNewVote.test.js new file mode 100644 index 00000000..25ecc5a9 --- /dev/null +++ b/src/components/StartNewVote/StartNewVote.test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import StartNewVote from '.'; + +jest.mock('../../utils/Validator'); + +describe('StartNewVote', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow().dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('should by default with empty state', () => { + expect( + wrapper.find('.new-vote__content--empty-text').length, + ).toEqual(1); + expect( + wrapper.find('.new-vote__content--empty-text').text(), + ).toEqual('other:newVoteEmptyStateText'); + }); + + it('handleSelect should show form instead empty state', () => { + wrapperInstance.handleSelect(); + expect(wrapper.find('form').length).toEqual(1); + expect( + wrapper.find('.new-vote__content--empty-text').length, + ).toEqual(0); + }); +}); diff --git a/src/components/StartNewVote/index.js b/src/components/StartNewVote/index.js new file mode 100644 index 00000000..072abaeb --- /dev/null +++ b/src/components/StartNewVote/index.js @@ -0,0 +1,3 @@ +import StartNewVote from './StartNewVote'; + +export default StartNewVote; diff --git a/src/components/StepIndicator/index.js b/src/components/StepIndicator/index.js index b518eae3..6ec789e9 100644 --- a/src/components/StepIndicator/index.js +++ b/src/components/StepIndicator/index.js @@ -19,7 +19,16 @@ const StepIndicator = withTranslation()(({ t, currentStep, stepCount }) => { - {arr.map((item, index) => = index + 1} />)} + { + arr.map( + (item, index) => ( + = index + 1} + key={`step-indicator--${index + 1}`} + /> + ), + ) + } ); diff --git a/src/components/TokenTransfer/TokenTransfer.js b/src/components/TokenTransfer/TokenTransfer.js new file mode 100644 index 00000000..c558d76b --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.js @@ -0,0 +1,215 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import { withRouter } from 'react-router-dom'; +import TransferTokenForm from '../../stores/FormsStore/TransferTokenForm'; +import Dialog from '../Dialog/Dialog'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import UserStore from '../../stores/UserStore'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import DialogStore from '../../stores/DialogStore'; +import Input from '../Input'; +import { Password, Address, TokenCount } from '../Icons'; +import Button from '../Button/Button'; +import { EMPTY_DATA_STRING, tokenTypes } from '../../constants'; + +import styles from './TokenTransfer.scss'; + +/** + * Component form for transfer token + */ +@withRouter +@withTranslation() +@inject('membersStore', 'userStore', 'dialogStore', 'projectStore') +@observer +class TokenTransfer extends React.Component { + votingIsActive = false; + + passwordForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { + wallet, + groupAddress, + userStore, + dialogStore, + membersStore: { + rootStore: { + Web3Service, + contractService, + projectStore: { historyStore }, + }, + }, + history, + } = this.props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.show('progress_modal'); + const votingData = Web3Service.web3.eth.abi + .encodeParameters( + ['tuple(uint,uint,uint,uint,uint)', 'address', 'address'], + [[0, 0, 0, 0, 0], groupAddress, wallet], + ); + + return userStore.readWallet(password) + .then(() => { + // eslint-disable-next-line max-len + const transaction = contractService.createVotingData(3, 0, votingData); + return transaction; + }) + .then((tx) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash))) + .then(() => { + userStore.getEthBalance(); + dialogStore.show('success_modal'); + historyStore.getActualState(); + history.push('/votings'); + }) + .catch((error) => { + dialogStore.show('error_modal'); + console.error(error); + }); + }, + onError: (form) => { + console.error(form.error); + }, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + wallet: PropTypes.string, + groupAddress: PropTypes.string.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + groupId: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + groupType: PropTypes.string.isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + form: PropTypes.instanceOf(TransferTokenForm).isRequired, + } + + static defaultProps = { + wallet: EMPTY_DATA_STRING, + } + + async componentDidMount() { + const { + projectStore: { + historyStore, + }, + } = this.props; + this.votingIsActive = historyStore.isVotingActive; + } + + handleClick = () => { + const { + groupId, dialogStore, + } = this.props; + dialogStore.show(`password_form-${groupId}`); + } + + render() { + const { props } = this; + const { + t, + wallet, + groupId, + groupType, + projectStore: { historyStore }, + form, + } = props; + return ( + + + {t('dialogs:tokenTransfer')} + + + + + + + + + + + + + + + + + + + + {t('buttons:transfer')} + + + {wallet} + + { + groupType !== tokenTypes.ERC20 + ? ( + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:designateGroupAdministrator')} + + + ) + : null + } + { + groupType !== tokenTypes.ERC20 + ? ( + + + + ) + : null + } + + + ); + } +} + +export default TokenTransfer; diff --git a/src/components/TokenTransfer/TokenTransfer.scss b/src/components/TokenTransfer/TokenTransfer.scss new file mode 100644 index 00000000..f0941416 --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.scss @@ -0,0 +1,55 @@ +@import '../../assets/styles/includes/mixin'; + +.token-transfer { + text-align: center; + + .input__wrapper { + margin-bottom: 32px; + + .field { + width: 100%; + max-width: 309px; + } + } + + .button__wrapper { + margin-top: 48px; + margin-bottom: 16px; + + .btn { + width: 100%; + max-width: 298px; + } + } + + .wallet__wrapper { + margin-top: 16px; + margin-bottom: 24px; + color: #808080; + font-size: 14px; + line-height: 16px; + } + + &__title { + @include title; + + margin-top: 72px; + margin-bottom: 73px; + } + + &__button { + // compensate padding in dialog + width: calc(100% + 80px); + margin: 0px -40px -10px -40px; + padding: 15px 20px; + color: #c8c9ca; + font-size: 14px; + line-height: 16px; + text-decoration: underline; + background-color: transparent; + border: unset; + border-top: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + } +} \ No newline at end of file diff --git a/src/components/TokenTransfer/TokenTransfer.stories.js b/src/components/TokenTransfer/TokenTransfer.stories.js new file mode 100644 index 00000000..4de57cb9 --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.stories.js @@ -0,0 +1,12 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import TokenTransfer from './TokenTransfer'; + +storiesOf('TokenTransfer', module) + .add('Without wallet', () => ( + + )) + .add('With wallet', () => ( + + )); diff --git a/src/components/TokenTransfer/TokenTransfer.test.js b/src/components/TokenTransfer/TokenTransfer.test.js new file mode 100644 index 00000000..62a9d0f9 --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.test.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import TokenTransfer from './TokenTransfer'; + +jest.mock('../../utils/Validator'); + +describe('TokenTransfer', () => { + it('should render correct without "wallet" prop', () => { + const wrapper = shallow().dive(); + expect(wrapper.length).toEqual(1); + }); + + it('should render correct with props', () => { + const wrapper = shallow( + , + ).dive(); + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.wallet__wrapper').text()).toEqual('0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54'); + }); +}); diff --git a/src/components/TokenTransfer/index.js b/src/components/TokenTransfer/index.js new file mode 100644 index 00000000..2bd64e07 --- /dev/null +++ b/src/components/TokenTransfer/index.js @@ -0,0 +1,3 @@ +import TokenTransfer from './TokenTransfer'; + +export default { TokenTransfer }; diff --git a/src/components/User/User.scss b/src/components/User/User.scss index 28733112..0797bfe4 100644 --- a/src/components/User/User.scss +++ b/src/components/User/User.scss @@ -1,6 +1,8 @@ @import '../../assets/styles/partials/variables'; +@import '../../assets/styles/includes/mixin'; .user { + position: relative; display: inline-block; font-size: 0; border: 1px solid $primary; @@ -10,22 +12,116 @@ vertical-align: middle; } + &__info { + position: absolute; + top: calc(100% + 1px); + right: -1px; + left: -1px; + display: none; + padding: 8px 10px; + background-color: #fff; + border: 1px solid $primary; + border-top: unset; + cursor: default; + } + + &__copy { + padding-bottom: 7px; + color: rgba($color: $primary, $alpha: 0.3); + font-size: 12px; + line-height: 14px; + text-align: center; + border-bottom: 1px solid #e6e6e6; + } + &__wallet { margin: 0 10px; font-size: 12px; vertical-align: middle; background-color: $white; + &--full { display: none; } } + + &__balances { + padding-top: 12px; + padding-bottom: 20px; + border-bottom: 1px solid #e6e6e6; + + &-top { + padding-bottom: 12px; + color: $primary; + font-size: 12px; + line-height: 100%; + + span { + font-weight: 700; + + + &:last-child { + float: right; + } + } + } + } + + &__balance { + &-item { + display: block; + padding-bottom: 8px; + overflow: hidden; + color: $primary; + font-size: 12px; + line-height: 100%; + letter-spacing: 0.01em; + + @include clearfix; + + span { + &:first-child { + position: relative; + float: left; + + &::after { + position: absolute; + left: calc(100% + 5px); + color: #c8c9ca; + content: '.....................................................................................................................'; + } + } + + &:last-child { + position: relative; + float:right; + padding-left: 5px; + background-color: #fff; + } + } + } + } + + &__button { + padding-top: 16px; + padding-bottom: 9px; + text-align: center; + } + &:hover { - .user__wallet { - &--full { - display: inline-block; + .user { + &__wallet { + &--full { + display: inline-block; + } + + &--half { + display: none; + } } - &--half { - display: none; + + &__info { + display: block; } } } diff --git a/src/components/User/index.js b/src/components/User/index.js index 93457481..0f8a4d54 100644 --- a/src/components/User/index.js +++ b/src/components/User/index.js @@ -1,17 +1,177 @@ import React from 'react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import uniqKey from 'react-id-generator'; +import AppStore from '../../stores/AppStore'; +import UserStore from '../../stores/UserStore/UserStore'; +import { MembersStore } from '../../stores/MembersStore'; +import ProjectStore from '../../stores/ProjectStore'; +import NotificationStore from '../../stores/NotificationStore'; +import Button from '../Button/Button'; + import styles from './User.scss'; -const User = ({ children }) => ( - - - {children} - {`${children.substr(0, 8)}...${children.substr(35, 41)}`} - -); - -User.propTypes = { - children: propTypes.string.isRequired, -}; +@withRouter +@withTranslation() +@inject( + 'userStore', + 'membersStore', + 'projectStore', + 'appStore', + 'notificationStore', +) +@observer +class User extends React.Component { + timeoutCopy = 2000; + + timerId; + + static propTypes = { + children: PropTypes.string.isRequired, + t: PropTypes.func.isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + appStore: PropTypes.instanceOf(AppStore).isRequired, + notificationStore: PropTypes.instanceOf(NotificationStore).isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + }; + + constructor() { + super(); + this.state = { + isCopied: false, + }; + } + + componentDidMount() { + const { props } = this; + const { userStore } = props; + userStore.getEthBalance(); + } + + /** + * Method for handle copy event + */ + handleCopy = () => { + if (this.timerId) clearTimeout(this.timerId); + this.setState({ isCopied: true }); + this.timerId = setTimeout(() => { + this.setState({ isCopied: false }); + }, this.timeoutCopy); + } + + handleToggleUser = () => { + const { props } = this; + const { + appStore, + userStore, + projectStore, + projectStore: { + historyStore, + questionStore, + }, + membersStore, + notificationStore, + history, + } = props; + history.push('/'); + appStore.reset(); + userStore.reset(); + historyStore.reset(); + projectStore.reset(); + membersStore.reset(); + notificationStore.reset(); + questionStore.reset(); + } + + render() { + const { isCopied } = this.state; + const { props } = this; + const { + children, + t, + userStore, + projectStore, + membersStore: { + groups, + }, + } = props; + return ( + + + + {children} + + {`${children.substr(0, 8)}...${children.substr(35, 41)}`} + + + { + isCopied + ? t('other:copied') + : t('other:clickOnAddressForCopy') + } + + + + {t('other:groups')} + {t('other:tokens')} + + + + {t('other:privateBalance')} + + + {userStore.userBalance} + + + { + groups + && groups.length + ? ( + groups.map((item) => ( + + + {item.name} + + + {item.fullUserBalance} + + + )) + ) + : null + } + + + + {t('other:toggleUser')} + + + + + ); + } +} export default User; diff --git a/src/components/VoterList/VoterList.js b/src/components/VoterList/VoterList.js new file mode 100644 index 00000000..cc8dbeb4 --- /dev/null +++ b/src/components/VoterList/VoterList.js @@ -0,0 +1,71 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import { + Tab, + Tabs, + TabList, + TabPanel, +} from 'react-tabs'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import VoterListTable from './VoterListTable'; + +import 'react-tabs/style/react-tabs.css'; +import styles from './VoterList.scss'; + +@withTranslation() +@observer +class VoterList extends React.PureComponent { + static propTypes = { + t: PropTypes.func.isRequired, + data: PropTypes.shape({ + positive: PropTypes.arrayOf(PropTypes.shape()).isRequired, + negative: PropTypes.arrayOf(PropTypes.shape()).isRequired, + }).isRequired, + }; + + render() { + const { props } = this; + const { t, data } = props; + return ( + + + + + {t('other:voterList')} + + + + {t('other:agree')} + + + {t('other:against')} + + + + + + + + + + + + + + ); + } +} + +export default VoterList; diff --git a/src/components/VoterList/VoterList.scss b/src/components/VoterList/VoterList.scss new file mode 100644 index 00000000..104edceb --- /dev/null +++ b/src/components/VoterList/VoterList.scss @@ -0,0 +1,143 @@ +@import '../../assets/styles/includes/mixin'; + +.voter-list { + // compensate dialog padding + width: calc(100% + 80px); + margin: -10px -40px; + + &__top { + padding: 12px 0 12px 30px; + background-color: #fff; + border-bottom: 1px solid #e1e4e8; + } + + &__tab-list, + &__title { + display: inline-block; + width: 50%; + vertical-align: middle; + } + + &__tab { + display: inline-block; + padding: 15px; + color: #000; + font-size: 16px; + line-height: 100%; + letter-spacing: 0.01em; + vertical-align: middle; + border: unset; + outline: unset; + cursor: pointer; + + &--selected { + font-weight: 700; + background-image: url('../../assets/images/activeTab.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + } + + &-list { + padding-right: 45px; + padding-left: 15px; + text-align: right; + } + } + + &__title { + font-weight: 700; + font-size: 24px; + line-height: 28px; + text-align: left; + } + + &__content { + height: 350px; + padding-top: 10px; + overflow-y: auto; + background-color: #fff; + + @include scrollbar; + } + + &__table { + width: 100%; + background-color: #fff; + border-collapse: collapse; + + &-th { + padding: 8px; + color: #808080; + font-size: 11px; + line-height: 13px; + text-align: left; + + &--weight { + text-align: center; + } + } + + &-td { + padding: 10px; + + &--is { + min-width: 20px; + padding: 0; + text-align: center; + } + + &--img { + width: 24px; + padding: 0; + } + + &--wallet { + padding: 10px 8px; + color: #000; + font-size: 14px; + line-height: 16px; + } + + &--weight { + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-align: center; + } + } + + &-tr { + background-color: #fff; + cursor: pointer; + transition: background-color 0.5s; + + &:hover { + background: #e1e4e8; + } + } + } + + &__no-data { + width: 100%; + padding: 18px 32px; + background: #fff; + + &-icon, + &-text { + display: inline-block; + vertical-align: middle; + } + + &-icon { + margin-right: 10px; + } + + &-text { + color: #4d4d4d; + font-size: 11px; + line-height: 13px; + white-space: pre-line; + } + } +} diff --git a/src/components/VoterList/VoterList.test.js b/src/components/VoterList/VoterList.test.js new file mode 100644 index 00000000..9a31d900 --- /dev/null +++ b/src/components/VoterList/VoterList.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VoterList from './VoterList'; + +describe('VoterList', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow().dive(); + }); + + it('should render without error', () => { + console.log(wrapper.debug()); + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/VoterList/VoterListTable.js b/src/components/VoterList/VoterListTable.js new file mode 100644 index 00000000..aea98766 --- /dev/null +++ b/src/components/VoterList/VoterListTable.js @@ -0,0 +1,125 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { AdminIcon, Pudding } from '../Icons'; + +import styles from './VoterList.scss'; + +@withTranslation() +class VoterListTable extends React.PureComponent { + static propTypes = { + t: PropTypes.func.isRequired, + list: PropTypes.arrayOf( + PropTypes.shape({ + isAdmin: PropTypes.bool, + wallet: PropTypes.string.isRequired, + weight: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + }; + + renderTable = () => { + const { props } = this; + const { t, list } = props; + return ( + + + + {/* eslint-disable-next-line */} + + {/* eslint-disable-next-line */} + + + {t('other:walletAddress')} + + + {t('other:weightVote')} + + + { + list.map((item, index) => ( + + + {item.isAdmin ? : ''} + + + + + + {item.wallet} + + + {`${item.weight}%`} + + + )) + } + + + ); + } + + render() { + const { props } = this; + const { t, list } = props; + return ( + <> + { + list && list.length + ? this.renderTable() + : ( + + + + + + {t('other:noData')} + + + ) + } + > + ); + } +} + +export default VoterListTable; diff --git a/src/components/VoterList/VoterListTable.test.js b/src/components/VoterList/VoterListTable.test.js new file mode 100644 index 00000000..ee7339ec --- /dev/null +++ b/src/components/VoterList/VoterListTable.test.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VoterListTable from './VoterListTable'; + +describe('VoterListTable', () => { + describe('List is empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + }); + + describe('List with correct data', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.voter-list__table-tr').length).toEqual(2); + expect( + wrapper.find('.voter-list__table-tr').at(0).text(), + ).toEqual('0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc542%'); + }); + }); +}); diff --git a/src/components/VoterList/index.js b/src/components/VoterList/index.js new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Voting/Voting.js b/src/components/Voting/Voting.js new file mode 100644 index 00000000..c35389c7 --- /dev/null +++ b/src/components/Voting/Voting.js @@ -0,0 +1,274 @@ +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import { computed, observable } from 'mobx'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import queryString from 'query-string'; +import VotingTop from './VotingTop'; +import VotingFilter from './VotingFilter'; +import Container from '../Container'; +import Footer from '../Footer'; +import Pagination from '../Pagination'; +import Dialog from '../Dialog/Dialog'; +import StartNewVote from '../StartNewVote'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; +import Notification from '../Notification/Notification'; +import ProjectStore from '../../stores/ProjectStore'; +import DialogStore from '../../stores/DialogStore'; +import VotingList from './VotingList'; +import Loader from '../Loader'; + +import styles from './Voting.scss'; + +@withTranslation() +@inject('dialogStore', 'projectStore', 'userStore') +@observer +class Voting extends React.Component { + @observable votingIsActive = false; + + @observable _loading = false; + + passwordForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { props } = this; + const { + // eslint-disable-next-line no-unused-vars + dialogStore, + projectStore: { + historyStore, + rootStore: { Web3Service, contractService, appStore }, + votingData, + votingQuestion, + votingGroupId, + }, + userStore, + } = props; + dialogStore.toggle('progress_modal_voting'); + const { password } = form.values(); + userStore.setPassword(password); + appStore.setTransactionStep('compileOrSign'); + return userStore.readWallet(password) + .then(() => { + // eslint-disable-next-line max-len + const transaction = contractService.createVotingData(Number(votingQuestion), Number(votingGroupId), votingData); + return transaction; + }) + .then((tx) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + })) + .then(() => { + appStore.setTransactionStep('success'); + dialogStore.show('success_modal_voting'); + userStore.getEthBalance(); + historyStore.getActualState(); + }) + .catch((error) => { + dialogStore.show('error_modal_voting'); + console.error(error); + }); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }); + + voteStatus = { + inProgress: 0, + success: 1, + failed: 2, + } + + static propTypes = { + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + userStore: PropTypes.shape().isRequired, + t: PropTypes.func.isRequired, + location: PropTypes.shape({ + search: PropTypes.string.isRequired, + }).isRequired, + }; + + constructor(props) { + super(props); + this.state = { + // eslint-disable-next-line react/no-unused-state + status: this.voteStatus.inProgress, + }; + } + + async componentDidMount() { + const { props } = this; + const { + location, + dialogStore, + projectStore: { + historyStore, + rootStore: { + eventEmitterService, + }, + questionStore: { + newVotingOptions, + }, + }, + } = props; + const parsed = queryString.parse(location.search); + if (parsed.modal && parsed.option) { + dialogStore.show(parsed.modal); + const targetOption = newVotingOptions[Number(parsed.option)]; + eventEmitterService.emit('new_vote:toggle', targetOption); + } + this.votingIsActive = historyStore.isVotingActive; + } + + @computed + get loading() { + const { projectStore: { historyStore } } = this.props; + return historyStore.loading; + } + + closeModal = (name) => { + const { dialogStore } = this.props; + dialogStore.hide(name); + } + + onCloseNewVote = () => { + const { props } = this; + const { + projectStore: { + rootStore: { + eventEmitterService, + }, + }, + } = props; + eventEmitterService.emit('new_vote:closed'); + } + + render() { + const { + props, + voteStatus, + state, + loading, + } = this; + const { status } = state; + const { + t, + dialogStore, + projectStore: { + historyStore: { + pagination, + isVotingActive, + }, + }, + } = props; + return ( + <> + + + {/* FIXME remove comment */} + + { + !loading + ? ( + <> + + { dialogStore.show('start_new_vote'); }} + votingIsActive={isVotingActive} + /> + + { + pagination + ? ( + + ) + : null + } + > + ) + : ( + + + + ) + } + + + + + + + + + + + + { dialogStore.hide(); }} /> + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + + > + ); + } +} + +export default Voting; diff --git a/src/components/Voting/Voting.scss b/src/components/Voting/Voting.scss new file mode 100644 index 00000000..f20122b6 --- /dev/null +++ b/src/components/Voting/Voting.scss @@ -0,0 +1,600 @@ +@import '../../assets/styles/includes/mixin'; +@import '../../assets/styles/partials/variables'; + +.voting { + &-page { + margin-bottom: 40px; + + &__list { + text-align: center; + + &-empty { + position: relative; + height: 40vh; + min-height: 200px; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + p { + display: inline-block; + margin: 0; + padding: 30px 0; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + text-align: center; + vertical-align: middle; + } + } + + .loader { + margin-top: 50px; + }; + } + + &__loader { + text-align: center; + } + } + + &-info { + margin-bottom: 40px; + + &__back { + display: inline-block; + vertical-align: middle; + } + + &__date { + float: right; + padding: 5px 0; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + vertical-align: middle; + + span { + &:last-child { + padding-left: 24px; + } + } + } + + &__card { + margin-top: 7px; + background-color: #fff; + + &-inner { + padding: 20px 45px 20px; + border: 1px solid #e1e4e8; + } + } + + &__index { + margin-bottom: 16px; + color: rgba(0, 0, 0, 0.2); + font-size: 11px; + line-height: 13px; + } + + &__title { + margin-bottom: 8px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + &__main { + margin-bottom: 32px; + font-size: 0; + } + + &--long-params { + .voting-info__description { + width: 40%; + } + + .voting-info__data { + display: inline-flex; + flex-flow: row wrap; + width: 60%; + & > div { + width: 50%; + &.voting-info__block { + width: 100%; + } + } + } + } + + &__description { + display: inline-block; + width: 50%; + padding-right: 50px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + vertical-align: top; + } + + &__data { + display: inline-block; + width: 50%; + padding-left: 50px; + color: rgba(0, 0, 0, 1); + font-size: 11px; + line-height: 13px; + vertical-align: top; + + &-title { + margin-bottom: 4px; + } + + &-value { + margin-bottom: 16px; + font-weight: 700; + } + } + + &__formula { + margin-bottom: 16px; + color: #808080; + font-size: 11px; + line-height: 13px; + } + + &__buttons { + + button { + float: left; + width: 50%; + padding: 28px 30px 23px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + background-color: #fff; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + + &:first-child { + position: relative; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + // &::after { + // position: absolute; + // top: 0; + // right: 0; + // width: 1px; + // height: 100%; + // background-color: #e1e4e8; + // content: ''; + // } + } + &:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + @include clearfix; + } + + &__button { + &-icon { + margin-bottom: 8px; + text-align: center; + + svg { + width: 32px; + height: 32px; + } + } + + &--close { + display: inline-block; + width: 100%; + padding: 46px; + color: #000; + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + background: transparent; + border: unset; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + transition: .2s linear; + + &:hover { + border: 1px solid $primary + } + + &:active { + color: $white; + background-color: $primary; + } + + &:disabled { + cursor: default; + opacity: 0.6; + } + } + } + + &__stats { + margin-top: 16px; + + svg { + width: auto; + height: auto; + } + + button { + padding: 11px 15px; + + svg path { + fill: #fff; + } + + &:hover { + svg path { + fill: #fff; + } + } + + &:active { + svg path { + fill: #000; + } + } + } + + &-button { + margin-bottom: 16px; + text-align: center; + } + + &-content { + margin-bottom: 40px; + padding: 40px 60px 53px; + background: #fff; + border: 1px solid #e1e4e8; + } + } + + &__progress { + display: inline-block; + margin-left: 5px; + + &-container { + padding-right: 24px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + } + + .progress-bar__indicator { + width: 4px; + height: 4px; + margin-right: 2px; + + &--filled { + background-color: rgba(0, 0, 0, 0.7); + } + } + } + + &__decision { + padding: 49px 20px; + text-align: center; + border: 1px solid #e1e4e8; + + &-text { + display: inline-block; + margin-right: 8px; + color: #000; + font-size: 11px; + line-height: 13px; + vertical-align: middle; + } + + &-icon { + display: inline-block; + color: #000; + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + vertical-align: middle; + + svg { + display: inline-block; + width: auto; + height: auto; + margin-right: 4px; + vertical-align: middle; + } + } + } + + &__result { + display: inline-block; + width: 100%; + padding: 33px 20px 44px; + text-align: center; + border: 1px solid #e1e4e8; + + &-item { + display: inline-block; + margin: 0 32px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: left; + vertical-align: top; + + &-value { + margin-top: 3px; + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 14px; + line-height: 16px; + } + } + + .voting-info__decision-icon { + margin-top: 3px; + } + } + } + + &__filter { + &-dropdown { + display: inline-flex; + flex-flow: row nowrap; + justify-content: space-between; + width: 500px; + margin-right: 29px; + + .dropdown { + width: 220px; + } + } + + &-date { + float: right; + } + } + + &__top { + width: 100%; + margin-top: 36px; + margin-bottom: 16px; + } + + &__item { + min-height: 126px; + margin-bottom: 8px; + padding: 10px 8px 16px 30px; + font-size : 0; + background: #fff; + border: 1px solid #e1e4e8; + transition: border-color 0.3s; + + &-date, + &-progress { + display: inline-block; + margin-top: 8px; + vertical-align: middle; + } + + &-info { + display: inline-block; + width: 54%; + text-align: left; + vertical-align: top; + } + + &-date { + width: 24%; + padding: 10px 0; + text-align: center; + border-right: 1px solid rgba(0, 0, 0, 0.1); + border-left: 1px solid rgba(0, 0, 0, 0.1); + + &-block { + display: inline-block; + padding: 0 39px; + + &:first-child { + margin-bottom: 16px; + } + } + + &-text { + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 15px; + text-align: left; + } + } + + &-progress { + width: 22%; + padding: 0 36px; + } + + &-index { + margin-bottom: 5px; + color: rgba(0, 0, 0, 0.2); + font-size: 11px; + line-height: 13px; + } + + &-title { + margin-bottom: 8px; + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + &-description { + margin-bottom: 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + } + + &:hover { + border: 1px solid #000; + } + + &--new { + position: relative; + border: 1px dashed #000; + + &::before { + left: -20px; + } + + &::after { + right: -20px; + transform: rotate(180deg); + } + + &::after, + &::before { + position: absolute; + top: 12%; + width: 12px; + height: 76%; + background-image: url('../../assets/images/activeVoting.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + content: ''; + } + } + } + + &__decision { + &--progress { + .voting__decision { + &-title { + margin-bottom: 8px; + color: #000; + font-size: 14px; + line-height: 16px; + } + } + } + + &-progress-bar { + text-align: center; + } + + &-state { + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-align: center; + text-transform: uppercase; + } + + &-icon { + margin-top: 16px; + text-align: center; + + svg { + width: 32px; + height: 32px; + } + } + + &-title { + margin-bottom: 4px; + color: rgba(0, 0, 0, 0.7); + font-weight: 700; + font-size: 11px; + line-height: 13px; + text-align: center; + } + + &-text { + margin-top: 16px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + + &--remained { + text-align: left; + } + } + } + + &__back { + display: inline-block; + padding: 5px 35px; + color: #000; + font-size: 14px; + line-height: 107.5%; + background: #fff; + border: 1px solid #000; + border-radius: 2px; + transition: .2s linear; + &:hover { + color: #fff; + background: #000; + } + &:active { + color: #000; + background: #fff; + } + } + + &__stats { + text-align: center; + + &-col { + display: inline-block; + width: 33.3333%; + vertical-align: top; + + .recharts-wrapper { + margin: 0 auto; + } + + &-title { + margin-top: 17px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + text-align: center; + } + + &-subtitle { + margin-top: 9px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + } + } + + @include clearfix; + } +} \ No newline at end of file diff --git a/src/components/Voting/Voting.test.js b/src/components/Voting/Voting.test.js new file mode 100644 index 00000000..c4c18f18 --- /dev/null +++ b/src/components/Voting/Voting.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Voting from '.'; + +describe('Voting', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingDecision.js b/src/components/Voting/VotingDecision.js new file mode 100644 index 00000000..32d6e780 --- /dev/null +++ b/src/components/Voting/VotingDecision.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { VerifyIcon, RejectIcon, NoQuorum } from '../Icons'; + +import styles from './Voting.scss'; + +const Decision = ({ + prosState, + t, +}) => { + switch (prosState) { + case true: + return ( + + + {t('other:pros')} + + + + + + ); + case false: + return ( + + + {t('other:cons')} + + + + + + ); + case null: + return ( + + + {t('other:notAccepted')} + + + + + + ); + default: + return null; + } +}; + +Decision.propTypes = { + prosState: PropTypes.oneOfType([ + PropTypes.bool, + () => null, + ]), + t: PropTypes.func.isRequired, +}; + +Decision.defaultProps = { + prosState: null, +}; + +const DecisionTranslated = withTranslation()(Decision); + +/** + * Voting decision for pros & cons state + * + * @param {object} param0 data for component + * @param {boolean} param0.prosState decision is pros state + * @returns {Node} ready component + */ +const VotingDecision = ({ + prosState, + t, +}) => ( + + { + prosState === null + ? ( + + {t('other:decision')} + + ) + : ( + + {t('other:decisionIsMade')} + + ) + } + + +); + +VotingDecision.propTypes = { + prosState: PropTypes.oneOfType([ + PropTypes.bool, + () => null, + ]), + t: PropTypes.func.isRequired, +}; + +VotingDecision.defaultProps = { + prosState: null, +}; + +export default withTranslation()(VotingDecision); diff --git a/src/components/Voting/VotingDecision.test.js b/src/components/Voting/VotingDecision.test.js new file mode 100644 index 00000000..a2c4b5f7 --- /dev/null +++ b/src/components/Voting/VotingDecision.test.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingDecision from './VotingDecision'; +import { VerifyIcon, RejectIcon } from '../Icons'; + +describe('VotingDecision', () => { + it('should render without error with true prosState', () => { + const wrapper = shallow().dive(); + expect(wrapper.find('.voting__decision-state').text()).toEqual('PROS'); + expect(wrapper.find(VerifyIcon).length).toEqual(1); + expect(wrapper.length).toEqual(1); + }); + + it('should render without error with true prosState', () => { + const wrapper = shallow().dive(); + expect(wrapper.find('.voting__decision-state').text()).toEqual('CONS'); + expect(wrapper.find(RejectIcon).length).toEqual(1); + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingDecisionProgress.js b/src/components/Voting/VotingDecisionProgress.js new file mode 100644 index 00000000..ba0b1ffd --- /dev/null +++ b/src/components/Voting/VotingDecisionProgress.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import ProgressBar from '../ProgressBar/ProgressBar'; + +import styles from './Voting.scss'; + +/** + * Component render voting decision + * in progress state + * + * @param {object} param0 data for component + * @param {number} param0.progress progress in percent + * @param {string} param0.remained time remained + * @returns {Node} ready component + */ +const VotingDecisionProgress = ({ + progress, + remained, + t, +}) => ( + + + Voting + + + { + progress >= 100 + ? ( + + {t('other:endOfVoteRequired')} + + ) + : ( + + {t('other:timeLeft')} + + {`~${remained}`} + + ) + } + +); + +VotingDecisionProgress.propTypes = { + progress: PropTypes.number.isRequired, + remained: PropTypes.string.isRequired, + t: PropTypes.func.isRequired, +}; + +export default withTranslation()(VotingDecisionProgress); diff --git a/src/components/Voting/VotingDecisionProgress.test.js b/src/components/Voting/VotingDecisionProgress.test.js new file mode 100644 index 00000000..f40c6964 --- /dev/null +++ b/src/components/Voting/VotingDecisionProgress.test.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingDecisionProgress from './VotingDecisionProgress'; + +describe('VotingDecisionProgress', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingFilter.js b/src/components/Voting/VotingFilter.js new file mode 100644 index 00000000..34092e56 --- /dev/null +++ b/src/components/Voting/VotingFilter.js @@ -0,0 +1,155 @@ +import React from 'react'; +import { observer, inject } from 'mobx-react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import nextId from 'react-id-generator'; +import { withTranslation } from 'react-i18next'; +import { computed } from 'mobx'; +import SimpleDropdown from '../SimpleDropdown'; +import { QuestionIcon, DescisionIcon } from '../Icons'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import DatePicker from '../DatePicker'; + +import styles from './Voting.scss'; + +@withTranslation() +@inject('projectStore') +@observer +class VotingFilter extends React.PureComponent { + static propTypes = { + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + t: PropTypes.func.isRequired, + }; + + getStatusOptions() { + const { t } = this.props; + return [{ + label: 'All', + value: '*', + }, { + label: t('other:notAccepted'), + value: '0', + }, { + label: t('other:pros'), + value: '1', + }, { + label: t('other:cons'), + value: '2', + }]; + } + + @computed + get dateInit() { + const { + projectStore: { + historyStore: { filter: { rules } }, + }, + } = this.props; + if (!rules.date) { + return { + startDate: null, + endDate: null, + }; + } + return { + startDate: moment(rules.date.start * 1000), + endDate: moment(rules.date.end * 1000), + }; + } + + @computed + get indexForDescision() { + const { + projectStore: { + historyStore: { filter: { rules } }, + }, + } = this.props; + const options = this.getStatusOptions(); + const [element] = options.filter((item) => item.value === rules.descision); + return options.indexOf(element); + } + + /** + * Method for handle sort + * + * @param {object} selected new sort data + * @param {string|number} selected.value new sort value + * @param {string|number} selected.label new sort label + */ + handleQuestionSelect = (selected) => { + const { projectStore: { historyStore: { addFilterRule } } } = this.props; + addFilterRule({ questionId: selected.value.toString() }); + } + + handleStatusSelect = (selected) => { + const { projectStore: { historyStore: { addFilterRule } } } = this.props; + addFilterRule({ descision: selected.value.toString() }); + } + + /** + * Method for handle date change + */ + handleDateSelect = ({ + startDate, + endDate, + }) => { + const { projectStore: { historyStore: { addFilterRule } } } = this.props; + addFilterRule({ + date: { + // Convert to vote time format + start: startDate.valueOf() / 1000, + end: endDate.valueOf() / 1000, + }, + }); + } + + /** + * Method for handle clear date + */ + handleDateClear = () => { + const { projectStore: { historyStore } } = this.props; + historyStore.removeFilterRule('date'); + } + + render() { + const { + projectStore: { + questionStore: { options }, + historyStore: { filter: { rules } }, + }, + } = this.props; + + return ( + <> + + {/* Is not work correctly without key */} + + + + + + + + + + + > + ); + } +} + +export default VotingFilter; diff --git a/src/components/Voting/VotingInfo.js b/src/components/Voting/VotingInfo.js new file mode 100644 index 00000000..974f1b80 --- /dev/null +++ b/src/components/Voting/VotingInfo.js @@ -0,0 +1,363 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import uniqKey from 'react-id-generator'; +import { withTranslation } from 'react-i18next'; +import { Collapse } from 'react-collapse'; +import { observer } from 'mobx-react'; +import { computed, action, observable } from 'mobx'; +import { + statusStates, + votingStates, + userVotingStates, +} from '../../constants'; +import { + Stats, +} from '../Icons'; +import { getDateString } from './utils'; +import Button from '../Button/Button'; +import VotingStats from './VotingStats'; +import ProgressBar from '../ProgressBar/ProgressBar'; +import { progressByDateRange } from '../../utils/Date'; +import VotingInfoButtons from './VotingInfoButtons'; +import VotingInfoResult from './VotingInfoResult'; +import VotingInfoUserDecision from './VotingInfoUserDecision'; + +import styles from './Voting.scss'; + +@withTranslation() +@observer +class VotingInfo extends React.PureComponent { + @observable progress; + + intervalProgress = 5000; + + static propTypes = { + t: PropTypes.func.isRequired, + /** Index voting in list */ + index: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + /** Description voting */ + description: PropTypes.string.isRequired, + /** Title voting */ + title: PropTypes.string.isRequired, + /** Formula */ + formula: PropTypes.string.isRequired, + /** All needed date for voting */ + date: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + }).isRequired, + voting: PropTypes.shape({ + id: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + status: PropTypes.string.isRequired, + descision: PropTypes.string.isRequired, + userVote: PropTypes.number.isRequired, + closeVoteInProgress: PropTypes.bool, + }).isRequired, + params: PropTypes.arrayOf(PropTypes.array).isRequired, + dataStats: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + pros: PropTypes.number.isRequired, + cons: PropTypes.number.isRequired, + abstained: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + onVerifyClick: PropTypes.func.isRequired, + onRejectClick: PropTypes.func.isRequired, + onCompleteVoteClick: PropTypes.func.isRequired, + onBarClick: PropTypes.func.isRequired, + isUserReturnTokensActual: PropTypes.bool.isRequired, + }; + + constructor() { + super(); + this.state = { + isOpen: false, + }; + } + + componentDidMount() { + const { props } = this; + const { date } = props; + const initProgress = progressByDateRange(date); + this.setProgress(initProgress); + if (initProgress !== 100) { + const intervalId = setInterval(() => { + this.setProgress(progressByDateRange(date)); + if (this.progress === 100) { + clearInterval(intervalId); + } + }, this.intervalProgress); + } + } + + @action + setProgress = (progress) => { + this.progress = progress; + } + + /** + * Method for render dynamic content + * based on voting status + * + * @returns {Node} user decision Node element + */ + @computed + get renderDynamicContent() { + const { props, progress } = this; + const { + voting, + voting: { + userVote, + status, + descision, + closeVoteInProgress, + }, + isUserReturnTokensActual, + date, + onVerifyClick, + onRejectClick, + onCompleteVoteClick, + t, + } = props; + // TODO refactor this switch + switch (true) { + case ( + status === statusStates.active + && descision === votingStates.default + && userVote === userVotingStates.notAccepted + && progress < 100 + ): + return ( + + ); + case ( + status === statusStates.active + && descision === votingStates.default + && userVote !== userVotingStates.notAccepted + && progress < 100 + ): + return ( + + ); + case ( + status === statusStates.active + && descision === votingStates.default + && userVote === userVotingStates.notAccepted + && progress >= 100 + ): + return ( + + {t('buttons:completeTheVote')} + + ); + case ( + status === statusStates.active + && descision === votingStates.default + && userVote !== userVotingStates.notAccepted + && progress >= 100 + ): + return ( + + {t('buttons:completeTheVote')} + + ); + case ( + status === statusStates.closed + ): + return ( + + ); + default: + return null; + } + } + + /** + * Method for change isOpen state + */ + toggleOpen = () => { + this.setState((prevState) => ({ + isOpen: !prevState.isOpen, + })); + } + + render() { + const { isOpen } = this.state; + const { props, progress } = this; + const { + t, + date, + description, + index, + title, + formula, + params, + voting, + onBarClick, + dataStats, + } = props; + return ( + + + + {t('buttons:back')} + + + + { + voting.status === statusStates.active + && voting.descision === votingStates.default + && progress < 100 + ? ( + + {t('other:votingInProgress')} + : + + + ) + : ( + + {t('other:votingDone')} + + ) + } + + {t('other:start')} + : + {getDateString(date.start)} + + + {t('other:end')} + : + {getDateString(date.end)} + + + + + + # + {index} + + + {title} + + 3 ? styles['voting-info--long-params'] : ''}`} + > + + {description} + + + {params.map((item) => ( + 3 + && (new RegExp(/(0x)+([0-9 a-f A-F]){40}/g)).test(item[1])) + ? styles['voting-info__block'] + : '' + } + > + + {item[0]} + + + {item[1]} + + + ))} + + + + {`${t('other:votingFormula')}: ${formula}`} + + + {this.renderDynamicContent} + + + + )} + onClick={this.toggleOpen} + > + {t('other:statistics')} + + + + + + + + + + ); + } +} + +export default VotingInfo; diff --git a/src/components/Voting/VotingInfo.test.js b/src/components/Voting/VotingInfo.test.js new file mode 100644 index 00000000..9c1e948d --- /dev/null +++ b/src/components/Voting/VotingInfo.test.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingInfo from './VotingInfo'; +import { EMPTY_DATA_STRING } from '../../constants'; + +describe('VotingInfo', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + { console.log('onVerifyClick'); }} + /* eslint-disable-next-line */ + onRejectClick={() => { console.log('onRejectClick'); }} + />, + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('toggleOpen should change isOpen state', () => { + expect(wrapper.state('isOpen')).toEqual(false); + wrapperInstance.toggleOpen(); + expect(wrapper.state('isOpen')).toEqual(true); + wrapperInstance.toggleOpen(); + expect(wrapper.state('isOpen')).toEqual(false); + }); + + it('getDateString should ', () => { + const dateString = wrapperInstance.getDateString(); + expect(dateString).toEqual(EMPTY_DATA_STRING); + }); +}); diff --git a/src/components/Voting/VotingInfoButtons.js b/src/components/Voting/VotingInfoButtons.js new file mode 100644 index 00000000..3f6e3629 --- /dev/null +++ b/src/components/Voting/VotingInfoButtons.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { VerifyIcon, RejectIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './Voting.scss'; + +/** + * Component for render decision buttons + * + * @returns {Node} element with decision buttons + */ +const VotingInfoButtons = ({ + onVerifyClick, + onRejectClick, + t, + disabled, +}) => ( + + )} + iconPosition="top" + onClick={onVerifyClick} + disabled={disabled} + hint={ + disabled + ? ( + + Return tokens first + + ) + : null + } + > + {t('other:iAgree')} + + )} + iconPosition="top" + onClick={onRejectClick} + disabled={disabled} + hint={ + disabled + ? ( + + Return tokens first + + ) + : null + } + > + {t('other:iAmAgainst')} + + +); + +VotingInfoButtons.propTypes = { + onVerifyClick: PropTypes.func.isRequired, + onRejectClick: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, + disabled: PropTypes.bool.isRequired, +}; + +export default withTranslation()(VotingInfoButtons); diff --git a/src/components/Voting/VotingInfoResult.js b/src/components/Voting/VotingInfoResult.js new file mode 100644 index 00000000..e79420ea --- /dev/null +++ b/src/components/Voting/VotingInfoResult.js @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import moment from 'moment'; +import { EMPTY_DATA_STRING } from '../../constants'; +import renderDecisionIcon from './utils'; +import { getTimeLeftString } from '../../utils/Date'; + +import styles from './Voting.scss'; + +const VotingInfoResult = ({ + voting: { userVote, descision }, + date, + t, +}) => { + const startDate = moment(date.start * 1000); + const endDate = moment(date.end * 1000); + return ( + + + {t('other:decisionWasMade')} + + {renderDecisionIcon({ state: Number(descision) })} + + + + {t('other:yourDecision')} + + {renderDecisionIcon({ state: Number(userVote) })} + + + + {t('other:totalVoted')} + + {/* TODO add total from stats */} + {EMPTY_DATA_STRING} + + + + {t('other:theVoteLasted')} + + { + getTimeLeftString({ + startDate, + endDate, + }) + } + + + + ); +}; + +VotingInfoResult.propTypes = { + t: PropTypes.func.isRequired, + date: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + }).isRequired, + voting: PropTypes.shape({ + descision: PropTypes.string.isRequired, + userVote: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + }).isRequired, +}; + +export default withTranslation()(VotingInfoResult); diff --git a/src/components/Voting/VotingInfoUserDecision.js b/src/components/Voting/VotingInfoUserDecision.js new file mode 100644 index 00000000..b46d8cc1 --- /dev/null +++ b/src/components/Voting/VotingInfoUserDecision.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { userVotingStates } from '../../constants'; +import renderDecisionIcon from './utils'; + +import styles from './Voting.scss'; + +/** + * Component for render user decision + * + * @returns {Node} user decision Node element + */ +const VotingInfoUserDecision = ({ + voting: { userVote }, + t, +}) => { + switch (userVote) { + case userVotingStates.decisionFor: + return ( + + + {t('other:youVoted')} + + {renderDecisionIcon({ state: userVotingStates.decisionFor })} + + ); + case userVotingStates.decisionAgainst: + return ( + + + {t('other:youVoted')} + + {renderDecisionIcon({ state: userVotingStates.decisionAgainst })} + + ); + default: + return null; + } +}; + +VotingInfoUserDecision.propTypes = { + t: PropTypes.func.isRequired, + voting: PropTypes.shape({ + userVote: PropTypes.number.isRequired, + }).isRequired, +}; + +export default withTranslation()(VotingInfoUserDecision); diff --git a/src/components/Voting/VotingInfoWrapper.js b/src/components/Voting/VotingInfoWrapper.js new file mode 100644 index 00000000..cc7d0bb1 --- /dev/null +++ b/src/components/Voting/VotingInfoWrapper.js @@ -0,0 +1,537 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { action, observable } from 'mobx'; +import { withTranslation } from 'react-i18next'; +import VotingInfo from './VotingInfo'; +import Container from '../Container'; +import Dialog from '../Dialog/Dialog'; +import DecisionAgree from '../Decision/DecisionAgree'; +import DecisionClose from '../Decision/DecisionClose'; +import DecisionReject from '../Decision/DecisionReject'; +import VoterList from '../VoterList/VoterList'; +import Footer from '../Footer'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import { AgreedMessage, RejectMessage, ERC20TokensUsed } from '../Message'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import { + systemQuestionsId, + statusStates, + userVotingStates, + tokenTypes, + votingDecisionStates, +} from '../../constants'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import UserStore from '../../stores/UserStore/UserStore'; +import DialogStore from '../../stores/DialogStore'; +import AsyncInterval from '../../utils/AsyncUtils'; + +@withTranslation() +@inject( + 'dialogStore', + 'projectStore', + 'userStore', + 'membersStore', + 'appStore', +) +@observer +class VotingInfoWrapper extends React.PureComponent { + question; + + @observable dataStats = []; + + @observable dataVotes = { + 0: { + positive: [], + negative: [], + }, + }; + + @observable selectedGroup = 0 + + votingId = 0; + + votingForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { votingId } = this; + const { decision } = this.state; + const { + userStore, + dialogStore, + userStore: { + rootStore: { + contractService, + }, + }, + projectStore: { + historyStore, + }, + } = this.props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.toggle('progress_modal_voting_info_wrapper'); + return contractService.sendVote(votingId, decision) + .then(async () => { + await historyStore.fetchAndUpdateLastVoting(); + const [voting] = historyStore.getVotingById(Number(votingId)); + this.getVotingStats(); + this.getVotes(); + if (String(voting.status) === statusStates.closed) { + dialogStore.toggle('success_modal_voting_info_wrapper'); + this.updateAfterCompleteVoting(voting); + return; + } + switch (Number(voting.userVote)) { + case (userVotingStates.decisionFor): + dialogStore.toggle('decision_agree_voting_info_wrapper_message'); + break; + case (userVotingStates.decisionAgainst): + dialogStore.toggle('decision_reject_voting_info_wrapper_message'); + break; + default: + dialogStore.toggle('success_modal_voting_info_wrapper'); + break; + } + }) + .catch((error) => { + console.log(error); + dialogStore.toggle('error_modal_voting_info_wrapper'); + }); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }) + + closingForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { props } = this; + const { + match: { params: { id } }, + userStore, + projectStore: { + historyStore, + rootStore: { + contractService, + }, + }, + dialogStore, + } = props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.toggle('progress_modal_voting_info_wrapper'); + const [voting] = historyStore.getVotingById(Number(id)); + voting.update({ + closeVoteInProgress: true, + }); + return contractService.closeVoting() + .then(() => { + historyStore.fetchAndUpdateLastVoting(); + this.updateAfterCompleteVoting(voting); + this.getVotingStats(); + this.getVotes(); + dialogStore.toggle('success_modal_voting_info_wrapper'); + }) + .catch((e) => { + console.error(e); + dialogStore.toggle('error_modal_voting_info_wrapper'); + }) + .finally(() => { + voting.update({ + closeVoteInProgress: false, + }); + }); + }, + onError: () => {}, + }, + }) + + static propTypes = { + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + t: PropTypes.func.isRequired, + }; + + constructor() { + super(); + this.state = { + decision: votingDecisionStates.default, + }; + } + + componentDidMount() { + const { props } = this; + const { + match: { params: { id } }, + projectStore: { + historyStore, + questionStore, + rootStore: { + configStore: { UPDATE_INTERVAL }, + }, + }, + } = props; + const [voting] = historyStore.getVotingById(Number(id)); + const [question] = questionStore.getQuestionById(Number(voting.questionId)); + this.question = question; + this.getVotingStats(); + this.getVotes(); + this.interval = new AsyncInterval({ + timeoutInterval: UPDATE_INTERVAL, + cb: this.updateData, + }); + } + + componentWillUnmount() { + this.interval.cancel(); + this.interval = null; + } + + /** + * Method for checking whether the type +of voting is ERC20 + * + * @returns {boolean} is ERC20 or not + * @param address + */ + isERC20Type = (address) => { + const { props } = this; + const { membersStore } = props; + const targetGroup = membersStore.getMemberGroupByAddress(address); + if (!targetGroup || !targetGroup.groupType) return false; + return targetGroup.groupType === tokenTypes.ERC20; + } + + /** + * Method for update question list + * if close voting have questionId=1 + * + * @param {object} voting voting object + * @param {string} voting.questionId voting question id + */ + updateAfterCompleteVoting = (voting) => { + const { props } = this; + const { + projectStore: { + questionStore, + historyStore, + }, + membersStore, + } = props; + const { + addingNewQuestion, + connectGroupUsers, + connectGroupQuestions, + assignGroupAdmin, + } = systemQuestionsId; + historyStore.getActualState(); + switch (Number(voting.questionId)) { + case addingNewQuestion: + questionStore.getActualQuestions(); + break; + case connectGroupUsers: + membersStore.fetchUserGroups(); + break; + case connectGroupQuestions: + questionStore.fetchActualQuestionGroups(); + break; + case assignGroupAdmin: + membersStore.getAddressesForAdminDesignate(voting.data) + .then((result) => { + membersStore.updateAdmin(result['1']); + }); + break; + default: + break; + } + } + + onVerifyClick = () => { + const { dialogStore } = this.props; + this.setState({ + decision: votingDecisionStates.agree, + }); + dialogStore.toggle('decision_agree_voting_info_wrapper'); + } + + onRejectClick = () => { + const { dialogStore } = this.props; + this.setState({ + decision: votingDecisionStates.reject, + }); + dialogStore.toggle('decision_reject_voting_info_wrapper'); + } + + onClosingClick = () => { + const { dialogStore } = this.props; + dialogStore.toggle('descision_close_voting_info_wrapper'); + } + + updateData = () => { + this.getVotes(); + this.getVotingStats(); + } + + @action getVotes = async () => { + const { props } = this; + const { + match: { params: { id } }, + projectStore: { + historyStore, + }, + } = props; + const votes = await historyStore.getVoterList(Number(id)); + this.dataVotes = { ...this.dataVotes, ...votes }; + } + + @action + getVotingStats = async () => { + const { props } = this; + const { + match: { params: { id } }, + membersStore, + projectStore: { + historyStore, + rootStore: { + contractService: { + _contract: { + methods, + }, + }, + }, + }, + } = props; + const [voting] = historyStore.getVotingById(Number(id)); + + const { allowedGroups } = voting; + if (allowedGroups.length === 0) { + return; + } + + allowedGroups.forEach(async (group) => { + this.dataStats = []; + const memberGroup = membersStore.getMemberGroupByAddress(group); + let { + positive, + negative, + totalSupply, + } = await methods.getGroupVotes(id, memberGroup.wallet).call(); + positive = parseInt(positive, 10); + negative = parseInt(negative, 10); + totalSupply = parseInt(totalSupply, 10); + const decimalPercent = totalSupply / 100; + const abstained = (totalSupply - (positive + negative)) / decimalPercent; + const duplicateStat = this.dataStats.find((item) => item.name === memberGroup.name); + if (!duplicateStat) { + this.dataStats.push({ + name: memberGroup.name, + address: group, + pros: positive / decimalPercent, + cons: negative / decimalPercent, + abstained, + }); + } + }); + } + + /** + * Method for opening previous dialog + */ + openPreviousDialog = () => { + const { props } = this; + const { dialogStore } = props; + dialogStore.back(3); + } + + // eslint-disable-next-line class-methods-use-this + prepareParameters(voting, question) { + const { projectStore: { rootStore: { Web3Service } } } = this.props; + const { data } = voting; + const { paramTypes, paramNames, id } = question; + // const votingData = `0x${data.slice(10)}`; + let decodedRawParams; + let decodedParams; + if (id !== 0) { + decodedRawParams = data !== '0x' + ? Web3Service.web3.eth.abi.decodeParameters(['tuple(uint,uint,uint,uint,uint)', `tuple(${paramTypes.join(',')})`], data) + : []; + delete decodedRawParams[0]; + decodedParams = paramNames.map((param, index) => [param, decodedRawParams[1][index]]); + } else { + const parameters = [ + 'tuple(uint,uint,uint,uint,uint)', + 'tuple(bool, string, string, uint, uint, string[], string[], address, bytes4, string, bytes)', + ]; + decodedRawParams = Web3Service.web3.eth.abi.decodeParameters(parameters, data); + delete decodedRawParams[0]; + // eslint-disable-next-line no-unused-vars + const [active, name, text, + groupId, time, parametersNames, + parametersTypes, target, method, formula, + ] = decodedRawParams[1]; + const decoded = [ + groupId, name, text, + time, method, formula, + parametersNames.join(','), parametersTypes.join(','), target, + ]; + decodedParams = paramNames.map((param, index) => [param, decoded[index]]); + } + return decodedParams; + } + + render() { + const { props, dataStats } = this; + const { + dialogStore, + projectStore: { historyStore, questionStore, rootStore: { contractService } }, + match: { params: { id } }, + t, + } = props; + this.votingId = Number(id); + const { isUserReturnTokensActual } = historyStore; + const [voting] = historyStore.getVotingById(Number(id)); + const [question] = questionStore.getQuestionById(voting.questionId); + const params = this.prepareParameters(voting, question); + return ( + <> + + { this.onVerifyClick(); }} + onRejectClick={() => { this.onRejectClick(); }} + onCompleteVoteClick={() => { this.onClosingClick(); }} + onBarClick={ + (group) => { + this.selectedGroup = group; + if (this.isERC20Type(group) === true) { + dialogStore.show('is_erc20_modal_voting_info_wrapper'); + return; + } + dialogStore.show('voter_list_voting_info_wrapper'); + } + } + /> + + + + + + + + + + + + + + + + + + + dialogStore.hide()} /> + + + + dialogStore.hide()} /> + + + + + + + + { dialogStore.hide(); }} /> + + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + { dialogStore.hide(); }} /> + + + + > + ); + } +} + +export default VotingInfoWrapper; diff --git a/src/components/Voting/VotingInfoWrapper.test.js b/src/components/Voting/VotingInfoWrapper.test.js new file mode 100644 index 00000000..0099d552 --- /dev/null +++ b/src/components/Voting/VotingInfoWrapper.test.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingInfoWrapper from './VotingInfoWrapper'; + +describe('VotingInfoWrapper', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow().dive().dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingItem.js b/src/components/Voting/VotingItem.js new file mode 100644 index 00000000..4d142e56 --- /dev/null +++ b/src/components/Voting/VotingItem.js @@ -0,0 +1,217 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Link } from 'react-router-dom'; +import { computed, observable, action } from 'mobx'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import VotingDecisionProgress from './VotingDecisionProgress'; +import { statusStates, votingStates } from '../../constants'; +import VotingDecision from './VotingDecision'; +import { progressByDateRange, getTimeLeftString } from '../../utils/Date'; +import { getDateString } from './utils'; + +import styles from './Voting.scss'; + +@withTranslation() +@observer +class VotingItem extends React.PureComponent { + @observable progress = 0; + + intervalId = null; + + intervalProgress = 5000; + + static propTypes = { + index: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + t: PropTypes.func.isRequired, + actualStatus: PropTypes.string.isRequired, + actualDecisionStatus: PropTypes.string.isRequired, + newForUser: PropTypes.bool.isRequired, + date: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + }).isRequired, + onMouseEnter: PropTypes.func, + }; + + static defaultProps = { + onMouseEnter: () => {}, + } + + componentDidMount() { + const { props, ref } = this; + const { date, onMouseEnter } = props; + const initProgress = progressByDateRange(date); + this.setProgress(initProgress); + if (initProgress !== 100) { + this.intervalId = setInterval(() => { + this.setProgress(progressByDateRange(date)); + if (this.progress === 100) { + clearInterval(this.intervalId); + } + }, this.intervalProgress); + } + ref.addEventListener('mouseenter', () => { + onMouseEnter(); + }); + } + + @action + setProgress = (progress) => { + this.progress = progress; + } + + /** + * Method for render decision state + * + * @returns {Node} element for actual + * state + */ + @computed + get renderDecisionState() { + const { props, progress } = this; + const { date } = props; + switch (true) { + case (progress < 100): + return ( + + ); + case (progress >= 100): + return this.getVotingDecision(); + default: + return null; + } + } + + getVotingDecision = () => { + const { props } = this; + const { actualStatus, actualDecisionStatus } = props; + switch (true) { + case ( + actualStatus === statusStates.active + && actualDecisionStatus === votingStates.default + ): + return ( + + ); + case ( + actualStatus === statusStates.closed + && actualDecisionStatus === votingStates.decisionFor + ): + return (); + case ( + actualStatus === statusStates.closed + && actualDecisionStatus === votingStates.decisionAgainst + ): + return (); + case ( + actualStatus === statusStates.closed + && actualDecisionStatus === votingStates.default + ): + return (); + default: + return null; + } + } + + render() { + const { props } = this; + const { + index, + title, + description, + t, + date, + newForUser, + actualStatus, + } = props; + return ( + + { + this.ref = element; + } + } + > + + + {`#${index}`} + + + {title} + + + {description} + + + + + + {t('other:start')} + + {getDateString(date.start)} + + + + + {t('other:end')} + + {getDateString(date.end)} + + + + + {this.renderDecisionState} + + + + ); + } +} + +export default VotingItem; diff --git a/src/components/Voting/VotingItem.test.js b/src/components/Voting/VotingItem.test.js new file mode 100644 index 00000000..e89459ed --- /dev/null +++ b/src/components/Voting/VotingItem.test.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingItem from './VotingItem'; +import VotingDecision from './VotingDecision'; +import VotingDecisionProgress from './VotingDecisionProgress'; + +describe('VotingItem', () => { + describe('actualStatus is cons', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(VotingDecision).props()).toEqual({ prosState: false }); + }); + }); + + describe('actualStatus is pros', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(VotingDecision).props()).toEqual({ prosState: true }); + }); + }); + + describe('actualStatus in progress', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(VotingDecisionProgress).length).toEqual(1); + }); + }); +}); diff --git a/src/components/Voting/VotingList.js b/src/components/Voting/VotingList.js new file mode 100644 index 00000000..540b1647 --- /dev/null +++ b/src/components/Voting/VotingList.js @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { computed } from 'mobx'; +import { inject, observer } from 'mobx-react'; +import uniqKey from 'react-id-generator'; +import { Trans } from 'react-i18next'; +import VotingItem from './VotingItem'; +import ProjectStore from '../../stores/ProjectStore'; + +import styles from './Voting.scss'; +import { statusStates } from '../../constants'; + +/** + * Component for render list of voting + */ +@inject('projectStore') +@observer +class VotingList extends React.Component { + static propTypes = { + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + }; + + @computed + get paginatedVotings() { + const { props } = this; + const { + projectStore: { + historyStore, + }, + } = props; + return historyStore.paginatedList; + } + + handleVotingMouseEnter = (item) => { + const { props } = this; + const { + projectStore: { + historyStore, + }, + } = props; + if ( + item.status === statusStates.active + && item.newForUser === true + ) { + const [voting] = historyStore.getVotingById(item.id); + if (voting) { + voting.update({ + newForUser: false, + }); + historyStore.writeVotingsToFile(); + } + } + } + + render() { + const { + paginatedVotings, + props, + } = this; + const { + projectStore: { + historyStore: { + votings, + }, + }, + } = props; + if (!votings || !votings.length) { + return ( + + + + + No polls created + + They will be displayed here later + + + + + ); + } + return ( + + { + paginatedVotings && paginatedVotings.length + ? paginatedVotings.map((item) => ( + this.handleVotingMouseEnter(item)} + /> + )) + : ( + + + + No voting matches + + the selected filter + + + + ) + } + + ); + } +} + +export default VotingList; diff --git a/src/components/Voting/VotingRoute.js b/src/components/Voting/VotingRoute.js new file mode 100644 index 00000000..67c906d7 --- /dev/null +++ b/src/components/Voting/VotingRoute.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { + Route, + withRouter, + Switch, +} from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { inject } from 'mobx-react'; +import Voting from '.'; +import VotingInfoWrapper from './VotingInfoWrapper'; + +@inject('projectStore') +class VotingRoute extends React.Component { + static propTypes = { + match: PropTypes.shape({ + path: PropTypes.string.isRequired, + }).isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.shape({ + resetFilter: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + }; + + componentDidMount() { + const { projectStore } = this.props; + const { + historyStore: { + resetFilter, + }, + } = projectStore; + resetFilter(); + } + + render() { + const { props } = this; + const { match } = props; + return ( + + + + + ); + } +} + +export default VotingRoute; diff --git a/src/components/Voting/VotingStats.js b/src/components/Voting/VotingStats.js new file mode 100644 index 00000000..3ad29bd9 --- /dev/null +++ b/src/components/Voting/VotingStats.js @@ -0,0 +1,147 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import uniqKey from 'react-id-generator'; +import { BarChart, Bar, LabelList } from 'recharts'; +import { Trans, withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; + +import styles from './Voting.scss'; + +@withTranslation() +@observer +class VotingStats extends React.PureComponent { + static propTypes = { + t: PropTypes.func.isRequired, + onBarClick: PropTypes.func.isRequired, + data: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + pros: PropTypes.number.isRequired, + cons: PropTypes.number.isRequired, + abstained: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + }; + + /** + * Method for render label in BarChart + * + * @param {object} props property label from BarChart + * @param {string} option custom property for label + * @returns {Node} ready label + */ + renderLabel = (props, option) => { + const { + x, y, width, value, + } = props; + return ( + + + {`${value} %`} + + + { + option === 'pros' + ? () + : () + } + + + ); + } + + render() { + const { props } = this; + const { t, onBarClick, data } = props; + return ( + + { + data && data.length + ? data.map((item) => ( + + + onBarClick(item.address)} + > + this.renderLabel(contentProps, 'pros')} + /> + + onBarClick(item.address)} + > + this.renderLabel(contentProps, 'cons')} + /> + + + + { + item.abstained === 0 + ? t('other:everyoneVoted') + : `${t('other:didNotVote')} ${item.abstained || 0}%` + } + + + {item.name} + + + )) + : ('Empty state') + } + + ); + } +} + +export default VotingStats; diff --git a/src/components/Voting/VotingStats.test.js b/src/components/Voting/VotingStats.test.js new file mode 100644 index 00000000..6754581c --- /dev/null +++ b/src/components/Voting/VotingStats.test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingStats from './VotingStats'; + +describe('VotingStats', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow().dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('renderLabel should return correct data', () => { + const label = wrapperInstance.renderLabel({ + x: 20, + y: 100, + width: 200, + value: 60, + }, 'pros'); + expect(label.props.x).toEqual(120); + expect(label.props.y).toEqual(90); + }); +}); diff --git a/src/components/Voting/VotingTop.js b/src/components/Voting/VotingTop.js new file mode 100644 index 00000000..9bb1d0c3 --- /dev/null +++ b/src/components/Voting/VotingTop.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { PlayCircleIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './Voting.scss'; + +/** + * Component in top voting page + * + * @returns {Node} component + */ +const VotingTop = ({ + onClick, + votingIsActive, + t, +}) => ( + + )} + theme="with-play-icon" + onClick={onClick} + disabled={votingIsActive} + hint={ + votingIsActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:startNewVote')} + + +); + +VotingTop.propTypes = { + onClick: PropTypes.func, + t: PropTypes.func.isRequired, + votingIsActive: PropTypes.bool.isRequired, +}; + +VotingTop.defaultProps = { + onClick: () => {}, +}; + +export default withTranslation('other')(VotingTop); diff --git a/src/components/Voting/VotingTop.test.js b/src/components/Voting/VotingTop.test.js new file mode 100644 index 00000000..7942c19c --- /dev/null +++ b/src/components/Voting/VotingTop.test.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingTop from './VotingTop'; + +describe('VotingTop', () => { + it('should render correct without onClick props', () => { + const wrapper = shallow().dive(); + expect(wrapper.length).toEqual(1); + }); + + it('button onClick should call mockClick with onClick prop', () => { + const mockClick = jest.fn(); + const wrapper = shallow( + , + ).dive(); + expect(wrapper.length).toEqual(1); + const button = wrapper.find('button'); + button.prop('onClick')(); + expect(mockClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Voting/index.js b/src/components/Voting/index.js new file mode 100644 index 00000000..79646671 --- /dev/null +++ b/src/components/Voting/index.js @@ -0,0 +1,8 @@ +import Voting from './Voting'; +import VotingTop from './VotingTop'; + +export default Voting; + +export { + VotingTop, +}; diff --git a/src/components/Voting/utils.js b/src/components/Voting/utils.js new file mode 100644 index 00000000..1fec25fc --- /dev/null +++ b/src/components/Voting/utils.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { withTranslation, Trans } from 'react-i18next'; +import moment from 'moment'; +import { EMPTY_DATA_STRING } from '../../constants'; +import { RejectIcon, NoQuorum, VerifyIcon } from '../Icons'; + +import styles from './Voting.scss'; + +/** + * Method for render icon with text + * + * @param {string} state state for icon + * @returns {Node} ready node element + */ +const renderDecisionIcon = ({ + state, + t, +}) => { + switch (state) { + case 0: + return ( + + + {t('other:notAccepted')} + + ); + case 1: + return ( + + + {t('other:pros')} + + ); + case 2: + return ( + + + {t('other:cons')} + + ); + default: + return EMPTY_DATA_STRING; + } +}; + + +/** + * Method for getting formatted date string + * + * @param {Date} date date for formatting + * @returns {string} formatted date + */ +const getDateString = (date) => { + if ( + !date + || typeof date !== 'number' + ) return EMPTY_DATA_STRING; + return ( + + ); +}; + +export default withTranslation()(renderDecisionIcon); + +export { + getDateString, +}; diff --git a/src/config.json b/src/config.json index c19978c1..d8984d9c 100644 --- a/src/config.json +++ b/src/config.json @@ -1,17 +1,14 @@ { "host": "https://ropsten.infura.io/v3/14b2319f08e24f3aadfe1aa933301b38", + "hostBackup": "http://hive3.thexproject.ru:21595", + "minGasPrice": 40, + "maxGasPrice": 70, + "interval": 30, + "gasLimit": 7900000, "projects": [ { - "name": "tetete", - "address": "0x1Df6AdA9f170D0FdFb075140333A44c0C651f4AC" - }, - { - "name": "test", - "address": "0x1Cd9D97EC3f3283cD564bB82f7d0Ee2737D6F352" - }, - { - "name": "T3st", - "address": "0xACA55c33D67d549CacA4f700D0319B865D8E0FC5" + "name": "Test1", + "address": "0x0a021E388c66Acf96a9e2a8DEbdf51446573Cd3e" } ] } \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js index 0da98827..78a7c428 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,13 +1,59 @@ /* eslint-disable no-useless-escape */ +export const statusStates = { + active: '1', + closed: '0', +}; + export const votingStates = { + default: '0', + decisionFor: '1', + decisionAgainst: '2', +}; + +export const userVotingStates = { + notAccepted: 0, + decisionFor: 1, + decisionAgainst: 2, +}; + +export const systemQuestionsId = { + addingNewQuestion: 0, + connectGroupUsers: 1, + connectGroupQuestions: 2, + assignGroupAdmin: 3, +}; + +export const tokenTypes = { + ERC20: '0', + Custom: '1', +}; + +export const votingDecisionStates = { default: 0, - prepared: 1, - active: 2, + agree: 1, + reject: 2, +}; + +export const languages = { + RUS: 'ru', + ENG: 'en', }; -export const SOL_PATH_REGEXP = new RegExp(/(\"|\')((\.{1,2}\/){1,})(\w+\/){0,}?(\w+\.(?:sol))(\"|\')/g); -export const SOL_IMPORT_REGEXP = new RegExp(/(import)*.(\"|\')((\.{1,2}\/){1,})(\w+\/){0,}?(\w+\.(?:sol))(\"|\')(;)/g); -export const SOL_VERSION_REGEXP = new RegExp(/(pragma).(solidity).((\^)?)([0-9](.)?){1,}/g); +export const transactionSteps = { + compileOrSign: 0, + sending: 1, + txHash: 2, + txReceipt: 3, + questionsUploading: 4, + success: 5, +}; + +export const SOL_PATH_REGEXP = new RegExp(/(\"|\')(((\.{1,2}\/){1,})||(zeroone-voting-vm\/))(\w+\/){0,}?(\w+\.(?:sol))(\"|\')/g); +export const VM_IMPORT_REGEXP = new RegExp(/(zeroone-voting-vm)([\/\\]\w+[\/\\]).{1,}(\w+\.(?:sol))/g); +export const SOL_IMPORT_REGEXP = new RegExp(/(import)*.(\"|\')((\.{1,}\/)||(zeroone-voting-vm\/))+((\w+\/*())+(\w+\.(?:sol)))(\"|\')(;)/g); +export const SOL_ENCODER_REGEXP = new RegExp(/(pragma experimental ABIEncoderV2;)/g); +export const SOL_VERSION_REGEXP = new RegExp(/(pragma).(solidity).((\^)?)([0-9](.)?){1,}.(;)/g); + export const EMPTY_DATA_STRING = '-/-'; export const walletHdPath = "m/44'/60'/0'/0/0"; diff --git a/src/constants/windowModules.js b/src/constants/windowModules.js index e588d5e0..f132938b 100644 --- a/src/constants/windowModules.js +++ b/src/constants/windowModules.js @@ -5,16 +5,24 @@ const ENV = process.env.NODE_ENV || 'development'; window.__ENV = ENV; const devPath = window.process.env.INIT_CWD; -const prodPath = window.process.env.PORTABLE_EXECUTABLE_DIR || path.join(window.__dirname, '../src'); +const prodPath = window.process.env.PORTABLE_EXECUTABLE_DIR || path.join(window.__dirname, ''); export const ROOT_DIR = window.__ENV === 'production' ? prodPath : path.join(devPath, './src/'); +export const PATH_TO_CONFIG = window.__ENV === 'production' + ? path.join(prodPath, '../../build/config.json') + : path.join(devPath, './src/config.json'); + export const PATH_TO_WALLETS = window.__ENV === 'production' - ? path.join(prodPath, './wallets/') + ? path.join(prodPath, '../../build/wallets') : path.join(devPath, './src/wallets/'); export const PATH_TO_CONTRACTS = window.__ENV === 'production' - ? path.join(prodPath, './contracts/') + ? path.join(prodPath, '../../build/contracts') : path.join(devPath, './src/contracts/'); + +export const PATH_TO_DATA = window.__ENV === 'production' + ? path.join(prodPath, '../../build/data') + : path.join(devPath, './src/data/'); diff --git a/src/contracts/CustomToken.abi b/src/contracts/CustomToken.abi new file mode 100644 index 00000000..d4a42fa6 --- /dev/null +++ b/src/contracts/CustomToken.abi @@ -0,0 +1,403 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "holder", + "type": "address" + } + ], + "name": "HolderAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "holder", + "type": "address" + } + ], + "name": "HolderRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + } + ], + "name": "ProjectAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + } + ], + "name": "ProjectRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "TokensLocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "TokensUnlocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_project", + "type": "address" + } + ], + "name": "addToProjects", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getHolders", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getProjects", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isOwner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "isProjectAddress", + "outputs": [ + { + "internalType": "bool", + "name": "isProject", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_project", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "isTokenLocked", + "outputs": [ + { + "internalType": "bool", + "name": "isLocked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_project", + "type": "address" + } + ], + "name": "removeFromProjects", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "revoke", + "outputs": [ + { + "internalType": "bool", + "name": "isUnlocked", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_reciepient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/contracts/ERC20.abi b/src/contracts/ERC20.abi index 6e054f53..6921ea43 100644 --- a/src/contracts/ERC20.abi +++ b/src/contracts/ERC20.abi @@ -1 +1,280 @@ -[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"sender","type":"address"},{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"name","type":"string"},{"name":"symbol","type":"string"},{"name":"totalSupply","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"}] \ No newline at end of file +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/contracts/ERC20.sol b/src/contracts/ERC20.sol index c7dd2d5b..abc2465a 100644 --- a/src/contracts/ERC20.sol +++ b/src/contracts/ERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "./IERC20.sol"; import "./SafeMath.sol"; @@ -39,7 +39,7 @@ contract ERC20 is IERC20 { string private _symbol; - constructor (string name, string symbol, uint256 totalSupply) public { + constructor (string memory name, string memory symbol, uint256 totalSupply) public { _name = name; _symbol = symbol; _totalSupply = totalSupply; @@ -63,13 +63,13 @@ contract ERC20 is IERC20 { /** * @dev Get the symbol of token. */ - function symbol() public view returns (string) { + function symbol() public view returns (string memory) { return _symbol; } /** * @dev Get the name of token. */ - function name() public view returns (string) { + function name() public view returns (string memory) { return _name; } diff --git a/src/contracts/IERC20.sol b/src/contracts/IERC20.sol index 0501a2f9..29b8ff9a 100644 --- a/src/contracts/IERC20.sol +++ b/src/contracts/IERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; /** * @dev Interface of the ERC20 standard as defined in the EIP. Does not include @@ -12,11 +12,11 @@ interface IERC20 { /** * @dev Returns the amount of tokens in existence. */ - function name() external view returns (string); + function name() external view returns (string memory); /** * @dev Returns the amount of tokens in existence. */ - function symbol() external view returns (string); + function symbol() external view returns (string memory); /** * @dev Returns the amount of tokens owned by `account`. diff --git a/src/contracts/MERC20.abi b/src/contracts/MERC20.abi index 8f465543..a0e33850 100644 --- a/src/contracts/MERC20.abi +++ b/src/contracts/MERC20.abi @@ -1,235 +1,289 @@ [ - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "who", - "type": "address" - } - ], - "name": "balanceOfERC", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "admin", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "name", - "type": "string" - }, - { - "name": "symbol", - "type": "string" - }, - { - "name": "decimals", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getUsers", - "outputs": [ - { - "name": "userList", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "who", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "user", - "type": "address" - } - ], - "name": "findUser", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "findEmptyUser", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "user", - "type": "address" - }, - { - "name": "balance", - "type": "uint256" - } - ], - "name": "_addUser", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_who", - "type": "address" - }, - { - "name": "_to", - "type": "address" - }, - { - "name": "value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_newAdmin", - "type": "address" - } - ], - "name": "setAdmin", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint256", + "name": "decimals", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "name": "_addUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "who", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "findEmptyUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "findUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "getAdmin", + "outputs": [ + { + "internalType": "address", + "name": "adminitstrator", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "getUsers", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_newAdmin", + "type": "address" + } + ], + "name": "setAdmin", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferBeetweenUsers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_who", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } ] \ No newline at end of file diff --git a/src/contracts/MERC20.sol b/src/contracts/MERC20.sol index 70cc00be..f2d05c70 100644 --- a/src/contracts/MERC20.sol +++ b/src/contracts/MERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; contract MERC20 { string private _name; @@ -11,7 +11,7 @@ contract MERC20 { mapping (uint => mapping (address => uint256)) userBalances; address[] users; - constructor (string name, string symbol, uint256 decimals ) public { + constructor (string memory name, string memory symbol, uint256 decimals ) public { _name = name; _symbol = symbol; _decimals = decimals; @@ -23,17 +23,17 @@ contract MERC20 { users.push(msg.sender); } - function symbol() external returns(string) { + function symbol() external returns(string memory) { return _symbol; } - function name() external returns(string) { + function name() external returns(string memory) { return _name; } function totalSupply() external returns(uint256) { return _decimals; } - function getUsers() external returns (address[]) { + function getUsers() external returns (address[] memory) { return users; } @@ -56,7 +56,7 @@ contract MERC20 { uint usersLength = users.length; uint matched = 0; for (uint i = 0; i < usersLength; i++) { - if (users[i] == 0) { + if (users[i] == address(0)) { matched = i; } } @@ -74,9 +74,13 @@ contract MERC20 { return balances[user]; } + function transferBeetweenUsers(address sender, address recipient, uint256 amount) public returns (bool) { + require((msg.sender == admin) || (msg.sender == sender)); + this.transferFrom(sender, recipient, amount); + } + - function transferFrom(address _who, address _to, uint256 value) external { - require(msg.sender == admin); + function transferFrom(address _who, address _to, uint256 value) public returns (bool) { require(_who != address(0), "MERC20: transfer from the zero address"); require(_to != address(0), "MERC20: transfer to the zero address"); require(balances[_who] >= value, "MERC20: Token value must be lower or equal"); @@ -99,5 +103,9 @@ contract MERC20 { function setAdmin(address _newAdmin) external { admin = _newAdmin; } + + function getAdmin() external returns (address adminitstrator) { + return admin; + } } \ No newline at end of file diff --git a/src/contracts/MERCInterface.sol b/src/contracts/MERCInterface.sol index b9f4a773..f678b36c 100644 --- a/src/contracts/MERCInterface.sol +++ b/src/contracts/MERCInterface.sol @@ -1,11 +1,11 @@ -pragma solidity 0.5; + pragma solidity ^0.5.15; interface MERCInterface { - function symbol() external returns (string); + function symbol() external returns (string memory); - function name() external returns (string); + function name() external returns (string memory); function totalSupply() external returns (uint256); diff --git a/src/contracts/Migrations.sol b/src/contracts/Migrations.sol new file mode 100644 index 00000000..d25f2c23 --- /dev/null +++ b/src/contracts/Migrations.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.5.15; + + +contract Migrations { + address public owner; + uint public lastCompletedMigration; + + modifier restricted() { + if (msg.sender == owner) _; + } + + constructor() public { + owner = msg.sender; + } + + function setCompleted(uint completed) public restricted { + lastCompletedMigration = completed; + } + + function upgrade(address _newAddress) public restricted { + Migrations upgraded = Migrations(_newAddress); + upgraded.setCompleted(lastCompletedMigration); + } +} \ No newline at end of file diff --git a/src/contracts/Project/Project.sol b/src/contracts/Project/Project.sol index b27720b0..42b79287 100644 --- a/src/contracts/Project/Project.sol +++ b/src/contracts/Project/Project.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; contract Project { diff --git a/src/contracts/SafeMath.sol b/src/contracts/SafeMath.sol index 62cf6358..305cdd38 100644 --- a/src/contracts/SafeMath.sol +++ b/src/contracts/SafeMath.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; /** * @title SafeMath diff --git a/src/contracts/Voter.abi b/src/contracts/Voter.abi index 0efeb78e..becda8e6 100644 --- a/src/contracts/Voter.abi +++ b/src/contracts/Voter.abi @@ -1,609 +1,618 @@ [ { - "constant": true, "inputs": [ { - "name": "_id", - "type": "uint256" - } - ], - "name": "getVotingDescision", - "outputs": [ - { - "name": "result", - "type": "uint256" + "internalType": "address", + "name": "_address", + "type": "address" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x0d24ef38" + "stateMutability": "nonpayable", + "type": "constructor" }, { - "constant": true, + "anonymous": false, "inputs": [ { - "name": "_id", - "type": "uint256" - } - ], - "name": "question", - "outputs": [ - { + "indexed": false, + "internalType": "uint256", "name": "groupId", "type": "uint256" }, { + "indexed": false, + "internalType": "enum Questions.Status", "name": "status", "type": "uint8" }, { + "indexed": false, + "internalType": "string", "name": "caption", "type": "string" }, { + "indexed": false, + "internalType": "string", "name": "text", "type": "string" }, { + "indexed": false, + "internalType": "uint256", "name": "time", "type": "uint256" }, { + "indexed": false, + "internalType": "address", "name": "target", "type": "address" }, { + "indexed": false, + "internalType": "bytes4", "name": "methodSelector", "type": "bytes4" + } + ], + "name": "NewQuestion", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" }, { - "name": "_formula", - "type": "uint256[]" + "indexed": false, + "internalType": "uint256", + "name": "questionId", + "type": "uint256" }, { - "name": "_parameters", - "type": "bytes32[]" + "indexed": false, + "internalType": "enum Votings.Status", + "name": "status", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "starterGroup", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "starterAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "startblock", + "type": "uint256" } ], - "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x1e16f68b" + "name": "NewVoting", + "type": "event" }, { "constant": true, "inputs": [], - "name": "votings", + "name": "ERC20", "outputs": [ { - "name": "votingIdIndex", - "type": "uint256" + "internalType": "contract IERC20", + "name": "", + "type": "address" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x352485b7" + "type": "function" }, { "constant": true, - "inputs": [ - { - "name": "_id", - "type": "uint256" - } - ], - "name": "getUserGroup", + "inputs": [], + "name": "External", "outputs": [ { - "name": "name", - "type": "string" - }, - { - "name": "groupType", - "type": "string" - }, - { - "name": "status", - "type": "uint8" - }, - { - "name": "groupAddress", + "internalType": "contract ExternalContract", + "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x352704b7" + "type": "function" }, { "constant": true, "inputs": [], - "name": "getQuestionGroupsLength", + "name": "addresses", "outputs": [ { - "name": "length", - "type": "uint256" + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address", + "name": "instance", + "type": "address" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x392e1c84" + "type": "function" }, { "constant": false, "inputs": [ { + "internalType": "uint256", "name": "votingId", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "votingData", + "type": "bytes" } ], - "name": "returnTokens", + "name": "applyVotingData", "outputs": [ { - "name": "status", + "internalType": "bool", + "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0x3ae1786f" + "type": "function" }, { "constant": false, - "inputs": [ - { - "name": "group", - "type": "address" - }, - { - "name": "admin", - "type": "address" - } - ], - "name": "setCustomGroupAdmin", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], + "inputs": [], + "name": "closeVoting", + "outputs": [], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0x3cc0a953" + "type": "function" }, { "constant": true, - "inputs": [], - "name": "getUserWeight", + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "findLastUserVoting", "outputs": [ { - "name": "weight", + "internalType": "uint256", + "name": "votingId", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x42429139" + "type": "function" }, { "constant": false, "inputs": [ { - "name": "votingId", - "type": "uint256" - }, - { + "internalType": "address", "name": "user", "type": "address" } ], - "name": "isUserReturnTokens", + "name": "findUserGroup", "outputs": [ { - "name": "result", - "type": "bool" + "internalType": "uint256", + "name": "", + "type": "uint256" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0x4a1e82a0" + "type": "function" }, { - "constant": true, + "constant": false, "inputs": [], - "name": "getERCTotal", + "name": "getCount", "outputs": [ { - "name": "balance", + "internalType": "uint256", + "name": "length", "type": "uint256" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x5b1bb4a2" + "stateMutability": "nonpayable", + "type": "function" }, { "constant": true, - "inputs": [], - "name": "groups", - "outputs": [ + "inputs": [ { - "name": "groupIdIndex", + "internalType": "uint256", + "name": "_id", "type": "uint256" } ], - "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x5bf89d9e" - }, - { - "constant": false, - "inputs": [ - { - "name": "_idsAndTime", - "type": "uint256[]" - }, - { - "name": "_status", - "type": "uint8" - }, - { - "name": "_caption", - "type": "string" - }, + "name": "getQuestionGroup", + "outputs": [ { - "name": "_text", + "internalType": "string", + "name": "name", "type": "string" }, { - "name": "_target", - "type": "address" - }, - { - "name": "_methodSelector", - "type": "bytes4" - }, - { - "name": "_formula", - "type": "uint256[]" - }, - { - "name": "_parameters", - "type": "bytes32[]" - } - ], - "name": "saveNewQuestion", - "outputs": [ - { - "name": "_saved", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0x686c52c4" - }, - { - "constant": true, - "inputs": [], - "name": "getERCSymbol", - "outputs": [ - { - "name": "symbol", - "type": "string" + "internalType": "enum QuestionGroups.GroupType", + "name": "groupType", + "type": "uint8" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x7264420a" + "type": "function" }, { "constant": true, "inputs": [], - "name": "questions", + "name": "getQuestionGroupsLength", "outputs": [ { - "name": "questionIdIndex", + "internalType": "uint256", + "name": "length", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x77a49821" + "type": "function" }, { "constant": true, "inputs": [ { + "internalType": "uint256", "name": "_id", "type": "uint256" } ], - "name": "getQuestionGroup", + "name": "getUserGroup", "outputs": [ { + "internalType": "string", "name": "name", "type": "string" }, { + "internalType": "string", "name": "groupType", + "type": "string" + }, + { + "internalType": "enum UserGroups.GroupStatus", + "name": "status", "type": "uint8" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getUserGroupsLength", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x7fd60dfd" + "type": "function" }, { "constant": true, "inputs": [ { + "internalType": "uint256", "name": "_voteId", "type": "uint256" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" } ], "name": "getUserVote", "outputs": [ { + "internalType": "uint256", "name": "vote", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x86194c19" + "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_name", - "type": "string" + "internalType": "uint256", + "name": "_voteId", + "type": "uint256" }, { - "name": "_address", + "internalType": "address", + "name": "_user", "type": "address" - }, - { - "name": "_type", - "type": "string" } ], - "name": "saveNewUserGroup", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0x952b627c" - }, - { - "constant": false, - "inputs": [], - "name": "getCount", + "name": "getUserVoteWeight", "outputs": [ { - "name": "length", + "internalType": "uint256", + "name": "tokenCount", "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xa87d942c" + "stateMutability": "view", + "type": "function" }, { "constant": true, "inputs": [], - "name": "getUserBalance", + "name": "getUserWeight", "outputs": [ { - "name": "balance", + "internalType": "uint256", + "name": "weight", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xb7013dc1" + "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_address", - "type": "address" + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" } ], - "name": "setERC20", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc29a6fda" - }, - { - "constant": true, - "inputs": [], - "name": "isActiveVoting", + "name": "getVotes", "outputs": [ { - "name": "", - "type": "bool" + "internalType": "uint256[3]", + "name": "_votes", + "type": "uint256[3]" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xc39c89df" - }, - { - "constant": false, - "inputs": [], - "name": "closeVoting", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc631b292" + "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_questionId", - "type": "uint256" - }, - { - "name": "_status", - "type": "uint8" - }, - { - "name": "_starterGroup", + "internalType": "uint256", + "name": "_id", "type": "uint256" - }, - { - "name": "_data", - "type": "bytes" } ], - "name": "startNewVoting", + "name": "getVotingDescision", "outputs": [ { - "name": "", - "type": "bool" + "internalType": "uint256", + "name": "result", + "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc7ce7f10" + "stateMutability": "view", + "type": "function" }, { - "constant": false, - "inputs": [ - { - "name": "_choice", - "type": "uint256" - } - ], - "name": "sendVote", + "constant": true, + "inputs": [], + "name": "getVotingsCount", "outputs": [ { - "name": "result", - "type": "uint256" - }, - { - "name": "votePos", - "type": "uint256" - }, - { - "name": "voteNeg", + "internalType": "uint256", + "name": "count", "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc8f5714e" + "stateMutability": "view", + "type": "function" }, { "constant": true, "inputs": [], - "name": "ERC20", + "name": "groups", "outputs": [ { - "name": "", - "type": "address" + "internalType": "uint256", + "name": "groupIdIndex", + "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xcc4aa204" + "type": "function" }, { "constant": true, "inputs": [], - "name": "getERCAddress", + "name": "isActiveVoting", "outputs": [ { - "name": "_address", - "type": "address" + "internalType": "bool", + "name": "", + "type": "bool" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xd094b706" + "type": "function" }, { "constant": true, - "inputs": [], - "name": "userGroups", + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isUserReturnTokens", "outputs": [ { - "name": "groupIdIndex", - "type": "uint256" + "internalType": "bool", + "name": "result", + "type": "bool" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xd7843f54" + "type": "function" }, { "constant": true, - "inputs": [], - "name": "addresses", + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "question", "outputs": [ { - "name": "user", - "type": "address" + "internalType": "uint256", + "name": "groupId", + "type": "uint256" }, { - "name": "instance", + "internalType": "enum Questions.Status", + "name": "status", + "type": "uint8" + }, + { + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "internalType": "string", + "name": "text", + "type": "string" + }, + { + "internalType": "uint256", + "name": "time", + "type": "uint256" + }, + { + "internalType": "address", + "name": "target", "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "uint256[]", + "name": "_formula", + "type": "uint256[]" + }, + { + "internalType": "bytes32[]", + "name": "_parameters", + "type": "bytes32[]" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xda0321cd" + "type": "function" }, { "constant": true, "inputs": [], - "name": "getVotingsCount", + "name": "questions", "outputs": [ { - "name": "count", + "internalType": "uint256", + "name": "questionIdIndex", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xdae7c92c" + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "returnTokens", + "outputs": [ + { + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" }, { "constant": false, "inputs": [ { + "internalType": "string", "name": "_name", "type": "string" } @@ -611,229 +620,309 @@ "name": "saveNewGroup", "outputs": [ { + "internalType": "uint256", "name": "id", "type": "uint256" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0xdf831328" + "type": "function" }, { - "constant": true, - "inputs": [], - "name": "getUserGroupsLength", + "constant": false, + "inputs": [ + { + "internalType": "uint256[]", + "name": "_idsAndTime", + "type": "uint256[]" + }, + { + "internalType": "enum Questions.Status", + "name": "_status", + "type": "uint8" + }, + { + "internalType": "string", + "name": "_caption", + "type": "string" + }, + { + "internalType": "string", + "name": "_text", + "type": "string" + }, + { + "internalType": "address", + "name": "_target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "_methodSelector", + "type": "bytes4" + }, + { + "internalType": "uint256[]", + "name": "_formula", + "type": "uint256[]" + }, + { + "internalType": "bytes32[]", + "name": "_parameters", + "type": "bytes32[]" + } + ], + "name": "saveNewQuestion", "outputs": [ { - "name": "length", - "type": "uint256" + "internalType": "bool", + "name": "_saved", + "type": "bool" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0xec4d819c" + "stateMutability": "nonpayable", + "type": "function" }, { "constant": false, "inputs": [ { - "name": "user", + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "address", + "name": "_address", "type": "address" + }, + { + "internalType": "string", + "name": "_type", + "type": "string" } ], - "name": "findUserGroup", + "name": "saveNewUserGroup", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_choice", + "type": "uint256" + } + ], + "name": "sendVote", "outputs": [ { - "name": "", + "internalType": "uint256", + "name": "result", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "votePos", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "voteNeg", "type": "uint256" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0xf0c4c3e6" + "type": "function" }, { "constant": false, "inputs": [ { - "name": "_who", + "internalType": "address", + "name": "group", "type": "address" }, { - "name": "_value", - "type": "uint256" + "internalType": "address", + "name": "admin", + "type": "address" } ], - "name": "transferERC20", + "name": "setCustomGroupAdmin", "outputs": [ { - "name": "newBalance", - "type": "uint256" + "internalType": "bool", + "name": "", + "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0xf7448a31" + "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_id", - "type": "uint256" + "internalType": "address", + "name": "_address", + "type": "address" } ], - "name": "voting", - "outputs": [ + "name": "setERC20", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ { - "name": "id", + "internalType": "uint256", + "name": "_questionId", "type": "uint256" }, { - "name": "status", + "internalType": "enum Votings.Status", + "name": "_status", "type": "uint8" }, { - "name": "caption", - "type": "string" - }, - { - "name": "text", - "type": "string" - }, - { - "name": "startTime", - "type": "uint256" - }, - { - "name": "endTime", + "internalType": "uint256", + "name": "_starterGroup", "type": "uint256" }, { - "name": "data", + "internalType": "bytes", + "name": "_data", "type": "bytes" } ], + "name": "startNewVoting", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0xfd4a77f1" + "stateMutability": "nonpayable", + "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_votingId", + "internalType": "address", + "name": "_who", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", "type": "uint256" } ], - "name": "getVotes", + "name": "transferERC20", "outputs": [ { - "name": "_votes", - "type": "uint256[3]" + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0xff981099" + "stateMutability": "nonpayable", + "type": "function" }, { - "inputs": [ + "constant": true, + "inputs": [], + "name": "userGroups", + "outputs": [ { - "name": "_address", - "type": "address" + "internalType": "uint256", + "name": "groupIdIndex", + "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "constructor", - "signature": "constructor" + "stateMutability": "view", + "type": "function" }, { - "anonymous": false, + "constant": true, "inputs": [ { - "indexed": false, - "name": "groupId", + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "voting", + "outputs": [ + { + "internalType": "uint256", + "name": "id", "type": "uint256" }, { - "indexed": false, + "internalType": "enum Votings.Status", "name": "status", "type": "uint8" }, { - "indexed": false, + "internalType": "string", "name": "caption", "type": "string" }, { - "indexed": false, + "internalType": "string", "name": "text", "type": "string" }, { - "indexed": false, - "name": "time", + "internalType": "uint256", + "name": "startTime", "type": "uint256" }, { - "indexed": false, - "name": "target", - "type": "address" + "internalType": "uint256", + "name": "endTime", + "type": "uint256" }, { - "indexed": false, - "name": "methodSelector", - "type": "bytes4" + "internalType": "bytes", + "name": "data", + "type": "bytes" } ], - "name": "NewQuestion", - "type": "event", - "signature": "0xf74c837bcb7177c5fc07298a351f91ebaf73da24a2665c4dd592976c4e1780c9" + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "id", - "type": "uint256" - }, - { - "indexed": false, - "name": "questionId", - "type": "uint256" - }, - { - "indexed": false, - "name": "status", - "type": "uint8" - }, - { - "indexed": false, - "name": "starterGroup", - "type": "uint256" - }, - { - "indexed": false, - "name": "starterAddress", - "type": "address" - }, + "constant": true, + "inputs": [], + "name": "votings", + "outputs": [ { - "indexed": false, - "name": "startblock", + "internalType": "uint256", + "name": "votingIdIndex", "type": "uint256" } ], - "name": "NewVoting", - "type": "event", - "signature": "0x20cd0a5d2be6a115945cf28e1511206f69d34a385fb4b9c8ee18f39fa52471c7" + "payable": false, + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file diff --git a/src/contracts/Voter/ExternalInterface.sol b/src/contracts/Voter/ExternalInterface.sol new file mode 100644 index 00000000..2a8b02b6 --- /dev/null +++ b/src/contracts/Voter/ExternalInterface.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.5.15; + +interface ExternalContract { + function applyVotingData(uint votingId, uint questionId, bytes calldata data) external returns (bool); +} \ No newline at end of file diff --git a/src/contracts/Voter/Voter.sol b/src/contracts/Voter/Voter.sol index eefe9132..e929cfae 100644 --- a/src/contracts/Voter/Voter.sol +++ b/src/contracts/Voter/Voter.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "./VoterBase.sol"; diff --git a/src/contracts/Voter/VoterBase.sol b/src/contracts/Voter/VoterBase.sol index 8297ee95..e60b3704 100644 --- a/src/contracts/Voter/VoterBase.sol +++ b/src/contracts/Voter/VoterBase.sol @@ -1,10 +1,11 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "../libs/QuestionGroups.sol"; import "../libs/UserGroups.sol"; import "../libs/Questions.sol"; import "../libs/Votings.sol"; import "./VoterInterface.sol"; +import "./ExternalInterface.sol"; import "../IERC20.sol"; @@ -21,6 +22,7 @@ contract VoterBase is VoterInterface { UserGroups.List public userGroups; IERC20 public ERC20; + ExternalContract public External; constructor() public { questions.init(); @@ -30,7 +32,6 @@ contract VoterBase is VoterInterface { // METHODS function setERC20(address _address) public { - ERC20 = IERC20(_address); userGroups.init(_address); } @@ -55,7 +56,7 @@ contract VoterBase is VoterInterface { address _target, bytes4 _methodSelector, uint[] memory _formula, - bytes32[] memory _parameters + bytes32[] memory _parameters ) private returns (Questions.Question memory _question) { Questions.Question memory question = Questions.Question({ groupId: _idsAndTime[1], @@ -88,14 +89,14 @@ contract VoterBase is VoterInterface { * @return new question id */ function saveNewQuestion( - uint[] _idsAndTime, + uint[] calldata _idsAndTime, Questions.Status _status, - string _caption, - string _text, + string calldata _caption, + string calldata _text, address _target, bytes4 _methodSelector, - uint[] _formula, - bytes32[] _parameters + uint[] calldata _formula, + bytes32[] calldata _parameters ) external returns (bool _saved){ Questions.Question memory question = createNewQuestion( @@ -118,7 +119,7 @@ contract VoterBase is VoterInterface { * @return new question id */ function saveNewGroup( - string _name + string calldata _name ) external returns (uint id) { QuestionGroups.Group memory group = QuestionGroups.Group({ name: _name, @@ -164,7 +165,7 @@ contract VoterBase is VoterInterface { } function getQuestionGroup(uint _id) public view returns ( - string name, + string memory name, QuestionGroups.GroupType groupType ) { return ( @@ -177,8 +178,8 @@ contract VoterBase is VoterInterface { return groups.groupIdIndex ; } function getUserGroup(uint _id) public view returns ( - string name, - string groupType, + string memory name, + string memory groupType, UserGroups.GroupStatus status, address groupAddress ) { @@ -210,7 +211,7 @@ contract VoterBase is VoterInterface { uint _questionId, Votings.Status _status, uint _starterGroup, - bytes _data + bytes calldata _data ) external returns (bool) { bool canStart; uint votingId = votings.votingIdIndex - 1; @@ -253,7 +254,7 @@ contract VoterBase is VoterInterface { string memory text, uint startTime, uint endTime, - bytes data + bytes memory data ){ uint votingId = _id; uint questionId = votings.voting[_id].questionId; @@ -319,7 +320,7 @@ contract VoterBase is VoterInterface { if (quorumPercent >= percent) { if (positiveVotes > negativeVotes) { votings.descision[votingId] = 1; - address(this).call(votings.voting[votingId].data); + callExternal(votingId, questionId); } else if (positiveVotes < negativeVotes) { votings.descision[votingId] = 2; } else if (positiveVotes == negativeVotes) { @@ -331,7 +332,7 @@ contract VoterBase is VoterInterface { if (quorumPercent <= percent) { if (positiveVotes > negativeVotes) { votings.descision[votingId] = 1; - address(this).call(votings.voting[votingId].data); + callExternal(votingId, questionId); } else if (positiveVotes < negativeVotes) { votings.descision[votingId] = 2; } else if (positiveVotes == negativeVotes) { @@ -344,6 +345,18 @@ contract VoterBase is VoterInterface { votings.voting[votingId].status = Votings.Status.ENDED; } + function callExternal(uint votingId, uint questionId) internal { + address target = questions.question[questionId].target; + ExternalContract controlled = ExternalContract(target); + controlled.applyVotingData(votingId, questionId, votings.voting[votingId].data); + } + + + function applyVotingData(uint votingId, uint questionId, bytes calldata votingData) external returns (bool) { + address(this).call(votingData); + return true; + } + function getVotes(uint _votingId) external view returns (uint256[3] memory _votes) { uint questionId = votings.voting[_votingId].questionId; @@ -357,32 +370,41 @@ contract VoterBase is VoterInterface { return votes; } - function returnTokens(uint votingId) public returns (bool status){ + function returnTokens() public returns (bool status){ + uint votingId = this.findLastUserVoting(msg.sender); uint questionId = votings.voting[votingId].questionId; uint groupId = questions.question[questionId].groupId; string memory groupType = userGroups.group[groupId].groupType; + string memory groupName = userGroups.names[groupId]; IERC20 group = IERC20(userGroups.group[groupId].groupAddr); uint256 weight = votings.voting[votingId].voteWeigths[address(group)][msg.sender]; - bool isReturned = this.isUserReturnTokens(votingId, msg.sender); + bool isReturned = this.isUserReturnTokens(msg.sender); + uint userVote = this.getUserVote(votingId, msg.sender); if (!isReturned) { - if( bytes4(keccak256(groupType)) == bytes4(keccak256("ERC20"))) { + if(keccak256(abi.encodePacked((groupType))) == keccak256(abi.encodePacked(("ERC20")))) { group.transfer(msg.sender, weight); } else { group.transferFrom(address(this), msg.sender, weight); } + if (votings.voting[votingId].status != Votings.Status.ENDED) { + votings.voting[votingId].votes[address(group)][msg.sender] = 0; + votings.voting[votingId].voteWeigths[address(group)][msg.sender] = 0; + votings.voting[votingId].descisionWeights[userVote][groupName] -= weight; + } votings.voting[votingId].tokenReturns[address(group)][msg.sender] = weight; } return true; } - function isUserReturnTokens(uint votingId, address user) returns (bool result) { + function isUserReturnTokens(address user) external view returns (bool result) { + uint votingId = this.findLastUserVoting(user); uint questionId = votings.voting[votingId].questionId; uint groupId = questions.question[questionId].groupId; string memory groupType = userGroups.group[groupId].groupType; IERC20 group = IERC20(userGroups.group[groupId].groupAddr); uint256 returnedTokens = votings.voting[votingId].tokenReturns[address(group)][user]; - return returnedTokens > 0; + return votingId == 0 ? true : returnedTokens > 0; } @@ -423,36 +445,41 @@ contract VoterBase is VoterInterface { this.closeVoting(); } return ( - votings.voting[_voteId].votes[address(group)][msg.sender] = _choice, + votings.voting[_voteId].votes[address(group)][msg.sender], votings.voting[_voteId].descisionWeights[1][groupName], votings.voting[_voteId].descisionWeights[2][groupName] ); } - function getERCAddress() external view returns (address _address) { - return address(ERC20); - } - - function getUserBalance() external view returns (uint256 balance) { - uint256 _balance = ERC20.balanceOf(msg.sender); - return _balance; - } - - function getERCTotal() external view returns (uint256 balance) { - return ERC20.totalSupply(); + function getUserVote(uint _voteId, address _user) external view returns (uint vote) { + uint questionId = votings.voting[_voteId].questionId; + uint groupId = questions.question[questionId].groupId; + IERC20 group = IERC20(userGroups.group[groupId].groupAddr); + return votings.voting[_voteId].votes[address(group)][_user]; } - function getERCSymbol() external view returns (string symbol) { - return ERC20.symbol(); + function getUserVoteWeight(uint _voteId, address _user) external view returns (uint tokenCount) { + uint questionId = votings.voting[_voteId].questionId; + uint groupId = questions.question[questionId].groupId; + IERC20 group = IERC20(userGroups.group[groupId].groupAddr); + return votings.voting[_voteId].voteWeigths[address(group)][_user]; } - function getUserVote(uint _voteId) external view returns (uint vote) { - uint questionId = votings.voting[_voteId].questionId; + function findLastUserVoting(address _address) external view returns (uint votingId){ + uint maxVoteId = votings.votingIdIndex - 1; + uint questionId = votings.voting[maxVoteId].questionId; uint groupId = questions.question[questionId].groupId; IERC20 group = IERC20(userGroups.group[groupId].groupAddr); - return votings.voting[_voteId].votes[address(group)][msg.sender]; + while((votings.voting[maxVoteId].votes[address(group)][_address] == 0) && (maxVoteId >= 1)) { + maxVoteId--; + questionId = votings.voting[maxVoteId].questionId; + groupId = questions.question[questionId].groupId; + group = IERC20(userGroups.group[groupId].groupAddr); + } + return maxVoteId; } + function getUserWeight() external view returns (uint256 weight) { uint _voteId = votings.votingIdIndex - 1; return votings.voting[_voteId].voteWeigths[address(ERC20)][msg.sender]; @@ -470,7 +497,7 @@ contract VoterBase is VoterInterface { ); } - function saveNewUserGroup (string _name, address _address, string _type) external { + function saveNewUserGroup (string calldata _name, address _address, string calldata _type) external { UserGroups.UserGroup memory userGroup = UserGroups.UserGroup({ name: _name, groupType: _type, @@ -480,8 +507,8 @@ contract VoterBase is VoterInterface { userGroups.save(userGroup); } - function setCustomGroupAdmin(address group, address admin) external returns (bool) { - require(group.call( bytes4( keccak256("setAdmin(address)")), admin)); + function setCustomGroupAdmin(address group, address admin) external returns (bool) { + group.call(abi.encodeWithSignature("setAdmin(address)", admin)); return true; } } diff --git a/src/contracts/Voter/VoterInterface.sol b/src/contracts/Voter/VoterInterface.sol index 51a10280..eab75094 100644 --- a/src/contracts/Voter/VoterInterface.sol +++ b/src/contracts/Voter/VoterInterface.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "../libs/QuestionGroups.sol"; import "../libs/Questions.sol"; @@ -53,14 +53,14 @@ interface VoterInterface { * return new question id */ function saveNewQuestion( - uint[] _idsAndTime, + uint[] calldata _idsAndTime, Questions.Status _status, - string _caption, - string _text, + string calldata _caption, + string calldata _text, address _target, bytes4 _methodSelector, - uint[] _formula, - bytes32[] _parameters + uint[] calldata _formula, + bytes32[] calldata _parameters ) external returns (bool _saved); /** @@ -69,7 +69,7 @@ interface VoterInterface { * @return new question id */ function saveNewGroup( - string _name + string calldata _name ) external returns (uint id); /** @@ -97,7 +97,7 @@ interface VoterInterface { uint questionId, Votings.Status status, uint starterGroup, - bytes data + bytes calldata data ) external returns (bool); function voting(uint id) external view returns ( @@ -107,7 +107,7 @@ interface VoterInterface { string memory text, uint startTime, uint endTime, - bytes data + bytes memory data ); function getVotingsCount() external view returns (uint length); } diff --git a/src/contracts/ZeroOne.abi b/src/contracts/ZeroOne.abi new file mode 100644 index 00000000..55d99c07 --- /dev/null +++ b/src/contracts/ZeroOne.abi @@ -0,0 +1,1271 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "owners", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes4", + "name": "selector", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bool", + "name": "result", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "name": "Call", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + } + ], + "name": "QuestionAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "name": "QuestionGroupAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "group", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum VM.Vote", + "name": "userVote", + "type": "uint8" + } + ], + "name": "UpdatedUserVote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "groupAddress", + "type": "address" + } + ], + "name": "UserGroupAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum VM.Vote", + "name": "descision", + "type": "uint8" + } + ], + "name": "UserVote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "votingId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum VM.Vote", + "name": "descision", + "type": "uint8" + } + ], + "name": "VotingEnded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "votingId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + } + ], + "name": "VotingStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct IZeroOne.MetaData", + "name": "_meta", + "type": "tuple" + } + ], + "name": "ZeroOneCall", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timeLimit", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "paramNames", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "paramTypes", + "type": "string[]" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "string", + "name": "rawFormula", + "type": "string" + }, + { + "internalType": "bytes", + "name": "formula", + "type": "bytes" + } + ], + "internalType": "struct QuestionType.Question", + "name": "_question", + "type": "tuple" + } + ], + "name": "addQuestion", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timeLimit", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "paramNames", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "paramTypes", + "type": "string[]" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "string", + "name": "rawFormula", + "type": "string" + }, + { + "internalType": "bytes", + "name": "formula", + "type": "bytes" + } + ], + "internalType": "struct QuestionType.Question", + "name": "_question", + "type": "tuple" + } + ], + "name": "addQuestion", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct GroupType.Group", + "name": "_questionGroup", + "type": "tuple" + } + ], + "name": "addQuestionGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct GroupType.Group", + "name": "_questionGroup", + "type": "tuple" + } + ], + "name": "addQuestionGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + }, + { + "internalType": "enum UserGroup.Type", + "name": "groupType", + "type": "uint8" + } + ], + "internalType": "struct UserGroup.Group", + "name": "_group", + "type": "tuple" + } + ], + "name": "addUserGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + }, + { + "internalType": "enum UserGroup.Type", + "name": "groupType", + "type": "uint8" + } + ], + "internalType": "struct UserGroup.Group", + "name": "_group", + "type": "tuple" + } + ], + "name": "addUserGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "didUserVote", + "outputs": [ + { + "internalType": "bool", + "name": "confirm", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "findLastUserVoting", + "outputs": [ + { + "internalType": "uint256", + "name": "votingId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + } + ], + "name": "getGroupVotes", + "outputs": [ + { + "internalType": "uint256", + "name": "positive", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "negative", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getQuestion", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timeLimit", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "paramNames", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "paramTypes", + "type": "string[]" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "string", + "name": "rawFormula", + "type": "string" + }, + { + "internalType": "bytes", + "name": "formula", + "type": "bytes" + } + ], + "internalType": "struct QuestionType.Question", + "name": "question", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getQuestionGroup", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct GroupType.Group", + "name": "questionGroup", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getQuestionGroupsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getQuestionsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getUserGroup", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + }, + { + "internalType": "enum UserGroup.Type", + "name": "groupType", + "type": "uint8" + } + ], + "internalType": "struct UserGroup.Group", + "name": "group", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getUserGroupsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getUserVote", + "outputs": [ + { + "internalType": "enum VM.Vote", + "name": "descision", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getUserVoteWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getVoting", + "outputs": [ + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "starterGroupId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "starterAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "enum BallotType.BallotStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "votingData", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + } + ], + "name": "getVotingResult", + "outputs": [ + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVotingsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isProject", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "isUserReturnTokens", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revoke", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_status", + "type": "bool" + } + ], + "name": "setActiveStatus", + "outputs": [ + { + "internalType": "bool", + "name": "changed", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "setGroupAdmin", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + } + ], + "name": "setQuestionGroupName", + "outputs": [ + { + "internalType": "bool", + "name": "changed", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum VM.Vote", + "name": "_descision", + "type": "uint8" + } + ], + "name": "setVote", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "starterGroupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTime", + "type": "uint256" + }, + { + "internalType": "address", + "name": "starterAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct BallotList.BallotSimple", + "name": "_votingPrimary", + "type": "tuple" + } + ], + "name": "startVoting", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "submitVoting", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_newVoteWeight", + "type": "uint256" + } + ], + "name": "updateUserVote", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/contracts/libs/QuestionGroups.sol b/src/contracts/libs/QuestionGroups.sol index 06012284..4b37c8d6 100644 --- a/src/contracts/libs/QuestionGroups.sol +++ b/src/contracts/libs/QuestionGroups.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library QuestionGroups { diff --git a/src/contracts/libs/Questions.sol b/src/contracts/libs/Questions.sol index 6d66d600..8242f04b 100644 --- a/src/contracts/libs/Questions.sol +++ b/src/contracts/libs/Questions.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library Questions { diff --git a/src/contracts/libs/UserGroups.sol b/src/contracts/libs/UserGroups.sol index d283237e..41408b3c 100644 --- a/src/contracts/libs/UserGroups.sol +++ b/src/contracts/libs/UserGroups.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library UserGroups { diff --git a/src/contracts/libs/Votings.sol b/src/contracts/libs/Votings.sol index cba423c8..7658f1aa 100644 --- a/src/contracts/libs/Votings.sol +++ b/src/contracts/libs/Votings.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library Votings { diff --git a/src/contracts/project.sol b/src/contracts/project.sol index 85f98ad3..57653a11 100644 --- a/src/contracts/project.sol +++ b/src/contracts/project.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.5; +pragma solidity ^0.5.15; contract UsingERC20 { diff --git a/src/contracts/sysQuestions.json b/src/contracts/sysQuestions.json index 67343d14..3c62e032 100644 --- a/src/contracts/sysQuestions.json +++ b/src/contracts/sysQuestions.json @@ -1,128 +1,80 @@ { - "1": { - "id": 1, - "group": 1, + "0": { + "groupId": 0, "name": "Добавить Вопрос", - "caption": "Добавление нового вопроса", - "time": 5, - "method": "0x686c52c4", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ + "description": "Добавление нового вопроса", + "timeLimit": 300, + "methodSelector": "0x5fe495c3", + "paramNames": [ "GroupId", - "uint", "Name", - "string", "Caption", - "string", "Time", - "uint", "MethodSelector", - "bytes4", "Formula", + "paramNames", + "paramTypes", + "Target" + ], + "paramTypes": [ + "uint", + "string", + "string", + "uint", + "bytes4", "string", - "parameters", - "bytes32[]" + "string[]", + "string[]", + "address" ], - "hints": { - "0": { - "desc": "Id группы вопросов, к которому он будет принадлежать", - "example": "1 - системные вопросы, 2 - вопросы для группы владельцев кастомных токенов" - }, - "1": { - "desc": "Название вопроса", - "example": "Уничтожить проект" - }, - "2": { - "desc": "Описание вопроса", - "example": "Уничтожение всего проекта, так как дальнейшее существование потеряло смысл" - }, - "3": { - "desc": "Время, в течении которого можно будет проголосовать после того, как будет начато голосование по данному вопросу (в минутах)", - "example": "10" - }, - "4": { - "desc": "Селектор метода в контракте, который будет вызываться в случае положительного исхода голосования", - "example": "0xcaF1deb4" - }, - "5": { - "desc": "Формула, по которой будут подсчитываться результаты голосования", - "example": "(group (Owners) => condition (positive >= 20% of all)) \r\n (group (Designers) => condition (quorum >= 20%))" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, - "2": { - "id": 2, - "group": 1, + "1": { + "groupId": 0, "name": "Подключить группу пользователей", - "caption": "Подключить новую группу пользователей для участия в голосованиях", - "time": 5, - "method": "0x952b627c", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ + "description": "Подключить новую группу пользователей для участия в голосованиях", + "timeLimit": 300, + "methodSelector": "0x6b13e1e0", + "paramNames": [ "Name", - "string", "Address", + "Type" + ], + "paramTypes": [ + "string", "address", - "Type", - "string" + "uint8" ], - "hints": { - "0": { - "desc": "Название группы пользователей", - "example": "Дизайнеры" - }, - "1": { - "desc": "Адрес контракта, в котором находятся токены, распределенные среди участников группы", - "example": "0х0000000000000000000000000000000000000000" - }, - "2": { - "desc": "Тип контракта данной группы (ERC20 для контракта токенов ERC20, Custom для контракта токенов, созданного в этом приложении)", - "example": "ERC20, Custom" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, - "3": { - "id": 3, - "group": 1, + "2": { + "groupId": 0, "name": "Добавить группу вопросов", - "caption": "Добавить новую группу вопросов", - "time": 5, - "method": "0xdf831328", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ - "Name", + "description": "Добавить новую группу вопросов", + "timeLimit": 300, + "methodSelector": "0xb9253b2b", + "paramNames": [ + "Name" + ], + "paramTypes": [ "string" ], - "hints": { - "0": { - "desc": "Имя для группы вопросов", - "example": "Административные" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, - "4": { - "id": 4, - "group": 1, + "3": { + "groupId": 0, "name": "Установить администратора группы", - "caption": "Установка администратора в группе кастомных токенов", - "time": 5, - "method": "0x3cc0a953", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ - "Group", + "description": "Установка администратора в группе кастомных токенов", + "timeLimit": 300, + "methodSelector": "0xa589e384", + "paramNames": [ + "Group Address", + "New Admin Address" + ], + "paramTypes": [ "address", - "Admin address", "address" ], - "hints": { - "0": { - "desc": "Aдрес группы, в которой произойдет установка администратора", - "example": "0х0000000000000000000000000000000000000000" - }, - "1": { - "desc": "Адрес пользователя, который получит администраторские привелегии", - "example": "0х0000000000000000000000000000000000000000" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" } } \ No newline at end of file diff --git a/src/electron.js b/src/electron.js index 59301c66..07d8a400 100644 --- a/src/electron.js +++ b/src/electron.js @@ -1,10 +1,52 @@ -const { app, BrowserWindow } = require('electron'); -const electronLocalshortcut = require('electron-localshortcut'); +const { + app, BrowserWindow, shell, ipcMain, dialog, +} = require('electron'); +const electronLocalShortcut = require('electron-localshortcut'); const isDev = require('electron-is-dev'); const path = require('path'); +const fs = require('fs'); +const solc = require('solc'); +const linker = require('solc/linker'); + +require.extensions['.sol'] = function (module, filename) { + module.exports = fs.readFileSync(filename, 'utf8'); +}; + let mainWindow; +let loadingScreen; +/** + * + */ +function createLoadingScreen() { + loadingScreen = new BrowserWindow({ + minWidth: 539, + minHeight: 539, + width: 539, + height: 539, + center: true, + backgroundColor: '#fff', + webPreferences: { + nodeIntegration: true, + webSecurity: false, + }, + frame: false, + skipTaskbar: true, + resizable: false, + alwaysOnTop: false, + }); + loadingScreen.setResizable(false); + loadingScreen.loadURL(`file://${__dirname}/splash.html`); + // eslint-disable-next-line no-return-assign + loadingScreen.on('closed', () => (loadingScreen = null)); + loadingScreen.webContents.on('did-finish-load', () => { + loadingScreen.show(); + }); +} +/** + * + */ function createWindow() { mainWindow = new BrowserWindow({ useContentSize: true, @@ -15,19 +57,86 @@ function createWindow() { webPreferences: { nodeIntegration: true, }, + // show to false mean than the window will proceed with its + // lifecycle, but will not render until we will show it up + show: false, }); mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`); - // eslint-disable-next-line no-unused-expressions - isDev - ? process.env.NODE_ENV = 'production' - : process.env.NODE_ENV = 'development'; + + process.env.NODE_ENV = isDev + ? 'production' + : 'development'; + + mainWindow.setMenu(null); + + // eslint-disable-next-line no-return-assign mainWindow.on('closed', () => mainWindow = null); - electronLocalshortcut.register(mainWindow, 'F12', () => { + + mainWindow.webContents.on('new-window', (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }); + + electronLocalShortcut.register(mainWindow, 'F12', () => { mainWindow.webContents.toggleDevTools(); }); + + // keep listening on the did-finish-load event, when the mainWindow content has loaded + mainWindow.webContents.on('did-finish-load', () => { + // then close the loading screen window and show the main window + if (loadingScreen) { + loadingScreen.close(); + } + mainWindow.show(); + }); + + ipcMain.on('config-problem', (event, filePath) => { + dialog.showErrorBox('File reading error', `File ${filePath} is corrupted, please check it`); + loadingScreen.close(); + }); + + ipcMain.on('change-language:request', ((event, value) => { + mainWindow.webContents.send('change-language:confirm', value); + })); + + ipcMain.on('compile-request', ((event, input) => { + const { contract, type } = input; + const data = { + language: 'Solidity', + sources: { + 'test.sol': { + content: contract, + }, + }, + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + outputSelection: { + '*': { + '*': ['*'], + }, + }, + }, + }; + const output = JSON.parse(solc.compile(JSON.stringify(data))); + if (type === 'ZeroOne') { + const contracts = { + ZeroOne: output.contracts['test.sol'][type], + ZeroOneVM: output.contracts['test.sol'].ZeroOneVM, + }; + mainWindow.webContents.send('contract-compiled', contracts); + } else { + mainWindow.webContents.send('contract-compiled', output.contracts['test.sol'][type]); + } + })); } -app.on('ready', createWindow); +app.on('ready', () => { + createLoadingScreen(); + createWindow(); +}); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { diff --git a/src/i18n.js b/src/i18n.js index e6c87b28..ad3326da 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,5 +1,6 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; +import moment from 'moment'; import headingsEn from './locales/ENG/headings'; import headingsRu from './locales/RUS/headings'; import explanationsEn from './locales/ENG/explanations'; @@ -14,6 +15,9 @@ import errorsEn from './locales/ENG/errors'; import errorsRu from './locales/RUS/errors'; import dialogsEn from './locales/ENG/dialogs'; import dialogsRu from './locales/RUS/dialogs'; +import 'moment/locale/ru'; +import 'moment/locale/en-gb'; +import { getCorrectMomentLocale } from './utils/Date'; const resources = { ENG: { @@ -49,6 +53,8 @@ i18n nsMode: 'default', useSuspense: false, }, + }, () => { + moment.locale(getCorrectMomentLocale(i18n.language)); }); window.i18n = i18n; export default i18n; diff --git a/src/index.js b/src/index.js index 1bb212a8..240cc737 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable no-unused-vars */ import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'mobx-react'; @@ -8,13 +10,28 @@ import './i18n'; import './assets/styles/style.scss'; -const { userStore, appStore, dialogStore } = rootStore; +const { ipcRenderer } = window.require('electron'); +window.ipcRenderer = ipcRenderer; + +const { + userStore, + appStore, + dialogStore, + membersStore, + projectStore, + notificationStore, + configStore, +} = rootStore; render( diff --git a/src/locales/ENG/buttons.js b/src/locales/ENG/buttons.js index 7303b775..9c5299f8 100644 --- a/src/locales/ENG/buttons.js +++ b/src/locales/ENG/buttons.js @@ -17,5 +17,23 @@ const buttons = { withTokens: 'Connect contract and create project', withoutTokens: 'Create new contract and project', toWallets: 'To wallets', + createQuestion: 'Create question', + createQuestionGroup: 'Create group of questions', + startNewVoting: 'Start new voting', + transfer: 'Transfer', + designateGroupAdministrator: 'start voting for appointment as group administrator', + vote: 'Vote', + startNewVote: 'Start a new vote', + start: 'Start', + nextStep: 'Next step', + addParameter: '+ Add parameter', + apply: 'Apply', + completeTheVote: 'Complete the vote', + pickUpTokens: 'pick up tokens', + pickUpTokensCapital: 'Pick up tokens', + clear: 'Clear', + retry: 'Retry', + saveAndReload: 'Save and reload app', + saveWithoutReload: 'Save without reload', }; export default buttons; diff --git a/src/locales/ENG/dialogs.js b/src/locales/ENG/dialogs.js index bd2146a9..fba093c2 100644 --- a/src/locales/ENG/dialogs.js +++ b/src/locales/ENG/dialogs.js @@ -1,10 +1,16 @@ const dialog = { definetelyAgree: 'Do you definitely agree?', + definetelyReject: 'Do you definitely reject?', agreedMessage: 'You agreed', rejectMessage: 'You voted against', transferInProgress: 'Token transfer in progress', someTimeText: 'It will take some time', tokenTransferSuccess: 'Tokens successfully transferred!', + tokenTransfer: 'Transfer token', + tokenTransferError: 'Transfer error', + createAGroupOfQuestions: 'Create a group of questions', + completionOfVoting: 'Completion of voting', + ERC20TokensUsed: 'This vote uses ERC20 tokens', }; export default dialog; diff --git a/src/locales/ENG/errors.js b/src/locales/ENG/errors.js index 73888097..7e341be9 100644 --- a/src/locales/ENG/errors.js +++ b/src/locales/ENG/errors.js @@ -6,6 +6,8 @@ const errors = { emptyFields: 'Form have empty fields', lowBalance: 'Your balance is to low for this', hostUnreachable: 'Host is unreachable, please check your internet connection and try again', + transferIfNotAdmin: 'You cannot tranfer tokens from other wallets', + transferLocked: 'You cannot send custom tokens', }; export default errors; diff --git a/src/locales/ENG/explanations.js b/src/locales/ENG/explanations.js index 6dc9bd22..557ebd8d 100644 --- a/src/locales/ENG/explanations.js +++ b/src/locales/ENG/explanations.js @@ -6,20 +6,21 @@ const explanations = { symbol: 'a special character', length: 'minimum 6 char length', }, - seed: ['The phrase gives you complete control over your account', 'Be sure to write down and do’t tell it to anyone'], + seed: ['The phrase gives you complete control over your account', 'Be sure to write down and don’t tell it to anyone'], project: { name: 'The project title is set by you and appears in the project selection page', address: 'The address is provided by the creator of the project' }, token: { left: { wallet: ['The contract will be uploaded to the network', ' by a wallet:'], balance: 'Balance: ', - tokens: ['Tokens will be credited to this wallet', ' Тhey can be distributed later'], + tokens: ['Tokens will be credited to this wallet', ' They can be distributed later'], }, right: { symbol: 'A token symbol is its abbreviated name. For example: ETH, BTC, etc.', count: 'The total number of tokens is set by you. They can be distributed among the project participants later.', }, }, + freeze: 'On contract compiling app will freeze on several seconds, please be patient', }; export default explanations; diff --git a/src/locales/ENG/fields.js b/src/locales/ENG/fields.js index 80666846..85dd92ef 100644 --- a/src/locales/ENG/fields.js +++ b/src/locales/ENG/fields.js @@ -8,6 +8,34 @@ const placeholders = { quantity: 'Quantity', projectTitle: 'Project title', contractAddress: 'Enter contract address', + address: 'Address', + countTokens: 'Count tokens', + question: 'Question', + status: 'Status', + date: 'Date', + descision: 'Descision', + durationInBlocks: 'Duration of circulation in blocks', + questionTitle: 'Question title', + questionLifeTime: 'Question lifetime', + methodSelector: 'Function selector', + parameter: 'Parameter', + enterNewParameterName: 'Enter new parameter name', + votingFormula: 'Voting formula', + questionDescription: 'Question description', + selectParameterType: 'Select parameter type', + titleGroupQuestions: 'Title group questions', + descriptionOrComment: 'Description or comment', + dateFrom: 'Date from', + dateTo: 'Date to', + questionGroup: 'Question group', + targetContractAddress: 'Target Contract Address', + functionSelector: 'Function selector', + nodeUrl: 'Node URL', + chooseTheQuestion: 'Choose the question', + minGasPrice: 'Min. gas price, Gwei', + maxGasPrice: 'Max. gas price, Gwei', + interval: 'Data update interval, sec', + selectQuestionGroup: 'Select group of questions', }; export default placeholders; diff --git a/src/locales/ENG/headings.js b/src/locales/ENG/headings.js index 9df808df..8c9452cb 100644 --- a/src/locales/ENG/headings.js +++ b/src/locales/ENG/headings.js @@ -2,25 +2,32 @@ const headings = { login: { heading: 'Sign In', subheading: 'Get ready for a new era of voting' }, logging: { heading: 'Sighning in', subheading: 'It does not take much time' }, projects: { heading: 'Project selection', subheading: 'Chose a project or create a new one' }, - addingProject: { heading: 'Adding a project', subheading: 'Create a new one or connect already existing' }, - passwordCreation: { heading: 'Password creation', subheading: 'Will be used to enter the wallet and transactions confirmation' }, + addingProject: { heading: 'Adding a project', subheading: ['Create a new one or connect', 'already existing'] }, + passwordCreation: { heading: 'Password creation', subheading: ['Will be used to enter the wallet', 'and transactions confirmation'] }, showSeed: { heading: 'Reserve phrase', subheading: 'Will be used for password recovery' }, seedCheck: { heading: 'Reserve phrase check', subheading: ['Checking the seed', 'Enter the phrase u wrote down'] }, - сonnectProject: { heading: 'Project connecting', subheading: 'Create a new one or connect already existing' }, + connectProject: { heading: 'Project connecting', subheading: 'Create a new one or connect already existing' }, projectChecking: { heading: 'Checking the project address', subheading: 'It does not take much time' }, projectConnected: { heading: 'Project is connected!', subheading: 'Now you can start working with it or choose another project' }, newProject: { heading: 'Creating a new project', subheading: 'Choose the suitable option ' }, - existingTokens: { heading: 'Connecting contarct of tokens', subheading: 'The owner of this contract will be considered the owner of the created project' }, + existingTokens: { heading: 'Connecting contract of tokens', subheading: 'The owner of this contract will be considered the owner of the created project' }, checkingTokens: { heading: 'Checking the project address', subheading: 'It does not take much time' }, - checkingTokensConfirm: { heading: 'The contract is checked', subheading: 'Verify the data before proceeding' }, - newTokens: { heading: 'Token creating', subheading: '' }, + checkingTokensConfirm: { heading: 'The contract is checked', subheading: ['Verify the data', 'before proceeding'] }, + newTokens: { heading: 'Creating token ', subheading: '' }, tokensCreating: { heading: 'Creating ERC20 tokens', subheading: 'It will take some time' }, tokensCreated: { heading: 'Tokens are created!', subheading: 'Now you need to create a project' }, - projectCreating: { heading: 'Creating a project', subheading: 'The project contract will be uploaded to the network by a wallet' }, + projectCreating: { heading: 'Creating a project', subheading: ['The project contract will be uploaded', 'to the network by a wallet'] }, uploadingProject: { heading: 'Uploading a contract', subheading: 'It can take up to 5 minutes' }, - projectCreated: { heading: 'Contract is created!', subheading: 'Now you can start working with it or choose another project' }, - walletRestored: { heading: 'Wallet restoring', subheading: 'Wallet is succesfully restored' }, - walletCreated: { heading: 'Wallet creating', subheading: 'Wallet is succesfully created' }, - walletRestoring: { heading: 'Wallet restoring', subheading: 'Check the data accuracy before continuing' }, + projectCreated: { heading: 'Contract is created!', subheading: ['Now you can start working with', 'it or choose another project'] }, + walletRestored: { heading: 'Wallet restoring', subheading: 'Wallet is successfully restored' }, + walletCreated: { heading: 'Wallet creating', subheading: 'Wallet is successfully created' }, + walletRestoring: { heading: 'Wallet restoring', subheading: ['Check the data accuracy', 'before continuing'] }, + nodeConnection: 'Node connection', + creatingAndUpload: 'Create and upload contract', + interfaceLanguage: 'Interface language', + sendingTransaction: 'Sending transaction', + successfullTransaction: 'Transaction sended successfully', + failedTransaction: { heading: 'Transaction sending failed', subheading: 'Please, try again' }, + other: 'Other', }; export default headings; diff --git a/src/locales/ENG/other.js b/src/locales/ENG/other.js index 8395020a..eec6c710 100644 --- a/src/locales/ENG/other.js +++ b/src/locales/ENG/other.js @@ -6,6 +6,7 @@ const other = { sending: 'Sendind', txHash: 'Awaiting hash', txReceipt: 'Awaiting receipt', + txSigning: 'Transaction signing', questionsUploading: 'Uploading questions', walletAddress: 'Wallet', balance: 'Balance', @@ -14,7 +15,79 @@ const other = { withTokens: 'If you have ERC20 tokens', withoutTokens: "If you don't have ERC20 tokens", yourBalance: 'Your balance', + notEnoughTokens: 'Perhaps there are not enough tokens', + enterPassForConfirm: 'Enter your password to confirm your decision.', + connectOuterGroupToProject: 'Connect an external group of participants to the "{{project}}" project', + noData: 'No data', + noDataAdmins: 'Unfortunately, no one can see the administrators. \nSuch is the secret design and limitation of the technologies used.', + weightVote: 'Weight vote', + page: 'Page', + goTo: 'Go to', + voterList: 'Voter list', + agree: 'Agree', + against: 'Against', + startANewVote: 'Start a new vote', + newVoteEmptyStateText: 'As soon as you select a question, all information on it will appear here.', + selectQuestionGroup: 'Select a question group to start creating a new question.', + createANewQuestion: 'Create a new question', + basicInfo: 'Basic information', + stepProgress: 'Step {{current}} from {{total}}', RUS: 'Русский', ENG: 'English', + start: 'Start', + end: 'End', + timeLeft: 'Time left', + dateInFormat: '{{date}} in {{time}}', + decision: 'Decision', + decisionIsMade: 'Decision is made', + dateOfApplication: 'Date of application', + durationInBlocks: 'Duration of circulation in blocks', + newAddressContract: 'New address contract', + votingFormula: 'Voting formula', + iAgree: 'I agree', + iAmAgainst: 'I\'m against', + statistics: 'Statistics', + didNotVote: 'Did not vote', + everyoneVoted: 'Everyone voted', + voteLaunchDescription: 'A vote will be launched to create a question; if the decision is positive, a question will be created', + voteLaunchAdminDescription: 'A vote will be launched among administrators to create a group, if the decision is positive, the group will be created', + createGroupQuestionsDescription: 'Project participants can then be divided into groups \n \nFor example: Designers who are only in the Design group will be able to vote on issues of only this group', + createNameForTheGroupQuestions: 'Come up with a name that best reflects the essence of the group', + endOfVoteRequired: 'End \nof vote \nrequired', + pros: 'Pros', + cons: 'Cons', + notAccepted: 'Not accepted', + votingInProgress: 'Voting in progress', + youVoted: 'You voted', + votingDone: 'Voting done', + decisionWasMade: 'The decision was made', + yourDecision: 'Your decision', + totalVoted: 'Total voted', + theVoteLasted: 'The vote lasted', + voting: 'Voting', + questions: 'Questions', + members: 'Members', + votingCompletedButTokensInContract: 'Voting is completed, but your tokens are still in contract.', + youVotedAndTokensInContract: 'You voted and your tokens are in the contract. To cancel the voice', + pickUpTokens: 'Pick up tokens', + sendingTransaction: 'Sending transaction', + parameters: 'Parameters', + select: 'Select', + hintFunctionalityNotAvailable: 'During active voting, this <1/> functionality is not available.', + outOf: 'out of', + clickOnAddressForCopy: 'Click on address for copy', + copied: 'Copied', + groups: 'Groups', + tokens: 'Tokens', + privateBalance: 'Private balance', + toggleUser: 'Toggle user', + returnTokensFirst: 'Return tokens first', + selectorNonexistentFunctionDescription: 'If you specify the selector of a nonexistent function, the voting results will not be applied', + erc20ListIsNotViewable: 'ERC20 tokens are arranged so \n that the voter list is not viewable', + votingListIsEmpty: 'No polls created <1/> They will be displayed here later', + noVotingFilterMatches: 'No voting matches <1/> the selected filter', + noQuestionsInThisGroup: 'No questions have been <1/> created in this group yet', + reloadNotification: 'Will apply on next launch', + loadingToggleDisabled: "While all data is not loaded, you can't change user", }; export default other; diff --git a/src/locales/RUS/buttons.js b/src/locales/RUS/buttons.js index 0dba007a..7713407b 100644 --- a/src/locales/RUS/buttons.js +++ b/src/locales/RUS/buttons.js @@ -17,5 +17,24 @@ const buttons = { withTokens: 'Подключить контракт и создать проект', withoutTokens: 'Создать новые токены и проект', toWallets: 'К выбору кошелька', + createQuestion: 'Создать вопрос', + createQuestionGroup: 'Создать группу вопросов', + startNewVoting: 'Начать новое голосование', + transfer: 'Перевести', + designateGroupAdministrator: 'начать голосование за назначение администратором группы', + vote: 'Голосовать', + startNewVote: 'Начать новое голосование', + start: 'Начать', + nextStep: 'Следующий шаг', + addParameter: '+ Добавить параметр', + apply: 'Применить', + completeTheVote: 'Завершить голосование', + pickUpTokens: 'заберите токены', + pickUpTokensCapital: 'Заберите токены', + clear: 'Понятно', + retry: 'Попробовать снова', + saveAndReload: 'Сохранить и перезапустить', + saveWithoutReload: 'Сохранить без перезагрузки', + }; export default buttons; diff --git a/src/locales/RUS/dialogs.js b/src/locales/RUS/dialogs.js index 4a7ff65b..ed2d0720 100644 --- a/src/locales/RUS/dialogs.js +++ b/src/locales/RUS/dialogs.js @@ -1,10 +1,16 @@ const dialog = { definetelyAgree: 'Вы точно согласны?', + definetelyReject: 'Вы точно против?', agreedMessage: 'Вы выразили согласие', rejectMessage: 'Вы проголосовали против', transferInProgress: 'Переводим токены', someTimeText: 'Это займет некоторое время', tokenTransferSuccess: 'Токены успешно переведены!', + tokenTransfer: 'Перевести токены', + tokenTransferError: 'Ошибка перевода', + createAGroupOfQuestions: 'Создать группу вопросов', + completionOfVoting: 'Завершение голосования', + ERC20TokensUsed: 'В этом голосовании\n используются токены ERC20', }; export default dialog; diff --git a/src/locales/RUS/errors.js b/src/locales/RUS/errors.js index 6c54fb3d..d782ecd2 100644 --- a/src/locales/RUS/errors.js +++ b/src/locales/RUS/errors.js @@ -6,5 +6,7 @@ const errors = { emptyFields: 'Вы ввели не все данные, пожалуйста, введите все данные', lowBalance: 'Ваш баланс слишком мал для этого действия', hostUnreachable: 'Соединение с хостом потеряно, проверьте доступ к интернету и повторите еще раз', + transferIfNotAdmin: 'Вы не можете отправлять токены с других кошельков', + transferLocked: 'Вы не можете отправлять кастомные токены', }; export default errors; diff --git a/src/locales/RUS/explanations.js b/src/locales/RUS/explanations.js index bb55eede..d05aa5f2 100644 --- a/src/locales/RUS/explanations.js +++ b/src/locales/RUS/explanations.js @@ -23,6 +23,7 @@ const explanations = { count: 'Общее число токенов задаете вы. В дальнейшем их можно будет распределить между участниками проекта', }, }, + freeze: 'При загрузке контракта приложение зависнет на несколько секунд, будьте терпеливы', }; export default explanations; diff --git a/src/locales/RUS/fields.js b/src/locales/RUS/fields.js index 81c14fb6..f0df92b2 100644 --- a/src/locales/RUS/fields.js +++ b/src/locales/RUS/fields.js @@ -8,6 +8,34 @@ const placeholders = { quantity: 'Количество', projectTitle: 'Придумайте название проекта', contractAddress: 'Введите адрес контракта', + address: 'Адрес кошелька', + countTokens: 'Количество токенов', + question: 'Вопрос', + status: 'Статус', + date: 'Дата', + descision: 'Решение', + durationInBlocks: 'Продолжительность тиража в блоках', + questionTitle: 'Название вопроса', + questionLifeTime: 'Время жизни вопроса', + methodSelector: 'Селектор функции', + parameter: 'Параметр', + enterNewParameterName: 'Введите название нового параметра', + votingFormula: 'Формула голосования', + questionDescription: 'Описание вопроса', + selectParameterType: 'Выберите тип параметра', + titleGroupQuestions: 'Название группы вопросов', + descriptionOrComment: 'Описание или комментарий', + dateFrom: 'Дата с', + dateTo: 'Дата по', + questionGroup: 'Группа вопросов', + targetContractAddress: 'Адрес целевого контракта', + functionSelector: 'Function selector', + nodeUrl: 'URL ноды', + chooseTheQuestion: 'Выберите вопрос', + minGasPrice: 'Цена газа MIN, Gwei', + maxGasPrice: 'Цена газа MAX, Gwei', + selectQuestionGroup: 'Выберите группу вопросов', + }; export default placeholders; diff --git a/src/locales/RUS/headings.js b/src/locales/RUS/headings.js index c8abc999..bee57029 100644 --- a/src/locales/RUS/headings.js +++ b/src/locales/RUS/headings.js @@ -1,12 +1,12 @@ const headings = { login: { heading: 'Вход в систему', subheading: 'Приготовьтесь к новой эре в сфере голосования' }, logging: { heading: 'Выполняется вход', subheading: 'Это не займет много времени' }, + projects: { heading: 'Выбор проекта', subheading: 'Выберите проект или создайте новый' }, + addingProject: { heading: 'Добавление проекта', subheading: ['Cоздайте новый или подключите', 'уже существующий'] }, passwordCreation: { heading: 'Создание пароля', subheading: ['Будет использоваться для входа в', 'кошелек и подтверждения транзакций'] }, showSeed: { heading: 'Резервная фраза', subheading: 'Нужна для восстановления пароля' }, seedCheck: { heading: 'Проверка резервной фразы', subheading: ['Проверяется фраза', 'Введите фразу, которую вы записали'] }, - projects: { heading: 'Выбор проекта', subheading: 'Выберите проект или создайте новый' }, - addingProject: { heading: 'Добавление проекта', subheading: ['Cоздайте новый или подключите', 'уже существующий'] }, - сonnectProject: { heading: 'Подключить проект', subheading: 'Подключите уже существующий проект' }, + connectProject: { heading: 'Подключить проект', subheading: 'Подключите уже существующий проект' }, projectChecking: { heading: 'Проверяем адрес проекта', subheading: 'Это не займет много времени' }, projectConnected: { heading: 'Проект успешно подключен!', subheading: 'Теперь можно начать работу с ним или выбрать другой проект' }, newProject: { heading: 'Создание нового проекта', subheading: 'Выберите подходящий вам вариант' }, @@ -22,5 +22,12 @@ const headings = { walletRestored: { heading: 'Восстановление кошелька', subheading: 'Кошелек успешно восстанолен' }, walletCreated: { heading: 'Создание кошелька', subheading: 'Кошелек успешно создан' }, walletRestoring: { heading: 'Процесс восстановления кошелька', subheading: ['Проверьте правильность данных', ' перед тем, как продолжить'] }, + nodeConnection: 'Подключение ноды', + creatingAndUpload: 'Создание или загрузка контрактов', + interfaceLanguage: 'Язык интерфейса', + sendingTransaction: 'Отправка транзакции', + successfullTransaction: 'Транзакция успешно отправлена', + failedTransaction: { heading: 'Ошибка отправки транзакции', subheading: 'Пожалуйста, повторите попытку' }, + other: 'Прочее', }; export default headings; diff --git a/src/locales/RUS/other.js b/src/locales/RUS/other.js index 6234e3ae..512cc5d6 100644 --- a/src/locales/RUS/other.js +++ b/src/locales/RUS/other.js @@ -6,6 +6,7 @@ const other = { sending: 'Отправка', txHash: 'Получение хэша', txReceipt: 'Получение чека', + txSigning: 'Подпись транзакции', questionsUploading: 'Загрузка вопросов', walletAddress: 'Кошелек', balance: 'Баланс', @@ -14,7 +15,79 @@ const other = { withTokens: 'Если есть токены ERC20', withoutTokens: 'Если токенов ERC20 нет', yourBalance: 'Ваш баланс', + notEnoughTokens: 'Возможно не хватает токенов', + enterPassForConfirm: 'Введите пароль, чтобы подтвердить свое решеине', + connectOuterGroupToProject: 'Подключить внешнюю группу участников к проекту "{{project}}"', + noData: 'Нет данных', + noDataAdmins: 'К сожалению никто не может увидеть администраторов. \nТаков тайный замысел и ограничение, используемых технологий.', + weightVote: 'Вес голоса', + page: 'Страница', + goTo: 'Перейти', + voterList: 'Список проголосавших ', + agree: 'Согласны', + against: 'Против', + startANewVote: 'Начать новое голосование', + newVoteEmptyStateText: 'Как только вы выберите вопрос здесь появится вся информация по нему', + selectQuestionGroup: 'Выберите группу вопросов, чтобы приступить к созданию нового вопроса', + createANewQuestion: 'Создать вопрос', + basicInfo: 'Основная информация', + stepProgress: 'Шаг {{current}} из {{total}}', RUS: 'Русский', ENG: 'English', + start: 'Начало', + end: 'Конец', + timeLeft: 'Осталось', + dateInFormat: '{{date}} в {{time}}', + decision: 'Решение', + decisionIsMade: 'Решение принято', + dateOfApplication: 'Дата применения', + durationInBlocks: 'Продолжительность тиража в блоках', + newAddressContract: 'Новый адрес контракта', + votingFormula: 'Формула голосования', + iAgree: 'Я согласен', + iAmAgainst: 'Я против', + statistics: 'Статистика', + didNotVote: 'Не проголосовали', + everyoneVoted: 'Проголосовали все', + voteLaunchDescription: 'Будет запущено голосование по созданию вопроса, при положительном решении вопрос будет создан', + voteLaunchAdminDescription: 'Будет запущено голосование среди администраторов по созданию группы, при положительном решении группа будет создана', + createGroupQuestionsDescription: 'Участников проекта затем можно распределить по группам \n \nНапример: Дизайнеры, находящиеся только в группе “Дизайн” смогут голосовать по вопросам только этой группы', + createNameForTheGroupQuestions: 'Придумайте название, которое лучше всего отражает суть группы', + endOfVoteRequired: 'Требуется \nзавершение \nголосования', + pros: 'За', + cons: 'Против', + notAccepted: 'Не принято', + votingInProgress: 'Идет голосование', + youVoted: 'Вы проголосовали', + votingDone: 'Голосование проведено', + decisionWasMade: 'Принято решение', + yourDecision: 'Ваш голос', + totalVoted: 'Проголосовало', + theVoteLasted: 'Голосование длилось', + voting: 'Голосования', + questions: 'Вопросы', + members: 'Участники', + votingCompletedButTokensInContract: 'Голосование завершено, но ваши токены все еще находятся в контракте.', + youVotedAndTokensInContract: 'Вы проголосовали и ваши токены находятся в контракте. Для отмены голоса', + pickUpTokens: 'Забрать токены', + sendingTransaction: 'Отправка транзакции', + parameters: 'Параметры', + select: 'Выберите', + hintFunctionalityNotAvailable: 'Во время активного голосования <1/> этот функционал недоступен', + outOf: 'из', + clickOnAddressForCopy: 'Нажмите на адрес кошелька, чтобы скопировать', + copied: 'Скопировано', + groups: 'Группы', + tokens: 'Токены', + privateBalance: 'Личный баланс', + toggleUser: 'Сменить пользователя', + returnTokensFirst: 'Сначала верните токены', + selectorNonexistentFunctionDescription: 'Если укажете селектор несуществующей функции, то результаты голосования не будут применены', + erc20ListIsNotViewable: 'ERC20 токены устроены так, что список\n проголосовавших недоступен для просмотра', + votingListIsEmpty: 'Не создано ни одного голосования <1/> В дальнейшем они будут отображены здесь', + noVotingFilterMatches: 'Выбранному фильтру не <1/> соответствует ни одно голосование', + noQuestionsInThisGroup: 'В этой группе пока не <1/> создано ни одного вопроса', + reloadNotificaion: 'Применятся при следующем запуске', + loadingToggleDisabled: 'Вы не можете сменить пользователя, пока не загруженны данные', }; export default other; diff --git a/src/models/FormModel/index.js b/src/models/FormModel/index.js index f1e4bd9a..316ce621 100644 --- a/src/models/FormModel/index.js +++ b/src/models/FormModel/index.js @@ -2,15 +2,28 @@ import { extendObservable, action } from 'mobx'; import { Form } from 'mobx-react-form'; import dvr from 'mobx-react-form/lib/validators/DVR'; import plugins from '../../utils/Validator'; +import i18n from '../../i18n'; +import { languages } from '../../constants'; class ExtendedForm extends Form { constructor(data) { const { hooks } = data || {}; super(); extendObservable(this, { loading: false }); + Object.keys(hooks).forEach((hook) => { this.addHook(hook, hooks[hook]); }); + + this.addHook('onLangChange', () => { + this.fields.forEach((field) => { + field.set('placeholder', i18n.t(`fields:${field.label}`)); + }); + }); + window.ipcRenderer.on('change-language:confirm', (event, value) => { + this.fireHook('onLangChangeHook'); + window.validator.useLang(languages[value]); + }); } // eslint-disable-next-line class-methods-use-this diff --git a/src/services/ContractService/ContractService.js b/src/services/ContractService/ContractService.js index dd2a68e0..eaed616f 100644 --- a/src/services/ContractService/ContractService.js +++ b/src/services/ContractService/ContractService.js @@ -1,12 +1,17 @@ +/* eslint-disable no-console */ /* eslint-disable no-unused-vars */ -import browserSolc from 'browser-solc'; -import { BN } from 'ethereumjs-util'; -import { SOL_IMPORT_REGEXP, SOL_PATH_REGEXP, SOL_VERSION_REGEXP } from '../../constants'; +import * as linker from 'solc/linker'; +import { compile } from 'zeroone-translator'; +import { + SOL_IMPORT_REGEXP, + SOL_VERSION_REGEXP, + tokenTypes, +} from '../../constants'; import { fs, PATH_TO_CONTRACTS, path, } from '../../constants/windowModules'; -import Question from './entities/Question'; import readSolFile from '../../utils/fileUtils/index'; +import UserStore from '../../stores/UserStore/UserStore'; /** * Class for work with contracts @@ -15,12 +20,25 @@ class ContractService { constructor(rootStore) { this._contract = {}; this.rootStore = rootStore; - this.sysQuestions = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './sysQuestions.json'), 'utf8')); - this.ercAbi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ERC20.abi'))); + const pathToQuestions = path.join(PATH_TO_CONTRACTS, './sysQuestions.json'); + const pathToErcAbi = path.join(PATH_TO_CONTRACTS, './ERC20.abi'); + + try { + this.sysQuestions = JSON.parse(fs.readFileSync(pathToQuestions), 'utf8'); + } catch (e) { + alert(`Error while reading file ${pathToQuestions}, please check this.`); + } + + try { + this.ercAbi = JSON.parse(fs.readFileSync(pathToErcAbi)); + } catch (e) { + alert(`Error while reading file ${pathToErcAbi}, please check this.`); + } } /** * sets instance of contract to this._contract + * * @param {object} instance instance of contract created by Web3Service */ // eslint-disable-next-line consistent-return @@ -32,56 +50,90 @@ class ContractService { /** * compiles contracts and returning type of compiled contract, bytecode & abi + * * @param {string} type - ERC20 - if compiling ERC20 token contract, project - if project contract + * @param {string} password password * @returns {object} contains type of compiled contract, his bytecode and abi for deploying */ - compileContract(type) { + // eslint-disable-next-line class-methods-use-this + compileContract(type, password) { + const { rootStore: { Web3Service, userStore } } = this; + const { address } = userStore; + window.linker = linker; return new Promise((resolve, reject) => { - window.BrowserSolc.getVersions((sources, releases) => { - const version = releases['0.4.24']; - const contract = this.combineContract(type); - const contractName = type === 'ERC20' - ? ':ERC20' - : ':Voter'; - window.BrowserSolc.loadVersion(version, (compiler) => { - const compiledContract = compiler.compile(contract); - const contractData = compiledContract.contracts[contractName]; - if (contractData.interface !== '') { - const { bytecode, metadata } = contractData; - const { output: { abi } } = JSON.parse(metadata); - resolve({ type, bytecode, abi }); - } else reject(new Error('Something went wrong on contract compiling')); - }); + let bytecode; + let abi; + + const contract = this.combineContract(type); + window.ipcRenderer.send('compile-request', { contract, type }); + window.ipcRenderer.once('contract-compiled', async (event, compiledContract) => { + console.log(compiledContract); + if (type === 'ZeroOne') { + const { ZeroOne, ZeroOneVM } = compiledContract; + const { evm: { bytecode: { object: libraryBytecode } } } = ZeroOneVM; + const { evm: { bytecode: { object: ZeroOneBytecode } }, abi: ZeroOneABI } = ZeroOne; + const libTX = { data: `0x${libraryBytecode}` }; + await Web3Service.createTxData(address, libTX) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then(({ contractAddress }) => { + console.log(`library at ${contractAddress}`); + abi = ZeroOneABI; + const [link] = Object.keys(linker.findLinkReferences(ZeroOneBytecode)); + bytecode = linker.linkBytecode(ZeroOneBytecode, { [link]: contractAddress }); + }); + } else if (compiledContract.abi !== '') { + const { evm: { bytecode: { object } }, abi: contractAbi } = compiledContract; + abi = contractAbi; + bytecode = object; + } else reject(new Error('Something went wrong on contract compiling')); + + fs.writeFileSync(path.join(PATH_TO_CONTRACTS, `${type}.abi`), JSON.stringify(abi, null, '\t')); + resolve({ type, bytecode, abi }); }); }); } /** * reading all imports in main contract file and importing all files in one output file + * * @param {string} type type of project - ERC20 for ERC-20 tokens, Project for project contract * @returns {string} combined contracts */ // eslint-disable-next-line class-methods-use-this combineContract(type) { - const dir = type === 'ERC20' ? './' : './Voter/'; - const compiler = 'pragma solidity ^0.4.24;'; - const pathToMainFile = type === 'ERC20' - ? path.join(PATH_TO_CONTRACTS, `${dir}ERC20.sol`) - : path.join(PATH_TO_CONTRACTS, `${dir}Voter.sol`); + let dir; + const compiler = 'pragma solidity 0.6.1;'; + switch (type) { + case ('ERC20'): + dir = '../../node_modules/zeroone-contracts/contracts/__vendor__/'; + break; + case ('CustomToken'): + dir = '../../node_modules/zeroone-contracts/contracts/Token/'; + break; + case ('ZeroOne'): + dir = '../../node_modules/zeroone-contracts/contracts/ZeroOne/'; + break; + default: + break; + } + const pathToMainFile = path.join(PATH_TO_CONTRACTS, `${dir}${type}.sol`); const importedFiles = {}; let output = readSolFile(pathToMainFile, importedFiles); - output = output.replace(SOL_VERSION_REGEXP, compiler); - output = output.replace(/(calldata)/g, ''); + output = output.replace(SOL_VERSION_REGEXP, compiler).replace((SOL_IMPORT_REGEXP), ''); + // output = output.replace(/(calldata)/g, ''); return output; } /** - * Sendind transaction with contract to blockchain + * Sending transaction with contract to blockchain + * * @param {object} params parameters for deploying - * @param {array} params.deployArgs ERC20 - [Name, Symbol, Count], Project - [tokenAddress] + * @param {Array} params.deployArgs ERC20 - [Name, Symbol, Count], Project - [tokenAddress] * @param {string} params.bytecode bytecode of contract * @param {JSON} params.abi JSON interface of contract * @param {string} params.password password of user wallet @@ -92,31 +144,36 @@ class ContractService { }) { const { rootStore: { Web3Service, userStore } } = this; const { address } = userStore; - const maxGasPrice = 30000000000; const contract = Web3Service.createContractInstance(abi); - const txData = contract.deploy({ + const data = contract.deploy({ data: `0x${bytecode}`, arguments: deployArgs, }).encodeABI(); + const tx = { - data: txData, - gasLimit: 8000000, - gasPrice: maxGasPrice, + data, + from: userStore.address, + value: '0x0', }; - return new Promise((resolve) => { - Web3Service.createTxData(address, tx, maxGasPrice) + return new Promise((resolve, reject) => { + Web3Service.createTxData(address, tx) .then((formedTx) => userStore.singTransaction(formedTx, password)) .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) - .then((txHash) => resolve(txHash)); + .then((txHash) => { + userStore.getEthBalance(); + resolve(txHash); + }) + .catch((err) => reject(err)); }); } /** * checks erc20 tokens contract on totalSupply and symbol + * * @param {string} address address of erc20 contract - * @return {object} {totalSypply, symbol} + * @returns {object} {totalSypply, symbol} */ async checkTokens(address) { const { rootStore: { Web3Service }, ercAbi } = this; @@ -129,117 +186,397 @@ class ContractService { /** * checks is the address of contract + * * @param {string} address address of contract - * @return {Promise} Promise with function which resolves, if address is contract + * @returns {Promise} Promise with function which resolves, if address is contract */ - // eslint-disable-next-line class-methods-use-this checkProject(address) { const { rootStore: { Web3Service } } = this; return new Promise((resolve, reject) => { - Web3Service.web3.eth.getCode(address).then((bytecode) => { - if (bytecode === '0x') reject(); - resolve(bytecode); - }); + const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'))); + const contract = Web3Service.createContractInstance(abi); + contract.options.address = address; + contract.methods.getQuestionGroupsAmount().call() + .then((data) => resolve()) + .catch((err) => reject(err)); }); } /** * calling contract method + * * @param {string} method method, which will be called - * @param {string} from address of caller - * @param params parameters for method + * @param {any} params parameters for method + * @returns {object} data from method */ async callMethod(method, ...params) { const data = await this._contract.methods[method](...params).call(); return data; } + // TODO add correct js doc + /** + * Method create data for voting + * + * @returns {object} voting data + * @param votingQuestion + * @param votingGroupId + * @param votingData + */ + createVotingData(votingQuestion, votingGroupId, votingData) { + const { rootStore: { userStore, Web3Service: { web3: { eth: { abi } } } }, _contract } = this; + const votingInfo = { + starterGroupId: votingGroupId, + endTime: 0, + starterAddress: userStore.address, + questionId: votingQuestion, + data: votingData, + }; + // eslint-disable-next-line max-len + const data = { + // eslint-disable-next-line max-len + data: _contract.methods.startVoting(votingInfo).encodeABI(), + from: userStore.address, + value: '0x0', + to: _contract.options.address, + }; + return data; + } + /** * checks count of uploaded to contract questions and total count of system questions + * * @function * @returns {object} {countOfUploaded, totalCount} */ async checkQuestions() { - const countOfUploaded = await this._contract.methods.getCount().call(); + const countOfUploaded = await this._contract.methods.getQuestionsAmount().call(); const totalCount = Object.keys(this.sysQuestions).length; return ({ countOfUploaded, totalCount }); } /** * send question to created contract + * * @param {number} idx id of question; - * @return {Promise} Promise, which resolves on transaction hash + * @returns {Promise} Promise, which resolves on transaction hash */ async sendQuestion(idx) { + console.log(`question id = ${idx}`); + const { _contract: contract, rootStore } = this; const { Web3Service, userStore, - } = this.rootStore; - const sysQuestion = this.sysQuestions[idx]; - await this.fetchQuestion(idx).then((result) => { - if (result.caption === '') { - const { address, password } = userStore; - const question = new Question(sysQuestion); - const contractAddr = this._contract.options.address; - const params = question.getUploadingParams(contractAddr); - - const dataTx = this._contract.methods.saveNewQuestion(...params).encodeABI(); - - const maxGasPrice = 30000000000; - const rawTx = { - to: contractAddr, - data: dataTx, - gasLimit: 8000000, - value: '0x0', - }; - - return new Promise((resolve) => { - Web3Service.createTxData(address, rawTx, maxGasPrice) - .then((formedTx) => userStore.singTransaction(formedTx, password)) - .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) - .then((txHash) => resolve(txHash)); + } = rootStore; + const question = this.sysQuestions[idx]; + const owners = await contract.methods.getUserGroup(0).call(); + + const { address, password } = userStore; + const contractAddr = contract.options.address; + question.target = contractAddr; + question.rawFormula = question.rawFormula.replace('%s', owners.groupAddress); + question.formula = compile(question.rawFormula); + question.active = true; + + console.log(question); + const dataTx = contract.methods.addQuestion(question).encodeABI(); + console.log(dataTx); + const rawTx = { + to: contractAddr, + data: dataTx, + value: '0x0', + }; + + return new Promise((resolve) => { + Web3Service.createTxData(address, rawTx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then((receipt) => { + userStore.getEthBalance(); + resolve(receipt); }); - } - return Promise.reject(); }); } /** * Fetching one question from contract + * * @param {number} id id of question - * @returns {Object} Question data from contract + * @returns {object} Question data from contract */ fetchQuestion(id) { - return this.callMethod('question', [id]); + return this.callMethod('getQuestion', id); } /** * getting one voting + * * @param {number} id id of voting - * @param {string} from address who calls method + * @returns {object} Voting data */ async fetchVoting(id) { - return this.callMethod('getVoting', [id]); + return this.callMethod('getVoting', id); } + /** * getting votes weights for voting + * * @param {number} id id of voting - * @param {string} from address, who calls + * @returns {object} Voting stats data + * @deprecated */ + // TODO delete me after async fetchVotingStats(id) { return this.callMethod('getVotingStats', [id]); } + /** + * Fetch length of usergroups in contract + * + * @returns {number} amount groups + */ + fetchUserGroupsLength() { + return this._contract.methods.getUserGroupsAmount().call(); + } + /** * Starting the voting - * @param {id} id id of question + * + * @param {string|number} id id of question * @param {string} from address, who starts - * @param params parameters of voting + * @param {any} params parameters of voting + * @returns {Promise} promise + * @deprecated */ + // TODO delete me after async sendVotingStart(id, from, params) { return (this, id, from, params); } + /** + * creates transaction for sending decision about voting + * + * @param {number} votingId voting + * @param {number} decision 0 - negative, 1 - positive + * @returns {Promise} promise + */ + // eslint-disable-next-line consistent-return + async sendVote(votingId, decision) { + const { + ercAbi, + _contract, + rootStore: { + appStore, + Web3Service, + userStore, + membersStore, + projectStore: { + historyStore, + questionStore, + }, + }, + } = this; + appStore.setTransactionStep('compileOrSign'); + const [voting] = historyStore.getVotingById(votingId); + const { allowedGroups } = voting; + const { length: groupsLength } = allowedGroups; + + const data = _contract.methods.setVote(decision).encodeABI(); + + for (let i = 0; i < groupsLength; i += 1) { + const group = membersStore.getMemberGroupByAddress(allowedGroups[i]); + if (group.groupType === tokenTypes.ERC20) { + console.log('approving'); + // eslint-disable-next-line no-await-in-loop + await this.approveErc(group); + console.log('approved'); + } + } + + const tx = { + from: userStore.address, + to: _contract.options.address, + value: '0x0', + data, + }; + + // eslint-disable-next-line consistent-return + return new Promise((resolve, reject) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then((rec) => { + appStore.setTransactionStep('success'); + historyStore.updateVotingById({ + id: votingId, + newState: { + userVote: Number(decision), + }, + }); + for (let i = 0; i < groupsLength; i += 1) { + const group = membersStore.getMemberGroupByAddress(allowedGroups[i]); + group.updateUserBalance(); + } + userStore.getEthBalance(); + resolve(rec); + }) + .catch((err) => reject(err))); + } + + closeVoting() { + const { + _contract, + rootStore: { + Web3Service, + userStore, + contractService, + appStore, + }, + } = this; + + const tx = { + from: userStore.address, + data: _contract.methods.submitVoting().encodeABI(), + value: '0x0', + to: _contract.options.address, + }; + + console.log('sending TX'); + appStore.setTransactionStep('compileOrSign'); + return Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + userStore.getEthBalance(); + }); + } + + /** + * Method for start voting + * + * @returns {Promise} promise + * @deprecated + */ + // startVoting(questionId, params) { + // const { + // _contract, + // rootStore: { + // projectStore: { questionStore }, + // Web3Service, + // userStore, + // }, + // } = this; + // const [question] = questionStore.getQuestionById(questionId); + // const { parameters } = question; + // const data = Web3Service.web3.eth.abi.encodeParameters(parameters, params); + // const votingData = (data).replace('0x', question.methodSelector); + // const votingInfo = { + // starterGroupId: 0, + // endTime: 0, + // starterAddress: userStore.address, + // questionId, + // data: votingData, + // }; + // console.log('votingInfo', votingInfo); + // const tx = { + // data: _contract.methods.startVoting(votingInfo).encodeABI(), + // from: userStore.address, + // to: _contract.options.address, + // value: '0x0', + // }; + // console.log('tx', tx); + // return Web3Service.createTxData(userStore.address, tx) + // .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + // .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + // .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + // .then(() => { + // userStore.getEthBalance(); + // }); + // } + + returnTokens() { + const { + _contract, + rootStore: { + Web3Service, + userStore, + membersStore, + projectStore: { + historyStore, + questionStore, + }, + }, + } = this; + const data = _contract.methods.returnTokens().encodeABI(); + const tx = { + from: userStore.address, + to: _contract.options.address, + value: '0x0', + data, + }; + return Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then(async (rec) => { + userStore.getEthBalance(); + const lastVotingId = await _contract.methods.findLastUserVoting().call(); + const [voting] = historyStore.getVotingById(Number(lastVotingId)); + const { questionId } = voting; + const [question] = questionStore.getQuestionById(Number(questionId)); + const { groupId } = question; + const [group] = membersStore.getMemberById(Number(groupId)); + group.updateUserBalance(); + }); + } + + /** + * approve token transfer from user to Voter contract + * + * @param {object} group group instance + */ + approveErc(group) { + const { + ercAbi, + _contract, + rootStore: { + Web3Service, + userStore, + }, + } = this; + const ercContract = Web3Service.createContractInstance(ercAbi); + ercContract.options.address = group.wallet; + // eslint-disable-next-line max-len + const txData = ercContract.methods.approve(_contract.options.address, userStore.address).encodeABI(); + const tx = { + data: txData, + from: userStore.address, + value: '0x0', + to: group.wallet, + }; + return Web3Service.createTxData(userStore.address, tx) + .then((createdTx) => userStore.singTransaction(createdTx, userStore.password)) + .then((formedTx) => Web3Service.sendSignedTransaction(`0x${formedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then(() => { + userStore.getEthBalance(); + }); + } + /** * Finishes the voting */ diff --git a/src/services/ContractService/entities/Question.js b/src/services/ContractService/entities/Question.js index a8a792c6..ecff0eb8 100644 --- a/src/services/ContractService/entities/Question.js +++ b/src/services/ContractService/entities/Question.js @@ -1,8 +1,17 @@ -import web3 from 'web3'; +import { compile } from 'zeroone-translator'; class Question { constructor({ - id, group, name, caption, time, method, formula, parameters, + id, + group, + name, + caption, + time, + method, + formula, + parameters, + paramTypes, + paramNames, }) { this.id = id; this.group = group; @@ -10,42 +19,44 @@ class Question { this.caption = caption; this.time = time; this.method = method; - this.formula = formula; + this.rawFormula = formula; this.parameters = parameters; - } - - /** - * convert simple formula of system question for contract - * @param {string} formula text implimentation of formula - * @returns {array} numeric implimentation of formula for smart contract - */ - getFormulaForContract() { - const FORMULA_REGEXP = new RegExp(/(group)|((?:[a-zA-Z0-9]{1,}))|((quorum|positive))|(>=|<=)|([0-9%]{1,})|(quorum|all)/g); - const matched = this.formula.match(FORMULA_REGEXP); - const convertedFormula = []; - convertedFormula.push(matched[0] === 'group' ? 0 : 1); - convertedFormula.push(matched[1] === 'Owners' ? 1 : 2); - convertedFormula.push(matched[3] === 'quorum' ? 0 : 1); - convertedFormula.push(matched[4] === '<=' ? 0 : 1); - convertedFormula.push(Number(matched[5])); - if (matched.length === 9) { - convertedFormula.push(matched[8] === 'quorum' ? 0 : 1); - } - return convertedFormula; + this.paramTypes = paramTypes; + this.paramNames = paramNames; + this.formula = compile(formula); } /** * getting formed parameters for contract + * * @param {string} contractAddr address of target contract - * @returns {array} formed data for encoding transaction + * @returns {Array} formed data for encoding transaction */ getUploadingParams(contractAddr) { const { - id, group, name, caption, time, method, parameters, + name, + caption, + group, + time, + paramNames, + paramTypes, + method, + rawFormula, + formula, } = this; - const convertedFormula = this.getFormulaForContract(); - const params = parameters.map((param) => web3.utils.utf8ToHex(param)); - return [[id, group, time], 0, name, caption, contractAddr, method, convertedFormula, params]; + return [ + true, + name, + caption, + group, + time, + paramNames, + paramTypes, + contractAddr, + method, + rawFormula, + formula, + ]; } } export default Question; diff --git a/src/services/EventEmitterService/index.js b/src/services/EventEmitterService/index.js index e60b8ee7..01a5076d 100644 --- a/src/services/EventEmitterService/index.js +++ b/src/services/EventEmitterService/index.js @@ -12,6 +12,10 @@ class EventEmitterService { if (!this.events[event]) this.events[event] = []; this.events[event].push(callback); } + + off(event) { + delete this.events[event]; + } } export default EventEmitterService; diff --git a/src/services/Web3Service/Web3Service.js b/src/services/Web3Service/Web3Service.js index 634a11bc..eeb99b65 100644 --- a/src/services/Web3Service/Web3Service.js +++ b/src/services/Web3Service/Web3Service.js @@ -6,7 +6,9 @@ import { BN } from 'ethereumjs-util'; */ class Web3Service { /** - * @constructor + * @class + * @param url + * @param rootStore * @param {string} provider - provider for this.web3 */ constructor(url, rootStore) { @@ -19,36 +21,38 @@ class Web3Service { return new Contract(abi); } - createTxData(address, tx, maxGasPrice) { - const { web3: { eth } } = this; + createTxData(address, tx) { + const { web3: { eth }, rootStore } = this; + // eslint-disable-next-line no-unused-vars + const { configStore: { MIN_GAS_PRICE, MAX_GAS_PRICE, GAS_LIMIT: gasLimit } } = rootStore; let transaction = { ...tx }; return eth.getTransactionCount(address, 'pending') .then((nonce) => { transaction = { ...tx, nonce }; - return eth.estimateGas(tx); + return eth.estimateGas(transaction); }) .then((gas) => { - if (!maxGasPrice) return (Promise.resolve(gas)); + transaction = { ...transaction, gas }; return this.getGasPrice() .then((gasPrice) => { - const minGasPrice = 10000000000; const gp = new BN(gasPrice); - const minGp = new BN(minGasPrice); - const maxGp = new BN(maxGasPrice); + const minGp = new BN(MIN_GAS_PRICE); + const maxGp = new BN(MAX_GAS_PRICE); transaction.gasPrice = (gp.gte(minGp) && gp.lte(maxGp)) ? gasPrice - : minGasPrice; + : MIN_GAS_PRICE; return Promise.resolve(transaction.gasPrice); }) .catch(Promise.reject); }) // eslint-disable-next-line no-unused-vars - .then((gasPrice) => (transaction)) + .then((gasPrice) => ({ ...transaction, gasPrice })) .catch((err) => Promise.reject(err)); } /** * getting gas price + * * @returns {number} gasPrice from network */ getGasPrice() { @@ -58,8 +62,10 @@ class Web3Service { /** * Sending transaction to contract + * + * @param rawTx * @param {string} txData Raw transaction (without 0x) - * @return {Promise} promise with web3 transaction PromiEvent + * @returns {Promise} promise with web3 transaction PromiEvent */ sendSignedTransaction(rawTx) { const { web3: { eth: { sendSignedTransaction } } } = this; @@ -76,8 +82,9 @@ class Web3Service { /** * checking transaction receipt by hash every 5 seconds + * * @param {string} txHash hash of transaction - * @return {Promise} Promise which resolves on successful receipt fetching + * @returns {Promise} Promise which resolves on successful receipt fetching */ subscribeTxReceipt(txHash) { const { web3 } = this; diff --git a/src/services/models/FormModel/index.js b/src/services/models/FormModel/index.js deleted file mode 100644 index f1e4bd9a..00000000 --- a/src/services/models/FormModel/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import { extendObservable, action } from 'mobx'; -import { Form } from 'mobx-react-form'; -import dvr from 'mobx-react-form/lib/validators/DVR'; -import plugins from '../../utils/Validator'; - -class ExtendedForm extends Form { - constructor(data) { - const { hooks } = data || {}; - super(); - extendObservable(this, { loading: false }); - Object.keys(hooks).forEach((hook) => { - this.addHook(hook, hooks[hook]); - }); - } - - // eslint-disable-next-line class-methods-use-this - plugins() { - return { dvr: dvr(plugins.dvr) }; - } - - hooks() { - const $this = this; - return { - onSubmit: (form) => { - $this.setLoading(true); - $this.fireHook('onSubmitHook', form); - // Trigger hide mobile keyboard - document.activeElement.blur(); - }, - onSuccess: (form) => { - const promise = $this.fireHook('onSuccessHook', form); - promise - .finally(() => { - $this.setLoading(false); - }); - }, - onError: (form) => { - $this.setLoading(false); - $this.fireHook('onErrorHook', form); - }, - }; - } - - @action addHook(hook, fn) { - this[`${hook}Hook`] = fn; - } - - @action setLoading(status) { - this.loading = status; - } - - fireHook(hook, form) { - const fire = this[hook]; - if (fire && typeof fire === 'function') { - return fire(form); - } - return null; - } -} - -export default ExtendedForm; diff --git a/src/splash.html b/src/splash.html new file mode 100644 index 00000000..70335ba8 --- /dev/null +++ b/src/splash.html @@ -0,0 +1,142 @@ + + + + + + ZeroOne + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/stores/AppStore/AppStore.js b/src/stores/AppStore/AppStore.js index 27400adc..6130f521 100644 --- a/src/stores/AppStore/AppStore.js +++ b/src/stores/AppStore/AppStore.js @@ -1,7 +1,8 @@ import { observable, action, computed } from 'mobx'; import { - fs, path, PATH_TO_WALLETS, ROOT_DIR, PATH_TO_CONTRACTS, + fs, path, PATH_TO_WALLETS, PATH_TO_CONTRACTS, PATH_TO_CONFIG, } from '../../constants/windowModules'; +import { transactionSteps } from '../../constants'; import Alert from './entities/Alert'; class AppStore { @@ -11,6 +12,8 @@ class AppStore { @observable projectList = []; + @observable transactionStep = 0; + @observable ERC = { }; @@ -27,13 +30,25 @@ class AppStore { @observable userInProject = false; + @observable _projectAddress = ''; + constructor(rootStore) { this.rootStore = rootStore; this.readWalletList(); } + /** + * set current transaction step for displaying if in modal + * + * @param {string} step key of step + */ + @action setTransactionStep(step) { + this.transactionStep = transactionSteps[step]; + } + /** * Getting list of url's for sending this to wallet service + * * @function */ @action readWalletList() { @@ -42,17 +57,23 @@ class AppStore { const files = fs.readdirSync(PATH_TO_WALLETS); files.forEach((file) => { - const wallet = JSON.parse(fs.readFileSync(path.join(PATH_TO_WALLETS, file), 'utf8')); - const walletObject = {}; - eth.getBalance(wallet.address) - .then((balance) => { this.balances[wallet.address] = utils.fromWei(balance); }); - walletObject[wallet.address] = wallet; - this.walletList = Object.assign(this.walletList, walletObject); + let wallet; + try { + wallet = JSON.parse(fs.readFileSync(path.join(PATH_TO_WALLETS, file), 'utf8')); + const walletObject = {}; + eth.getBalance(wallet.address) + .then((balance) => { this.balances[wallet.address] = utils.fromWei(balance); }); + walletObject[wallet.address] = wallet; + this.walletList = Object.assign(this.walletList, walletObject); + } catch { + alert(`Error occuried on reaing file ${path.join(PATH_TO_WALLETS, file)}. Please check it.`); + } }); } /** * selecting encrypted wallet and pushing this to userStore + * * @param {string} address address of wallet */ selectWallet = (address) => { @@ -63,31 +84,41 @@ class AppStore { /** * Reading list of projects for displaing them in project list + * * @function */ @action readProjectList() { - const config = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, './config.json'))); + const config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG)); this.projectList = config.projects; } /** * compile contract by given type and arguments + * * @param {string} type type of contract - ERC20 for erc tokens, project - for project contract + * @param deployArgs + * @param password * @returns {Promise} Function which compiles contract and deploy contract to network on success */ @action deployContract(type, deployArgs, password) { const { contractService } = this.rootStore; - return new Promise((resolve) => { - contractService.compileContract(type) - .then(({ bytecode, abi }) => contractService.deployContract({ - type, deployArgs, bytecode, abi, password, - })) - .then((txhash) => resolve(txhash)); + return new Promise((resolve, reject) => { + this.setTransactionStep('compileOrSign'); + contractService.compileContract(type, password) + .then(({ bytecode, abi }) => { + this.setTransactionStep('sending'); + return contractService.deployContract({ + type, deployArgs, bytecode, abi, password, + }); + }) + .then((txhash) => resolve(txhash)) + .catch((err) => reject(err)); }); } /** * checks given address on ERC20 tokens + * * @param {string} address address of ERC20 contract * @returns {Promise} resolves on success checking and set information about ERC token */ @@ -107,33 +138,33 @@ class AppStore { /** * checks if given address is contract in network + * * @param {string} address address, which will be ckecked on contract instance */ @action checkProject(address) { const { contractService } = this.rootStore; return contractService.checkProject(address) - .then((data) => { - Promise.resolve(data); - }) - .catch(() => { Promise.reject(); }); + .then((data) => Promise.resolve(data)) + .catch((e) => Promise.reject(e)); } /** * Upload questions to created project + * * @param {string} address address of smart contract, where will be uploaded questions - * @return Promise.resolve() + * @returns Promise.resolve() */ @action async deployQuestions(address) { const { Web3Service, contractService } = this.rootStore; - const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './Voter.abi'), 'utf8')); + const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'), 'utf8')); const contract = Web3Service.createContractInstance(abi); contract.options.address = address; contractService.setContract(contract); const { countOfUploaded, totalCount } = await contractService.checkQuestions(); this.countOfQuestions = Number(totalCount); this.uploadedQuestion = Number(countOfUploaded); - let idx = Number(countOfUploaded) === 0 ? 1 : Number(countOfUploaded); - for (idx; idx <= totalCount; idx += 1) { + let idx = Number(countOfUploaded) === 0 ? 0 : Number(countOfUploaded); + for (idx; idx < totalCount; idx += 1) { // eslint-disable-next-line no-await-in-loop await contractService.sendQuestion(idx); this.uploadedQuestion += 1; @@ -143,25 +174,73 @@ class AppStore { /** * add project to config and update config saved in file + * * @param {object} data data about project {name, address} */ // eslint-disable-next-line class-methods-use-this @action addProjectToList(data) { - const config = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, './config.json'), 'utf8')); + const config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG, 'utf8')); config.projects.push(data); - fs.writeFileSync(path.join(ROOT_DIR, './config.json'), JSON.stringify(config, null, '\t')); + fs.writeFileSync(PATH_TO_CONFIG, JSON.stringify(config, null, '\t')); + } + + /** + * checks count of uploaded Questions + * + * @param {string} address address of project + * @returns {boolean} countOfUploaded > totalQuestionCount + */ + async checkIsQuestionsUploaded(address) { + const { Web3Service, contractService } = this.rootStore; + const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'), 'utf8')); + const contract = Web3Service.createContractInstance(abi); + contract.options.address = address; + contractService.setContract(contract); + const { countOfUploaded, totalCount } = await contractService.checkQuestions(); + return countOfUploaded >= totalCount; + } + + // eslint-disable-next-line class-methods-use-this + parseFormula(rawFormula) { + if (!rawFormula || !rawFormula.length) return ''; + const f = rawFormula.map((text) => Number(text)); + const r = []; + let ready = '( )'; + r.push(f[0] === 0 ? 'group( ' : 'user(0x298e231fcf67b4aa9f41f902a5c5e05983e1d5f8) => condition('); + r.push(f[1] === 1 ? 'Owner) => condition(' : 'Custom) => condition('); + r.push(f[2] === 0 ? 'quorum ' : 'positive'); + r.push(f[3] === 0 ? ' <= ' : ' >= '); + + if (f.length === 6) { + r.push(`${f[4]} %`); + r.push(f[5] === 0 ? ' of quorum)' : ' of all)'); + } else { + r.push(`${f[4]} % )`); + } + const formula = r.join(''); + ready = ready.replace(' ', formula); + return ready; } /** * Check transaction receipt + * * @param {string} hash Transaction hash - * @return {Promise} Promise with interval, which resolves on succesfull receipt recieving + * @returns {Promise} Promise with interval, which resolves on succesfull receipt recieving */ @action checkReceipt(hash) { const { Web3Service } = this.rootStore; return Web3Service.subscribeTxReceipt(hash); } + // eslint-disable-next-line class-methods-use-this + nodeChange(url) { + const config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG, 'utf8')); + config.host = url; + fs.writeFileSync(PATH_TO_CONFIG, JSON.stringify(config, null, '\t'), 'utf8'); + window.location.reload(); + } + /** * * @param {string} text error text @@ -186,6 +265,10 @@ class AppStore { return this.userInProject; } + @computed get projectAddress() { + return this._projectAddress; + } + @action setProjectName(value) { this.name = value; } @@ -193,6 +276,30 @@ class AppStore { @action setDeployArgs(value) { this.deployArgs = value; } + + @action gotoProject({ address, name }) { + const { rootStore } = this; + this.setProjectAddress(address); + rootStore.initProject({ address, name }); + this.userInProject = true; + } + + @action setProjectAddress(value) { + this._projectAddress = value; + } + + @action + reset = () => { + this.projectList = []; + this.ERC = {}; + this.deployArgs = []; + this.name = ''; + this.alert = new Alert(); + this.uploadedQuestion = 0; + this.countOfQuestions = 0; + this.userInProject = false; + this._projectAddress = ''; + } } export default AppStore; diff --git a/src/stores/ConfigStore/index.js b/src/stores/ConfigStore/index.js new file mode 100644 index 00000000..aa225773 --- /dev/null +++ b/src/stores/ConfigStore/index.js @@ -0,0 +1,72 @@ +import { observable, action, computed } from 'mobx'; +import { + fs, PATH_TO_CONFIG, +} from '../../constants/windowModules'; + +class ConfigStore { + @observable config + + constructor() { + this.getConfig(); + } + + @action + getConfig() { + try { + this.config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG)); + this.updateValues(this.config); + // eslint-disable-next-line no-empty + } catch { + + } + } + + @action + updateValues({ + minGasPrice, maxGasPrice, interval, gasLimit, + }) { + const { config } = this; + console.log(PATH_TO_CONFIG); + config.minGasPrice = minGasPrice < 1 ? 1 : Number(minGasPrice); + config.maxGasPrice = maxGasPrice < 1 ? 1 : Number(maxGasPrice); + config.interval = interval < 10 ? 10 : Number(interval); + config.gasLimit = Number(gasLimit); + this.updateConfig(); + } + + @action + updateConfig() { + const { config } = this; + console.log(PATH_TO_CONFIG); + fs.writeFileSync(PATH_TO_CONFIG, JSON.stringify(config, null, '\t'), 'utf8'); + } + + @action + addProject(project) { + const { config } = this; + config.projects.push(project); + this.updateConfig(); + } + + @computed get MIN_GAS_PRICE() { + const { config } = this; + return config.minGasPrice * 1000000000; + } + + @computed get MAX_GAS_PRICE() { + const { config } = this; + return config.maxGasPrice * 1000000000; + } + + @computed get GAS_LIMIT() { + const { config } = this; + return config.gasLimit; + } + + @computed get UPDATE_INTERVAL() { + const { config } = this; + return config.interval * 1000; + } +} + +export default ConfigStore; diff --git a/src/stores/DialogStore/DialogStore.js b/src/stores/DialogStore/DialogStore.js index 73bf9304..a098c061 100644 --- a/src/stores/DialogStore/DialogStore.js +++ b/src/stores/DialogStore/DialogStore.js @@ -9,160 +9,175 @@ import DialogItem from './DialogItemModel'; * Class for manage all dialogs in app */ class DialogStore { - /** actual open state */ - @observable open = false; - - /** closing in progress state */ - @observable closing = false; - - /** current active name dialog */ - @observable dialog = null; - - /** history dialogs */ - history = []; - - /** list of all dialogs */ - list = {}; - - @computed - /** - * Actual open state - * - * @returns {string, boolean} name dialog or boolean state - */ - get isOpen() { - if (this.open && this.dialog) return this.dialog; - return false; - } + /** actual open state */ + @observable open = false; - /** - * Method for getting dialog by name in list - * - * @param {string} dialogName name dialog - * @return {object} dialog item model - */ - getDialog(dialogName) { - const { list } = this; - return list[dialogName]; - } + /** closing in progress state */ + @observable closing = false; - /** - * Method for checking the existence of a dialog - * - * @param {string} dialog dialog name - * @returns {boolean} dialog exists or does not exist - */ - doesExist(dialog) { - return this.getDialog(dialog) !== undefined; - } + /** current active name dialog */ + @observable dialog = null; - /** - * Method for adding new dialog in list - * - * @param {string} name name new dialog - * @param {object} options options for new dialog - * @param {boolean} options.history add to history or not - * @param {Function} options.onOpen method that is called - * on open dialog - * @param {Function} options.onClose method that is called - * on close dialog - */ - add(name, options) { - this.list[name] = new DialogItem(name, options); - } + /** history dialogs */ + history = []; + + /** list of all dialogs */ + list = {}; + + @computed + /** + * Actual open state + * + * @returns {string|boolean} name dialog or boolean state + */ + get isOpen() { + if (this.open && this.dialog) return this.dialog; + return false; + } - /** - * Method for removing dialog from list dialogs - * - * @param {string} name name dialog - */ - remove(name) { - delete this.list[name]; + /** + * Method for getting dialog by name in list + * + * @param {string} dialogName name dialog + * @returns {object} dialog item model + */ + getDialog(dialogName) { + const { list } = this; + return list[dialogName]; + } + + /** + * Method for checking the existence of a dialog + * + * @param {string} dialog dialog name + * @returns {boolean} dialog exists or does not exist + */ + doesExist(dialog) { + return this.getDialog(dialog) !== undefined; + } + + /** + * Method for adding new dialog in list + * + * @param {string} name name new dialog + * @param {object} options options for new dialog + * @param {boolean} options.history add to history or not + * @param {Function} options.onOpen method that is called + * on open dialog + * @param {Function} options.onClose method that is called + * on close dialog + */ + add(name, options) { + this.list[name] = new DialogItem(name, options); + } + + /** + * Method for removing dialog from list dialogs + * + * @param {string} name name dialog + */ + remove(name) { + delete this.list[name]; + } + + @action + /** + * Method for showing dialog by name + * + * @param {string} dialogName dialog name + */ + show(dialogName) { + const { open, dialog: currentDialogName } = this; + const dialog = this.getDialog(dialogName); + // not found + if (!dialog) return this.hide(); + // this dialog is opened next already + if (dialog.name === currentDialogName) return Promise.resolve(); + // save provided dialog as next to open + if (open) { + this.next = dialogName; + return this.hide(true); } + document.body.classList.add('dialog-overlay'); + this.open = true; + this.dialog = dialog.name; + dialog.open(); + this.addToHistory(dialog); + // this.emit(`${dialog.name}:open`); + return Promise.resolve(); + } - @action - /** - * Method for showing dialog by name - * - * @param {string} dialogName dialog name - */ - show(dialogName) { - const { open, dialog: currentDialogName } = this; - const dialog = this.getDialog(dialogName); - // not found - if (!dialog) return this.hide(); - // this dialog is opened next already - if (dialog.name === currentDialogName) return Promise.resolve(); - // save provided dialog as next to open - if (open) { - this.next = dialogName; - return this.hide(true); - } - document.body.classList.add('dialog-overlay'); - this.open = true; - this.dialog = dialog.name; - dialog.open(); - this.addToHistory(dialog); - // this.emit(`${dialog.name}:open`); + @action + /** + * Method for hiding dialog + */ + hide() { + const { dialog: dialogName } = this; + const dialog = this.getDialog(dialogName); + // closing right now + if (this.closing || !dialog) { return Promise.resolve(); } + return new Promise((resolve) => { + this.closing = true; + setTimeout(() => { + const { next } = this; + this.open = false; + this.closing = false; + this.dialog = false; + if (dialog) dialog.close(); + if (!next || typeof next !== 'boolean') { + document.body.classList.remove('dialog-overlay'); + } + if (next) { + this.next = false; + this.show(next) + .then(resolve); + } + // this.emit(`${dialog.name}:hidden`); + return resolve(); + }, 400); + }); + } - @action - /** - * Method for hiding dialog - */ - hide() { - const { dialog: dialogName } = this; - const dialog = this.getDialog(dialogName); - // closing right now - if (this.closing || !dialog) { - return Promise.resolve(); - } - return new Promise((resolve) => { - this.closing = true; - setTimeout(() => { - const { next } = this; - this.open = false; - this.closing = false; - this.dialog = false; - if (dialog) dialog.close(); - if (!next || typeof next !== 'boolean') { - document.body.classList.remove('dialog-overlay'); - } - if (next) { - this.next = false; - this.show(next) - .then(resolve); - } - // this.emit(`${dialog.name}:hidden`); - return resolve(); - }, 400); - }); - } + /** + * Method for adding dialog in history dialogs + * + * @param {object} dialog dialog item model + * @returns {number} length history + */ + addToHistory(dialog) { + if (dialog.history === false) return false; + return this.history.push(dialog.name); + } - /** - * Method for adding dialog in history dialogs - * - * @param {object} dialog dialog item model - */ - addToHistory(dialog) { - if (dialog.history === false) return false; - return this.history.push(dialog.name); + /** + * Method for toggle dialogs + * + * @param {string} dialogName dialog name for opening + */ + toggle(dialogName) { + const { open } = this; + if (!open || this.dialog !== dialogName) { + this.hide().then(() => { this.show(dialogName); }); + } else { + this.hide(); } + } - /** - * Method for toggle dialogs - * - * @param {string} dialogName dialog name for opening - */ - toggle(dialogName) { - const { open } = this; - if (!open || this.dialog !== dialogName) { - this.hide().then(() => { this.show(dialogName); }); - } else { - this.hide(); - } - } + /** + * Method for toggle dialog back + * by history + * + * @param {number} length count for + * return back by history + */ + back(length) { + const { history } = this; + const offset = length || 1; + if (history.length - offset < 0) return; + this.show(history[history.length - offset]); + } } export default DialogStore; diff --git a/src/stores/FilterStore/FilterStore.js b/src/stores/FilterStore/FilterStore.js new file mode 100644 index 00000000..3204a355 --- /dev/null +++ b/src/stores/FilterStore/FilterStore.js @@ -0,0 +1,126 @@ +import { + action, + set, + remove, + entries, + computed, + observable, +} from 'mobx'; + +class FilterStore { + /** List of _rules for filtering data */ + @observable _rules = {}; + + @computed + get rules() { + const result = {}; + const ruleEntries = entries(this._rules); + ruleEntries.forEach((key) => { + const ruleKey = key[0]; + const ruleValue = key[1]; + result[ruleKey] = ruleValue; + }); + return result; + } + + /** + * Method for adding (or rewrite) + * a list filter rule + * + * @param {object} rule filter rule + * @param {Function} cb callback + */ + @action + addFilterRule(rule, cb) { + Object.keys(rule).forEach((key) => { + set(this._rules, key, rule[key]); + }); + if (cb) cb(); + } + + /** + * Method for removing rule from list rules + * + * @param {string} rule rule name for removing + * @param {Function} cb callback function + */ + @action + removeFilterRule(rule, cb) { + remove(this._rules, rule); + if (cb) cb(); + } + + /** + * Method for reset state this store + */ + @action + reset() { + this._rules = {}; + } + + /** + * Method for filter by date + * + * @param {Array} rawList raw data + * @returns {Array} list from date range + */ + filteredByDateList(rawList) { + let resultList = []; + const rulesKeys = Object.keys(this.rules); + if (rulesKeys.length) { + if (!this.rules.date) return rawList; + rulesKeys.forEach((key) => { + if (key === 'date') { + const { start, end } = this.rules.date; + // Filter list with startTime by start & end date rule + const filtered = rawList.filter( + (item) => ( + parseInt(item.startTime, 10) >= start + && parseInt(item.startTime, 10) <= end + ), + ); + resultList = resultList.concat(filtered); + } + }); + } else { + resultList = rawList; + } + return resultList; + } + + /** + * Method for getting list filtered + * by filter rules + * + * @param {Array} data raw data + * @returns {Array} correct list + */ + filteredList(data) { + const rawList = data; + const listByDate = this.filteredByDateList(rawList); + const rulesKeys = Object.keys(this.rules); + // If _rules not exist return list by date + if (!rulesKeys.length) { + return listByDate; + } + let resultList = listByDate; + rulesKeys.forEach((key) => { + if (key !== 'date') { + if (this.rules[key] !== '*') { + resultList = this.filterByKey(resultList, key, this.rules[key]); + } + // If exist only date rule result is list by date + } else if (rulesKeys.length === 1) { + resultList = listByDate; + } + }); + return resultList; + } + + // eslint-disable-next-line class-methods-use-this + filterByKey(list, key, value) { + return list.filter((item) => item[key] === value); + } +} + +export default FilterStore; diff --git a/src/stores/FilterStore/FilterStore.test.js b/src/stores/FilterStore/FilterStore.test.js new file mode 100644 index 00000000..56fcaa32 --- /dev/null +++ b/src/stores/FilterStore/FilterStore.test.js @@ -0,0 +1,434 @@ +import FilterStore from '.'; + +describe('FilterStore', () => { + describe('data with date', () => { + let filter; + const dataList = [ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]; + + beforeEach(() => { + filter = new FilterStore(); + }); + + it('addFilterRule by date should change rules object & filteredByDateList should be correct', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.rules).toEqual({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + }); + + it('filteredList should be correct only with date rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.rules).toEqual({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + }); + + it('filteredList should be correct with date & other rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + filter.addFilterRule({ data: 1 }); + expect(filter.rules).toEqual({ + date: { + start: 1566383552, + end: 1566385552, + }, + data: 1, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('filteredList should be correct with different filter for one item', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: 1, test: 1 }); + expect(filter.rules).toEqual({ data: 1, test: 1 }); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('reset should clear filter rules', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: 1 }); + expect(filter.rules).toEqual({ data: 1 }); + filter.reset(); + expect(filter.rules).toEqual({}); + }); + + it('filteredByDateList & filteredList should be correct with date, then with other rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + filter.addFilterRule({ data: 1 }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('filteredByDateList & filteredList should be correct with rule, then with date rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: 1 }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566384552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('* key rules should work correct', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: '*' }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566384552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + ]); + }); + }); +}); diff --git a/src/stores/FilterStore/index.js b/src/stores/FilterStore/index.js new file mode 100644 index 00000000..4a7f7f79 --- /dev/null +++ b/src/stores/FilterStore/index.js @@ -0,0 +1,3 @@ +import FilterStore from './FilterStore'; + +export default FilterStore; diff --git a/src/stores/FormsStore/ConfigForm.js b/src/stores/FormsStore/ConfigForm.js new file mode 100644 index 00000000..82294c90 --- /dev/null +++ b/src/stores/FormsStore/ConfigForm.js @@ -0,0 +1,40 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class ConfigForm extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'minGasPrice', + type: 'text', + label: 'minGasPrice', + placeholder: i18n.t('fields:minGasPrice'), + rules: 'numeric|min:1|max:100', + }, { + name: 'maxGasPrice', + type: 'text', + label: 'maxGasPrice', + placeholder: i18n.t('fields:maxGasPrice'), + rules: 'numeric|min:1|max:100', + }, + { + name: 'gasLimit', + type: 'text', + label: 'gasLimit', + value: 7900000, + placeholder: i18n.t('fields:gasLimit'), + rules: 'numeric|min:25000|max:7900000', + }, + { + name: 'interval', + type: 'text', + label: 'interval', + placeholder: i18n.t('fields:interval'), + rules: 'numeric|min:10', + }], + }; + } +} + +export default ConfigForm; diff --git a/src/stores/FormsStore/ConnectProject.js b/src/stores/FormsStore/ConnectProject.js index 1fa25943..1a50868a 100644 --- a/src/stores/FormsStore/ConnectProject.js +++ b/src/stores/FormsStore/ConnectProject.js @@ -8,13 +8,13 @@ class ConnectProjectForm extends ExtendedForm { fields: [{ name: 'name', type: 'text', - label: 'Project Name', + label: 'projectTitle', placeholder: i18n.t('fields:projectTitle'), rules: 'required|string|between:3,20', }, { name: 'address', type: 'text', - label: 'Token Address', + label: 'contractAddress', placeholder: i18n.t('fields:contractAddress'), rules: 'required|string|address', }], diff --git a/src/stores/FormsStore/ConnectToken.js b/src/stores/FormsStore/ConnectToken.js index 3c247bb2..fd5771bb 100644 --- a/src/stores/FormsStore/ConnectToken.js +++ b/src/stores/FormsStore/ConnectToken.js @@ -8,7 +8,7 @@ class ConnectTokenForm extends ExtendedForm { fields: [{ name: 'address', type: 'text', - label: 'Token Address', + label: 'contractAddress', placeholder: i18n.t('fields:contractAddress'), rules: 'required|string|address', }], diff --git a/src/stores/FormsStore/CreateGroupQuestionsForm.js b/src/stores/FormsStore/CreateGroupQuestionsForm.js new file mode 100644 index 00000000..4c817bb4 --- /dev/null +++ b/src/stores/FormsStore/CreateGroupQuestionsForm.js @@ -0,0 +1,25 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateGroupQuestionsForm extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'name', + type: 'text', + label: 'titleGroupQuestions', + placeholder: i18n.t('fields:titleGroupQuestions'), + rules: 'required|string', + }, { + name: 'description', + type: 'text', + label: 'descriptionOrComment', + placeholder: i18n.t('fields:descriptionOrComment'), + rules: 'string', + }], + }; + } +} + +export default CreateGroupQuestionsForm; diff --git a/src/stores/FormsStore/CreateProject.js b/src/stores/FormsStore/CreateProject.js index a35ed028..86fab110 100644 --- a/src/stores/FormsStore/CreateProject.js +++ b/src/stores/FormsStore/CreateProject.js @@ -2,19 +2,19 @@ import i18n from 'i18next'; import ExtendedForm from '../../models/FormModel'; -class СreateProjectForm extends ExtendedForm { +class CreateProjectForm extends ExtendedForm { setup() { return { fields: [{ name: 'name', type: 'text', - label: 'Project name', + label: 'projectTitle', placeholder: i18n.t('fields:projectTitle'), rules: 'required|string|between:3,20', }, { name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }], @@ -22,4 +22,4 @@ class СreateProjectForm extends ExtendedForm { } } -export default СreateProjectForm; +export default CreateProjectForm; diff --git a/src/stores/FormsStore/CreateProjectInSettings.js b/src/stores/FormsStore/CreateProjectInSettings.js new file mode 100644 index 00000000..9ca8103e --- /dev/null +++ b/src/stores/FormsStore/CreateProjectInSettings.js @@ -0,0 +1,31 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateProjectInSettings extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'address', + type: 'text', + label: 'contractAddress', + placeholder: i18n.t('fields:contractAddress'), + rules: 'required|string|address', + }, { + name: 'name', + type: 'text', + label: 'projectTitle', + placeholder: i18n.t('fields:projectTitle'), + rules: 'required|string|between:3,20', + }, { + name: 'password', + type: 'password', + label: 'enterPassword', + placeholder: i18n.t('fields:enterPassword'), + rules: 'required|password', + }], + }; + } +} + +export default CreateProjectInSettings; diff --git a/src/stores/FormsStore/CreateQuestionBasicForm.js b/src/stores/FormsStore/CreateQuestionBasicForm.js new file mode 100644 index 00000000..7d54cd31 --- /dev/null +++ b/src/stores/FormsStore/CreateQuestionBasicForm.js @@ -0,0 +1,63 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateQuestionBasicForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'GroupId', + type: 'text', + label: 'questionGroup', + placeholder: i18n.t('fields:questionGroup'), + rules: '', + }, + { + name: 'question_title', + type: 'text', + label: 'questionTitle', + placeholder: i18n.t('fields:questionTitle'), + rules: 'required', + }, + { + name: 'question_life_time', + type: 'text', + label: 'questionLifeTime', + placeholder: i18n.t('fields:questionLifeTime'), + rules: 'required|numeric', + }, + { + name: 'target', + type: 'text', + label: 'targetContractAddress', + placeholder: i18n.t('fields:targetContractAddress'), + rules: 'required|string|address', + }, + { + name: 'methodSelector', + type: 'text', + label: 'methodSelector', + placeholder: i18n.t('fields:methodSelector'), + rules: 'string|bytes4', + }, + { + name: 'voting_formula', + type: 'text', + label: 'votingFormula', + placeholder: i18n.t('fields:votingFormula'), + rules: 'required', + }, + { + name: 'description', + type: 'text', + label: 'questionDescription', + placeholder: i18n.t('fields:questionDescription'), + rules: 'required', + }, + ], + }; + } +} + +export default CreateQuestionBasicForm; diff --git a/src/stores/FormsStore/CreateQuestionDynamicForm.js b/src/stores/FormsStore/CreateQuestionDynamicForm.js new file mode 100644 index 00000000..343ef347 --- /dev/null +++ b/src/stores/FormsStore/CreateQuestionDynamicForm.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateQuestionDynamicForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'input--id0', + type: 'text', + label: 'enterNewParameterName', + placeholder: i18n.t('fields:enterNewParameterName'), + rules: 'required', + }, + { + name: 'select--id0', + type: 'text', + label: 'selectParameterType', + placeholder: i18n.t('fields:selectParameterType'), + rules: 'required', + }, + ], + }; + } +} + +export default CreateQuestionDynamicForm; diff --git a/src/stores/FormsStore/CreateToken.js b/src/stores/FormsStore/CreateToken.js index 6efd78d4..1c5988f8 100644 --- a/src/stores/FormsStore/CreateToken.js +++ b/src/stores/FormsStore/CreateToken.js @@ -7,26 +7,26 @@ class CreateTokenForm extends ExtendedForm { return { fields: [{ name: 'name', + label: 'tokenTitle', type: 'text', - label: 'Имя', placeholder: i18n.t('fields:tokenTitle'), rules: 'required|string', }, { name: 'symbol', + label: 'symbol', type: 'text', - label: 'Символ Токена', placeholder: i18n.t('fields:symbol'), rules: 'required|string|between:3,5', }, { name: 'count', + label: 'quantity', type: 'text', - label: 'Количество токенов', placeholder: i18n.t('fields:quantity'), rules: 'required|numeric|min:1|max:2147483647 ', }, { name: 'password', + label: 'enterPassword', type: 'password', - label: 'Password', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }], diff --git a/src/stores/FormsStore/CreateWalletForm.js b/src/stores/FormsStore/CreateWalletForm.js index e01a1760..b05e7a32 100644 --- a/src/stores/FormsStore/CreateWalletForm.js +++ b/src/stores/FormsStore/CreateWalletForm.js @@ -8,13 +8,13 @@ class CreateWalletForm extends ExtendedForm { fields: [{ name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }, { name: 'passwordConfirm', type: 'password', - label: 'Password Confirmation', + label: 'repeatPassword', placeholder: i18n.t('fields:repeatPassword'), rules: 'required|same:password', }], diff --git a/src/stores/FormsStore/FinPassForm.js b/src/stores/FormsStore/FinPassForm.js index ec5ff96a..5d225f68 100644 --- a/src/stores/FormsStore/FinPassForm.js +++ b/src/stores/FormsStore/FinPassForm.js @@ -8,7 +8,7 @@ class FinPassForm extends ExtendedForm { fields: [{ name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }], diff --git a/src/stores/FormsStore/LoginForm.js b/src/stores/FormsStore/LoginForm.js index d96e27fa..c0e5351f 100644 --- a/src/stores/FormsStore/LoginForm.js +++ b/src/stores/FormsStore/LoginForm.js @@ -8,13 +8,13 @@ class LoginForm extends ExtendedForm { fields: [{ name: 'wallet', type: 'text', - label: 'Wallet', + label: 'wallet', placeholder: i18n.t('fields:wallet'), rules: 'required|string', }, { name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', diff --git a/src/stores/FormsStore/NodeChangeForm.js b/src/stores/FormsStore/NodeChangeForm.js new file mode 100644 index 00000000..72280c06 --- /dev/null +++ b/src/stores/FormsStore/NodeChangeForm.js @@ -0,0 +1,36 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable no-restricted-globals */ +/* eslint-disable class-methods-use-this */ +/* eslint-disable no-unused-vars */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; +import { fs, PATH_TO_CONFIG } from '../../constants/windowModules'; + +const { ipcRenderer } = window.require('electron'); + +let config; +try { + config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG), 'utf8'); +} catch { + ipcRenderer.send('config-problem', PATH_TO_CONFIG); + // alert(`Something wrong with config file + // located in ${PATH_TO_CONFIG}. + // Please check it, without this you can't continue.`); +} + +class NodeChangeForm extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'url', + type: 'text', + label: 'nodeUrl', + placeholder: i18n.t('fields:nodeUrl'), + rules: 'required|url', + value: config.host, + }], + }; + } +} + +export default NodeChangeForm; diff --git a/src/stores/FormsStore/StartNewVoteForm.js b/src/stores/FormsStore/StartNewVoteForm.js new file mode 100644 index 00000000..17b2f457 --- /dev/null +++ b/src/stores/FormsStore/StartNewVoteForm.js @@ -0,0 +1,23 @@ +/* eslint-disable no-alert */ +/* eslint-disable no-console */ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class StartNewVoteForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'question', + type: 'text', + label: 'chooseTheQuestion', + placeholder: i18n.t('fields:chooseTheQuestion'), + rules: 'required|integer|min:1', + }, + ], + }; + } +} + +export default StartNewVoteForm; diff --git a/src/stores/FormsStore/TransferTokenForm.js b/src/stores/FormsStore/TransferTokenForm.js new file mode 100644 index 00000000..61b75cf5 --- /dev/null +++ b/src/stores/FormsStore/TransferTokenForm.js @@ -0,0 +1,35 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class TransferTokenForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'address', + type: 'text', + label: 'address', + placeholder: i18n.t('fields:address'), + rules: 'required|string|address', + }, + { + name: 'count', + type: 'text', + label: 'countTokens', + placeholder: i18n.t('fields:countTokens'), + rules: 'required|numeric', + }, + { + name: 'password', + type: 'password', + label: 'password', + placeholder: i18n.t('fields:password'), + rules: 'required|password', + }, + ], + }; + } +} + +export default TransferTokenForm; diff --git a/src/stores/FormsStore/VotingFilterForm.js b/src/stores/FormsStore/VotingFilterForm.js new file mode 100644 index 00000000..f5287b34 --- /dev/null +++ b/src/stores/FormsStore/VotingFilterForm.js @@ -0,0 +1,32 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class VotingFilterForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'question', + type: 'text', + label: 'question', + placeholder: i18n.t('fields:question'), + }, + { + name: 'date_before', + type: 'text', + label: 'dateBefore', + placeholder: i18n.t('fields:dateBefore'), + }, + { + name: 'date_after', + type: 'text', + label: 'dateAfter', + placeholder: i18n.t('fields:dateAfter'), + }, + ], + }; + } +} + +export default VotingFilterForm; diff --git a/src/stores/HistoryStore/HistoryStore.js b/src/stores/HistoryStore/HistoryStore.js index e96b6627..9f67593a 100644 --- a/src/stores/HistoryStore/HistoryStore.js +++ b/src/stores/HistoryStore/HistoryStore.js @@ -1,73 +1,600 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-await-in-loop */ import { observable, action, computed } from 'mobx'; import Voting from './entities/Voting'; +import { PATH_TO_DATA } from '../../constants/windowModules'; +import { readDataFromFile, writeDataToFile } from '../../utils/fileUtils/data-manager'; +import FilterStore from '../FilterStore/FilterStore'; +import PaginationStore from '../PaginationStore'; +import { statusStates } from '../../constants'; +import AsyncInterval from '../../utils/AsyncUtils'; class HistoryStore { - @observable votings = []; + @observable pagination = null; - constructor(projectAddress) { - this.fetchVotings(projectAddress); + /** Voting list */ + @observable _votings = []; + + /** Voting data is loading, should be true only on first load */ + @observable loading = false; + + /** User tokens is return (actual state, updated by interval) */ + @observable isUserReturnTokensActual = false; + + /** Is there an active vote */ + @observable isActiveVoting = false; + + constructor(rootStore) { + this.rootStore = rootStore; + const { configStore: { UPDATE_INTERVAL } } = rootStore; + this.loading = false; + this.filter = new FilterStore(); + this.updateHistoryInterval = new AsyncInterval({ + cb: async () => { + await this.getActualState(() => { + this.returnToLastPage(); + }); + await this.setLoadingFinish(); + }, + timeoutInterval: UPDATE_INTERVAL, + }); + } + + @action setLoadingFinish() { + this.loading = false; + } + + /** + * Actual list of voting + * + * @returns {Array} list of voting + */ + @computed get votings() { + return this._votings; + } + + /** + * Get filtered list votings + * + * @returns {Array} filtered by rules + * votings list + */ + @computed + get list() { + return this.filter.filteredList(this.votings); + } + + /** + * Get paginated voting list + * + * @returns {Array} paginated voting list + */ + @computed + get paginatedList() { + let range; + if (!this.pagination || !this.pagination.paginationRange) { + range = [0, 5]; + } else { + range = this.pagination.paginationRange; + } + return this.list.slice(range[0], range[1] + 1); + } + + /** + * Get raw voting list + * + * @returns {Array} raw voting list + */ + get rawList() { + return this._votings.map((voting) => ({ + ...voting.raw, + })); + } + + /** + * Method for check that user return token + * + * @returns {boolean} user return tokens or not + */ + @action + fetchUserReturnTokens = async () => { + const isReturn = await this.isUserReturnTokens(); + this.isUserReturnTokensActual = isReturn; + return isReturn; + } + + /** + * Method for getting actual state for + * this store. This includes: current voting list, + * whether the user returned tokens, whether + * there is an active voting + * + * @param {Function} cb callback function + */ + @action + async getActualState(cb) { + await this.getActualVotingList(); + this.isActiveVoting = await this.hasActiveVoting(); + await this.fetchUserReturnTokens(); + if (cb) cb(); + } + + /** + * Method for returning on last page after votings + * list update + */ + @action + returnToLastPage() { + let currentPage = 1; + if (this.pagination !== null) { + currentPage = this.pagination.getCurrentPage(); + } + this.pagination = new PaginationStore({ + totalItemsCount: this.list.length, + }); + this.pagination.handleChange(currentPage); } /** * recieving voting length for fetching them from contract + * * @function - * @param {string} address user address * @returns {number} count of votings */ - @action fetchVotingsCount = (address) => address + @action fetchVotingsCount = async () => { + // eslint-disable-next-line no-unused-vars + const { contractService: { _contract } } = this.rootStore; + const votingsLength = await _contract.methods.getVotingsAmount().call(); + return Number(votingsLength); + } /** * recieving voting for local using + * * @function - * @param {string} address user address */ - @action fetchVotings = (address) => { - this.fetchVotingsCount(address); - this.votings.push(new Voting()); + @action fetchVotings = async () => { + let length = await this.fetchVotingsCount(); + length -= 1; + for (length; length >= 0; length -= 1) { + const voting = await this.getVotingFromContractById(length); + const duplicateVoting = this._votings.find((item) => item.id === voting.id); + if (!duplicateVoting) this._votings.push(new Voting(voting)); + } } /** - * Getting full info about one voting, selected by id - * @function - * @param {number} id id of voting - * @return {object} selected voting + * Method for update voting with active + * status state + */ + @action + async updateVotingWithActiveState() { + const { votings } = this; + votings.forEach(async (votingItem) => { + if (votingItem.status === statusStates.active) { + const { id } = votingItem; + const voting = await this.getVotingFromContractById(id); + votingItem.update(voting); + } + }); + } + + /** + * Method for getting actual question + * from the contract & file */ - @action getVotingsById = (id) => this.votings.filter((voting) => voting.id === id) + @action + getActualVotingList = async () => { + const votings = await this.getFilteredVotingListFromFile(); + this.writeVotingListToState(votings); + if (!votings || !votings.length) { + await this.getVotingListFromContract(); + return; + } + await this.getFilteredVotingList(); + await this.updateVotingWithActiveState(); + } /** - * Getting stats about votes in voting, selected by id + * Getting full info about one voting, selected by id + * * @function * @param {number} id id of voting - * @return {array} stats + * @returns {object} selected voting */ - @action getVotingStats = (id) => id + @action getVotingById = (id) => this._votings.filter((voting) => voting.id === id) /** - * filtering voting by given parameters - * @function - * @param {object} params parameters for filtering - * @param {number} params.questionId filter votings by questionId - * @param {number} params.descision filter voting by descision - * @param {string} params.dateFrom filter voting by startTime - * @param {string} params.dateTo filter voting by endTime - * @return {array} Filtered question + * Method for update specific data + * for voting by id + * + * @param {object} param0 data + * @param {string|number} param0.id id voting + * @param {object} param0.newState new state for data update */ - @action filterVotings = (params) => params + @action + updateVotingById = ({ + id, + newState, + }) => { + const [voting] = this.getVotingById(id); + voting.update(newState); + } /** - * @function - * @return {bool} True if project have not ended voting + * Method for update last voting from list. + * By default used for update voting after closed. */ + @action + fetchAndUpdateLastVoting = async () => { + const lastIndex = await this.fetchVotingsCount(); + const votingFromContract = await this.getVotingFromContractById(lastIndex - 1); + const [voting] = this.getVotingById(lastIndex - 1); + voting.update(votingFromContract); + this.writeVotingsToFile(); + } + + @action + reset = () => { + this.updateHistoryInterval.cancel(); + this.pagination = null; + this._votings = []; + this.loading = true; + } + @computed get isVotingActive() { - return this.votings; + return this.isActiveVoting; + } + + async getVotingListFromContract() { + await this.fetchVotings(); + this.writeVotingsToFile(); } /** - * @function - * @return {array} list of votings + * Method for getting list voting from file + * without duplicated item & with correct order + * + * @returns {Array} correct array of voting + */ + async getFilteredVotingListFromFile() { + const { contractService, userStore, projectStore: { questionStore } } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + const votings = []; + const votingsFromFile = await readDataFromFile({ + name: 'votings', + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + const votingsFromFileLength = votingsFromFile.data && votingsFromFile.data.length + ? votingsFromFile.data.length + : 0; + for (let i = 0; i < votingsFromFileLength; i += 1) { + let voting = votingsFromFile.data[i]; + if (voting) { + const { id } = voting; + let [question] = questionStore.getQuestionById(Number(voting.questionId)); + if (!question) { + question = await contractService.fetchQuestion(voting.questionId); + } + voting = await this.extendVotingInfo({ voting, question, id }); + const duplicateVoting = votings.find((item) => item.id === voting.id); + if (!duplicateVoting) votings.push(new Voting(voting)); + } + } + + votings.sort((a, b) => b.id - a.id); + return votings; + } + + /** + * Method for write voting list to + * state history + * + * @param {Array} votingList voting list + */ + writeVotingListToState(votingList) { + const votingListLength = votingList.length; + for (let i = 0; i < votingListLength; i += 1) { + const voting = votingList[i]; + const duplicateVoting = this._votings.find((item) => item.id === voting.id); + if (!duplicateVoting) this._votings.push(new Voting(voting)); + } + } + + /** Method for getting missing votings from contract */ + async getFilteredVotingList() { + const votingListFromFile = await this.getFilteredVotingListFromFile(); + const countOfVotings = await this.fetchVotingsCount(); + const votingListFromFileLength = votingListFromFile.length; + if (countOfVotings > votingListFromFileLength) { + for (let i = votingListFromFileLength; i < countOfVotings; i += 1) { + // eslint-disable-next-line no-await-in-loop + const voting = await this.getVotingFromContractById(i); + const duplicateVoting = this._votings.find((item) => item.id === voting.id); + if (!duplicateVoting) { + this._votings.unshift(new Voting(voting)); + } + } + this.writeVotingsToFile(); + } + } + + /** + * Add new filter rule + * + * @param {object} rule object with rules + */ + addFilterRule = (rule) => { + this.filter.addFilterRule(rule); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** + * Method for remove filter rule + * by name + * + * @param {string} rule name rule + */ + removeFilterRule = (rule) => { + this.filter.removeFilterRule(rule); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** + * Method for reset filter + * & update pagination + */ + resetFilter = () => { + this.filter.reset(); + if ( + this.pagination + && this.pagination.update + ) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** Write raw voting list data to file */ + writeVotingsToFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + writeDataToFile({ + name: 'votings', + data: { + data: this.rawList, + }, + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + } + + async getVoterList(votingId) { + const { + rootStore: { + projectStore: { + questionStore, + }, + contractService: { + _contract, + }, + membersStore, + }, + } = this; + const [voting] = this.getVotingById(votingId); + const { allowedGroups } = voting; + + const result = {}; + + for (let i = 0; i < allowedGroups.length; i += 1) { + const group = allowedGroups[i]; + const memberGroup = membersStore.getMemberGroupByAddress(group); + if (!memberGroup || !memberGroup.list) return []; + const { list, balance } = memberGroup; + result[memberGroup.wallet] = { + positive: [], + negative: [], + }; + for (let j = 0; j < list.length; j += 1) { + let info = {}; + const { wallet } = list[j]; + const vote = await _contract.methods + .getUserVote(votingId, group, wallet).call(); + const tokenCount = await _contract.methods + .getUserVoteWeight(votingId, group, wallet).call(); + const weight = ((tokenCount / Number(balance)) * 100).toFixed(2); + switch (vote) { + case ('1'): + info = { wallet, weight }; + if ( + result[memberGroup] + && result[memberGroup].positive + ) { + result[memberGroup].positive.push(info); + } + break; + case ('2'): + info = { wallet, weight }; + if ( + result[memberGroup.wallet] + && result[memberGroup.wallet].negative + ) { + result[memberGroup.wallet].negative.push(info); + } + break; + default: + break; + } + } + } + return result; + } + + /** + * Method for extending voting. Avoid + * duplicate code for @getVotingFromContractById method + * + * @returns {object} extended voting + */ + async extendVotingInfo({ + voting, + question, + id, + }) { + const resultVoting = voting; + resultVoting.caption = question && question.name; + resultVoting.text = question && question.description; + const formula = question.rawFormula ? question.rawFormula : question.formula; + resultVoting.allowedGroups = await this.getGroupsAllowedToVoting({ formula }); + const userVotes = await this.getUserVote(voting.allowedGroups, id); + resultVoting.userVote = userVotes.length === 1 ? userVotes[0] : 0; + return resultVoting; + } + + /** + * Method for getting actual voting + * from contract by id + * + * @param {string|number} id id voting + * @returns {object} actual voting form contract + */ + async getVotingFromContractById(id) { + const { contractService, projectStore: { questionStore } } = this.rootStore; + let voting = await contractService.fetchVoting(id); + const descision = await contractService.callMethod('getVotingResult', id); + voting.descision = descision; + let [question] = questionStore.getQuestionById(Number(voting.questionId)); + if (question) { + voting = await this.extendVotingInfo({ voting, question, id }); + } else { + // Get question from contract, for correct work! + question = await contractService.fetchQuestion(voting.questionId); + if (!question) throw new Error(`Question with id: ${voting.questionId}, not found!`); + voting = await this.extendVotingInfo({ voting, question, id }); + } + voting.data = voting.votingData; + delete voting.votingData; + voting.id = id; + for (let j = 0; j < 6; j += 1) { + delete voting[j]; + } + return voting; + } + + async getUserVote(allowedGroups, votingId) { + const { + contractService: { _contract }, + userStore: { address }, + } = this.rootStore; + const votes = []; + for (let i = 0; i < allowedGroups.length; i += 1) { + const vote = await _contract.methods.getUserVote(votingId, allowedGroups[i], address).call(); + votes.push(vote); + } + + const set = [...new Set(votes)]; + return set; + } + + /** + * Method for finding allowed groups from formula + * + * @param {object} question quiestion, which formula will be used */ - @computed get votingsList() { - return this.votings; + // eslint-disable-next-line class-methods-use-this + getGroupsAllowedToVoting({ formula }) { + const list = formula.match(/(erc20{((0x)+([0-9 a-f A-F]){40})})|(custom{((0x)+([0-9 a-f A-F]){40})})/g); + if (!list || !list.length) return []; + const groups = list.map((group) => group.replace(/(erc20({)|(}))|(custom({)|(}))/g, '')); + return groups; + } + + async lastUserVoting() { + const { contractService, userStore, membersStore } = this.rootStore; + const { list } = membersStore; + let lastVoting = 0; + // TODO fix empty list + if (list && list.length) { + const listLength = list.length; + for (let i = 0; i < listLength; i += 1) { + const lastVotingFromContract = await contractService._contract.methods.findLastUserVoting( + list[i].wallet, + userStore.address, + ).call(); + if (Number(lastVotingFromContract) > lastVoting) { + lastVoting = Number(lastVotingFromContract); + } + } + } + return lastVoting; + } + + /** + * Method for check active voting state + * + * @returns {boolean} has active voting state + */ + async hasActiveVoting() { + const countOfVotings = await this.fetchVotingsCount(); + const lastVote = countOfVotings - 1; + const voting = await this.getVotingFromContractById(lastVote); + return voting.status === statusStates.active; + } + + async isUserReturnTokens() { + const { contractService, userStore, membersStore } = this.rootStore; + const { list } = membersStore; + const listLength = list.length; + const isReturn = true; + for (let i = 0; i < listLength; i += 1) { + const isReturnTokens = await contractService._contract.methods + .isUserReturnTokens(list[i].wallet, userStore.address).call(); + if (isReturnTokens === false) return false; + } + return isReturn; + } + + async returnTokens() { + const { + contractService, Web3Service, userStore, appStore, + } = this.rootStore; + const { _contract } = contractService; + const { address, password } = userStore; + const tx = { + data: contractService._contract.methods.revoke().encodeABI(), + value: '0x0', + from: address, + to: _contract.options.address, + }; + + appStore.setTransactionStep('compileOrSign'); + return Web3Service.createTxData(address, tx) + .then((createdTx) => userStore.singTransaction(createdTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + appStore.setTransactionStep('success'); + userStore.getEthBalance(); + }); } } export default HistoryStore; diff --git a/src/stores/HistoryStore/entities/Voting.js b/src/stores/HistoryStore/entities/Voting.js index d2bff068..fffab676 100644 --- a/src/stores/HistoryStore/entities/Voting.js +++ b/src/stores/HistoryStore/entities/Voting.js @@ -1,17 +1,79 @@ +import { action, observable, computed } from 'mobx'; +import { statusStates } from '../../../constants'; + class Voting { + raw; + + @observable _userVote; + + @observable status; + + @observable descision; + + @observable closeVoteInProgress; + /** - * @constructor - * @param {Object} data object contains info adout voting - * @param {Number} data.votingId id of voting - * @param {Number} data.questionId id of question which selected for voting + * Voting is new for user. Used to highlight a new vote. + * It differs only for active voting. For closed, this is + * always false + */ + @observable newForUser = false; + + /** + * @class + * @param {object} data object contains info adout voting + * @param {number} data.id id of voting + * @param {number} data.questionId id of question which selected for voting * @param {Array} data.params parameters of voting */ constructor({ - votingId, questionId, params, + id, descision, questionId, + data, status, startTime, + endTime, caption, text, + userVote, newForUser, + allowedGroups, }) { - this.id = votingId; + this.raw = { + id, descision, questionId, data, status, startTime, endTime, caption, text, userVote, + }; + this.id = id; this.questionId = questionId; - this.params = params; + this.descision = descision; + this.data = data; + this.startTime = startTime; + this.endTime = endTime; + this.status = status; + this.caption = caption; + this.text = text; + this._userVote = Number(userVote); + this.closeVoteInProgress = false; + this.allowedGroups = allowedGroups; + if (status === statusStates.active) { + this.newForUser = newForUser !== undefined ? newForUser : true; + } + } + + @computed + get userVote() { + return Number(this._userVote); + } + + /** + * Method for update state voting + * + * @param {object} newState new state for voting + */ + @action + update = (newState) => { + Object.keys(newState).forEach((key) => { + if (key === 'userVote') { + this._userVote = newState.userVote; + this.raw.userVote = newState.userVote; + } else { + this[key] = newState[key]; + this.raw[key] = newState[key]; + } + }); } } diff --git a/src/stores/MembersStore/MemberItem.js b/src/stores/MembersStore/MemberItem.js new file mode 100644 index 00000000..f4924e51 --- /dev/null +++ b/src/stores/MembersStore/MemberItem.js @@ -0,0 +1,81 @@ +import { computed, action, observable } from 'mobx'; + +class MemberItem { + /** + * Class constructor + * + * @param {object} param0 data for constructor + * @param {string} param0.wallet wallet + * @param {number} param0.weight weight + * @param {number} param0.balance balance + * @param {string} param0.customTokenName custom token name + * @param {boolean} param0.isAdmin wallet is admin + */ + constructor({ + wallet, + weight, + balance, + customTokenName, + isAdmin, + }) { + if ( + !wallet + || !weight + || !customTokenName + || !balance + ) throw new Error('Incorrect data provided for MemberItem!'); + this.wallet = wallet; + this._weight = parseInt(weight, 10); + this.balance = parseInt(balance, 10); + this.customTokenName = customTokenName; + if (typeof isAdmin === 'boolean') this.isAdmin = isAdmin; + } + + /** Wallet address member */ + wallet = ''; + + /** Weight member in vote */ + @observable _weight = 0; + + /** Balance member */ + @observable balance = 0; + + /** Custom token name */ + customTokenName = ''; + + /** Wallet is admin */ + isAdmin = false; + + @computed + /** Method for getting full balance text */ + get fullBalance() { + return `${this.balance} ${this.customTokenName}`; + } + + @computed + get weight() { + return this._weight.toFixed(2); + } + + @action + setWeight(weight) { + this._weight = weight; + } + + @action + setTokenBalance(balance) { + this.balance = balance; + } + + @action + removeAdminPrivileges() { + this.isAdmin = false; + } + + @action + addAdminPrivileges() { + this.isAdmin = true; + } +} + +export default MemberItem; diff --git a/src/stores/MembersStore/MemberItem.test.js b/src/stores/MembersStore/MemberItem.test.js new file mode 100644 index 00000000..5d160bab --- /dev/null +++ b/src/stores/MembersStore/MemberItem.test.js @@ -0,0 +1,53 @@ +import MemberItem from './MemberItem'; + +describe('MemberItem', () => { + const defaultProps = { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 5, + balance: 120, + customTokenName: 'TKN', + }; + + describe('With correct data', () => { + let memberItem; + + beforeEach(() => { + memberItem = new MemberItem(defaultProps); + }); + + it('should has correct data', () => { + expect(memberItem.wallet).toEqual('0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54'); + expect(memberItem.weight).toEqual(5); + expect(memberItem.balance).toEqual(120); + expect(memberItem.customTokenName).toEqual('TKN'); + }); + + it('fullBalance should be correct', () => { + expect(memberItem.fullBalance).toEqual('120 TKN'); + }); + }); + + it('should cause error without wallet', () => { + expect( + () => (new MemberItem({ ...defaultProps, wallet: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); + + it('should cause error without weight', () => { + expect( + () => (new MemberItem({ ...defaultProps, weight: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); + + it('should cause error without balance', () => { + expect( + () => (new MemberItem({ ...defaultProps, balance: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); + + it('should cause error without customTokenName', () => { + expect( + () => (new MemberItem({ ...defaultProps, customTokenName: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); +}); diff --git a/src/stores/MembersStore/MembersGroup.js b/src/stores/MembersStore/MembersGroup.js new file mode 100644 index 00000000..b070ed8e --- /dev/null +++ b/src/stores/MembersStore/MembersGroup.js @@ -0,0 +1,192 @@ +import { observable, action, computed } from 'mobx'; +import MemberItem from './MemberItem'; +import AsyncInterval from '../../utils/AsyncUtils'; +import { tokenTypes } from '../../constants'; + +class MembersGroup { + /** + * Class constructor + * + * @param {object} param0 data for constructor + * @param {string} param0.name name group (Example: Admins) + * @param {string} param0.description description for group + * @param {string} param0.wallet wallet for group + * @param {number} param0.balance balance for group + * @param {string} param0.customTokenName custom token name + * @param {string} param0.tokenName token name + * @param {string} param0.textForEmptyState text for state when list is empty + * @param {Array} param0.list members list + */ + constructor({ + name, + groupAddress, + contract, + totalSupply, + groupType, + tokenSymbol, + userAddress, + interval, + members, + textForEmptyState, + groupId, + }) { + if ( + !name + || !contract + || !groupType + || !tokenSymbol + || Array.isArray(members) === false + || !groupAddress + ) throw new Error('Incorrect data provided!'); + this.name = name; + this.wallet = groupAddress; + this.groupType = groupType; + this.balance = totalSupply; + this.contract = contract; + this.customTokenName = tokenSymbol; + this.userAddress = userAddress; + this.groupId = groupId; + if (textForEmptyState && textForEmptyState.length) { + this.textForEmptyState = textForEmptyState; + } + this.addToList(members); + this.getUserBalanceInGroup(); + this.updateInterval = 60000; + this.interval = new AsyncInterval({ + timeoutInterval: interval, + cb: this.updateUserBalanceAndGroupAdmin, + }); + } + + /** Name group (Example: Admins) */ + name = ''; + + /** Description for group */ + description = ''; + + /** Wallet for group */ + wallet = null; + + /** Custom token name */ + customTokenName = ''; + + /** Basic token name */ + tokenName = ''; + + /** Balance group */ + balance; + + /** User balance in group */ + @observable userBalance; + + /** Text for state when list is empty */ + textForEmptyState = 'other:noData'; + + @computed + get fullBalance() { + return `${this.balance} ${this.customTokenName}`; + } + + @computed + get fullUserBalance() { + return `${this.userBalance} ${this.customTokenName}`; + } + + @computed + get groupAdmin() { + return this.list.filter((user) => user.isAdmin === true); + } + + /** + * Method for getting balance in group + */ + getUserBalanceInGroup = async () => { + if (!this.contract || !this.contract.methods) return; + const balance = await this.contract.methods.balanceOf(this.userAddress).call(); + this.userBalance = balance; + } + + /** Members list */ + @observable list = []; + + @action + /** + * Method for adding new member to group + * + * @param {object} member all data about member + */ + addToList = (members) => { + members.forEach((member) => { + if (!this.list.find((item) => item.wallet === member.wallet)) { + this.list.push( + new MemberItem({ + ...member, + weight: member.weight.toString(), + }), + ); + } + }); + } + + /** + * Method for updating a member’s balance + * + * @param {number} address address wallet member + */ + @action + updateMemberBalanceAndWeight = async (address) => { + const userBalance = await this.contract.methods.balanceOf(address).call(); + this.getUserBalanceInGroup(); + const user = this.list.find((member) => ( + member.wallet.toUpperCase() === address.toUpperCase() + )); + if ((!user || !user.setTokenBalance || !user.setWeight) && userBalance !== 0) { + const admin = this.groupType === tokenTypes.Custom + ? await this.contract.methods.owner().call() + : null; + this.list.push(new MemberItem({ + wallet: address, + balance: userBalance, + weight: (userBalance / Number(this.balance)) * 100, + customTokenName: this.customTokenName, + isAdmin: admin !== null + ? address === admin + : false, + })); + } else { + const weight = (userBalance / Number(this.balance)) * 100; + user.setTokenBalance(userBalance); + user.setWeight(weight); + } + } + + @action + updateUserBalanceAndGroupAdmin = () => { + this.updateUserBalance(); + this.setNewAdmin(); + } + + @action + updateUserBalance = async () => { + await this.updateMemberBalanceAndWeight(this.userAddress); + } + + @action + setNewAdmin = async () => { + const admin = this.list.find((member) => member.isAdmin === true); + const newAdmin = this.groupType === tokenTypes.Custom + ? await this.contract.methods.owner().call() + : null; + const user = this.list.find((member) => member.wallet === newAdmin); + if (admin) admin.removeAdminPrivileges(); + if (user) user.addAdminPrivileges(); + } + + @action + stopInterval = () => { + this.interval.cancel(); + this.interval = null; + } +} + +export default MembersGroup; diff --git a/src/stores/MembersStore/MembersGroup.test.js b/src/stores/MembersStore/MembersGroup.test.js new file mode 100644 index 00000000..77298826 --- /dev/null +++ b/src/stores/MembersStore/MembersGroup.test.js @@ -0,0 +1,71 @@ +import MembersGroup from './MembersGroup'; + +describe('MembersGroup', () => { + const defaultProps = { + name: 'Admins', + description: 'short description for group', + customTokenName: 'TKN', + tokenName: 'ERC20', + wallet: '0xB210af05Bf82eF6C6BA034B22D18c89B5D23Cc90', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + }, + ], + }; + + describe('with correct data', () => { + let membersGroup; + + beforeEach(() => { + membersGroup = new MembersGroup(defaultProps); + }); + + it('should has correct data', () => { + expect(membersGroup.name).toEqual('Admins'); + expect(membersGroup.description).toEqual('short description for group'); + expect(membersGroup.customTokenName).toEqual('TKN'); + expect(membersGroup.tokenName).toEqual('ERC20'); + expect(membersGroup.list.length).toEqual(1); + }); + }); + + it('should cause error without name', () => { + expect( + () => (new MembersGroup({ ...defaultProps, name: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without description', () => { + expect( + () => (new MembersGroup({ ...defaultProps, description: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without customTokenName', () => { + expect( + () => (new MembersGroup({ ...defaultProps, customTokenName: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without tokenName', () => { + expect( + () => (new MembersGroup({ ...defaultProps, tokenName: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without list', () => { + expect( + () => (new MembersGroup({ ...defaultProps, list: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without wallet', () => { + expect( + () => (new MembersGroup({ ...defaultProps, wallet: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); +}); diff --git a/src/stores/MembersStore/MembersStore.js b/src/stores/MembersStore/MembersStore.js new file mode 100644 index 00000000..29e6c760 --- /dev/null +++ b/src/stores/MembersStore/MembersStore.js @@ -0,0 +1,335 @@ +/* eslint-disable no-await-in-loop */ +import { observable, computed, action } from 'mobx'; +import MembersGroup from './MembersGroup'; +import { + fs, + path, + PATH_TO_CONTRACTS, + PATH_TO_DATA, +} from '../../constants/windowModules'; +import { readDataFromFile, writeDataToFile } from '../../utils/fileUtils/data-manager'; +import AsyncInterval from '../../utils/AsyncUtils'; +import { tokenTypes } from '../../constants'; + +/** + * Store for manage Members groups + * + * @param id + * @param data + * @param groupAddress + * @param admin + * @param address + * @param group + */ +class MembersStore { + transferSteps = { + input: 0, + transfering: 1, + success: 2, + error: 3, + } + + /** + * Create a member store + * + * @param {object} rootStore rootStore + */ + constructor(rootStore) { + this.rootStore = rootStore; + } + + @observable groups = []; + + @observable _transferStatus = 0; + + @observable loading = false; + + @action init() { + const { rootStore: { configStore: { UPDATE_INTERVAL } } } = this; + this.groups = []; + this.loading = true; + this.fetchUserGroups(); + this.asyncUpdater = new AsyncInterval({ + timeoutInterval: UPDATE_INTERVAL, + cb: async () => { + await this.fetchUserGroups(); + }, + }); + } + + async fetchUserGroupsLength() { + const { contractService } = this.rootStore; + return contractService.fetchUserGroupsLength(); + } + + async fetchUserGroups() { + await this.fetchUserGroupsLength() + .then((length) => this.getActualUserGroups(length)) + .then((groups) => this.getPrimaryGroupsInfo(groups)) + .then((groups) => this.getUsersBalances(groups)) + .then((groups) => { + groups.forEach((group) => { + this.addToGroups(group); + }); + this.loading = false; + }) + .catch((err) => { + console.error(err); + this.loading = false; + }); + } + + async getUserGroups(length) { + const { contractService } = this.rootStore; + const groups = []; + for (let i = 0; i < length; i += 1) { + const group = await contractService.callMethod('getUserGroup', i); + groups.push(group); + } + return groups; + } + + /** + * Method for getting groups from file + * without duplicated item + * + * @returns {Array} correct array of groups + */ + async getGroupsFromFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + const groups = []; + try { + const groupsFromFile = await readDataFromFile({ + name: 'groups', + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + const groupsFromFileLength = groupsFromFile.data && groupsFromFile.data.length + ? groupsFromFile.data.length + : 0; + for (let i = 0; i < groupsFromFileLength; i += 1) { + const group = groupsFromFile.data[i]; + if (group) { + const duplicateGroup = groups.find((item) => item.name === group.name); + if (!duplicateGroup) groups.push(group); + } + } + } catch { + return groups; + } + return groups; + } + + /** + * Method for write groups to file + * + * @param {Array} groups array of groups + */ + writeGroupsToFile(groups) { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + writeDataToFile({ + name: 'groups', + data: { + data: groups, + }, + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + } + + /** + * Method for getting groups from file + * or from contract + * + * @param {number} length length groups + * @returns {Array} actual groups data + */ + async getActualUserGroups(length) { + // Groups FROM FILE + let groups = await this.getGroupsFromFile(); + // Groups FROM CONTRACT + if ( + !groups + || !groups.length + || !groups.length < length + ) { + groups = await this.getUserGroups(length); + this.writeGroupsToFile(groups); + return groups; + } + return groups; + } + + async getPrimaryGroupsInfo(groups) { + const { Web3Service, userStore } = this.rootStore; + for (let i = 0; i < groups.length; i += 1) { + const group = groups[i]; + const abi = fs.readFileSync( + path.join(PATH_TO_CONTRACTS, group.groupType === tokenTypes.ERC20 ? './ERC20.abi' : './CustomToken.abi'), + ); + const contract = Web3Service.createContractInstance(JSON.parse(abi)); + contract.options.address = await group.groupAddress; + group.contract = contract; + group.totalSupply = await contract.methods.totalSupply().call(); + group.tokenSymbol = await contract.methods.symbol().call(); + group.users = group.groupType === tokenTypes.ERC20 + ? [userStore.address] + : await contract.methods.getHolders().call(); + group.groupId = i; + // eslint-disable-next-line no-param-reassign + groups[i] = group; + } + return groups; + } + + // eslint-disable-next-line class-methods-use-this + async getUsersBalances(groups) { + for (let i = 0; i < groups.length; i += 1) { + const group = groups[i]; + const { contract, groupType } = group; + group.members = []; + const admin = groupType === tokenTypes.Custom + ? await contract.methods.owner().call() + : null; + + for (let j = 0; j < group.users.length; j += 1) { + const user = group.users[j]; + const balance = await contract.methods.balanceOf(user).call(); + group.members.push({ + wallet: user, + balance, + weight: (balance / Number(group.totalSupply)) * 100, + customTokenName: group.tokenSymbol, + isAdmin: admin !== null + ? user === admin + : false, + }); + } + } + return groups; + } + + @action + /** + * Method for adding new group + * + * @param {object} group data for group + */ + addToGroups = (group) => { + const { userStore, configStore: { UPDATE_INTERVAL } } = this.rootStore; + const duplicateMembersGroup = this.groups.find((item) => item.name === group.name); + if (!duplicateMembersGroup) { + this.groups.push(new MembersGroup({ + ...group, + interval: UPDATE_INTERVAL, + userAddress: userStore.address, + })); + } + } + + @action + isUserInGroup(groupId, address) { + // eslint-disable-next-line max-len + const memberItem = this.groups[groupId].list.filter((user) => (user.wallet).toUpperCase() === address.toUpperCase()); + return memberItem.length > 0 ? this.groups[groupId] : null; + } + + @action setTransferStatus(status) { + this._transferStatus = this.transferSteps[status]; + } + + @action + transferTokens(groupId, from, to, count) { + const { rootStore: { appStore } } = this; + const { contract, groupType } = this.list[groupId]; + window.contract = contract; + console.log('contract', contract); + // eslint-disable-next-line no-unused-vars + const { Web3Service, userStore: { address, password }, userStore } = this.rootStore; + const data = groupType === tokenTypes.ERC20 + ? contract.methods.transfer(to, Number(count)).encodeABI() + : contract.methods.transferFrom(from, to, Number(count)).encodeABI(); + const txData = { + data, + from: userStore.address, + to: contract.options.address, + value: '0x0', + }; + appStore.setTransactionStep('compileOrSign'); + return new Promise((resolve, reject) => { + Web3Service.createTxData(address, txData) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + appStore.setTransactionStep('success'); + userStore.getEthBalance(); + resolve(); + }) + .catch((error) => { + reject(error); + }); + }); + } + + @action getMemberById = (id) => { + if (!this.list) return {}; + const [group] = this.list.filter((groupItem) => groupItem.groupId === Number(id)); + return group; + } + + @action getMemberGroupByAddress = (address) => { + if (!this.list) return {}; + const [group] = this.list.filter((groupItem) => ( + groupItem.wallet.toLowerCase() === address.toLowerCase() + )); + return group; + } + + getAddressesForAdminDesignate = (data) => new Promise((resolve) => { + const { Web3Service: { web3: { eth } } } = this.rootStore; + const parameters = ['tuple(uint,uint,uint,uint,uint)', 'address', 'address']; + resolve(Object.values(eth.abi.decodeParameters(parameters, data))); + }); + + @action + updateAdmin = (groupAddress) => { + const groups = this.groups.filter((group) => group.wallet === groupAddress); + groups.forEach((group) => { group.setNewAdmin(); }); + } + + @action + reset = () => { + this.asyncUpdater.cancel(); + this.groups.forEach((group) => { group.stopInterval(); }); + this.groups = []; + this._transferStatus = 0; + this.loading = true; + } + + @computed + get transferStatus() { + return this._transferStatus; + } + + @computed + get nonERC() { + return this.groups.filter((group) => group.groupType !== tokenTypes.ERC20) + .map((group) => ({ label: group.name, value: group.wallet })); + } + + @computed + get list() { + return this.groups; + } +} + +export default MembersStore; diff --git a/src/stores/MembersStore/MembersStore.test.js b/src/stores/MembersStore/MembersStore.test.js new file mode 100644 index 00000000..99fbce7c --- /dev/null +++ b/src/stores/MembersStore/MembersStore.test.js @@ -0,0 +1,53 @@ +import MembersStore from './MembersStore'; + +describe('MembersStore', () => { + const defaultGroup = { + name: 'Admins', + description: 'short description for group', + customTokenName: 'TKN', + tokenName: 'ERC20', + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + }, + ], + }; + + describe('correct input data', () => { + let memberStore; + + beforeEach(() => { + memberStore = new MembersStore([ + defaultGroup, + ]); + }); + + it('should create without error', () => { + expect(memberStore).toBeTruthy(); + }); + + it('list length should be equal 1', () => { + expect(memberStore.list.length).toEqual(1); + }); + }); + + it('should cause error with incorrect data', () => { + expect( + () => (new MembersStore({ data: 1 })), + ).toThrow(new Error('Incorrect groups provided')); + }); + + it('store with textEmptyForState should be correct', () => { + const memberStore = new MembersStore([ + { + ...defaultGroup, + textEmptyForState: 'text for empty', + }, + ]); + expect(memberStore).toBeTruthy(); + }); +}); diff --git a/src/stores/MembersStore/index.js b/src/stores/MembersStore/index.js new file mode 100644 index 00000000..82c4ced3 --- /dev/null +++ b/src/stores/MembersStore/index.js @@ -0,0 +1,7 @@ +import MembersStore from './MembersStore'; + +export default MembersStore; + +export { + MembersStore, +}; diff --git a/src/stores/NotificationStore/NotificationItem.js b/src/stores/NotificationStore/NotificationItem.js new file mode 100644 index 00000000..46bd2991 --- /dev/null +++ b/src/stores/NotificationStore/NotificationItem.js @@ -0,0 +1,50 @@ +import { observable, action } from 'mobx'; + +const defaultOpenState = false; +const defaultStatus = 'info'; + +/** Class for manage notifications */ +class NotificationItem { + constructor(props) { + const { + isOpen, + content, + id, + status, + } = props; + if (!id && id !== 0) throw Error('Incorrect NotificationItem "id" provided'); + this.id = id; + if (!content) throw Error('Incorrect NotificationItem "content" provided'); + this.content = content; + this.status = status || defaultStatus; + this.setIsOpen(isOpen || defaultOpenState); + } + + /** + * Id notification + */ + @observable id; + + /** Notification is open state */ + @observable isOpen = defaultOpenState; + + /** Notification content */ + @observable content; + + /** Notification status. [info, important] */ + @observable status; + + // TODO add notification name for simple manage notification + + /** + * Set new is open state + * + * @param {boolean} newState new is open state + */ + @action + setIsOpen(newState) { + this.isOpen = Boolean(newState); + } +} + +export default NotificationItem; diff --git a/src/stores/NotificationStore/NotificationItem.test.js b/src/stores/NotificationStore/NotificationItem.test.js new file mode 100644 index 00000000..e2f2a8de --- /dev/null +++ b/src/stores/NotificationStore/NotificationItem.test.js @@ -0,0 +1,39 @@ +import NotificationItem from './NotificationItem'; + +describe('NotificationItem', () => { + let notificationItem; + + beforeEach(() => { + notificationItem = new NotificationItem({ + id: 0, + content: 'test', + }); + }); + + it('should have correct init state', () => { + expect(notificationItem.id).toEqual(0); + expect(notificationItem.isOpen).toEqual(false); + expect(notificationItem.content).toEqual('test'); + expect(notificationItem.content).toEqual('test'); + }); + + it('setIsOpen should work correct', () => { + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen(true); + expect(notificationItem.isOpen).toEqual(true); + notificationItem.setIsOpen(false); + expect(notificationItem.isOpen).toEqual(false); + }); + + it('setIsOpen should work correct with incorrect input data', () => { + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen(null); + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen(1); + expect(notificationItem.isOpen).toEqual(true); + notificationItem.setIsOpen(undefined); + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen('true'); + expect(notificationItem.isOpen).toEqual(true); + }); +}); diff --git a/src/stores/NotificationStore/NotificationStore.js b/src/stores/NotificationStore/NotificationStore.js new file mode 100644 index 00000000..0e996312 --- /dev/null +++ b/src/stores/NotificationStore/NotificationStore.js @@ -0,0 +1,54 @@ +import { observable, action } from 'mobx'; +import uniqKey from 'react-id-generator'; +import NotificationItem from './NotificationItem'; + +/** Class for manage notifications */ +class NotificationStore { + /** Notification list */ + @observable list = []; + + /** + * Method for adding new notification + * + * @param {object} newNotification new notification data + * @param {boolean} newNotification.isOpen open state notification + * @param {string|Node} newNotification.content content notification + */ + @action + add(newNotification) { + this.list.push( + new NotificationItem({ + ...newNotification, + id: uniqKey(), + }), + ); + } + + getNotification(id) { + return [this.list.filter((notification) => ( + notification.id === id + ))]; + } + + // TODO add manage notification by name after refactor NotificationItem + + /** + * Method fore removing notification from list + * + * @param {string} id id notification + */ + @action + remove(id) { + const filtered = this.list.filter((value) => ( + value.id !== id + )); + this.list = filtered; + } + + @action + reset = () => { + this.list = []; + } +} + +export default NotificationStore; diff --git a/src/stores/NotificationStore/NotificationStore.test.js b/src/stores/NotificationStore/NotificationStore.test.js new file mode 100644 index 00000000..0aaebf1a --- /dev/null +++ b/src/stores/NotificationStore/NotificationStore.test.js @@ -0,0 +1,55 @@ +import NotificationStore from '.'; + +describe('NotificationStore', () => { + let notificationStore; + + beforeEach(() => { + notificationStore = new NotificationStore(); + }); + + it('should have correct init state', () => { + expect(notificationStore.list).toEqual([]); + }); + + it('add method should work correct', () => { + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(1); + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(2); + }); + + it('remove method should work correct', () => { + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(2); + const { id } = notificationStore.list[0]; + notificationStore.remove(id); + expect(notificationStore.list.length).toEqual(1); + }); + + it('remove for non-exist notification should work correct', () => { + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(2); + notificationStore.remove('random_identificator_1'); + expect(notificationStore.list.length).toEqual(2); + }); +}); diff --git a/src/stores/NotificationStore/index.js b/src/stores/NotificationStore/index.js new file mode 100644 index 00000000..97db3ca5 --- /dev/null +++ b/src/stores/NotificationStore/index.js @@ -0,0 +1,3 @@ +import NotificationStore from './NotificationStore'; + +export default NotificationStore; diff --git a/src/stores/PaginationStore/PaginationStore.js b/src/stores/PaginationStore/PaginationStore.js new file mode 100644 index 00000000..bd32c7d1 --- /dev/null +++ b/src/stores/PaginationStore/PaginationStore.js @@ -0,0 +1,65 @@ +import { observable, action, computed } from 'mobx'; + +const DEFAULT_ITEMS_PER_PAGE = 5; +const DEFAULT_PAGE_RANGE = 5; + +class PaginationStore { + constructor({ + totalItemsCount, + itemsCountPerPage, + pageRangeDisplayed, + }) { + this.totalItemsCount = totalItemsCount; + this.itemsCountPerPage = itemsCountPerPage || DEFAULT_ITEMS_PER_PAGE; + this.pageRangeDisplayed = pageRangeDisplayed || DEFAULT_PAGE_RANGE; + } + + @observable activePage = 1; + + @computed + get lastPage() { + return Math.ceil(this.totalItemsCount / this.itemsCountPerPage); + } + + @observable itemsCountPerPage; + + @observable totalItemsCount; + + @observable pageRangeDisplayed; + + @action + update = (newState) => { + Object.keys(newState).forEach((key) => { + this[key] = newState[key]; + }); + } + + @action + handleChange = (page) => { + let activePage = page; + if (page > this.lastPage) activePage = this.lastPage; + if (page < 1) activePage = 1; + this.activePage = activePage; + } + + getCurrentPage() { + return this.activePage; + } + + /** + * Method for getting pagination range + * + * @returns {Array} [lowRange, highRange] + * lowRange is index first element for current activePage pagination + * highRange is index last element for current activePage pagination + */ + @computed + get paginationRange() { + const { activePage, itemsCountPerPage } = this; + const lowRange = (activePage - 1) * itemsCountPerPage; + const highRange = (activePage) * itemsCountPerPage - 1; + return [lowRange, highRange]; + } +} + +export default PaginationStore; diff --git a/src/stores/PaginationStore/PaginationStore.test.js b/src/stores/PaginationStore/PaginationStore.test.js new file mode 100644 index 00000000..004a25fa --- /dev/null +++ b/src/stores/PaginationStore/PaginationStore.test.js @@ -0,0 +1,37 @@ +import PaginationStore from '.'; + +describe('PaginationStore', () => { + it('should be with correct data for total 101 & items per page 10', () => { + const pagination = new PaginationStore({ + totalItemsCount: 101, + }); + expect(pagination.lastPage).toEqual(11); + expect(pagination.activePage).toEqual(1); + expect(pagination.pageRangeDisplayed).toEqual(5); + }); + + it('should be with correct data for total 21 & items per page 2', () => { + const pagination = new PaginationStore({ + totalItemsCount: 21, + itemsCountPerPage: 2, + }); + expect(pagination.lastPage).toEqual(11); + expect(pagination.activePage).toEqual(1); + expect(pagination.pageRangeDisplayed).toEqual(5); + }); + + it('handleChange should set correct page', () => { + const pagination = new PaginationStore({ + totalItemsCount: 100, + itemsCountPerPage: 10, + }); + expect(pagination.activePage).toEqual(1); + pagination.handleChange(0); + expect(pagination.activePage).toEqual(1); + pagination.handleChange(5); + expect(pagination.activePage).toEqual(5); + expect(pagination.lastPage).toEqual(10); + pagination.handleChange(11); + expect(pagination.activePage).toEqual(10); + }); +}); diff --git a/src/stores/PaginationStore/index.js b/src/stores/PaginationStore/index.js new file mode 100644 index 00000000..f058c5fb --- /dev/null +++ b/src/stores/PaginationStore/index.js @@ -0,0 +1,3 @@ +import PaginationStore from './PaginationStore'; + +export default PaginationStore; diff --git a/src/stores/ProjectStore/ProjectStore.js b/src/stores/ProjectStore/ProjectStore.js index 063499fc..5bf2fc17 100644 --- a/src/stores/ProjectStore/ProjectStore.js +++ b/src/stores/ProjectStore/ProjectStore.js @@ -1,28 +1,72 @@ +/* eslint-disable no-unused-vars */ import { observable, action } from 'mobx'; import UsergroupStore from '../UsergroupStore'; import QuestionStore from '../QuestionStore'; import HistoryStore from '../HistoryStore'; import { votingStates } from '../../constants'; +import { fs, path, PATH_TO_CONTRACTS } from '../../constants/windowModules'; /** * Class implements whole project */ class ProjectStore { - @observable projectAddress = '' + @observable projectAddress = ''; + + @observable name = ''; @observable prepared = 0; + @observable votingData = ''; + + @observable votingQuestion = ''; + + @observable votingGroupId = ''; + @observable userGrops = []; @observable questionStore; @observable historyStore; - constructor(projectAddress) { - this.projectAddress = projectAddress; - this.questionStore = new QuestionStore(projectAddress); - this.historyStore = new HistoryStore(projectAddress); - this.userGrops = this.fetchUserGroups(projectAddress); + @observable isInitiated = true; + + timer = null; + + constructor(rootStore) { + this.rootStore = rootStore; + try { + this.projectAbi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'))); + } catch { + alert(`Error occuried when trying to read ${path.join(PATH_TO_CONTRACTS, './ZeroOne.abi')}. Please check it.`); + } + } + + @action init({ address, name }) { + const { contractService, Web3Service, membersStore } = this.rootStore; + const contract = Web3Service.createContractInstance(this.projectAbi); + this.name = name; + this.isInitiated = false; + contract.options.address = address; + contractService.setContract(contract); + this.questionStore = new QuestionStore(this.rootStore); + this.historyStore = new HistoryStore(this.rootStore); + membersStore.init(); + this.timer = setInterval(() => { + this.getInitStatus(); + }, 1000); + } + + @action getInitStatus() { + const { rootStore } = this; + if (this.questionStore && this.historyStore && rootStore.membersStore) { + const { membersStore } = rootStore; + const { questionStore, historyStore } = this; + this.isInitiated = !(questionStore.loading || historyStore.loading || membersStore.loading); + if (this.isInitiated) { + clearInterval(this.timer); + this.timer = null; + } + } } /** @@ -32,10 +76,17 @@ class ProjectStore { this.prepared = votingStates.active; } + @action setVotingData(questionId, groupId, data) { + this.votingQuestion = questionId; + this.votingGroupId = groupId; + this.votingData = data; + } + /** * Preparing app for start voting + * * @param {number} questionId - * @param {array} parameters + * @param {Array} parameters */ @action prepareVoting(questionId, parameters) { /** @@ -58,14 +109,29 @@ class ProjectStore { /** * getting usergroups from contract + * * @param {number} projectAddress address of project - * @return {array} list of usergroups */ @action fetchUserGroups = (projectAddress) => { this.fetchUserGroupsLength(projectAddress); const data = {}; this.userGrops.push(new UsergroupStore(data)); } + + @action + reset = () => { + clearInterval(this.timer); + this.timer = null; + this.projectAddress = ''; + this.name = ''; + this.prepared = 0; + this.votingData = ''; + this.votingQuestion = ''; + this.votingGroupId = ''; + this.userGrops = []; + this.questionStore = null; + this.historyStore = null; + } } export default ProjectStore; diff --git a/src/stores/QuestionStore/QuestionStore.js b/src/stores/QuestionStore/QuestionStore.js index aea16405..8a46a0f9 100644 --- a/src/stores/QuestionStore/QuestionStore.js +++ b/src/stores/QuestionStore/QuestionStore.js @@ -1,62 +1,348 @@ import { observable, action, computed } from 'mobx'; import Question from './entities/Question'; +import { readDataFromFile, writeDataToFile } from '../../utils/fileUtils/data-manager'; +import { PATH_TO_DATA } from '../../constants/windowModules'; +import FilterStore from '../FilterStore/FilterStore'; +import PaginationStore from '../PaginationStore'; +import AsyncInterval from '../../utils/AsyncUtils'; /** * Contains methods for working + * + * @param id */ class QuestionStore { + @observable pagination; + + /** List models Question */ @observable _questions; - constructor(projectAddress) { + @observable _questionGroups; + + @observable loading = false; + + @observable filter; + + constructor(rootStore) { this._questions = []; - this.fetchQuestionsCount(projectAddress); + this._questionGroups = []; + this.rootStore = rootStore; + const { configStore: { UPDATE_INTERVAL } } = rootStore; + this.loading = false; + this.filter = new FilterStore(); + this.interval = new AsyncInterval({ + cb: async () => { + await this.getActualState(() => { + this.pagination = new PaginationStore({ + totalItemsCount: this.list.length, + }); + }); + }, + timeoutInterval: UPDATE_INTERVAL, + }); } /** - * Recieving questions count for fetching them from contract + * Getting list of questions for displaying + * * @function - * @param {string} address user address - * @returns {number} count of questions + * @returns {Array} list of all questions + */ + @computed get questions() { + return this._questions; + } + + /** + * Get raw questions list + * + * @returns {Array} raw questions list + */ + get rawList() { + return this._questions.map((question) => ({ + ...question.raw, + })); + } + + /** + * Get list questions + * + * @returns {Array} filtered by rules + * questions list + */ + @computed + get list() { + return this.filter.filteredList(this._questions); + } + + /** + * Get paginated list questions + * + * @returns {Array} paginated questions list */ - @action fetchQuestionsCount = (address) => address + @computed + get paginatedList() { + let range; + if (!this.pagination || !this.pagination.paginationRange) { + range = [0, 5]; + } else { + range = this.pagination.paginationRange; + } + return this.list.slice(range[0], range[1] + 1); + } + + @computed get options() { + return this._questions.reduce((acc, question) => ([ + ...acc, + { + value: question.id, + label: question.name, + }, + ]), [{ + value: '*', + label: 'All', + }]); + } + + @computed get newVotingOptions() { + return this._questions.map((question) => ( + { + value: question.id, + label: question.name, + } + )); + } + + @computed get questionGroups() { + return this._questionGroups.reduce((acc, group) => ([ + ...acc, + { + value: group.groupId, + label: group.name, + }, + ]), [{ + value: '*', + label: 'All', + }]); + } + + @computed get questionGroupsForVoting() { + return this._questionGroups.map((group) => ( + { + value: group.groupId, + label: group.name, + })); + } /** - * Recieving question from contract + * Method for getting actual state for + * this store. + * + * @param {Function} cb callback function + */ + @action + async getActualState(cb) { + await this.fetchActualQuestionGroups(); + await this.getActualQuestions(); + this.loading = false; + if (cb) cb(); + } + + /** + * Method for getting question groups + * from contract + */ + @action + async fetchActualQuestionGroups() { + const { contractService } = this.rootStore; + const localGroupsLength = this._questionGroups.length; + const contractGroupsLength = Number(await contractService.callMethod('getQuestionGroupsAmount')); + if (localGroupsLength < contractGroupsLength) { + for (let i = localGroupsLength; i < contractGroupsLength; i += 1) { + // eslint-disable-next-line no-await-in-loop + const element = await contractService.callMethod('getQuestionGroup', i); + const [groupName] = element; + const groupId = i; + this._questionGroups[groupId] = { groupId, name: groupName }; + } + } + } + + /** + * fetching questions from smart contract + * * @function - * @param {string} address user address */ - @action fetchQuestions = (address) => { - this.fetchQuestionsCount(address); - /** - * gets the question - */ - this.addQuestion(); + @action + async fetchQuestions() { + const { contractService } = this.rootStore; + const data = await contractService.checkQuestions(); + const countOfUploaded = Number(data.countOfUploaded); + for (let i = 0; i < countOfUploaded; i += 1) { + // eslint-disable-next-line no-await-in-loop + const question = await contractService.fetchQuestion(i); + question.groupId = Number(question.groupId); + if (question.name !== '') this.addQuestion(i, question); + } + } + + /** + * Method for getting questions from contract + * & save then to json file + */ + async getQuestionsFromContract() { + await this.fetchQuestions(); + this.writeQuestionsToFile(); + } + + /** + * Method for getting & adding questions from file + * without duplicated item + * + * @returns {Array} correct array of questions + */ + async getQuestionsFromFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + const questions = []; + try { + const questionsFromFile = await readDataFromFile({ + name: 'questions', + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + const questionsFromFileLength = questionsFromFile.data && questionsFromFile.data.length + ? questionsFromFile.data.length + : 0; + for (let i = 0; i < questionsFromFileLength; i += 1) { + const question = questionsFromFile.data[i]; + if (question) { + const duplicateQuestion = questions.find((item) => item.caption === question.name); + if (!duplicateQuestion) questions.push(question); + } + } + } catch { + return questions; + } + return questions; + } + + /** + * Get & add questions that are not in the file, + * but are in the contract + * + * @param {Array} questions array of questions + */ + async getMissingQuestions(questions) { + const firstQuestionIndex = 0; + const { contractService } = this.rootStore; + const { countOfUploaded } = await contractService.checkQuestions(); + const questionsFromFileLength = questions.length; + const countQuestionFromContract = countOfUploaded - firstQuestionIndex; + if (countQuestionFromContract > questionsFromFileLength) { + this.getQuestionsFromContract(); + } + } + + /** Write raw voting list data to file */ + writeQuestionsToFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + writeDataToFile({ + name: 'questions', + data: { + data: this.rawList, + }, + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + } + + /** + * Method for getting actual question + * from the contract & file + */ + async getActualQuestions() { + const questions = await this.getQuestionsFromFile(); + this.writeQuestionsListToState(questions); + if (!questions || !questions.length) { + await this.getQuestionsFromContract(); + return; + } + await this.getMissingQuestions(questions); + } + + /** + * Method for write questions list to + * this state + * + * @param {Array} questionsList questions list + */ + writeQuestionsListToState(questionsList) { + const questionsListLength = questionsList.length; + for (let i = 0; i < questionsListLength; i += 1) { + const question = questionsList[i]; + this.addQuestion(i, question); + } + } + + /** + * Add new filter rule + * + * @param {object} rule object with rules + */ + addFilterRule = (rule) => { + this.filter.addFilterRule(rule); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** + * Method for reset filter + * & update pagination + */ + resetFilter = () => { + this.filter.reset(); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } } /** * Adding question to the list + * * @function + * @param {number} id id question * @param {object} question Question which will be added */ - @action addQuestion = (question) => { - this._questions.push(new Question(question)); + @action addQuestion = (id, question) => { + const duplicatedQuestion = this._questions.find((item) => item.name === question.name); + if (!duplicatedQuestion) this._questions.push(new Question(id, question)); } /** * Getting question by given id + * * @function * @param {number} id id of question - * @returns {object} question matched by id + * @returns {Array} array with lenght == 1, contains question matched by id */ - @action getQuestionById = (id) => this._questions.filter((question) => question.id === id) + @action getQuestionById = (id) => this._questions.filter((question) => question.id === Number(id)) - /** - * Getting list of questions for displaying - * @function - * @returns {Array} list of all questions - */ - @computed get questions() { - return this._questions; + @action getQuestionGroupById = (id) => ( + this._questionGroups.filter((group) => group.groupId === id) + ) + + @action reset = () => { + this._questions = []; + this._questionGroups = []; + this.interval.cancel(); } } diff --git a/src/stores/QuestionStore/entities/Question.js b/src/stores/QuestionStore/entities/Question.js index e5bbe555..914308ee 100644 --- a/src/stores/QuestionStore/entities/Question.js +++ b/src/stores/QuestionStore/entities/Question.js @@ -1,21 +1,78 @@ class Question { + raw; + + id; + + name; + + groupId; + + description; + + methodSelector; + + status; + + target; + + paramNames; + + paramTypes; + + formula; + /** - * @constructor - * @param {object} data data about question - * @param {number} data.id question id - * @param {number} data.groupId id of group, which can start voting for this question - * @param {string} data.caption question caption - * @param {string} data.text description of the question - * @param {Array} data.params parameters which will be used after voting + * @class + * @param {string} id id question + * @param {object} question data about question + * @param {number} question.groupId id of group, which can start voting for this question + * @param {string} question.name question name + * @param {string} question.description description of the question + * @param {Array} question.paramNames array contains parameters names which + * will be used after voting + * @param {Array} question.paramNames array contains parameters types which + * will be used after voting + * @param {string} question.rawFormula formula string + * @param {string} question.target address, which method will be called after end of the voting + * @param {string} question.methodSelector hex (4 bytes) - function signature of target contract + * @param {number} question.status status of question: 0 - can't start voting, + * 1 - can start voting */ - constructor({ - id, groupId, caption, text, params, - }) { + constructor(id, question) { + const { + groupId, + name, + description, + target, + timeLimit: time, + active: status, + methodSelector, + rawFormula, + paramNames, + paramTypes, + } = question; + + this.raw = question; + this.id = id; - this.caption = caption; - this.groupId = groupId; - this.text = text; - this.params = params; + this.name = name; + this.groupId = Number(groupId); + this.description = description; + this.time = time; + this.methodSelector = methodSelector; + this.status = status; + this.target = target; + this.paramNames = paramNames; + this.paramTypes = paramTypes; + this.formula = rawFormula; + } + + getParameters() { + return this.params.map((param) => param[1]); + } + + getFormula() { + return this.formula; } } diff --git a/src/stores/RootStore/RootStore.js b/src/stores/RootStore/RootStore.js index b564760c..c03e1888 100644 --- a/src/stores/RootStore/RootStore.js +++ b/src/stores/RootStore/RootStore.js @@ -3,11 +3,14 @@ import AppStore from '../AppStore'; import UserStore from '../UserStore'; import ProjectStore from '../ProjectStore'; import DialogStore from '../DialogStore'; +import NotificationStore from '../NotificationStore'; import Web3Service from '../../services/Web3Service'; import WalletService from '../../services/WalletService'; import ContractService from '../../services/ContractService'; +import { MembersStore } from '../MembersStore'; import EventEmitterService from '../../services/EventEmitterService'; -import { fs, path, ROOT_DIR } from '../../constants/windowModules'; +// import { fs, path, ROOT_DIR } from '../../constants/windowModules'; +import ConfigStore from '../ConfigStore'; class RootStore { // stores @@ -19,6 +22,8 @@ class RootStore { dialogStore; + notificationStore; + // services walletService; @@ -29,23 +34,26 @@ class RootStore { eventEmitterService; constructor() { - const configRaw = fs.readFileSync(path.join(ROOT_DIR, './config.json'), 'utf8'); - const config = JSON.parse(configRaw); - this.Web3Service = new Web3Service(config.host, this); + this.configStore = new ConfigStore(); + this.Web3Service = new Web3Service(this.configStore.config.host, this); this.appStore = new AppStore(this); this.userStore = new UserStore(this); + this.projectStore = new ProjectStore(this); this.walletService = new WalletService(); this.eventEmitterService = new EventEmitterService(); this.contractService = new ContractService(this); this.dialogStore = new DialogStore(); + this.membersStore = new MembersStore(this); + this.notificationStore = new NotificationStore(this); } /** * initiating project + * * @param {string} address adress of project */ - @action initProject(address) { - this.projectStore = new ProjectStore(address); + @action async initProject({ address, name }) { + this.projectStore.init({ address, name }); } } diff --git a/src/stores/UserStore/UserStore.js b/src/stores/UserStore/UserStore.js index 2c8c684c..56d14ee5 100644 --- a/src/stores/UserStore/UserStore.js +++ b/src/stores/UserStore/UserStore.js @@ -1,6 +1,8 @@ import { observable, action, computed } from 'mobx'; import { Transaction as Tx } from 'ethereumjs-tx'; import i18n from 'i18next'; +import weiToFixed from '../../utils/EthUtils/wei-to-fixed'; +import AsyncInterval from '../../utils/AsyncUtils'; /** * Describes store with user data */ @@ -23,20 +25,34 @@ class UserStore { @observable password = ''; + @observable currency = 'ETH'; + + @observable fullCurrencyName = 'ether'; + + updateBalanceInterval = null; + constructor(rootStore) { this.rootStore = rootStore; } + @computed + get userBalance() { + return `${weiToFixed(this.balance, this.fullCurrencyName)} ${this.currency}`; + } + /** * saves password to store for decoding wallet and transaction signing - *@param {string} value password from form - */ +param {string} value password from form + * + * @param {string} value new pass value + */ @action setPassword(value) { this.password = value; } /** * saves v3 keystore and wallet address to store + * * @param {object} wallet JSON Keystore V3 */ @action setEncryptedWallet(wallet) { @@ -45,6 +61,7 @@ class UserStore { /** * checking Ethereum balance for given address + * * @param {string} address wallet adddress * @returns {Promise} resolves with balance rounded to 5 decimal places */ @@ -59,8 +76,9 @@ class UserStore { /** * create wallet by given password + * * @param {string} password password which will be used for wallet decrypting - * @return {Promise} resolves on success with {v3wallet, mnemonic, privateKey, walletName} + * @returns {Promise} resolves on success with {v3wallet, mnemonic, privateKey, walletName} */ @action createWallet(password) { return new Promise((resolve, reject) => { @@ -83,13 +101,14 @@ class UserStore { /** * recovering wallet by mnemonic + * * @param {string} password * @returns {Promise} resolves with {v3wallet, privateKey} */ - @action recoverWallet() { + @action recoverWallet(password = undefined) { const seed = this._mnemonicRepeat.join(' '); return new Promise((resolve, reject) => { - this.rootStore.walletService.createWallet(undefined, seed).then((data) => { + this.rootStore.walletService.createWallet(password, seed).then((data) => { if (data.v3wallet) { const { v3wallet, mnemonic, privateKey, walletName, @@ -108,16 +127,22 @@ class UserStore { /** * method for authorize wallet for working with projects + * * @param {string} password password for wallet * @returns {Promise} resolve on success authorization */ @action login(password) { - const { appStore } = this.rootStore; + const { appStore, configStore: { UPDATE_INTERVAL } } = this.rootStore; return this.readWallet(password) .then((data) => { this.privateKey = data.privateKey; this.setEncryptedWallet(JSON.parse(data.wallet)); this.authorized = true; + this.setPassword(password); + this.updateBalanceInterval = new AsyncInterval({ + timeoutInterval: UPDATE_INTERVAL, + cb: this.getEthBalance, + }); Promise.resolve(); }).catch(() => { appStore.displayAlert(i18n.t('errors:wrongPassword'), 3000); @@ -127,6 +152,7 @@ class UserStore { /** * read wallet for any operations with it + * * @param {string} password password for wallet * @returns {Promise} resolves with object {v3wallet, privateKey} */ @@ -140,8 +166,8 @@ class UserStore { } else { reject(); } - }).catch(() => { - reject(); + }).catch((err) => { + reject(err); }); }); } @@ -157,6 +183,7 @@ class UserStore { /** * checks is seed valid with walletService + * * @param {string} mnemonic mnemonic * @returns {bool} is valid */ @@ -167,13 +194,16 @@ class UserStore { /** * Signing transactions with private key + * * @function * @param {string} data rawTx * @param {string} password password which was used to encode Keystore V3 - * @return Signed TX data + * @returns Signed TX data */ - @action singTransaction(data, password) { - return new Promise((resolve) => { + @action async singTransaction(data, password) { + const { rootStore: { Web3Service: { web3: { eth } } } } = this; + const chainId = await eth.net.getId(); + return new Promise((resolve, reject) => { // eslint-disable-next-line consistent-return this.readWallet(password).then((info) => { if (info instanceof Error) return false; @@ -181,16 +211,27 @@ class UserStore { info.privateKey, 'hex', ); - const tx = new Tx(data, { chain: 'ropsten' }); + // const customCommon = Common.forCustomChain( + // 'mainnet', + // { + // chainId, + // }, + // 'byzantium', + // ); + const tx = new Tx(data, { chain: chainId }); tx.sign(privateKey); const serialized = tx.serialize().toString('hex'); resolve(serialized); - }); + }) + .catch((err) => { + reject(err); + }); }); } /** * Sending transaction from user + * * @function * @param {string} txData Raw transaction */ @@ -200,11 +241,15 @@ class UserStore { /** * Getting user Ethereum balance - * @return {number} balance in ETH + * + * @returns {number} balance in ETH */ - @action async getEthBalance() { + @action getEthBalance = async () => { const { Web3Service: { web3 } } = this.rootStore; - this.balance = await web3.eth.getBalance(this.address); + web3.eth.getBalance(this.address) + .then((result) => { + this.balance = result; + }); } @action setMnemonic(value) { @@ -227,6 +272,22 @@ class UserStore { @computed get mnemonic() { return this._mnemonic; } + + @action + reset = () => { + this.authorized = false; + this.redirectToProjects = false; + this.encryptedWallet = ''; + this.walletName = ''; + this.privateKey = ''; + this._mnemonic = Array(12); + this._mnemonicRepeat = Array(12); + this.balance = 0; + this.password = ''; + this.currency = 'ETH'; + this.fullCurrencyName = 'ether'; + this.updateBalanceInterval.cancel(); + } } export default UserStore; diff --git a/src/utils/AsyncUtils/index.js b/src/utils/AsyncUtils/index.js new file mode 100644 index 00000000..947b0033 --- /dev/null +++ b/src/utils/AsyncUtils/index.js @@ -0,0 +1,41 @@ +/** + * Class for calling cb by interval + */ +class AsyncInterval { + /** Updating is disabled */ + disabled = false; + + /** Timeout timer id */ + timerId; + + /** Interval update period */ + timeoutInterval; + + /** Callback function for calling by interval */ + cb; + + constructor({ + cb, + timeoutInterval, + }) { + this.cb = cb; + this.timeoutInterval = timeoutInterval; + this.start(); + } + + start = async () => { + if (this.disabled) return; + await this.cb(); + if (this.disabled) return; + this.timerId = setTimeout(() => { + this.start(); + }, this.timeoutInterval); + } + + cancel = () => { + this.disabled = true; + clearTimeout(this.timerId); + } +} + +export default AsyncInterval; diff --git a/src/utils/Date/Date.test.js b/src/utils/Date/Date.test.js new file mode 100644 index 00000000..9a73c512 --- /dev/null +++ b/src/utils/Date/Date.test.js @@ -0,0 +1,44 @@ +import moment from 'moment'; +import progressByDateRange from '.'; + +describe('progressByDateRange', () => { + it('progressByDateRange should throw without date', () => { + expect(progressByDateRange).toThrow(); + }); + + it('progressByDateRange for yesterday & tomorrow should be 50', () => { + const tomorrow = moment(new Date()).add(1, 'days').valueOf() / 1000; + const yesterday = moment(new Date()).add(-1, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: yesterday, + end: tomorrow, + })).toEqual(50); + }); + + it('progressByDateRange for yesterday & now should be 100', () => { + const now = moment(new Date()).valueOf() / 1000; + const yesterday = moment(new Date()).add(-1, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: yesterday, + end: now, + })).toEqual(100); + }); + + it('progressByDateRange for -2 & -1 day should be 100', () => { + const day1 = moment(new Date()).add(-2, 'days').valueOf() / 1000; + const day2 = moment(new Date()).add(-1, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: day1, + end: day2, + })).toEqual(100); + }); + + it('progressByDateRange for +1 & +2 day should be 0', () => { + const day1 = moment(new Date()).add(1, 'days').valueOf() / 1000; + const day2 = moment(new Date()).add(2, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: day1, + end: day2, + })).toEqual(0); + }); +}); diff --git a/src/utils/Date/index.js b/src/utils/Date/index.js new file mode 100644 index 00000000..552709dd --- /dev/null +++ b/src/utils/Date/index.js @@ -0,0 +1,82 @@ +import moment from 'moment'; + +/** + * Method for get progress by date range + * + * @param {object} date date range in sec + * @param {string} date.start start range date in sec + * @param {string} date.end end range date in sec + * @returns {number} progress value NOW in % + */ +const progressByDateRange = (date) => { + if (!date || !date.start || !date.end) throw new Error('Incorrect date provided'); + const nowFormatted = moment().valueOf() / 1000; + const percentValue = (Number(date.end) - Number(date.start)) / 100; + const progressValue = Math.floor((nowFormatted - Number(date.start)) / percentValue); + if (progressValue < 0) return 0; + if (progressValue > 100) return 100; + return progressValue; +}; + +/** + * Method for getting correct moment + * locale name + * + * @param {string} locale locale for convert + * @returns {string} correct moment locale + */ +const getCorrectMomentLocale = (locale) => { + switch (locale) { + case 'RUS': + return 'ru'; + case 'ENG': + return 'en-gb'; + default: + return 'en-gb'; + } +}; + +/** + * Method for getting correct litepicker + * locale name + * + * @param {string} locale locale for convert + * @returns {string} correct locale + */ +const getCorrectPickerLocale = (locale) => { + switch (locale) { + case 'RUS': + return 'ru-RU'; + case 'ENG': + return 'en-US'; + default: + return 'en-US'; + } +}; + +/** + * Method for obtaining human-readable + * difference value for a given period + * of time + * + * @param {object} param0 date start & end + * @param {moment} param0.endDate moment js date object + * @param {moment} param0.startDate moment js date object + * @returns {string} readable period of time + */ +const getTimeLeftString = ({ + endDate, + startDate, +}) => { + const duration = endDate.diff(startDate); + return moment.duration(duration).humanize(); +}; + +export default progressByDateRange; + +export { + getTimeLeftString, + progressByDateRange, + getCorrectMomentLocale, + getCorrectPickerLocale, +}; diff --git a/src/utils/EthUtils/wei-to-fixed.js b/src/utils/EthUtils/wei-to-fixed.js new file mode 100644 index 00000000..49d5ed58 --- /dev/null +++ b/src/utils/EthUtils/wei-to-fixed.js @@ -0,0 +1,15 @@ +import { fromWei } from 'web3-utils'; + +export default (value = '0', currency = 'ether', options = {}) => { + const result = fromWei(value || '0', currency); + const floatPoint = result.indexOf('.'); + const zeros = floatPoint > -1 ? result.slice(floatPoint + 1, result.length) : null; + const maxFloats = { + ether: 4, + finney: 2, + szabo: 0, + ...options.maxFloats, + }; + const round = maxFloats[currency] || 0; + return zeros && zeros.length > round ? parseFloat(result).toFixed(round) : result; +}; diff --git a/src/utils/PasswordValidation/index.js b/src/utils/PasswordValidation/index.js index f2e2af90..92d0c469 100644 --- a/src/utils/PasswordValidation/index.js +++ b/src/utils/PasswordValidation/index.js @@ -1,8 +1,8 @@ const passwordValidation = (value) => { - const regexHigh = new RegExp(/^(?=[^A-Z]*[A-Z]).{1,}$/g); - const regexLow = new RegExp(/^(?=[^a-z]*[a-z]).{1,}$/g); - const regexNum = new RegExp(/^(?=[^0-9]*[0-9]).{1,}$/g); - const regexChar = new RegExp(/^(?=.*[!&$%&? "]).{1,}$/g); + const regexHigh = new RegExp(/[A-Z]/); + const regexLow = new RegExp(/[a-z]/); + const regexNum = new RegExp(/\d/g); + const regexChar = new RegExp(/[!&$%?"]/); const regexLength = new RegExp(/^.{6,}$/g); const values = { diff --git a/src/utils/Validator/index.js b/src/utils/Validator/index.js index cf5d328e..44e8b8b9 100644 --- a/src/utils/Validator/index.js +++ b/src/utils/Validator/index.js @@ -1,5 +1,6 @@ import validatorjs from 'validatorjs'; import i18n from 'i18next'; +import { languages } from '../../constants'; validatorjs.prototype.setAttributeNames = function setAttributeNames(attributes) { if (!attributes) return; @@ -14,22 +15,39 @@ validatorjs.prototype.setAttributeNames = function setAttributeNames(attributes) const rules = { password: { - function: (value) => value.match(/(?=.*[!@#$%^&*()_\-+=~])+(?=[a-z]*[A-Z]*[0-9]).{6,}/g), + function: (value) => value.match(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{6,}/), }, address: { function: (value) => value.match(/(0x)+([0-9 a-f A-F]){40}/g), }, + uint: { + // eslint-disable-next-line no-restricted-globals + function: (value) => !isNaN(Number(value)), + }, + uint8: { + // eslint-disable-next-line no-restricted-globals + function: (value) => !isNaN(Number(value)), + }, + bytes4: { + function: (value) => value.match(/(0x)+([0-9 a-f A-F]){8}/g), + }, + formula: { + function: (value) => value.match( + /\(\s*group\s*\(\s*[a-zA-Z0-9]{1,}\s*\)\s*=>\s*condition\s*\(\s*(quorum\s*(>=|<=)\s*[0-9]{1,} %\)\)|positive\s*(>=|<=)\s*[0-9]{1,}\s*% \s*of \s*(quorum|all)\s*\)\s*\))/, + ), + }, + url: { + // eslint-disable-next-line no-useless-escape + function: (value) => value.match(/^(?:(http(s)?|ws):\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/), + }, }; const plugins = { dvr: { package: validatorjs, extend: ({ validator }) => { + window.validator = validator; const { language } = i18n; - const languages = { - RUS: 'ru', - ENG: 'en', - }; Object.keys(rules).forEach( (key) => validator.register(key, rules[key].function, rules[key].message), ); @@ -39,12 +57,28 @@ const plugins = { same: 'Fields must be same', password: 'Field value not valid', address: 'Enter valid address', + numeric: 'Value is not numeric', + uint: 'Value is not numeric', + bytes4: 'Value is not bytes4 string', + between: 'Between :min and :max signs', + formula: 'Incorrect formula', + url: 'Not valid URL string', + min: 'Value less then :min', + max: 'Value larger then :max', }); validator.setMessages('ru', { required: 'Обязательное поле', same: 'Поля должны содержать одинаковые значения', password: 'Пароль не соответствует требованиям', address: 'Введите валидный адрес', + numeric: 'Значение не является числом', + uint: 'Значение не является числом', + bytes4: 'Значение не байтовая строка', + between: 'Между :min и :max знаками', + formula: 'Некорректная формула', + url: 'Неккоректный URL', + min: 'Значение меньше чем :min', + max: 'Значение больше чем :max', }); validator.stopOnError(true); }, diff --git a/src/utils/fileUtils/data-manager.js b/src/utils/fileUtils/data-manager.js new file mode 100644 index 00000000..12ae14b6 --- /dev/null +++ b/src/utils/fileUtils/data-manager.js @@ -0,0 +1,107 @@ +import { + fs, + path, + PATH_TO_DATA, +} from '../../constants/windowModules'; + +/** + * Method for creating directory with parents + * if needed + * + * @see https://stackoverflow.com/a/40686853/9965627 + * + * @param {string} targetDir target directory + * @returns {string} directory + */ +const mkDirByPathSync = (targetDir, { isRelativeToScript = false } = {}) => { + const { sep } = path; + const initDir = path.isAbsolute(targetDir) ? sep : ''; + const baseDir = isRelativeToScript ? __dirname : '.'; + + return targetDir.split(sep).reduce((parentDir, childDir) => { + const curDir = path.resolve(baseDir, parentDir, childDir); + try { + fs.mkdirSync(curDir); + } catch (err) { + if (err.code === 'EEXIST') { // curDir already exists! + return curDir; + } + + // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows. + if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure. + throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`); + } + + const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1; + if ( + !caughtErr + || (caughtErr && curDir === path.resolve(targetDir)) + ) { + throw err; // Throw if it's just the last created dir. + } + } + + return curDir; + }, initDir); +}; + +/** + * Method for write object data to file + * with some name + * + * @param {object} param0 data for writing + * @param {string} param0.name name to write to file + * @param {object} param0.data data object to write to a file + * @param {string} [param0.basicPath] basic path for write + */ +const writeDataToFile = async ({ + name, + data, + basicPath, +}) => { + const dataPath = path.join(basicPath || PATH_TO_DATA); + if (!fs.existsSync(path.join(PATH_TO_DATA))) { + await mkDirByPathSync(path.join(PATH_TO_DATA), { recursive: true }); + } + // Create folder for file, if folder does not exist + if (!fs.existsSync(dataPath)) { + await mkDirByPathSync(path.join(dataPath), { recursive: true }); + } + fs.writeFileSync( + path.join(basicPath || PATH_TO_DATA, `${name}.json`), + JSON.stringify(data, null, '\t'), + 'utf8', + ); +}; + +/** + * Method for reading file. In case error (file + * does not exist) return empty object. + * + * @param {object} param0 data for method + * @param {string} param0.name name file for reading + * @returns {object} JSON parsed data + * @param {string} [param0.basicPath] basic path for read + */ +const readDataFromFile = ({ + name, + basicPath, +}) => { + let dataFile; + try { + dataFile = fs.readFileSync( + path.join(basicPath || PATH_TO_DATA, `./${name}.json`), + 'utf8', + ); + } catch (err) { + return {}; + } + return JSON.parse(dataFile); +}; + +export default writeDataToFile; + +export { + writeDataToFile, + readDataFromFile, +}; diff --git a/src/utils/fileUtils/index.js b/src/utils/fileUtils/index.js index c336eb01..e1486175 100644 --- a/src/utils/fileUtils/index.js +++ b/src/utils/fileUtils/index.js @@ -1,22 +1,38 @@ -import { SOL_IMPORT_REGEXP, SOL_VERSION_REGEXP } from '../../constants'; +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable global-require */ +/* eslint-disable no-param-reassign */ +// eslint-disable-next-line no-unused-vars +import { + SOL_IMPORT_REGEXP, SOL_VERSION_REGEXP, VM_IMPORT_REGEXP, SOL_ENCODER_REGEXP, +} from '../../constants'; import getImports from './get-sol-imports'; -import { fs, path } from '../../constants/windowModules'; +import { fs, path, ROOT_DIR } from '../../constants/windowModules'; const readSolFile = (src, importedFiles) => { let mainImport; + let pathToFile; + if (VM_IMPORT_REGEXP.test(src)) { + [src] = src.match(VM_IMPORT_REGEXP); + src = path.join(ROOT_DIR, `../node_modules/${src}`); + } + if (!fs.existsSync(src)) throw new Error(`${src} - file not exist`); + mainImport = fs.readFileSync(src, 'utf8'); const importList = getImports(mainImport); - const currentFolder = src.replace(/(((\.\/|\.\.\/)).{1,})*([a-zA-z0-9])*(\.sol)/g, ''); + const currentFolder = src.replace(/(((\.\/|\.\.\/)).{1,})*([a-zA-Z0-9])*(\.sol)/g, ''); + importList.forEach((file) => { - const pathToFile = path.join(currentFolder, file); - if (!importedFiles[pathToFile] && (pathToFile !== src)) { - const includedFile = (readSolFile(pathToFile, importedFiles)).replace(SOL_VERSION_REGEXP, ''); - if (mainImport.match(SOL_IMPORT_REGEXP)) { - mainImport = mainImport.replace(mainImport.match(SOL_IMPORT_REGEXP)[0], includedFile); + pathToFile = path.join(currentFolder, file); + const [fileName] = pathToFile.match(/(\w+\.(?:sol))/g); + if (!importedFiles[fileName] && (pathToFile !== src)) { + if (!importedFiles[fileName]) { + const includedFile = (readSolFile(pathToFile, importedFiles)).replace(SOL_VERSION_REGEXP, '').replace(SOL_ENCODER_REGEXP, ''); + if (mainImport.match(SOL_IMPORT_REGEXP)) { + mainImport = mainImport.replace(mainImport.match(SOL_IMPORT_REGEXP)[0], includedFile); + } + importedFiles[fileName] = true; } - // eslint-disable-next-line no-param-reassign - importedFiles[pathToFile] = true; } else { mainImport = mainImport.replace(mainImport.match(SOL_IMPORT_REGEXP)[0], ''); } diff --git a/src/wallets/213123.json b/src/wallets/213123.json new file mode 100644 index 00000000..224d2c8b --- /dev/null +++ b/src/wallets/213123.json @@ -0,0 +1,21 @@ +{ + "version": 3, + "id": "4ebe0dc4-9071-4798-91fe-33e013ef7f85", + "address": "90a2929bd230f0fbf9d8bec7996b6dfb00b19116", + "crypto": { + "ciphertext": "86b75e585d91d56ef13604ccb41909745d3d73164fbc25170f1a25f12b6ae7fd", + "cipherparams": { + "iv": "8abb18309e678f06919d3916cada29fe" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "4bf4f2f356533286a06ea293f53573adb19c473b3c9059d0ab9f9b597a73cad7", + "n": 262144, + "r": 8, + "p": 1 + }, + "mac": "30fa6c46afbe9b8fe503b62b374bda4b6fa92d7fe86c63b4205105bdc72735db" + } +} \ No newline at end of file diff --git a/src/wallets/UTC--2019-11-29T07-19-05.154Z--7f51b0660a89f459a46313f391b4521963a8e5b7.json b/src/wallets/UTC--2019-11-29T07-19-05.154Z--7f51b0660a89f459a46313f391b4521963a8e5b7.json deleted file mode 100644 index 08f07869..00000000 --- a/src/wallets/UTC--2019-11-29T07-19-05.154Z--7f51b0660a89f459a46313f391b4521963a8e5b7.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": 3, - "id": "fc76e19f-f0cd-443a-a275-869053e418c7", - "address": "7f51b0660a89f459a46313f391b4521963a8e5b7", - "crypto": { - "ciphertext": "56953182a01baba41d0b8c5bde3ea1056696ff8b748944a5738519aabfc1184b", - "cipherparams": { - "iv": "2536ee3ec7985c1b708f1b97c5fdb1af" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "258cd6b009cd9cf397b18040f4871ddeead4cf3c6c781ccfafec83b8ff3c5229", - "n": 262144, - "r": 8, - "p": 1 - }, - "mac": "9908077786372dcb21cffe114b92b942a2568e92c6c6b184d8eb1f7512414a86" - } -} \ No newline at end of file diff --git a/src/wallets/UTC--2019-14-4T2-19-16.914000000Z--f6676e5138576e61b058b36fb3d2de089edc39b9.json b/src/wallets/UTC--2019-14-4T2-19-16.914000000Z--f6676e5138576e61b058b36fb3d2de089edc39b9.json deleted file mode 100644 index bc543d96..00000000 --- a/src/wallets/UTC--2019-14-4T2-19-16.914000000Z--f6676e5138576e61b058b36fb3d2de089edc39b9.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": 3, - "id": "ef8113ac-160d-4504-ad90-a2357d017a2b", - "address": "f6676e5138576e61b058b36fb3d2de089edc39b9", - "crypto": { - "ciphertext": "05d6c307d57c210dd68bf85232d754127bf55420bd35f9d08454a20e08bcd759", - "cipherparams": { - "iv": "8de293d09e53249324e414c5ba3e38a7" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "ae15fb2ae0160cbaec8af14d832877955537720805385ffacc15fccea34b913c", - "n": 262144, - "r": 8, - "p": 1 - }, - "mac": "f07befdb5f801d71ff5d79874e1fae3b6937cfc47855a3c45a9d1ed428c1ed1c" - } -} \ No newline at end of file diff --git a/webpack.dev.js b/webpack.dev.js index 616ae052..1fdd3d82 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -15,6 +15,9 @@ module.exports = { }), new CopyWebpackPlugin([ { from: './src/assets', to: './build/assets' }, + { from: './src/wallets', to: './wallets' }, + { from: './src/contracts', to: './contracts' }, + { from: './src/config.json', to: './config.json' }, ]), ], output: { @@ -40,7 +43,7 @@ module.exports = { loader: 'eslint-loader', options: { failOnError: true, - failOnWarning: true, + failOnWarning: false, }, }], }, {
{icon} @@ -34,17 +51,26 @@ const Button = ({ ); Button.propTypes = { - children: propTypes.oneOfType([ - propTypes.string, - propTypes.shape({}), + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + PropTypes.shape({}), ]).isRequired, - icon: propTypes.node, - iconPosition: propTypes.bool, - type: propTypes.string, - disabled: propTypes.bool, - onClick: propTypes.func.isRequired, - theme: propTypes.string, - size: propTypes.string, + icon: PropTypes.node, + iconPosition: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + ]), + type: PropTypes.string, + disabled: PropTypes.bool, + onClick: PropTypes.func, + theme: PropTypes.string, + size: PropTypes.string, + hint: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + className: PropTypes.string, }; Button.defaultProps = { @@ -54,6 +80,9 @@ Button.defaultProps = { icon: null, iconPosition: false, disabled: false, + onClick: () => {}, + hint: null, + className: '', }; export default Button; diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss index bfb36fb5..c1fe4c52 100644 --- a/src/components/Button/Button.scss +++ b/src/components/Button/Button.scss @@ -10,6 +10,49 @@ cursor: pointer; transition: 0.2s linear; + &__hint { + position: absolute; + bottom: calc(100% + 10px); + left: 50%; + z-index: 999; + text-align: left; + transform: translateX(-50%); + visibility: hidden; + opacity: 0; + transition: opacity 0.5s; + + &-content { + display: inline-block; + padding: 14px 16px 16px; + color: #4d4d4d; + font-size: 11px; + line-height: 13px; + white-space: nowrap; + text-align: left; + background: #fff; + border: 1px solid #000; + border-radius: 2px; + } + } + + &--with-hint { + position: relative; + + &:hover { + .btn__hint { + visibility: visible; + opacity: 1; + } + } + + &:active, + &:focus { + .btn__hint { + color: #000; + } + } + } + svg, &__icon, &__text { @@ -38,8 +81,11 @@ &[disabled]{ cursor: default; - opacity: .5; - } + + .btn__content { + opacity: .5; + } + } &--black { color: $white; @@ -61,6 +107,22 @@ } } + &--gray-bordered { + color: $gray; + font-size: 14px; + line-height: 107.5%; + background-color: transparent; + border: 1px solid $gray; + border-radius: 2px; + transition: color 0.2s, border-color 0.2s; + + &:hover, + &:active { + color: $primary; + border-color: $primary; + } + } + &--white { color: $primary; background-color: $white; @@ -152,11 +214,35 @@ } &:hover { color: $primary; - } + } + &:active { + color: $white; + } } &--showseed { @extend .btn--white; + svg{ + path{ + fill: $white; + } + } + &:hover { + svg { + path { + fill: $white; + stroke: $primary; + } + } + } + &:active { + svg { + path { + fill: $primary; + stroke: $white; + } + } + } } &--back { @@ -176,5 +262,94 @@ &--310 { width: 310px; } + + &--with-play-icon { + width: 100%; + padding: 26px 96px 26px 100px; + background: #fff; + border: 1px solid #e1e4e8; + + .btn__text { + color: #000; + font-weight: 300; + font-size: 18px; + font-family: "Roboto"; + line-height: 21px; + } + + svg { + width: auto; + height: auto; + } + } + + &--question-start { + display: inline-block; + background: #fff; + + .btn__text { + display: inline-block; + width: 65%; + color: $primary; + font-size: 11px; + } + + svg { + width: 30px; + height: 30px; + } + } + + &--toggle-user { + padding: 10px 16px 9px; + background: #fff; + border: 1px solid #e1e4e8; + border-radius: 2px; + + .btn__text { + color: #000; + font-weight: 500; + font-size: 14px; + font-family: "Roboto"; + line-height: 107.5%; + } + } + + &--voting-decision { + width: 50%; + padding: 28px 30px 23px; + background-color: #fff; + + .btn__text { + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + svg { + width: auto; + height: auto; + } + + &:hover { + border-color: $primary; + } + + &:active { + background-color: $primary; + + .btn__text { + color: $white; + } + + svg { + path { + stroke: $white; + } + } + } + } + } diff --git a/src/components/Button/index.js b/src/components/Button/index.js new file mode 100644 index 00000000..c49322d0 --- /dev/null +++ b/src/components/Button/index.js @@ -0,0 +1,7 @@ +import Button from './Button'; + +export default Button; + +export { + Button, +}; diff --git a/src/components/Container/Container.scss b/src/components/Container/Container.scss index 632cd7be..f4c86800 100644 --- a/src/components/Container/Container.scss +++ b/src/components/Container/Container.scss @@ -1,7 +1,14 @@ .container { position: relative; + top: 55px; width: 100%; max-width: 1120px; - height: 100vh; + // height: calc(100vh - 110px); + min-height: calc(100% - 108px); margin: 0 auto; + padding-bottom: 50px; + + &--small { + max-width: 845px; + } } diff --git a/src/components/Container/index.js b/src/components/Container/index.js index e1739d91..c666382b 100644 --- a/src/components/Container/index.js +++ b/src/components/Container/index.js @@ -3,13 +3,17 @@ import propTypes from 'prop-types'; import styles from './Container.scss'; -const Container = ({ children }) => ( - +const Container = ({ children, className }) => ( + {children} ); Container.propTypes = { children: propTypes.node.isRequired, + className: propTypes.string, +}; +Container.defaultProps = { + className: '', }; export default Container; diff --git a/src/components/CreateGroupQuestions/CreateGroupQuestions.js b/src/components/CreateGroupQuestions/CreateGroupQuestions.js new file mode 100644 index 00000000..78b6ba3b --- /dev/null +++ b/src/components/CreateGroupQuestions/CreateGroupQuestions.js @@ -0,0 +1,114 @@ +import React from 'react'; +import { withTranslation, Trans } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { Hint } from '../Hint'; +import CreateGroupQuestionsForm from '../../stores/FormsStore/CreateGroupQuestionsForm'; +import Input from '../Input'; +import { TokenName } from '../Icons'; +import Button from '../Button/Button'; +// import InputTextarea from '../Input/InputTextarea'; +import { systemQuestionsId } from '../../constants'; + +import styles from './CreateGroupQuestions.scss'; + +@withTranslation() +@inject('dialogStore', 'projectStore') +@observer +class CreateGroupQuestions extends React.PureComponent { + form = new CreateGroupQuestionsForm({ + hooks: { + onSuccess: (form) => { + const { + projectStore: { + rootStore: { + Web3Service, + }, + questionStore, + }, + projectStore, + dialogStore, + } = this.props; + const questionId = systemQuestionsId.connectGroupQuestions; + const { name } = form.values(); + const [question] = questionStore.getQuestionById(questionId); + const { paramTypes, groupId } = question; + const encodedParams = Web3Service.web3.eth.abi.encodeParameters(['tuple(uint256,uint256,uint256,uint256,uint256)', `tuple(${paramTypes.join(',')})`], [[0, 0, 0, 0, 0], [name]]); + // const votingData = encodedParams.replace('0x', methodSelector); + // TODO groupId fix + projectStore.setVotingData(questionId, groupId, encodedParams); + dialogStore.toggle('password_form_questions'); + return Promise.resolve(); + }, + onError: () => { + /* eslint-disable-next-line */ + console.error('error'); + }, + }, + }); + + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.shape({ + toggle: PropTypes.func.isRequired, + }).isRequired, + projectStore: PropTypes.shape().isRequired, + }; + + render() { + const { props, form } = this; + const { t, projectStore: { historyStore } } = props; + return ( + + + {t('dialogs:createAGroupOfQuestions')} + + {t('other:createGroupQuestionsDescription')} + + + + {t('other:createNameForTheGroupQuestions')} + + + + + + {/* */} + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:create')} + + + + {t('other:voteLaunchAdminDescription')} + + + ); + } +} + +export default CreateGroupQuestions; diff --git a/src/components/CreateGroupQuestions/CreateGroupQuestions.scss b/src/components/CreateGroupQuestions/CreateGroupQuestions.scss new file mode 100644 index 00000000..6f90303a --- /dev/null +++ b/src/components/CreateGroupQuestions/CreateGroupQuestions.scss @@ -0,0 +1,54 @@ +@import '../../assets/styles/includes/mixin'; + +.create-group-questions { + padding: 61px 30px 33px; + + &__title { + @include title; + + .hint { + margin-left: 16px; + vertical-align: middle; + } + } + + &__subtitle { + max-width: 297px; + margin: 0 auto; + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + text-align: center; + } + + &__subtext { + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + } + + form { + margin-top: 48px; + } + + button { + width: 100%; + margin-bottom: 8px; + } + + .field { + width: 100%; + margin-bottom: 32px; + + &__input--textarea { + width: 100%; + min-width: 100%; + max-height: 100px; + } + + &--textarea { + margin-bottom: 48px; + } + } +} \ No newline at end of file diff --git a/src/components/CreateGroupQuestions/CreateGroupQuestions.test.js b/src/components/CreateGroupQuestions/CreateGroupQuestions.test.js new file mode 100644 index 00000000..5ff49367 --- /dev/null +++ b/src/components/CreateGroupQuestions/CreateGroupQuestions.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CreateGroupQuestions from './CreateGroupQuestions'; + +jest.mock('../../utils/Validator'); + +describe('CreateGroupQuestions', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/CreateGroupQuestions/index.js b/src/components/CreateGroupQuestions/index.js new file mode 100644 index 00000000..2388dc76 --- /dev/null +++ b/src/components/CreateGroupQuestions/index.js @@ -0,0 +1,3 @@ +import CreateGroupQuestions from './CreateGroupQuestions'; + +export default CreateGroupQuestions; diff --git a/src/components/CreateNewProjectWithTokens/InputProjectData.js b/src/components/CreateNewProjectWithTokens/InputProjectData.js new file mode 100644 index 00000000..dfcd288a --- /dev/null +++ b/src/components/CreateNewProjectWithTokens/InputProjectData.js @@ -0,0 +1,70 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import FormBlock from '../FormBlock'; +import Heading from '../Heading'; +import Input from '../Input'; +import { TokenName, Password, BackIcon } from '../Icons'; +import Button from '../Button/Button'; +import Explanation from '../Explanation'; +import CreateProjectForm from '../../stores/FormsStore/CreateProject'; + +import styles from '../Login/Login.scss'; + +@withTranslation() +@observer +class InputProjectData extends React.Component { + static propTypes = { + form: PropTypes.instanceOf(CreateProjectForm).isRequired, + onClick: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, + }; + + render() { + const { props } = this; + const { onClick, form, t } = props; + return ( + + + {t('headings:projectCreating.heading')} + + {t('headings:projectCreating.subheading.0')} + + {t('headings:projectCreating.subheading.1')} + + + + + + + + + + + + {t('buttons:continue')} + + + + + + {t('explanations:project.name')} + + + + + {t('explanations:freeze')} + + + + } onClick={onClick} disabled={form.loading}> + {t('buttons:back')} + + + + ); + } +} + +export default InputProjectData; diff --git a/src/components/CreateNewProjectWithTokens/index.js b/src/components/CreateNewProjectWithTokens/index.js index 1bdf36f5..2c361f3c 100644 --- a/src/components/CreateNewProjectWithTokens/index.js +++ b/src/components/CreateNewProjectWithTokens/index.js @@ -1,4 +1,5 @@ import React, { Component } from 'react'; +import { observable } from 'mobx'; import { inject, observer } from 'mobx-react'; import propTypes from 'prop-types'; import { NavLink, Redirect } from 'react-router-dom'; @@ -10,14 +11,14 @@ import Container from '../Container'; import LoadingBlock from '../LoadingBlock'; import Input from '../Input'; import StepIndicator from '../StepIndicator'; -import Explanation from '../Explanation'; import { - BackIcon, Address, TokenName, Password, + BackIcon, Address, } from '../Icons'; import ConnectTokenForm from '../../stores/FormsStore/ConnectToken'; import CreateProjectForm from '../../stores/FormsStore/CreateProject'; import styles from '../Login/Login.scss'; +import InputProjectData from './InputProjectData'; @withTranslation() @inject('userStore', 'appStore') @@ -30,7 +31,7 @@ class CreateNewProjectWithTokens extends Component { }, }); - createProject = new CreateProjectForm({ + @observable createProject = new CreateProjectForm({ hooks: { onSuccess: (form) => this.gotoUploading(form), onError: () => {}, @@ -63,7 +64,8 @@ class CreateNewProjectWithTokens extends Component { checkToken = (form) => { const { steps } = this; - const { address } = form.values(); + const { address: rawAddress } = form.values(); + const address = rawAddress.trim(); const { appStore } = this.props; this.setState({ currentStep: steps.check, @@ -149,7 +151,7 @@ class CreateNewProjectWithTokens extends Component { render() { const { steps } = this; const { currentStep, indicatorStep } = this.state; - if (currentStep === steps.uploading) return ; + if (currentStep === steps.uploading) return ; return ( @@ -209,44 +211,6 @@ const ContractConfirmation = inject('appStore')(observer(withTranslation()(({ t, )))); -const InputProjectData = withTranslation()(({ - t, form, onClick, -}) => ( - - - {t('headings:projectCreating.heading')} - - {t('headings:projectCreating.subheading.0')} - - {t('headings:projectCreating.subheading.1')} - - - - - - - - - - - - {t('buttons:continue')} - - - - - - {t('explanations:project.name')} - - - - } onClick={onClick}> - {t('buttons:back')} - - - -)); - CreateNewProjectWithTokens.propTypes = { appStore: propTypes.shape({ checkErc: propTypes.func.isRequired, @@ -275,12 +239,5 @@ InputTokenAddress.propTypes = { ContractConfirmation.propTypes = { onSubmit: propTypes.func.isRequired, }; -InputProjectData.propTypes = { - form: propTypes.shape({ - $: propTypes.func.isRequired, - onSubmit: propTypes.func.isRequired, - }).isRequired, - onClick: propTypes.func.isRequired, -}; export default CreateNewProjectWithTokens; diff --git a/src/components/CreateNewProjectWithoutTokens/index.js b/src/components/CreateNewProjectWithoutTokens/index.js index cc2286a7..8f15194c 100644 --- a/src/components/CreateNewProjectWithoutTokens/index.js +++ b/src/components/CreateNewProjectWithoutTokens/index.js @@ -1,3 +1,4 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import propTypes from 'prop-types'; @@ -16,6 +17,8 @@ import { } from '../Icons'; import CreateTokenForm from '../../stores/FormsStore/CreateToken'; import CreateProjectForm from '../../stores/FormsStore/CreateProject'; +import AppStore from '../../stores/AppStore/AppStore'; +import UserStore from '../../stores/UserStore'; import styles from '../Login/Login.scss'; @@ -23,6 +26,12 @@ import styles from '../Login/Login.scss'; @inject('userStore', 'appStore') @observer class CreateNewProjectWithoutTokens extends Component { + static propTypes = { + appStore: propTypes.instanceOf(AppStore).isRequired, + userStore: propTypes.instanceOf(UserStore).isRequired, + t: propTypes.func.isRequired, + }; + form = new CreateTokenForm({ hooks: { onSuccess: (form) => this.createToken(form), @@ -42,6 +51,7 @@ class CreateNewProjectWithoutTokens extends Component { creation: 2, tokenCreated: 3, projectInfo: 4, + uploading: 5, } constructor(props) { @@ -180,8 +190,9 @@ class CreateNewProjectWithoutTokens extends Component { } render() { + const { steps } = this; const { currentStep, indicatorStep } = this.state; - if (currentStep === 'uploading') return ; + if (currentStep === steps.uploading) return ; return ( @@ -196,7 +207,9 @@ class CreateNewProjectWithoutTokens extends Component { const CreateTokenData = withTranslation()(inject('userStore', 'appStore')(observer((({ t, userStore: { address }, appStore: { balances }, form, }) => ( - + {t('headings:newTokens.heading')} {t('headings:newTokens.subheading')} @@ -328,6 +341,7 @@ const InputProjectData = withTranslation()(({ theme="back" icon={} onClick={onClick} + disabled={form.loading} > {t('buttons:back')} @@ -335,24 +349,6 @@ const InputProjectData = withTranslation()(({ )); -CreateNewProjectWithoutTokens.propTypes = { - appStore: propTypes.shape({ - deployContract: propTypes.func.isRequired, - checkReceipt: propTypes.func.isRequired, - deployArgs: propTypes.arrayOf(propTypes.any).isRequired, - displayAlert: propTypes.func.isRequired, - setProjectName: propTypes.func.isRequired, - password: propTypes.string.isRequired, - setDeployArgs: propTypes.func.isRequired, - }).isRequired, - userStore: propTypes.shape({ - readWallet: propTypes.func.isRequired, - checkBalance: propTypes.func.isRequired, - address: propTypes.string.isRequired, - setPassword: propTypes.func.isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; CreateTokenData.propTypes = { form: propTypes.shape({ onSubmit: propTypes.func.isRequired, @@ -360,9 +356,11 @@ CreateTokenData.propTypes = { loading: propTypes.bool.isRequired, }).isRequired, }; + TokenCreationAlert.propTypes = { onSubmit: propTypes.func.isRequired, }; + InputProjectData.propTypes = { form: propTypes.shape({ $: propTypes.func.isRequired, diff --git a/src/components/CreateNewQuestion/CreateNewQuestion.js b/src/components/CreateNewQuestion/CreateNewQuestion.js new file mode 100644 index 00000000..4258cf51 --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestion.js @@ -0,0 +1,126 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import SimpleDropdown from '../SimpleDropdown'; +import { QuestionIcon } from '../Icons'; +import CreateNewQuestionForm from './CreateNewQuestionForm'; +import StepIndicator from '../StepIndicator'; + +import styles from './CreateNewQuestion.scss'; + +/** + * Component for creating a new question + * + * @param selected + */ +@withTranslation() +@inject('projectStore') +@observer +class CreateNewQuestion extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + projectStore: PropTypes.shape().isRequired, + }; + + constructor() { + super(); + this.state = { + isSelected: false, + activeTab: 0, + selectedGroup: 0, + }; + } + + handleDropdownSelect = (selected) => { + this.setState({ isSelected: true, selectedGroup: selected.value }); + } + + /** + * Method for toggle active tab + * + * @param {number} number index active tab + */ + toggleActiveTab = (number) => { + this.setState({ activeTab: number }); + } + + render() { + const { isSelected, activeTab, selectedGroup } = this.state; + const { props } = this; + const { t, projectStore: { questionStore } } = props; + return ( + + + + + {t('other:createANewQuestion')} + + { + isSelected + ? ( + <> + + {t('other:basicInfo')} + + + + + > + ) + : null + } + + + + + + + + { + isSelected + ? ( + + {/* TODO change to description for selected group questions */} + Description text + + ) + : null + } + + + + { + isSelected + ? ( + this.toggleActiveTab(0)} + /> + ) + : ( + + + {t('other:selectQuestionGroup')} + + + ) + } + + + ); + } +} + +export default CreateNewQuestion; diff --git a/src/components/CreateNewQuestion/CreateNewQuestion.scss b/src/components/CreateNewQuestion/CreateNewQuestion.scss new file mode 100644 index 00000000..fef3e663 --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestion.scss @@ -0,0 +1,212 @@ +@import "../../assets/styles/partials/variables"; +@import "../../assets/styles/includes/mixin"; + +.create-question { + width: 100%; + padding: 40px 60px 0; + + &__top { + display: inline-block; + width: 100%; + margin: 0 -15px; + + .dropdown { + width: 100%; + } + + &-left, + &-right { + display: inline-block; + width: 50%; + padding: 0 15px; + text-align: left; + vertical-align: top; + } + } + + &__title { + color: $primary; + font-weight: 700; + font-size: 36px; + line-height: 42px; + } + + &__sub-title { + margin-top: 8px; + margin-bottom: 13px; + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + } + + &__content { + min-height: 324px; + padding: 10px 0; + + &--empty { + height: 100%; + max-height: 324px; + text-align: center; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + &-text { + display: inline-block; + max-width: 261px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + vertical-align: middle; + } + } + } + + &__description { + margin-top: 9px; + margin-bottom: 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + } + + &__step-progress { + .step-indicator { + position: relative; + left: unset; + text-align: left; + transform: unset; + } + + p { + &:first-child { + display: inline-block; + margin: 8px 13px; + } + + &:last-child { + float: left; + } + } + } + + &__field-remove { + position: absolute; + top: 50%; + padding: 15px 8px; + background-color: transparent; + border: unset; + outline: none; + transform: translateY(-50%); + cursor: pointer; + opacity: 0; + transition: opacity 0.2s; + + svg { + width: 10px; + height: 10px; + } + } + + &__field-description { + margin-top: 8px; + padding: 11px 18px 10px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 11px; + line-height: 13px; + background: #fff; + border: 1px dashed #000; + } + + &__form { + &--basic { + padding-top: 49px; + padding-bottom: 20px; + } + + &--dynamic { + padding-top: 46px; + padding-bottom: 29px; + } + + .field { + width: 100%; + + &__input--textarea { + width: 100%; + min-width: 100%; + } + } + + .dropdown { + width: 100%; + } + + &-row { + position: relative; + margin: 0 -15px; + padding: 15px 0; + text-align: left; + + &:hover { + .create-question__field-remove { + opacity: 1; + } + } + } + + &-col { + display: inline-block; + width: 50%; + padding: 0 15px; + vertical-align: top; + + &--full { + width: 100%; + } + + button { + width: 100%; + } + } + + &-text { + margin-top: 8px; + padding: 0 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + } + } + + &__button-new-param { + margin-bottom: 26px; + padding: 8px 0; + color: $placeholderColor; + font-size: 14px; + line-height: 16px; + text-align: left; + background-color: transparent; + border: unset; + border-bottom: 1px solid $placeholderColor; + outline: none; + cursor: pointer; + transition: color 0.2s, border-color 0.2s; + + &:hover, + &:active { + color: $primary; + border-bottom: 1px solid $primary; + } + } +} +.extra-padding { + padding-top: 20px; +} diff --git a/src/components/CreateNewQuestion/CreateNewQuestion.test.js b/src/components/CreateNewQuestion/CreateNewQuestion.test.js new file mode 100644 index 00000000..f9ac077a --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestion.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CreateNewQuestion from './CreateNewQuestion'; + +describe('CreateNewQuestion', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('handleDropdownSelect should change isSelected to true', () => { + expect(wrapperInstance.state.isSelected).toEqual(false); + wrapperInstance.handleDropdownSelect(); + expect(wrapperInstance.state.isSelected).toEqual(true); + }); + + it('toggleActiveTab with (1) should change activeTab to 1', () => { + expect(wrapperInstance.state.activeTab).toEqual(0); + wrapperInstance.toggleActiveTab(1); + expect(wrapperInstance.state.activeTab).toEqual(1); + }); +}); diff --git a/src/components/CreateNewQuestion/CreateNewQuestionForm.js b/src/components/CreateNewQuestion/CreateNewQuestionForm.js new file mode 100644 index 00000000..6f21b7bd --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestionForm.js @@ -0,0 +1,214 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import CreateQuestionBasicForm from '../../stores/FormsStore/CreateQuestionBasicForm'; +import CreateQuestionDynamicForm from '../../stores/FormsStore/CreateQuestionDynamicForm'; +import FormBasic from './FormBasic'; +import Question from '../../services/ContractService/entities/Question'; +import FormDynamic from './FormDynamic'; +import { systemQuestionsId } from '../../constants'; + +import styles from './CreateNewQuestion.scss'; + +@withTranslation() +@inject('dialogStore', 'projectStore') +@observer +class CreateNewQuestionForm extends React.PureComponent { + /** Form with basic info for new question */ + + formBasic = new CreateQuestionBasicForm({ + hooks: { + onSuccess: (form) => { + this.onBasicSubmit(form); + const { data: { groupId } } = this; + return Promise.resolve(); + }, + onError: () => { + console.log('error'); + }, + }, + }) + + /** Form with additional info for new question */ + formDynamic = new CreateQuestionDynamicForm({ + hooks: { + onSuccess: (form) => { + this.onDynamicSubmit(form); + return Promise.resolve(); + }, + onError: () => { + console.log('error'); + }, + }, + }); + + static propTypes = { + /** Current active tab */ + activeTab: PropTypes.number.isRequired, + /** Method called on toggle tab */ + onToggle: PropTypes.func.isRequired, + /** Method called on success fill all data */ + onComplete: PropTypes.func.isRequired, + dialogStore: PropTypes.shape({ + toggle: PropTypes.func.isRequired, + }).isRequired, + projectStore: PropTypes.shape().isRequired, + selectedGroup: PropTypes.number.isRequired, + }; + + constructor(props) { + super(props); + this.data = { + name: '', + description: '', + groupId: '', + time: 0, + formula: '', + target: '', + methodSelector: '', + }; + } + + /** + * Action on basic form submit + * + * @param form + */ + onBasicSubmit = (form) => { + const { props, data } = this; + const { selectedGroup } = props; + const { onToggle } = props; + const { + question_title: Name, + question_life_time: time, + description, + target, + methodSelector, + voting_formula: formula, + } = form.values(); + data.name = Name.trim(); + data.time = time; + data.formula = formula.trim(); + data.target = target.trim(); + data.description = description.trim(); + data.methodSelector = methodSelector.trim() || '0x00000000'; + data.groupId = selectedGroup; + onToggle(1); + } + + /** + * Method for getting uniq key for + * similar fields (input & select) + * + * @param {string} key name field + * @returns {string} uniq key for + * select & input + */ + getUniqKey = (key) => { + if (!key || !key.split) return ''; + return key.split('--')[1] || ''; + } + + /** + * Method for getting parameters array + * from form with dynamic fields + * + * @param {object} form form + * @returns {Array} array parameters + */ + getParametersFromForm = (form) => { + let values; + if (form.values()) { + values = form.values(); + } else { + values = {}; + } + const paramTypes = []; + const paramNames = []; + Object.keys(values).forEach((key, index) => { + if (Number.isInteger(index / 2) === false) return; + const uniqKey = this.getUniqKey(key); + const selectValue = values[`select--${uniqKey}`]; + const inputValue = values[`input--${uniqKey}`]; + paramTypes.push(selectValue); + paramNames.push(inputValue); + }); + // parameters = parameters.filter((e) => e !== ''); + return { paramTypes, paramNames }; + } + + /** + * Action on dynamic form submit + * + * @param form + */ + onDynamicSubmit = (form) => { + const { data } = this; + const { + dialogStore, + projectStore: { questionStore, rootStore: { Web3Service, contractService } }, + projectStore, + onComplete, + } = this.props; + const futureQuestionId = questionStore.questions.length + 1; + const { paramTypes, paramNames } = this.getParametersFromForm(form); + const question = new Question({ + id: futureQuestionId, + group: data.groupId, + name: data.name, + caption: data.description, + time: Number(data.time), + method: data.methodSelector, + formula: data.formula, + paramTypes, + paramNames, + }); + const rawVotingData = question.getUploadingParams(data.target); + const votingData = Web3Service.web3.eth.abi.encodeParameters( + ['tuple(uint, uint, uint, uint, uint)', 'tuple(bool, string, string, uint, uint, string[], string[], address, bytes4, string, bytes)'], + [[0, 0, 0, 0, 0], rawVotingData], + ); + projectStore.setVotingData(systemQuestionsId.addingNewQuestion, 0, votingData); + dialogStore.toggle('password_form_questions'); + this.formBasic.clear(); + form.clear(); + onComplete(); + } + + renderStep = () => { + const { props, formBasic, formDynamic } = this; + const { activeTab, onToggle } = props; + switch (activeTab) { + case 0: + return ( + + ); + case 1: + return ( + + ); + default: + return ( + + ); + } + } + + render() { + return ( + + {this.renderStep()} + + ); + } +} + +export default CreateNewQuestionForm; diff --git a/src/components/CreateNewQuestion/CreateNewQuestionForm.test.js b/src/components/CreateNewQuestion/CreateNewQuestionForm.test.js new file mode 100644 index 00000000..81ccb782 --- /dev/null +++ b/src/components/CreateNewQuestion/CreateNewQuestionForm.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import CreateNewQuestionForm from './CreateNewQuestionForm'; +import FormBasic from './FormBasic'; +import FormDynamic from './FormDynamic'; + +jest.mock('../../utils/Validator'); + +describe('CreateNewQuestionForm', () => { + describe('With activeTab 0', () => { + let wrapper; + let mockOnToggle; + let wrapperInstance; + + beforeEach(() => { + mockOnToggle = jest.fn(); + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error with correct form', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(FormBasic).length).toEqual(1); + expect(wrapper.find(FormDynamic).length).toEqual(0); + }); + + it('onBasicSubmit should call mockOnToggle with 1', () => { + wrapperInstance.onBasicSubmit(); + expect(mockOnToggle).toHaveBeenCalledWith(1); + }); + }); + + describe('With activeTab 1', () => { + let wrapper; + let mockOnToggle; + let wrapperInstance; + + beforeEach(() => { + mockOnToggle = jest.fn(); + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error with correct form', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(FormBasic).length).toEqual(0); + expect(wrapper.find(FormDynamic).length).toEqual(1); + }); + + it('onBasicSubmit should call mockOnToggle with 1', () => { + wrapperInstance.onBasicSubmit(); + expect(mockOnToggle).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/src/components/CreateNewQuestion/FormBasic.js b/src/components/CreateNewQuestion/FormBasic.js new file mode 100644 index 00000000..de926072 --- /dev/null +++ b/src/components/CreateNewQuestion/FormBasic.js @@ -0,0 +1,128 @@ +import React from 'react'; +import { withTranslation, Trans } from 'react-i18next'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import Input from '../Input'; +// import { SquareHint } from '../Hint'; +import { TokenName, DateIcon, Address } from '../Icons'; +import InputTextarea from '../Input/InputTextarea'; +import Button from '../Button/Button'; + +import styles from './CreateNewQuestion.scss'; +import { FormulaHint, SelectorHint } from '../Hint'; + +@withTranslation() +@inject('projectStore') +@observer +class FormBasic extends React.Component { + static propTypes = { + formBasic: PropTypes.shape({ + onSubmit: PropTypes.func.isRequired, + $: PropTypes.func.isRequired, + }).isRequired, + t: PropTypes.func.isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.shape({ + isVotingActive: PropTypes.bool.isRequired, + }), + }).isRequired, + }; + + render() { + const { props } = this; + const { formBasic, t, projectStore: { historyStore } } = props; + return ( + + + + + + + + + + + + + + + + } + > + + + { + !formBasic.$('methodSelector').error + ? ( + + {t('other:selectorNonexistentFunctionDescription')} + + ) + : null + } + + + + + + + + + + } + /> + + + + + + + + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:nextStep')} + + + + + ); + } +} + +export default FormBasic; diff --git a/src/components/CreateNewQuestion/FormBasic.test.js b/src/components/CreateNewQuestion/FormBasic.test.js new file mode 100644 index 00000000..39c2e674 --- /dev/null +++ b/src/components/CreateNewQuestion/FormBasic.test.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import FormBasic from './FormBasic'; +import CreateQuestionBasicForm from '../../stores/FormsStore/CreateQuestionBasicForm'; + +describe('FormBasic', () => { + let wrapper; + let formBasic; + + beforeEach(() => { + formBasic = new CreateQuestionBasicForm({ + hooks: { + onSuccess: () => (Promise.resolve()), + }, + }); + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/CreateNewQuestion/FormDynamic.js b/src/components/CreateNewQuestion/FormDynamic.js new file mode 100644 index 00000000..5a2c0e08 --- /dev/null +++ b/src/components/CreateNewQuestion/FormDynamic.js @@ -0,0 +1,210 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import uniqKey from 'react-id-generator'; +import Input from '../Input'; +import { TokenName, CloseIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './CreateNewQuestion.scss'; +import SimpleDropdown from '../SimpleDropdown'; + +@withTranslation() +@inject('projectStore') +@observer +class FormDynamic extends React.Component { + static propTypes = { + formDynamic: PropTypes.shape({ + onSubmit: PropTypes.func.isRequired, + $: PropTypes.func.isRequired, + map: PropTypes.func.isRequired, + add: PropTypes.func.isRequired, + del: PropTypes.func.isRequired, + }).isRequired, + t: PropTypes.func.isRequired, + onToggle: PropTypes.func.isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.shape({ + isVotingActive: PropTypes.bool.isRequired, + }), + }).isRequired, + }; + + /** + * Method for adding new fields (input & select) + * to dynamic form + */ + addDynamicFields = () => { + const { props } = this; + const { t, formDynamic } = props; + const key = uniqKey(); + // Add input field + formDynamic.add({ + // this format important!!! + // @see getFieldKey + // @see removeRowFields + name: `input--${key}`, + type: 'text', + label: 'parameter', + placeholder: t('fields:enterNewParameterName'), + rules: 'required', + }); + // Add select field + formDynamic.add({ + // this format important!!! + // @see getFieldKey + // @see removeRowFields + name: `select--${key}`, + type: 'text', + label: 'parameter', + placeholder: t('fields:selectParameterType'), + rules: 'required', + }); + } + + /** + * Method for getting uniq key for + * similar fields (input & select) + * + * @param {string} name name field + * @returns {string} uniq key for + * select & input + */ + getFieldKey = (name) => { + if (!name || !name.split) return ''; + return name.split('--')[1] || ''; + } + + /** + * Method for removing fields + * with similar uniq key (input & select) + * + * @param {string} name name field + */ + removeRowFields = (name) => { + const key = this.getFieldKey(name); + const { props } = this; + const { formDynamic } = props; + formDynamic.del(`input--${key}`); + formDynamic.del(`select--${key}`); + } + + render() { + const { props } = this; + const { + formDynamic, t, onToggle, projectStore: { historyStore }, + } = props; + const options = [{ + label: 'uint', + value: 'uint', + }, { + label: 'String', + value: 'string', + }, { + label: 'Address', + value: 'address', + }, { + label: 'bytes4', + value: 'bytes4', + }, { + label: 'bytes32', + value: 'bytes32', + }]; + return ( + + {/* Render dynamic fields start */} + { + formDynamic.map((field, index) => { + const key = this.getFieldKey(field.name); + // Since two fields are added at a time, + // duplicates need to be excluded + // @see addDynamicFields method + if (Number.isInteger(index / 2) === false) return null; + return ( + + + + + + + + {}} + > + + + + this.removeRowFields(field.name) + } + className={styles['create-question__field-remove']} + > + + + + ); + }) + } + {/* Render dynamic fields end */} + + + + {t('buttons:addParameter')} + + + + + + { + onToggle(0); + }} + > + {t('buttons:back')} + + + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:create')} + + + {t('other:voteLaunchDescription')} + + + + + ); + } +} + +export default FormDynamic; diff --git a/src/components/CreateNewQuestion/FormDynamic.test.js b/src/components/CreateNewQuestion/FormDynamic.test.js new file mode 100644 index 00000000..073d9ea3 --- /dev/null +++ b/src/components/CreateNewQuestion/FormDynamic.test.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import FormDynamic from './FormDynamic'; +import CreateQuestionDynamicForm from '../../stores/FormsStore/CreateQuestionDynamicForm'; + +jest.mock('../../utils/Validator'); + +describe('FormDynamic', () => { + let wrapper; + let wrapperInstance; + let formDynamic; + + beforeEach(() => { + formDynamic = new CreateQuestionDynamicForm({ + hooks: { + onSuccess: () => (Promise.resolve()), + }, + }); + wrapper = shallow( + {}} + />, + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('addDynamicFields should add fields', () => { + // Ignore error inside mobx (test work correctly!) + console.error = jest.fn(); + expect(formDynamic.fields.size).toEqual(2); + wrapperInstance.addDynamicFields(); + expect(formDynamic.fields.size).toEqual(4); + }); + + it('getFieldKey with some params should be correct', () => { + let result = wrapperInstance.getFieldKey('select--id1'); + expect(result).toEqual('id1'); + result = wrapperInstance.getFieldKey('select-id1'); + expect(result).toEqual(''); + result = wrapperInstance.getFieldKey(undefined); + expect(result).toEqual(''); + result = wrapperInstance.getFieldKey({}); + expect(result).toEqual(''); + result = wrapperInstance.getFieldKey(null); + expect(result).toEqual(''); + }); + + it('removeRowFields should remove fields', () => { + // Ignore error inside mobx (test work correctly!) + console.error = jest.fn(); + expect(formDynamic.fields.size).toEqual(2); + wrapperInstance.removeRowFields('input--0'); + expect(formDynamic.fields.size).toEqual(0); + }); +}); diff --git a/src/components/CreateNewQuestion/index.js b/src/components/CreateNewQuestion/index.js new file mode 100644 index 00000000..0728c1b7 --- /dev/null +++ b/src/components/CreateNewQuestion/index.js @@ -0,0 +1,3 @@ +import CreateNewQuestion from './CreateNewQuestion'; + +export default CreateNewQuestion; diff --git a/src/components/CreateWallet/PasswordForm.js b/src/components/CreateWallet/PasswordForm.js index 36c35e91..8c0464e7 100644 --- a/src/components/CreateWallet/PasswordForm.js +++ b/src/components/CreateWallet/PasswordForm.js @@ -15,6 +15,16 @@ import styles from '../Login/Login.scss'; @withTranslation() class PasswordForm extends Component { + static propTypes = { + state: propTypes.bool.isRequired, + form: propTypes.shape({ + $: propTypes.func.isRequired, + onSubmit: propTypes.func.isRequired, + loading: propTypes.bool.isRequired, + }).isRequired, + t: propTypes.func.isRequired, + }; + constructor(props) { super(props); this.state = { @@ -22,6 +32,15 @@ class PasswordForm extends Component { }; } + componentDidMount() { + const { form } = this.props; + if (form.$('password').value !== '') { + const { value } = form.$('password'); + const validity = passwordValidation(value); + this.setState({ validity }); + } + } + handleInput = (value) => { const validity = passwordValidation(value); this.setState({ validity }); @@ -30,7 +49,6 @@ class PasswordForm extends Component { render() { const { state, form, t } = this.props; const { validity } = this.state; - return ( @@ -65,26 +83,26 @@ class PasswordForm extends Component { { t('explanations:passwordCreating.1')} - + - + { t('explanations:passwordRules.numeric')} - + { t('explanations:passwordRules.upperCase')} - + { t('explanations:passwordRules.symbol')} - + { t('explanations:passwordRules.length')} - + @@ -101,13 +119,4 @@ class PasswordForm extends Component { } } -PasswordForm.propTypes = { - state: propTypes.bool.isRequired, - form: propTypes.shape({ - $: propTypes.func.isRequired, - onSubmit: propTypes.func.isRequired, - loading: propTypes.bool.isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; export default PasswordForm; diff --git a/src/components/CreateWallet/index.js b/src/components/CreateWallet/index.js index 38b36b31..eb270ac1 100644 --- a/src/components/CreateWallet/index.js +++ b/src/components/CreateWallet/index.js @@ -1,3 +1,4 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import propTypes from 'prop-types'; import { observer, inject } from 'mobx-react'; @@ -17,6 +18,19 @@ import styles from '../Login/Login.scss'; @inject('userStore', 'appStore') @observer class CreateWallet extends Component { + static propTypes = { + userStore: propTypes.shape({ + recoverWallet: propTypes.func.isRequired, + saveWalletToFile: propTypes.func.isRequired, + createWallet: propTypes.func.isRequired, + }).isRequired, + recover: propTypes.bool, + }; + + static defaultProps = { + recover: false, + }; + createForm = new CreateWalletForm({ hooks: { onSuccess: (form) => this.createWallet(form), @@ -95,13 +109,4 @@ const CreationLoader = withTranslation(['headings'])(({ t }) => ( )); -CreateWallet.propTypes = { - userStore: propTypes.shape({ - recoverWallet: propTypes.func.isRequired, - saveWalletToFile: propTypes.func.isRequired, - createWallet: propTypes.func.isRequired, - }).isRequired, - recover: propTypes.bool.isRequired, -}; - export default CreateWallet; diff --git a/src/components/DatePicker/DatePicker.js b/src/components/DatePicker/DatePicker.js new file mode 100644 index 00000000..3455f269 --- /dev/null +++ b/src/components/DatePicker/DatePicker.js @@ -0,0 +1,273 @@ +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import PropTypes from 'prop-types'; +import Litepicker from 'litepicker'; +import moment from 'moment'; +import { observer } from 'mobx-react'; +import { computed, observable, action } from 'mobx'; +import { withTranslation } from 'react-i18next'; +import { getCorrectPickerLocale } from '../../utils/Date'; +import i18n from '../../i18n'; +import { + ThinArrow, + Arrow, + DateIcon, + CloseIcon, +} from '../Icons'; + +import styles from './DatePicker.scss'; + +@withTranslation() +@observer +class DateTest extends React.Component { + @observable start = null; + + @observable end = null; + + ref; + + picker; + + static propTypes = { + t: PropTypes.func.isRequired, + onDatesSet: PropTypes.func.isRequired, + onDatesClear: PropTypes.func.isRequired, + init: PropTypes.shape({ + startDate: PropTypes.instanceOf(moment), + endDate: PropTypes.instanceOf(moment), + }), + }; + + static defaultProps = { + init: { + startDate: null, + endDate: null, + }, + } + + componentDidMount() { + const { props } = this; + const { + init: { + startDate, + endDate, + }, + } = props; + this.picker = new Litepicker({ + element: this.minRef, + elementEnd: this.maxRef, + format: 'DD.MM.YYYY', + firstDay: 1, + numberOfMonths: 2, + numberOfColumns: 2, + minDate: null, + maxDate: null, + minDays: null, + maxDays: null, + singleMode: false, + autoApply: true, + scrollToDate: true, + showWeekNumbers: false, + showTooltip: true, + disableWeekends: false, + splitView: true, + onSelect: this.handleSelect, + buttonText: { + previousMonth: ReactDOMServer.renderToStaticMarkup( + , + ), + nextMonth: ReactDOMServer.renderToStaticMarkup( + , + ), + }, + startDate, + endDate, + }); + this.updateLanguage(); + this.start = startDate; + this.end = endDate; + window.ipcRenderer.on('change-language:confirm', () => { + this.updateLanguage(); + }); + } + + componentWillUnmount() { + window.ipcRenderer.removeListener('change-language:confirm', () => { + this.updateLanguage(); + }); + } + + @computed + get startDate() { + return this.start; + } + + @computed + get endDate() { + return this.end; + } + + /** + * Method for handle date select + * + * @param {Date} startDate start date + * @param {Date} endDate end date + */ + @action + handleSelect = (startDate, endDate) => { + const { props } = this; + const { onDatesSet } = props; + const start = moment(startDate); + // To include the maximum date in the range + const end = moment(endDate) + .add('hours', 23) + .add('minutes', 59) + .add('seconds', 59); + this.start = start; + this.end = end; + onDatesSet({ startDate: start, endDate: end }); + } + + /** + * Method for clearing selected date + */ + @action + handleClear = () => { + const { props } = this; + const { onDatesClear } = props; + if (this.picker) { + this.picker.clearSelection(); + } + this.start = null; + this.end = null; + onDatesClear(); + } + + /** + * Method for getting correct plural + * text for tooltip + * + * @param {string} language actual language + * @returns {object} correct tooltip text + */ + getTooltipText = (language) => { + switch (language) { + case 'ru-RU': + return { + one: 'день', + many: 'дней', + few: 'дня', + }; + case 'en-US': + return { + one: 'day', + other: 'days', + }; + default: + return { + one: 'day', + other: 'days', + }; + } + } + + /** + * Method for update options in picker + * on change language event + */ + updateLanguage = () => { + const lang = getCorrectPickerLocale(i18n.language); + const tooltipText = this.getTooltipText(lang); + if (this.picker) { + this.picker.setOptions({ + lang: getCorrectPickerLocale(i18n.language), + tooltipText, + }); + } + } + + render() { + const { start, end, props } = this; + const { t } = props; + const filled = Boolean(start && end); + return ( + + { /* eslint-disable-next-line */} + + + + + { + this.minRef = el; + } + } + /> + + + {t('fields:dateFrom')} + + + + + + + + { /* eslint-disable-next-line */} + + { + this.maxRef = el; + } + } + /> + + + {t('fields:dateTo')} + + + + + + + + + ); + } +} + +export default DateTest; diff --git a/src/components/DatePicker/DatePicker.scss b/src/components/DatePicker/DatePicker.scss new file mode 100644 index 00000000..73a223b1 --- /dev/null +++ b/src/components/DatePicker/DatePicker.scss @@ -0,0 +1,293 @@ +.date-picker { + position: relative; + display: inline-block; + max-width: 240px; + + &__base { + position: relative; + } + + &__input { + position: relative; + display: inline-block; + width: 73px; + padding: 8px 0; + color: #181818; + font-size: 14px; + line-height: 16px; + background: transparent; + border: unset; + outline: none; + transition: border-bottom-color 0.3s; + + &-line { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 1px; + background-color: rgba(0, 0, 0, 0.1); + transition: background-color 0.3s; + } + + &-placeholder { + position: absolute; + top: 50%; + color: rgba(24, 24, 24, 0.5); + font-size: 14px; + line-height: 16px; + transform: translateY(-50%); + cursor: text; + transition: all 0.3s; + } + + &::-webkit-input-placeholder { + color: transparent; + } + + &:focus { + & + .date-picker__input-after { + .date-picker__input-line { + background-color: rgba(0, 0, 0, 1); + } + } + } + + &:focus, + &:not(:placeholder-shown) { + & + .date-picker__input-after { + .date-picker__input-placeholder { + top: 0; + font-size: 9px; + line-height: 11px; + } + } + } + } + + &__min { + margin-left: 22px; + + & + .date-picker__input-after { + .date-picker__input-placeholder { + left: 38px; + } + } + } + + label { + position: relative; + display: inline-block; + vertical-align: middle; + } + + &__arrow { + &--basic { + display: inline-block; + margin-right: 7px; + margin-left: 9px; + vertical-align: middle; + } + + svg { + width: auto; + height: auto; + } + } + + &__icon { + position: relative; + display: inline-block; + vertical-align: middle; + + &::after { + position: absolute; + top: 50%; + right: -10px; + width: 1px; + height: 13px; + background-color: rgba(0, 0, 0, 0.1); + transform: translate(-50%, -50%); + content: ""; + } + } + + &__clear { + position: absolute; + top: 50%; + right: -28px; + padding: 5px; + color: #e1e4e8; + background-color: #fff; + border: 1px solid #e1e4e8; + outline: none; + transform: translateY(-50%); + visibility: hidden; + cursor: pointer; + opacity: 0; + transition: color 0.3s, border-color 0.3s, opacity 0.3s; + + &:hover, + &:active { + color: #000; + border-color: #000; + } + + &--visible { + visibility: visible; + opacity: 1; + } + + svg { + width: 7px; + height: 7px; + } + } +} + +$month-padding: 21px; + +.litepicker { + .container__months { + &.columns-2 { + width: calc((var(--litepickerMonthWidth) * 2) + #{$month-padding * 4}) !important; + margin-top: 12px; + border: 1px solid #e1e4e8; + border-radius: 0; + box-shadow: unset; + } + + .month-item { + position: relative; + padding: 5px $month-padding !important; + + &::after { + position: absolute; + top: 0; + right: 0; + width: 1px; + height: 100%; + background-color: #e1e4e8; + content: ''; + } + + &:last-child { + &::after { + content: none; + } + } + + &-weekdays-row { + position: relative; + + &::after { + position: absolute; + bottom: 0; + left: -$month-padding; + width: calc(100% + #{$month-padding * 2}); + height: 1px; + background-color: #e1e4e8; + content: ''; + } + + & > div { + font-size: 14px; + line-height: 16px; + text-transform: lowercase; + } + } + + .button-previous-month, + .button-next-month { + position: absolute; + top: 9px; + cursor: pointer; + + svg { + width: auto; + height: auto; + } + } + + .button-previous-month { + left: -16px; + } + + .button-next-month { + right: -16px; + margin-top: 1px; + } + } + + .month-item-header { + position: relative; + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + } + } + + .container__days { + .day-item { + font-size: 14px; + line-height: 16px; + border-color: transparent; + border-radius: 0px !important; + box-shadow: unset !important; + + &.is-today { + font-weight: 700; + } + + &:hover { + background-color: rgba(128, 128, 128, 0.8) !important; + } + } + } + + .container__tooltip { + margin-top: -12px; + padding: 5px 8px; + color: #fff; + font-size: 14px; + line-height: 16px; + background-color: #000; + border-radius: 0; + + &::after { + bottom: -10px; + left: calc(50% - 10px); + border-top: 10px solid #000; + border-right: 10px solid transparent; + border-left: 10px solid transparent; + } + + &::before { + content: none; + } + } +} + +:root { + --litepickerBgColor: #fff !important; + --litepickerMonthHeaderTextColor: #000 !important; + --litepickerMonthButton: rgba(0, 0, 0, 0.5) !important; + --litepickerMonthButtonHover: rgba(0, 0, 0, 0.5) !important; + --litepickerMonthWidth: calc(var(--litepickerDayWidth) * 7) !important; + --litepickerMonthWeekdayColor: #c8c9ca !important; + --litepickerDayColor: #000 !important; + --litepickerDayColorHover: #000 !important; + --litepickerDayIsTodayColor: #000 !important; + --litepickerDayIsInRange: rgba(230, 230, 230, 0.8) !important; + --litepickerDayIsLockedColor: rgba(230, 230, 230, 0.2) !important; + --litepickerDayIsBookedColor: #9e9e9e !important; + --litepickerDayIsStartColor: #000 !important; + --litepickerDayIsStartBg: rgba(128, 128, 128, 0.8) !important; + --litepickerDayIsEndColor: #000 !important; + --litepickerDayIsEndBg: rgba(128, 128, 128, 0.8) !important; + --litepickerDayWidth: 32px !important; + --litepickerButtonCancelColor: #000 !important; + --litepickerButtonCancelBg: rgba(128, 128, 128, 0.8) !important; + --litepickerButtonApplyColor: #000 !important; + --litepickerButtonApplyBg: rgba(128, 128, 128, 0.8) !important; +} diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js new file mode 100644 index 00000000..949a9240 --- /dev/null +++ b/src/components/DatePicker/index.js @@ -0,0 +1,3 @@ +import DatePicker from './DatePicker'; + +export default DatePicker; diff --git a/src/components/Decision/Decision.js b/src/components/Decision/Decision.js new file mode 100644 index 00000000..4a8efae5 --- /dev/null +++ b/src/components/Decision/Decision.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; + +import styles from './Decision.scss'; + +@withTranslation() +class Decision extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + icon: PropTypes.oneOfType([ + () => null, + PropTypes.node, + ]), + title: PropTypes.string.isRequired, + buttonText: PropTypes.string.isRequired, + }; + + static defaultProps = { + icon: null, + } + + render() { + const { props } = this; + const { + t, icon, title, form, buttonText, + } = props; + return ( + + + {icon} + + + {title} + + + {t('other:enterPassForConfirm')} + + + + ); + } +} + +export default Decision; diff --git a/src/components/Decision/Decision.scss b/src/components/Decision/Decision.scss new file mode 100644 index 00000000..48143e0b --- /dev/null +++ b/src/components/Decision/Decision.scss @@ -0,0 +1,46 @@ +@import '../../assets/styles/includes/mixin'; + +.decision { + width: 100%; + text-align: center; + + &__title { + @include title; + } + + &__subtext { + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + text-align: center; + } + + &__icon { + svg { + width: auto; + height: auto; + margin-top: 47px; + } + } + + &__token-form { + width: 100%; + padding: 0 40px; + .field { + width: 100%; + margin-bottom: 20px; + } + &__group{ + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + .field{ + width: 45%; + &__input{ + width: 60%; + } + } + } + } +} \ No newline at end of file diff --git a/src/components/Decision/Decision.test.js b/src/components/Decision/Decision.test.js new file mode 100644 index 00000000..0b78456f --- /dev/null +++ b/src/components/Decision/Decision.test.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { DecisionReject, DecisionAgree } from '.'; + +describe('DecisionReject', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); + +describe('DecisionAgree', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Decision/DecisionAgree.js b/src/components/Decision/DecisionAgree.js new file mode 100644 index 00000000..e293ef35 --- /dev/null +++ b/src/components/Decision/DecisionAgree.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Decision from './Decision'; +import { VerifyIcon } from '../Icons'; + +@withTranslation() +@inject('dialogStore') +@observer +class DecisionAgree extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + render() { + const { props } = this; + const { t, form } = props; + return ( + <> + )} + form={form} + buttonText={t('buttons:vote')} + /> + > + ); + } +} + +export default DecisionAgree; diff --git a/src/components/Decision/DecisionClose.js b/src/components/Decision/DecisionClose.js new file mode 100644 index 00000000..6a087097 --- /dev/null +++ b/src/components/Decision/DecisionClose.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Decision from './Decision'; + +@withTranslation() +@inject('dialogStore') +@observer +class DecisionClose extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + render() { + const { props } = this; + // eslint-disable-next-line no-unused-vars + const { t, form } = props; + return ( + <> + + > + ); + } +} + +export default DecisionClose; diff --git a/src/components/Decision/DecisionReject.js b/src/components/Decision/DecisionReject.js new file mode 100644 index 00000000..b3e63429 --- /dev/null +++ b/src/components/Decision/DecisionReject.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Decision from './Decision'; +import { RejectIcon } from '../Icons'; + +@withTranslation() +@inject('dialogStore') +@observer +class DecisionReject extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + render() { + const { props } = this; + const { t, form } = props; + return ( + <> + )} + form={form} + buttonText={t('buttons:vote')} + /> + > + ); + } +} + +export default DecisionReject; diff --git a/src/components/Decision/index.js b/src/components/Decision/index.js new file mode 100644 index 00000000..9077e969 --- /dev/null +++ b/src/components/Decision/index.js @@ -0,0 +1,11 @@ +import Decision from './Decision'; +import DecisionReject from './DecisionReject'; +import DecisionAgree from './DecisionAgree'; + +export default Decision; + +export { + Decision, + DecisionReject, + DecisionAgree, +}; diff --git a/src/components/Dialog/Dialog.js b/src/components/Dialog/Dialog.js index c6fd776c..885413c6 100644 --- a/src/components/Dialog/Dialog.js +++ b/src/components/Dialog/Dialog.js @@ -18,6 +18,7 @@ class Dialog extends React.Component { 'sm', 'md', 'lg', + 'xlg', ]), name: PropTypes.string.isRequired, header: PropTypes.string, diff --git a/src/components/Dialog/Dialog.scss b/src/components/Dialog/Dialog.scss index 80c3a289..c34ba23f 100644 --- a/src/components/Dialog/Dialog.scss +++ b/src/components/Dialog/Dialog.scss @@ -13,7 +13,7 @@ &__inner { position: relative; z-index: 1; - min-height: 325px; + min-height: 309px; } } @@ -42,7 +42,7 @@ padding: 0; font-weight: 700; font-size: 24px; - font-family: "Grotesk"; + font-family: "Roboto"; line-height: 28px; } @@ -57,11 +57,17 @@ z-index: 5; box-sizing: border-box; width: 100%; - padding: 25px 10px; + padding: 0 10px 25px; &--default { padding-top: 55px; } + + .text { + padding: 0 40px; + color: #c8c9ca; + font-size: 14px; + } } &--open { @@ -110,8 +116,38 @@ &--lg { .content { - width: 740px; - min-width: 740px; + width: 754px; + min-width: 754px; + } + } + + &--xlg { + .content { + width: 845px; + min-width: 845px; + } + } + + dialog-success { + &_modal, + &_modal_voting_info_wrapper, + &_modal_contract_uploading, + &_modal_questions, + &_modal_return_tokens, + &_modal_voting { + .dialog { + &__header { + padding: 20px; + padding-top: 55px; + } + &__body { + padding-bottom: 10px; + } + } + .content__inner { + height: auto; + min-height: 255px; + } } } } @@ -152,6 +188,8 @@ } } + + @keyframes anim-open { 0% { transform: translate(0, -800px); diff --git a/src/components/Dialog/Dialog.test.js b/src/components/Dialog/Dialog.test.js index d1772409..1ed75529 100644 --- a/src/components/Dialog/Dialog.test.js +++ b/src/components/Dialog/Dialog.test.js @@ -203,11 +203,11 @@ describe('Dialog', () => { ).dive().dive(); }); - it('should has dialog--close class', () => { + it('should have dialog--close class', () => { expect(wrapper.find('.dialog').hasClass('dialog--close')).toEqual(true); }); - it('should has dialog--open class', () => { + it('should have dialog--open class', () => { expect(wrapper.find('.dialog').hasClass('dialog--open')).toEqual(true); }); }); diff --git a/src/components/Dialog/index.js b/src/components/Dialog/index.js index 16772f4f..4a07e93e 100644 --- a/src/components/Dialog/index.js +++ b/src/components/Dialog/index.js @@ -1,3 +1,8 @@ import Dialog from './Dialog'; +import DefaultDialogFooter from './DefaultDialogFooter'; export default { Dialog }; + +export { + DefaultDialogFooter, +}; diff --git a/src/components/Dropdown/Dropdown.scss b/src/components/Dropdown/Dropdown.scss index 90dba5cc..bd307ccc 100644 --- a/src/components/Dropdown/Dropdown.scss +++ b/src/components/Dropdown/Dropdown.scss @@ -2,11 +2,31 @@ .dropdown { position: relative; display: inline-block; - + button { background: transparent; outline: none; } + + &--simple { + &.dropdown--opened { + .dropdown__options { + height: 160px; + } + } + .dropdown__options { + width: 100%; + max-height: 160px; + } + .dropdown__option { + display: block; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-align: left; + text-overflow: ellipsis; + } + } &__head { position: relative; @@ -39,6 +59,27 @@ } } + &__error-text { + position: absolute; + bottom: -15px; + width: 100%; + font-size: 11px; + text-align: center; + visibility: hidden; + opacity: 0; + } + + &--error { + .dropdown__error-text { + visibility: visible; + opacity: 1; + } + + .dropdown__head { + border-bottom: 1px dashed rgba(0, 0, 0, 1); + } + } + &__arrow { position: absolute; top: 50%; @@ -47,10 +88,11 @@ transform: translateY(-50%) rotate(0deg); transition: 0.2s; svg { + width: 12px; path { opacity: 1; transition: 0.2s; - stroke: $lightGrey; + stroke: $border; } } } @@ -78,18 +120,26 @@ display: inline-block; max-width: 80%; overflow: hidden; + white-space: nowrap; text-overflow: ellipsis; vertical-align: middle; + &-label { + position: absolute; + bottom: 90%; + left: 45px; + color: rgba(0, 0, 0, 0.7); + font-size: 10px; + } } &__options { position: absolute; top: 100%; z-index: 1; - width: 155%; + width: 156%; height: 0; max-height: 150px; - padding: 5px 10px; + padding: 5px 0; overflow-x: hidden; overflow-y: auto; background-color: $white; @@ -97,6 +147,8 @@ opacity: 0; transition: .3s ease-in-out; &::-webkit-scrollbar { + position: relative; + right: -30px; width: 20px; } /* Track */ @@ -131,16 +183,32 @@ } &__option { - display: block; - padding: 10px 0; + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + width: 100%; + padding: 10px; + text-align: left; border: none; cursor: pointer; + transition: .2s; + + &:hover { + background-color: rgba($color: #E6E6E6, $alpha: .8); + } + + &-label { + display: inline-block; + width: 320px; + } } &__suboption { - margin-left: 30px; + margin-left: 20px; color: $linkColor; font-weight: 700; + font-size: 14px; + text-align: right; } &--opened { diff --git a/src/components/Dropdown/index.js b/src/components/Dropdown/index.js index 2e346f3d..d4b26bdb 100644 --- a/src/components/Dropdown/index.js +++ b/src/components/Dropdown/index.js @@ -1,18 +1,45 @@ import React, { Component } from 'react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; import DropdownOption from '../DropdownOption'; import { DropdownArrowIcon } from '../Icons'; +import i18n from '../../i18n'; + import styles from './Dropdown.scss'; class Dropdown extends Component { + static propTypes = { + children: PropTypes.element, + options: PropTypes.arrayOf(PropTypes.object).isRequired, + subOptions: PropTypes.shape({}), + onSelect: PropTypes.func.isRequired, + field: PropTypes.shape({ + set: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + placeholder: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]).isRequired, + validate: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, + error: PropTypes.string, + }).isRequired, + }; + + static defaultProps = { + children: '', + subOptions: {}, + }; + constructor(props) { super(props); this.state = { selectedValue: '', }; - this.setWrapperRef = this.setWrapperRef.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); + window.ipcRenderer.on('change-language:confirm', () => { + this.updateLanguage(); + }); } componentDidMount() { @@ -21,12 +48,20 @@ class Dropdown extends Component { componentWillUnmount() { document.addEventListener('mousedown', this.handleClickOutside); + window.ipcRenderer.removeListener('change-language:confirm', () => { + this.updateLanguage(); + }); } setWrapperRef(node) { this.wrapperRef = node; } + updateLanguage = () => { + const { field } = this.props; + field.set('placeholder', i18n.t(`fields:${field.label}`)); + } + toggleOptions = () => { const { opened } = this.state; this.setState({ opened: !opened }); @@ -36,12 +71,19 @@ class Dropdown extends Component { this.setState({ opened: false }); } + calculateHeight = () => { + const optionsLength = document.querySelectorAll('.dropdown__option').length; + const height = optionsLength * 40; + return height > 150 ? 150 : height; + } + handleSelect = (selected) => { const { onSelect, field } = this.props; this.setState({ selectedValue: selected, }); field.set(selected); + field.validate(); onSelect(selected); this.toggleOptions(); } @@ -69,7 +111,14 @@ class Dropdown extends Component { )); return ( - + {children ? {children} : ''} @@ -81,30 +130,20 @@ class Dropdown extends Component { - + {getOptions} + + {field.error} + ); } } -Dropdown.propTypes = { - children: propTypes.element, - options: propTypes.arrayOf(propTypes.object).isRequired, - subOptions: propTypes.shape({}), - onSelect: propTypes.func.isRequired, - field: propTypes.shape({ - set: propTypes.func.isRequired, - value: propTypes.string.isRequired, - placeholder: propTypes.string.isRequired, - }).isRequired, -}; - -Dropdown.defaultProps = { - children: '', - subOptions: {}, -}; - - export default Dropdown; diff --git a/src/components/DropdownOption/index.js b/src/components/DropdownOption/index.js index 69a6cf81..e183d6e7 100644 --- a/src/components/DropdownOption/index.js +++ b/src/components/DropdownOption/index.js @@ -11,7 +11,9 @@ const DropdownOption = ({ className={styles.dropdown__option} onClick={() => { select(value); }} > - {label} + + {label} + {subOption !== '' ? ( @@ -27,10 +29,11 @@ DropdownOption.propTypes = { value: propTypes.string.isRequired, label: propTypes.string.isRequired, select: propTypes.func.isRequired, - subOption: propTypes.string.isRequired, + subOption: propTypes.string, }; DropdownOption.defaultProps = { + subOption: '', }; export default DropdownOption; diff --git a/src/components/Explanation/Explanation.scss b/src/components/Explanation/Explanation.scss index 1d03db9b..d73449d0 100644 --- a/src/components/Explanation/Explanation.scss +++ b/src/components/Explanation/Explanation.scss @@ -17,4 +17,13 @@ } } } + &--bold { + color: $primary; + .explanation__string { + border-left: 2px solid $primary; + } + p { + font-weight: 700; + } + } } \ No newline at end of file diff --git a/src/components/Explanation/index.js b/src/components/Explanation/index.js index 61f3208b..cc3e26b5 100644 --- a/src/components/Explanation/index.js +++ b/src/components/Explanation/index.js @@ -2,17 +2,19 @@ import React from 'react'; import propTypes from 'prop-types'; import styles from './Explanation.scss'; -const Explanation = ({ children }) => ( - - {children} - +const Explanation = ({ children, bold }) => ( + + {children} + ); Explanation.defaultProps = { children: '', + bold: false, }; Explanation.propTypes = { children: propTypes.node, + bold: propTypes.bool, }; export default Explanation; diff --git a/src/components/FinPassFormWrapper/FinPassFormWrapper.js b/src/components/FinPassFormWrapper/FinPassFormWrapper.js new file mode 100644 index 00000000..3cec825c --- /dev/null +++ b/src/components/FinPassFormWrapper/FinPassFormWrapper.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import Input from '../Input'; +import { Password } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './FinPassFormWrapper.scss'; + +@withTranslation() +class FinPassFormWrapper extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape({ + onSubmit: PropTypes.func.isRequired, + $: PropTypes.func.isRequired, + }).isRequired, + buttonText: PropTypes.string.isRequired, + }; + + render() { + const { props } = this; + const { t, form, buttonText } = props; + return ( + + + + + + + + + + {buttonText || t('buttons:startNewVoting')} + + + + + ); + } +} + +export default FinPassFormWrapper; diff --git a/src/components/FinPassFormWrapper/FinPassFormWrapper.scss b/src/components/FinPassFormWrapper/FinPassFormWrapper.scss new file mode 100644 index 00000000..9e1ed67d --- /dev/null +++ b/src/components/FinPassFormWrapper/FinPassFormWrapper.scss @@ -0,0 +1,21 @@ +.form-fin-pass { + margin-top: 56px; + + .input__wrapper { + .field { + width: 100%; + max-width: 309px; + margin-bottom: 0px; + } + } + + .button__wrapper { + margin-top: 48px; + margin-bottom: 59px; + + button { + width: 100%; + max-width: 309px; + } + } +} diff --git a/src/components/FinPassFormWrapper/FinPassFormWrapper.stories.js b/src/components/FinPassFormWrapper/FinPassFormWrapper.stories.js new file mode 100644 index 00000000..6df33427 --- /dev/null +++ b/src/components/FinPassFormWrapper/FinPassFormWrapper.stories.js @@ -0,0 +1,24 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import FinPassFormWrapper from './FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; + +const form = new FinPassForm({ + hooks: { + onSuccess() { + return Promise.resolve(); + }, + onError() { + /* eslint-disable-next-line */ + console.error('error'); + }, + }, +}); + +storiesOf('FinPassFormWrapper', module) + .add('Default', () => ( + + )); diff --git a/src/components/Footer/Footer.scss b/src/components/Footer/Footer.scss new file mode 100644 index 00000000..ef7e038e --- /dev/null +++ b/src/components/Footer/Footer.scss @@ -0,0 +1,39 @@ +@import '../../assets/styles/partials/variables'; + +.footer { + position: relative; + z-index: 1; + padding: 15px 0; + text-align: center; + + a { + flex-flow: row nowrap; + align-items: center; + justify-content: center; + color: $border; + font-size: 11px; + text-align: center; + svg { + vertical-align: middle; + path { + transition: .2s; + } + } + span { + margin-left: 10px; + vertical-align: middle; + transition: .2s; + } + &:hover{ + svg { + path { + opacity: 1; + fill: $primary; + } + } + span { + color: $primary; + } + } + } +} \ No newline at end of file diff --git a/src/components/Footer/index.js b/src/components/Footer/index.js new file mode 100644 index 00000000..10d5f276 --- /dev/null +++ b/src/components/Footer/index.js @@ -0,0 +1,16 @@ + +import React from 'react'; +import { GithubIcon } from '../Icons'; + +import styles from './Footer.scss'; + +const Footer = () => ( + +); + +export default Footer; diff --git a/src/components/Forms/ProjectInputForm.js b/src/components/Forms/ProjectInputForm.js new file mode 100644 index 00000000..aa41e54b --- /dev/null +++ b/src/components/Forms/ProjectInputForm.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import Input from '../Input'; +import { + TokenName, Password, Address, +} from '../Icons'; +import Button from '../Button/Button'; + +import styles from '../Decision/Decision.scss'; + +@withTranslation() +@observer +class ProjectInputForm extends Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { props } = this; + const { + t, form, + } = props; + return ( + + + + + + + + + + + + + + {t('buttons:create')} + + + + + ); + } +} + +export default ProjectInputForm; diff --git a/src/components/Forms/TokenInputForm.js b/src/components/Forms/TokenInputForm.js new file mode 100644 index 00000000..d67a62ea --- /dev/null +++ b/src/components/Forms/TokenInputForm.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import Input from '../Input'; +import { + TokenName, TokenSymbol, TokenCount, Password, +} from '../Icons'; +import Button from '../Button/Button'; + +import styles from '../Decision/Decision.scss'; + +@withTranslation() +@observer +class TokenInputForm extends Component { + static propTypes = { + t: PropTypes.func.isRequired, + form: PropTypes.shape().isRequired, + }; + + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { props } = this; + const { + t, form, + } = props; + return ( + + + + + + + + + + + + + + + + + + + {t('buttons:create')} + + + + + ); + } +} + +export default TokenInputForm; diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss index 392cf70d..8f2ac892 100644 --- a/src/components/Header/Header.scss +++ b/src/components/Header/Header.scss @@ -1,7 +1,7 @@ @import '../../assets/styles/partials/variables'; .header { - position: absolute; + position: relative; top: 30px; left: 50%; z-index: 2; @@ -27,14 +27,31 @@ background-color: $lightGrey; border: none; transform: translate(-50%, -50%); + &--bold { + height: 2px; + background-color: $primary; + } } &__link { position: relative; - margin: 0 30px; + display: inline-block; + width: 105px; + margin: 0 20px; + text-align: center; transition: .2s linear; + svg { + path { + fill: $white; + } + } &.active{ font-weight: bold; + svg { + path { + fill: $primary; + } + } &:before, &:after { position: absolute; left: 50%; @@ -48,17 +65,54 @@ top: 100%; transform: translateX(-50%) rotate(180deg); } - } + } + } + + &__settings { + svg { + path { + fill: $white; + } + } + &.active{ + font-weight: bold; + svg { + path { + fill: $primary; + } + } + } } + &__right { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + background-color: #FAFBFC; + .user { + &:hover { + position: absolute; + right: 17px; + } + } + + } + + .lang { padding: 10px; background-color: #fafbfc; } - + .user { + margin-right: 10px; margin-left: 10px; vertical-align: middle; - background-color: #FAFBFC; + background-color: $white; } + +} +.is-logged { + width: 260px; } \ No newline at end of file diff --git a/src/components/Header/HeaderNav/index.js b/src/components/Header/HeaderNav/index.js index 169a1b5c..e4a93544 100644 --- a/src/components/Header/HeaderNav/index.js +++ b/src/components/Header/HeaderNav/index.js @@ -1,16 +1,24 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; +import { withTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; import styles from '../Header.scss'; -const HeaderNav = () => ( +const HeaderNav = ({ + t, +}) => ( - Голосования + {t('other:voting')} / - Вопросы + {t('other:questions')} / - Участники + {t('other:members')} ); -export default HeaderNav; +HeaderNav.propTypes = { + t: PropTypes.func.isRequired, +}; + +export default withTranslation()(HeaderNav); diff --git a/src/components/Header/index.js b/src/components/Header/index.js index 4cb974f4..44b53744 100644 --- a/src/components/Header/index.js +++ b/src/components/Header/index.js @@ -1,20 +1,31 @@ import React from 'react'; import { inject, observer } from 'mobx-react'; +import { NavLink } from 'react-router-dom'; import Logo from '../Logo'; import HeaderNav from './HeaderNav'; import LangSwitcher from '../LangSwitcher'; import User from '../User'; import styles from './Header.scss'; +import { SettingsIcon } from '../Icons'; const Header = inject('userStore', 'appStore')(observer(({ appStore: { inProject }, userStore: { authorized, address } }) => ( {inProject ? : ''} - - + + {authorized ? {address} : ''} + { + authorized + ? ( + + + + ) + : null + } ))); diff --git a/src/components/Heading/index.js b/src/components/Heading/index.js index fca314fa..f47ff389 100644 --- a/src/components/Heading/index.js +++ b/src/components/Heading/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; import styles from './Heading.scss'; const Heading = ({ children }) => ( @@ -10,7 +10,10 @@ const Heading = ({ children }) => ( ); Heading.propTypes = { - children: propTypes.arrayOf(propTypes.string).isRequired, + children: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.string, + ]).isRequired, }; export default Heading; diff --git a/src/components/Hint/Hint.scss b/src/components/Hint/Hint.scss index e1c23c5c..7047564e 100644 --- a/src/components/Hint/Hint.scss +++ b/src/components/Hint/Hint.scss @@ -17,11 +17,12 @@ line-height: 15px; text-align: center; transition: 0.2s; + &::before { position: absolute; top: 50%; left: 50%; - z-index: -1; + z-index: 2; width: 100%; height: 100%; background-color: $white; @@ -30,31 +31,72 @@ transition: 0.2s; content: ''; } + + &::after { + position: relative; + top: 1px; + z-index: 3; + font-weight: 400; + content: ' ? '; + } + &:hover { color: $white; + &:before { background-color: $primary; border-color: $primary; } + & + .hint__text { visibility: visible; opacity: 1; } } } - + &__text { position: absolute; - top:50%; + top:50%; left: 50%; - z-index: -2; - width: 185px; - padding: 24px; + z-index: 1; + width: 292px; + padding: 21px; font-size: 11px; + line-height: 13px; + white-space: pre-wrap; + text-align: left; + background-color: #fff; border: 1px solid $border; - transform: translate(-1px, 2px); + transform: translateZ(0); visibility: hidden; opacity: 0; transition: 0.2s; + strong { + font-weight: 700; + } + } + + &--square { + .hint__icon { + &::before{ + transform: translate(-50%, -50%); + } + } + } + &--formula { + .hint__text { + top: unset; + bottom: 50%; + width: 570px; + &>p { + margin: 5px 0; + font-size: 11px; + line-height: 13px; + &:first-child, &:last-child { + margin: 10px 0; + } + } + } } } \ No newline at end of file diff --git a/src/components/Hint/index.js b/src/components/Hint/index.js index e55184fa..11279efd 100644 --- a/src/components/Hint/index.js +++ b/src/components/Hint/index.js @@ -1,19 +1,109 @@ import React from 'react'; import propTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; import styles from './Hint.scss'; const Hint = ({ children }) => ( - ? + {children} ); +const SquareHint = ({ children }) => ( + + + + {children} + + +); + +const FormulaHint = withTranslation()(() => ( + + + + + Формула голосования записывается в примерном виде: + + {'erc20{0x123...EF}->exclude{0x234...FE}->conditions{quorum>50%, positive>50% of all}, где:'} + + + + {'1) erc20{0x123...EF} / custom{0x123...EF}'} + {' '} + - тип токенов и адрес токенов необходимой группы + + + {'2) exclude{0x234...FE}'} + {' '} + – пользователи, которые не должны голосовать (опционально) + + + {'3) conditions{quorum>50%, positive>50% of all}'} + {' '} + - условия для принятия решения по голосованию + + + {'3.1) quorum>50%'} + {' '} + min% голосов в общем + + + {'3.2) positive>50%'} + {' '} + min% голосов «ЗА» + + + 3.3 of quorum / of all + {' '} + – модификатор, от какого числа считать условие positive - от числа токенов, + которые учавствовали в голосовании, или от всех токенов из контракта группы + + + Вы можете связывать несколько групп пользователей, объединяя их формулы операторами + and + или + or + .Например: + «Формула 1» + or + «Формула 2» + and + «Формула 3» + + + +)); + + +const SelectorHint = () => ( + + + + Выглядит как 4 байта Keccak хэша от сигнатуры функции в ASCII кодировке + Пример: + + bytes4(keccak256(baz(uint32,bool))) = + {' '} + 0xcdcd77c0 + + + +); + Hint.propTypes = { - children: propTypes.arrayOf(propTypes.string).isRequired, + children: propTypes.string.isRequired, }; -export default Hint; +SquareHint.propTypes = { + children: propTypes.string.isRequired, +}; + + +export { + Hint, SquareHint, FormulaHint, SelectorHint, +}; diff --git a/src/components/Icons/entities/AdminIcon.js b/src/components/Icons/entities/AdminIcon.js new file mode 100644 index 00000000..b587a17e --- /dev/null +++ b/src/components/Icons/entities/AdminIcon.js @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const AdminIcon = ({ + width, + height, + color, +}) => ( + + + + + + + + +); + +AdminIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +AdminIcon.defaultProps = { + width: 16, + height: 16, + color: '#000', +}; + +export default AdminIcon; diff --git a/src/components/Icons/entities/Arrow.js b/src/components/Icons/entities/Arrow.js new file mode 100644 index 00000000..5d1123dc --- /dev/null +++ b/src/components/Icons/entities/Arrow.js @@ -0,0 +1,12 @@ +import React from 'react'; + +const Arrow = () => ( + + + +); + +export default Arrow; diff --git a/src/components/Icons/entities/BinaryIcon.js b/src/components/Icons/entities/BinaryIcon.js new file mode 100644 index 00000000..16bc2923 --- /dev/null +++ b/src/components/Icons/entities/BinaryIcon.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const BinaryIcon = ({ + width, + height, + color, +}) => ( + + + + +); + +BinaryIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +BinaryIcon.defaultProps = { + width: 16, + height: 16, + color: '#4D4D4D', +}; + +export default BinaryIcon; diff --git a/src/components/Icons/entities/BorderArrowIcon.js b/src/components/Icons/entities/BorderArrowIcon.js new file mode 100644 index 00000000..188de531 --- /dev/null +++ b/src/components/Icons/entities/BorderArrowIcon.js @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const BorderArrowIcon = ({ + width, + height, + color, +}) => ( + + + + +); + +BorderArrowIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +BorderArrowIcon.defaultProps = { + width: 25, + height: 25, + color: '#000', +}; + +export default BorderArrowIcon; diff --git a/src/components/Icons/entities/DateIcon.js b/src/components/Icons/entities/DateIcon.js new file mode 100644 index 00000000..4658fc83 --- /dev/null +++ b/src/components/Icons/entities/DateIcon.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const DateIcon = ({ + width, + height, + color, +}) => ( + + + + + + +); + +DateIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +DateIcon.defaultProps = { + width: 16, + height: 17, + color: '#4D4D4D', +}; +export default DateIcon; diff --git a/src/components/Icons/entities/DescisionIcon.js b/src/components/Icons/entities/DescisionIcon.js new file mode 100644 index 00000000..b750f54d --- /dev/null +++ b/src/components/Icons/entities/DescisionIcon.js @@ -0,0 +1,10 @@ +import React from 'react'; + +const DescisionIcon = () => ( + + + + +); + +export default DescisionIcon; diff --git a/src/components/Icons/entities/GithubIcon.js b/src/components/Icons/entities/GithubIcon.js new file mode 100644 index 00000000..f6f0d017 --- /dev/null +++ b/src/components/Icons/entities/GithubIcon.js @@ -0,0 +1,8 @@ +import React from 'react'; + +const GithubIcon = () => ( + + + +); +export default GithubIcon; diff --git a/src/components/Icons/entities/NoQuorum.js b/src/components/Icons/entities/NoQuorum.js new file mode 100644 index 00000000..ab485355 --- /dev/null +++ b/src/components/Icons/entities/NoQuorum.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const NoQuorum = ({ + width, + height, +}) => ( + + + + +); + +NoQuorum.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +NoQuorum.defaultProps = { + width: 32, + height: 32, +}; + +export default NoQuorum; diff --git a/src/components/Icons/entities/PlayCircleIcon.js b/src/components/Icons/entities/PlayCircleIcon.js new file mode 100644 index 00000000..0570e037 --- /dev/null +++ b/src/components/Icons/entities/PlayCircleIcon.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const PlayCircleIcon = ({ + width, + height, + color, + opacity, +}) => ( + + + + + + +); + +PlayCircleIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + opacity: PropTypes.number, + color: PropTypes.string, +}; + +PlayCircleIcon.defaultProps = { + width: 32, + height: 32, + opacity: 0.7, + color: '#000', +}; + +export default PlayCircleIcon; diff --git a/src/components/Icons/entities/Pudding.js b/src/components/Icons/entities/Pudding.js new file mode 100644 index 00000000..0a377b4c --- /dev/null +++ b/src/components/Icons/entities/Pudding.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Pudding = ({ + width, + height, + color, +}) => ( + + + +); + +Pudding.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, +}; + +Pudding.defaultProps = { + width: 19, + height: 17, + color: '#000', +}; + +export default Pudding; diff --git a/src/components/Icons/entities/QuestionIcon.js b/src/components/Icons/entities/QuestionIcon.js new file mode 100644 index 00000000..dbfe2cfd --- /dev/null +++ b/src/components/Icons/entities/QuestionIcon.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const QuestionIcon = ({ + width, + height, + opacity, + color, +}) => ( + + + + + + + +); + +QuestionIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + opacity: PropTypes.number, + color: PropTypes.string, +}; + +QuestionIcon.defaultProps = { + width: 16, + height: 16, + opacity: 0.7, + color: '#000', +}; + +export default QuestionIcon; diff --git a/src/components/Icons/entities/SettingsIcon.js b/src/components/Icons/entities/SettingsIcon.js new file mode 100644 index 00000000..5c6a6217 --- /dev/null +++ b/src/components/Icons/entities/SettingsIcon.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const SettingsIcon = () => ( + + + +); + +export default SettingsIcon; diff --git a/src/components/Icons/entities/SigningIcon.js b/src/components/Icons/entities/SigningIcon.js new file mode 100644 index 00000000..328b9831 --- /dev/null +++ b/src/components/Icons/entities/SigningIcon.js @@ -0,0 +1,15 @@ +import React from 'react'; + +const SigningIcon = () => ( + + + + + + + + + +); + +export default SigningIcon; diff --git a/src/components/Icons/entities/StartIcon.js b/src/components/Icons/entities/StartIcon.js new file mode 100644 index 00000000..699eba1b --- /dev/null +++ b/src/components/Icons/entities/StartIcon.js @@ -0,0 +1,12 @@ +import React from 'react'; + +const StartIcon = () => ( + + + + + + +); + +export default StartIcon; diff --git a/src/components/Icons/entities/StatsIcon.js b/src/components/Icons/entities/StatsIcon.js index 22cbe455..0f69a1ae 100644 --- a/src/components/Icons/entities/StatsIcon.js +++ b/src/components/Icons/entities/StatsIcon.js @@ -3,8 +3,18 @@ import React from 'react'; const Stats = () => ( - - + + diff --git a/src/components/Icons/entities/ThinArrow.js b/src/components/Icons/entities/ThinArrow.js new file mode 100644 index 00000000..f491cc06 --- /dev/null +++ b/src/components/Icons/entities/ThinArrow.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const ThinArrow = ({ + width, + height, + color, + reverse, +}) => ( + + + +); + +ThinArrow.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + color: PropTypes.string, + reverse: PropTypes.bool, +}; + +ThinArrow.defaultProps = { + width: 5, + height: 9, + color: 'currentColor', + reverse: false, +}; + +export default ThinArrow; diff --git a/src/components/Icons/index.js b/src/components/Icons/index.js index fa86a243..73c10f18 100644 --- a/src/components/Icons/index.js +++ b/src/components/Icons/index.js @@ -13,6 +13,8 @@ import EyeIcon from './entities/EyeIcon'; import IconInfo from './entities/InfoIcon'; import Login from './entities/LoginIcon'; import Password from './entities/PasswordIcon'; +import PlayCircleIcon from './entities/PlayCircleIcon'; +import Pudding from './entities/Pudding'; import QuestionUploadingIcon from './entities/QuestionUploadingIcon'; import SendingIcon from './entities/SendingIcon'; import Stats from './entities/StatsIcon'; @@ -22,7 +24,19 @@ import TokenName from './entities/TokenNameIcon'; import TxHashIcon from './entities/TxHashIcon'; import TxRecieptIcon from './entities/TxRecieptIcon'; import VerifyIcon from './entities/VerifyIcon'; +import StartIcon from './entities/StartIcon'; +import GithubIcon from './entities/GithubIcon'; import RejectIcon from './entities/RejectIcon'; +import BorderArrowIcon from './entities/BorderArrowIcon'; +import AdminIcon from './entities/AdminIcon'; +import QuestionIcon from './entities/QuestionIcon'; +import ThinArrow from './entities/ThinArrow'; +import DateIcon from './entities/DateIcon'; +import SettingsIcon from './entities/SettingsIcon'; +import NoQuorum from './entities/NoQuorum'; +import DescisionIcon from './entities/DescisionIcon'; +import Arrow from './entities/Arrow'; +import SigningIcon from './entities/SigningIcon'; export { AddIcon, @@ -40,6 +54,8 @@ export { IconInfo, Login, Password, + PlayCircleIcon, + Pudding, QuestionUploadingIcon, SendingIcon, Stats, @@ -49,5 +65,17 @@ export { TxHashIcon, TxRecieptIcon, VerifyIcon, + StartIcon, + GithubIcon, RejectIcon, + BorderArrowIcon, + AdminIcon, + QuestionIcon, + ThinArrow, + DateIcon, + SettingsIcon, + NoQuorum, + DescisionIcon, + Arrow, + SigningIcon, }; diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss index f07437e7..f164c17d 100644 --- a/src/components/Input/Input.scss +++ b/src/components/Input/Input.scss @@ -12,7 +12,7 @@ &__input { width: 85%; - margin-left: 20px; + margin-left: 17px; padding: 8px 0; vertical-align: middle; background: transparent; @@ -32,6 +32,26 @@ font-size: 9px; } } + + &--textarea { + max-width: 100%; + min-height: 80px; + padding: 10px; + background-color: transparent; + border: 1px solid #e1e4e8; + border-radius: 2px; + outline: none; + transition: .2s; + &::-webkit-input-placeholder { + opacity: 0; + } + &:focus { + border-color: $primary; + & + .field__label--textarea { + color: $primary; + } + } + } } &__label { @@ -43,6 +63,18 @@ font-size: 14px; transform: translateY(-50%); transition: 0.2s; + + &--textarea { + position: absolute; + bottom: 100%; + margin-bottom: 4px; + &>span { + color: $placeholderColor; + font-size: 14px; + line-height: 16px; + text-align: left; + } + } } &__error-text { @@ -54,7 +86,7 @@ visibility: hidden; opacity: 0; } - + &__line { position: absolute; bottom: -1px; @@ -62,6 +94,7 @@ width: 0; height: 1px; background-color: $primary; + border-bottom: 1px solid $primary; transition: 0.3s ease-in; } @@ -82,6 +115,10 @@ style: dashed; color: $primary; } + &.field--textarea { + padding-bottom: 1px; + border-bottom: none; + } .field__error-text { visibility: visible; opacity: 1; @@ -100,9 +137,37 @@ } &:focus { & ~ .field__line { - width: 0; + width: 100%; } } + + &--textarea { + border-color: $primary; + border-style: dashed; + } + } + + } + + &--textarea { + position: relative; + .field__error-text { + top: unset; + bottom: -17px; } + .hint { + position: relative; + z-index: 1; + margin-left: 10px; + transform: translate(0,0); + } + } + + .hint { + position: absolute; + top: 50%; + right: 0; + z-index: 1; + transform: translateY(-50%); } } diff --git a/src/components/Input/InputTextarea.js b/src/components/Input/InputTextarea.js new file mode 100644 index 00000000..f458a9d8 --- /dev/null +++ b/src/components/Input/InputTextarea.js @@ -0,0 +1,66 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React, { Component } from 'react'; +import propTypes from 'prop-types'; +import { observer } from 'mobx-react'; + +import styles from './Input.scss'; + +@observer +class InputTextarea extends Component { + handleOnChange = (e) => { + const { field, onInput } = this.props; + field.onChange(e); + onInput(field.value); + } + + render() { + const { + field, className, hint, + } = this.props; + return ( + + + + {field.placeholder} + {hint} + + + {field.error} + + + ); + } +} + +InputTextarea.propTypes = { + className: propTypes.string, + field: propTypes.shape({ + error: propTypes.string, + value: propTypes.string.isRequired, + placeholder: propTypes.string, + label: propTypes.string, + bind: propTypes.func.isRequired, + onChange: propTypes.func.isRequired, + }).isRequired, + onInput: propTypes.func, + hint: propTypes.element, +}; + +InputTextarea.defaultProps = { + className: '', + onInput: () => null, + hint: null, +}; + +export default InputTextarea; diff --git a/src/components/Input/index.js b/src/components/Input/index.js index d073daff..9b54c81c 100644 --- a/src/components/Input/index.js +++ b/src/components/Input/index.js @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable react/jsx-props-no-spreading */ import React, { Component } from 'react'; import propTypes from 'prop-types'; @@ -7,6 +9,15 @@ import styles from './Input.scss'; @observer class Input extends Component { + constructor(props) { + super(props); + + const { field } = props; + if (props.defaultValue) { + field.set(props.defaultValue); + } + } + handleOnChange = (e) => { const { field, onInput } = this.props; field.onChange(e); @@ -15,7 +26,7 @@ class Input extends Component { render() { const { - children, field, className, + children, field, className, hint, } = this.props; return ( @@ -26,7 +37,13 @@ class Input extends Component { value={field.value} onChange={this.handleOnChange} /> - {field.placeholder} + { field.focus(); }} + > + {field.placeholder} + + {hint} {field.error} @@ -37,21 +54,34 @@ class Input extends Component { } Input.propTypes = { - children: propTypes.element.isRequired, + children: propTypes.element, className: propTypes.string, field: propTypes.shape({ - error: propTypes.string.isRequired, - value: propTypes.string.isRequired, - placeholder: propTypes.string.isRequired, + error: propTypes.string, + value: propTypes.oneOfType([ + propTypes.string, + propTypes.number, + ]).isRequired, + placeholder: propTypes.oneOfType([ + propTypes.string, + propTypes.shape({}), + ]).isRequired, + set: propTypes.func.isRequired, + focus: propTypes.func.isRequired, bind: propTypes.func.isRequired, onChange: propTypes.func.isRequired, }).isRequired, + defaultValue: propTypes.oneOfType([propTypes.string, propTypes.number]), onInput: propTypes.func, + hint: propTypes.element, }; Input.defaultProps = { + children: null, className: '', onInput: () => null, + defaultValue: '', + hint: null, }; export default Input; diff --git a/src/components/InputSeed/SeedForm.js b/src/components/InputSeed/SeedForm.js index f390f8c2..bc64a6b6 100644 --- a/src/components/InputSeed/SeedForm.js +++ b/src/components/InputSeed/SeedForm.js @@ -8,6 +8,16 @@ import styles from '../Login/Login.scss'; @withTranslation() class SeedInput extends Component { + static propTypes = { + form: propTypes.shape({ + onSubmit: propTypes.func.isRequired, + loading: propTypes.bool.isRequired, + $: propTypes.func.isRequired, + }).isRequired, + seed: propTypes.arrayOf(propTypes.string).isRequired, + t: propTypes.func.isRequired, + }; + render() { const { seed, form, t, @@ -36,14 +46,4 @@ class SeedInput extends Component { } } -SeedInput.propTypes = { - form: propTypes.shape({ - onSubmit: propTypes.func.isRequired, - loading: propTypes.bool.isRequired, - $: propTypes.func.isRequired, - }).isRequired, - seed: propTypes.arrayOf(propTypes.string).isRequired, - t: propTypes.func.isRequired, -}; - export default SeedInput; diff --git a/src/components/InputSeed/index.js b/src/components/InputSeed/index.js index 2b1b3690..eced8159 100644 --- a/src/components/InputSeed/index.js +++ b/src/components/InputSeed/index.js @@ -1,3 +1,4 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import propTypes from 'prop-types'; @@ -11,6 +12,8 @@ import { BackIcon } from '../Icons'; import Loader from '../Loader'; import SeedForm from '../../stores/FormsStore/SeedForm'; import SeedInput from './SeedForm'; +import UserStore from '../../stores/UserStore/UserStore'; +import AppStore from '../../stores/AppStore/AppStore'; import styles from '../Login/Login.scss'; @@ -18,6 +21,13 @@ import styles from '../Login/Login.scss'; @inject('appStore', 'userStore') @observer class InputSeed extends Component { + static propTypes = { + userStore: propTypes.instanceOf(UserStore).isRequired, + appStore: propTypes.instanceOf(AppStore).isRequired, + recover: propTypes.bool.isRequired, + t: propTypes.func.isRequired, + }; + seedForm = new SeedForm({ hooks: { onSuccess: (form) => this.submitForm(form), @@ -108,21 +118,4 @@ class InputSeed extends Component { } } -InputSeed.propTypes = { - userStore: propTypes.shape({ - setMnemonicRepeat: propTypes.func.isRequired, - isSeedValid: propTypes.func.isRequired, - recoverWallet: propTypes.func.isRequired, - setEncryptedWallet: propTypes.func.isRequired, - getEthBalance: propTypes.func.isRequired, - saveWalletToFile: propTypes.func.isRequired, - mnemonic: propTypes.arrayOf(propTypes.string).isRequired, - }).isRequired, - appStore: propTypes.shape({ - displayAlert: propTypes.func.isRequired, - }).isRequired, - recover: propTypes.bool.isRequired, - t: propTypes.func.isRequired, -}; - export default InputSeed; diff --git a/src/components/LangSwitcher/LangSwitcher.scss b/src/components/LangSwitcher/LangSwitcher.scss index 046b9ecf..65394fe0 100644 --- a/src/components/LangSwitcher/LangSwitcher.scss +++ b/src/components/LangSwitcher/LangSwitcher.scss @@ -1,6 +1,7 @@ @import '../../assets/styles/partials/variables'; .lang { + position: relative; display: inline-block; &--opened { @@ -40,10 +41,12 @@ &__options { position: absolute; + left: 10px; display: inline-block; width: max-content; - padding: 5px 10px; - border: 1px solid $border; + padding: 5px 15px; + background-color: $white; + border: 1px solid #E1E4E8; visibility: hidden; opacity: 0; transition: 0.2s linear; @@ -51,10 +54,18 @@ &__option { display: block; padding: 5px 0; - font-size: 16px; + font-size: 14px; background-color: $white; border: none; outline: none; cursor: pointer; + + &:first-child { + padding: 5px 0 10px; + } + + &:last-child { + padding: 10px 0 5px; + } } } \ No newline at end of file diff --git a/src/components/LangSwitcher/index.js b/src/components/LangSwitcher/index.js index 5dc1669f..30f635d9 100644 --- a/src/components/LangSwitcher/index.js +++ b/src/components/LangSwitcher/index.js @@ -2,20 +2,39 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import React, { Component } from 'react'; import { withTranslation } from 'react-i18next'; +import moment from 'moment'; import propTypes from 'prop-types'; +import nextId from 'react-id-generator'; import i18n from '../../i18n'; import styles from './LangSwitcher.scss'; +import { getCorrectMomentLocale } from '../../utils/Date'; @withTranslation() class LangSwitcher extends Component { + static propTypes = { + t: propTypes.func.isRequired, + disabled: propTypes.bool, + onSelect: propTypes.func, + } + + static defaultProps = { + disabled: false, + onSelect: () => {}, + }; + constructor(props) { super(props); this.state = { opened: false, + language: null, }; this.setWrapperRef = this.setWrapperRef.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); + + window.ipcRenderer.on('change-language:confirm', (event, value) => { + this.changeLanguage(value); + }); } componentDidMount() { @@ -24,6 +43,9 @@ class LangSwitcher extends Component { componentWillUnmount() { document.addEventListener('mousedown', this.handleClickOutside); + window.ipcRenderer.removeListener('change-language:confirm', (event, value) => { + this.changeLanguage(value); + }); } setWrapperRef(node) { @@ -37,10 +59,21 @@ class LangSwitcher extends Component { }); } + changeLanguage = (value) => { + i18n.changeLanguage(value); + moment.locale(getCorrectMomentLocale(i18n.language)); + } + selectOption = (e) => { + const { props: { disabled, onSelect } } = this; const value = e.target.getAttribute('data-value'); this.toggleOptions(); - i18n.changeLanguage(value); + this.setState({ language: value }); + if (disabled) { + onSelect(value); + } else { + window.ipcRenderer.send('change-language:request', value); + } } closeOptions = () => { @@ -56,13 +89,13 @@ class LangSwitcher extends Component { } render() { - const { opened } = this.state; + const { opened, language: stateLanguage } = this.state; const { t } = this.props; const { language } = i18n; return ( - {language} + {stateLanguage !== null ? stateLanguage : language} { @@ -72,6 +105,7 @@ class LangSwitcher extends Component { className={styles.lang__option} data-value={item} onClick={this.selectOption} + key={nextId('lang_switcher_option')} > {`${t(`other:${item}`)} (${item})`} @@ -83,8 +117,5 @@ class LangSwitcher extends Component { } } -LangSwitcher.propTypes = { - t: propTypes.func.isRequired, -}; export default LangSwitcher; diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss index 1ad9986d..ebb56c1f 100644 --- a/src/components/Loader/Loader.scss +++ b/src/components/Loader/Loader.scss @@ -7,7 +7,7 @@ height: 14px; margin: 20px; background-color: $primary; - + &:before { position: absolute; top: 50%; @@ -20,6 +20,7 @@ animation: loaderSpin 2s ease-in infinite; content: ''; } + &:after { position: absolute; top: 50%; @@ -28,11 +29,23 @@ color: $white; font-weight: bolder; font-size: 102px; - line-height: 26px; + line-height: 26px; transform: translate(-50%, -50%); animation: loaderSpin 2s ease-in infinite; content: "+"; } + + &--white { + &::after { + color: $white; + } + } + + &--gray { + &::after { + color: #fafbfc; + } + } } @keyframes loaderSpin { diff --git a/src/components/Loader/index.js b/src/components/Loader/index.js index 70ceea1e..956659d3 100644 --- a/src/components/Loader/index.js +++ b/src/components/Loader/index.js @@ -1,9 +1,25 @@ import React from 'react'; +import PropTypes from 'prop-types'; import styles from './Loader.scss'; -const Loader = () => ( - +const Loader = ({ + theme, +}) => ( + ); +Loader.propTypes = { + theme: PropTypes.oneOf(['white', 'gray']), +}; + +Loader.defaultProps = { + theme: 'white', +}; + export default Loader; diff --git a/src/components/Login/Login.scss b/src/components/Login/Login.scss index bd4d362a..f1b10563 100644 --- a/src/components/Login/Login.scss +++ b/src/components/Login/Login.scss @@ -46,6 +46,12 @@ form, .add-project { padding: 0 50px; } + + &.create-token-data { + .btn { + margin: 38px auto 16px; + } + } } &__submit { @@ -147,10 +153,11 @@ text-align: left; &-id { display: inline-block; + width: 20px; margin-right: 10px; color: #181818; + text-align: right; opacity: .5; - } &-text { font-weight: bold; @@ -235,7 +242,7 @@ } } } -} +} .create { .btn--white { @@ -254,7 +261,7 @@ margin: 5px auto 0; } } - + } &__label { @@ -275,7 +282,7 @@ display: inline-block; width: 80px; height: 80px; - transition: .3s linear; + transition: .3s linear; &__icon { position: absolute; top: 50%; @@ -351,7 +358,7 @@ opacity: 1; } } - + &.active{ & > img { opacity: 1; diff --git a/src/components/Login/index.js b/src/components/Login/index.js index 52095573..2dfff961 100644 --- a/src/components/Login/index.js +++ b/src/components/Login/index.js @@ -1,6 +1,7 @@ +/* eslint-disable react/sort-comp */ import React, { Component } from 'react'; import { observer, inject } from 'mobx-react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; import { NavLink, Redirect } from 'react-router-dom'; import { withTranslation } from 'react-i18next'; import Container from '../Container'; @@ -12,6 +13,8 @@ import Input from '../Input'; import Button from '../Button/Button'; import LoadingBlock from '../LoadingBlock'; import LoginForm from '../../stores/FormsStore/LoginForm'; +import AppStore from '../../stores/AppStore/AppStore'; +import UserStore from '../../stores/UserStore/UserStore'; import styles from './Login.scss'; @@ -19,6 +22,12 @@ import styles from './Login.scss'; @inject('userStore', 'appStore') @observer class Login extends Component { + static propTypes = { + appStore: PropTypes.instanceOf(AppStore).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + t: PropTypes.func.isRequired, + }; + loginForm = new LoginForm({ hooks: { onSuccess: (form) => this.login(form), @@ -87,7 +96,10 @@ const InputForm = withTranslation()(({ - + @@ -109,27 +121,14 @@ const InputForm = withTranslation()(({ )); -Login.propTypes = { - appStore: propTypes.shape({ - displayAlert: propTypes.func.isRequired, - readWalletList: propTypes.func.isRequired, - }).isRequired, - userStore: propTypes.shape({ - logging: propTypes.bool.isRequired, - login: propTypes.func.isRequired, - authorized: propTypes.bool.isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; - InputForm.propTypes = { - appStore: propTypes.shape({ - wallets: propTypes.arrayOf(propTypes.object).isRequired, + appStore: PropTypes.shape({ + wallets: PropTypes.arrayOf(PropTypes.object).isRequired, }).isRequired, - form: propTypes.shape({ - $: propTypes.func.isRequired, - onSubmit: propTypes.func.isRequired, - loading: propTypes.bool.isRequired, + form: PropTypes.shape({ + $: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, }).isRequired, }; diff --git a/src/components/Logo/Logo.scss b/src/components/Logo/Logo.scss index 8ed4a7ce..31893530 100644 --- a/src/components/Logo/Logo.scss +++ b/src/components/Logo/Logo.scss @@ -35,7 +35,7 @@ span { font-weight: 700; font-size: 14px; - font-family: "Grotesk"; + font-family: "Roboto"; } } diff --git a/src/components/Logo/index.js b/src/components/Logo/index.js index ac442623..c1839dab 100644 --- a/src/components/Logo/index.js +++ b/src/components/Logo/index.js @@ -1,9 +1,11 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import { inject, observer } from 'mobx-react'; + import styles from './Logo.scss'; -const Logo = () => ( - +const Logo = inject('appStore')(observer(({ appStore: { inProject } }) => ( + 01 @@ -11,6 +13,6 @@ const Logo = () => ( ZeroOne -); +))); export default Logo; diff --git a/src/components/Members/MemberItem.js b/src/components/Members/MemberItem.js new file mode 100644 index 00000000..198d9d31 --- /dev/null +++ b/src/components/Members/MemberItem.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const MemberItem = ({ + name, +}) => ( + + {name} + +); + +MemberItem.propTypes = { + name: PropTypes.string.isRequired, +}; + +export default MemberItem; diff --git a/src/components/Members/Members.scss b/src/components/Members/Members.scss new file mode 100644 index 00000000..11168ac1 --- /dev/null +++ b/src/components/Members/Members.scss @@ -0,0 +1,272 @@ +.members { + &__page { + margin-bottom: 40px; + text-align: center; + + &-loader { + text-align: center; + } + + .loader { + margin-top: 50px; + } + } + + &__top { + width: 100%; + margin-bottom: 15px; + + &-button { + display: inline-block; + width: 100%; + padding: 26px 96px 26px 100px; + background: #fff; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + + &-icon, + &-text { + display: inline-block; + vertical-align: middle; + } + + &-icon { + margin-right: 16px; + + svg { + width: auto; + height: auto; + } + } + + &-text { + color: #000; + font-weight: 300; + font-size: 18px; + font-family: "Roboto"; + line-height: 21px; + } + } + } + + &__group { + margin-bottom: 8px; + + &-button { + width: 100%; + padding: 8px 0; + text-align: left; + background: #fff; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + } + + &-id { + margin-bottom: 4px; + color: rgba(0, 0, 0, 0.2); + font-weight: 300; + font-size: 11px; + font-family: "Roboto"; + line-height: 13px; + } + + &-main, + &-extra, + &-divider { + display: inline-block; + vertical-align: middle; + } + + &-main { + width: 75%; + } + + &-id, + &-main, + &-wallet { + padding-left: 29px; + } + + &-extra { + position: relative; + width: calc(25% - 1px); + padding: 10px 20px; + text-align: center; + } + + &-divider { + width: 1px; + height: 100%; + max-height: 112px; + margin-top: -6px; + margin-bottom: -6px; + background-color: rgba(0, 0, 0, 0.1); + } + + &-name { + margin-bottom: 8px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + &-description { + min-height: 65px; + margin-bottom: 8px; + padding-right: 15px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 11px; + line-height: 13px; + } + + &-wallet, + &-token { + color: rgba(200, 201, 202, 0.7); + font-size: 11px; + line-height: 13px; + } + + &-balance { + margin-bottom: 8px; + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 14px; + line-height: 16px; + } + + &-no-data { + box-sizing: border-box; + width: 100%; + padding: 18px 32px; + background: #fff; + border: 1px solid #e1e4e8; + border-top: unset; + + &-icon, + &-text { + display: inline-block; + vertical-align: middle; + } + + &-icon { + margin-right: 10px; + } + + &-text { + color: #4d4d4d; + font-size: 11px; + line-height: 13px; + white-space: pre-line; + } + } + + &-table { + width: 100%; + padding-top: 8px; + background: #fff; + border: 1px solid #e1e4e8; + border-top: unset; + border-collapse: collapse; + + &-th { + padding: 8px; + color: #808080; + font-size: 11px; + line-height: 13px; + text-align: left; + + &--weight, + &--balance { + padding-right: 40px; + text-align: right; + } + } + + &-td { + padding: 10px; + + button { + width: 100%; + height: 100%; + background-color: transparent; + border: none; + outline: unset; + cursor: pointer; + } + + &--is { + min-width: 20px; + padding: 0; + text-align: center; + } + + &--img { + width: 24px; + padding: 0; + } + + &--wallet { + padding: 10px 8px; + color: #000; + font-size: 14px; + line-height: 16px; + } + + &--weight, + &--balance { + padding-right: 40px; + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-align: right; + + button { + text-align: right; + } + } + + &--balance { + position: relative; + + span { + position: absolute; + top: 50%; + right: -14px; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.5s; + } + + svg { + width: auto; + height: auto; + } + } + } + + &-tr { + background-color: #fff; + transition: background-color 0.5s; + + &:hover { + background: #e1e4e8; + + .members__group-table-td--balance { + span { + opacity: 1; + } + } + } + } + } + } +} +.text { + &--left { + text-align: left; + } +} \ No newline at end of file diff --git a/src/components/Members/Members.stories.js b/src/components/Members/Members.stories.js new file mode 100644 index 00000000..960305ce --- /dev/null +++ b/src/components/Members/Members.stories.js @@ -0,0 +1,80 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import MembersTop from './MembersTop'; +import MembersGroupComponent from './MembersGroupComponent'; +import MembersStore from '../../stores/MembersStore/MembersStore'; + +const memberStore = new MembersStore([]); + +memberStore.addToGroups({ + name: 'Менеджеры', + description: 'Могут голосовать только по вопросам из групп: “Дизайн”, “Верстка”, “Бэкэнд”', + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + balance: '0,000654', + customTokenName: 'TKN', + tokenName: 'Кастомные токены', + textForEmptyState: 'other:noDataAdmins', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 25, + balance: '0,000004', + customTokenName: 'TKN', + isAdmin: true, + }, + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 25, + balance: '0,000004', + customTokenName: 'TKN', + }, + ], +}); + +memberStore.addToGroups({ + name: 'Администраторы', + description: 'Могут голосовать по любым вопросам.', + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + balance: '11,510156', + customTokenName: 'TKN', + tokenName: 'ERC20', + textForEmptyState: 'other:noDataAdmins', + list: [], +}); + +const groupWithList = memberStore.groups[0]; +const groupWithEmptyList = memberStore.groups[1]; + +storiesOf('Members', module) + .add('MembersTop', () => ( + + )) + .add('MembersGroupComponent with list', () => ( + + )) + .add('MembersGroupComponent with empty list', () => ( + + )); diff --git a/src/components/Members/MembersGroupComponent.js b/src/components/Members/MembersGroupComponent.js new file mode 100644 index 00000000..26b4cbba --- /dev/null +++ b/src/components/Members/MembersGroupComponent.js @@ -0,0 +1,323 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { computed } from 'mobx'; +import { Collapse } from 'react-collapse'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import MemberItem from '../../stores/MembersStore/MemberItem'; +import DialogStore from '../../stores/DialogStore'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import UserStore from '../../stores/UserStore'; +import AppStore from '../../stores/AppStore'; +import { Pudding } from '../Icons'; +import MembersGroupTable from './MembersGroupTable'; +import Dialog from '../Dialog/Dialog'; +import TokenTransfer from '../TokenTransfer/TokenTransfer'; +import TransferTokenForm from '../../stores/FormsStore/TransferTokenForm'; +import { + TokenInProgressMessage, + TransferSuccessMessage, + TransferErrorMessage, +} from '../Message'; +import { tokenTypes } from '../../constants'; + +import styles from './Members.scss'; + +/** + * Group members component + * + * @param selectedWallet selected wallet + * @param item + */ +@withTranslation() +@inject('dialogStore', 'membersStore', 'userStore', 'appStore') +@observer +class MembersGroupComponent extends React.Component { + transferSteps = { + input: 0, + transfering: 1, + success: 2, + error: 3, + } + + transferForm = new TransferTokenForm({ + hooks: { + onSuccess: (form) => { + const { + id, + membersStore, + userStore, + } = this.props; + const { selectedWallet } = this.state; + const groupId = id; + const { address: rawAddress, count, password } = form.values(); + const address = rawAddress.trim(); + userStore.setPassword(password); + membersStore.setTransferStatus('transfering'); + return membersStore.transferTokens(groupId, selectedWallet, address, count) + .then(() => { + membersStore.setTransferStatus('success'); + membersStore.list[groupId].updateMemberBalanceAndWeight(selectedWallet); + membersStore.list[groupId].updateMemberBalanceAndWeight(address); + }) + .catch(() => { + membersStore.setTransferStatus('error'); + }); + }, + onError: () => { + /* eslint-disable-next-line */ + console.error('form error'); + }, + }, + }) + + static propTypes = { + /** id group */ + id: PropTypes.number.isRequired, + /** name group */ + name: PropTypes.string.isRequired, + /** group token type */ + groupType: PropTypes.string.isRequired, + /** balance with token */ + fullBalance: PropTypes.string.isRequired, + /** info about group */ + description: PropTypes.string.isRequired, + /** wallet group */ + wallet: PropTypes.string.isRequired, + /** token group */ + token: PropTypes.string.isRequired, + /** member list */ + list: PropTypes.arrayOf(PropTypes.instanceOf(MemberItem)).isRequired, + /** text when list is empty */ + textForEmptyState: PropTypes.string.isRequired, + /** translate method */ + t: PropTypes.func.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + appStore: PropTypes.instanceOf(AppStore).isRequired, + admin: PropTypes.arrayOf( + PropTypes.shape({}), + ).isRequired, + } + + constructor() { + super(); + this.state = { + isOpen: false, + selectedWallet: '', + }; + } + + /** + * Return actual modal props state + * + * @returns {object} actual modal props + */ + @computed + get modalPropsSwitch() { + const { + membersStore: { transferStatus }, + } = this.props; + const { transferSteps } = this; + switch (transferStatus) { + case (transferSteps.input): + return { header: null, footer: null }; + case (transferSteps.transfering): + return { header: null, footer: null, closeable: false }; + case (transferSteps.success): + return { header: null, footer: null }; + case (transferSteps.error): + return { header: null, footer: null }; + default: + return null; + } + } + + /** + * Method for return actual modal content + * + * @returns {Node} actual modal content + */ + modalContentSwitch = () => { + const { + id, + wallet, + groupType, + membersStore, + dialogStore, + t, + } = this.props; + const { transferStatus } = membersStore; + const { selectedWallet } = this.state; + const { transferSteps } = this; + switch (transferStatus) { + case (transferSteps.input): + return ( + + ); + case (transferSteps.transfering): + return ; + case (transferSteps.success): + return dialogStore.hide()} />; + case (transferSteps.error): + return ( + { membersStore.setTransferStatus('input'); }} + buttonText={t('buttons:retry')} + /> + ); + default: + return null; + } + } + + /** + * Method for change isOpen state + */ + toggleOpen = () => { + const { membersStore } = this.props; + this.setState((prevState) => ({ + isOpen: !prevState.isOpen, + })); + membersStore.setTransferStatus('input'); + } + + handleClick = ({ selectedWallet }) => { + const { + membersStore, + admin: administrator, + userStore: { address }, + appStore, + groupType, + t, + } = this.props; + let isAdmininstrator = false; + const isCustomToken = groupType === tokenTypes.Custom; + const isIdentical = selectedWallet.toUpperCase() === address.toUpperCase(); + if (isCustomToken) { + const [groupAdmin] = administrator; + isAdmininstrator = (address.toUpperCase() === groupAdmin.wallet.toUpperCase()); + } + if (isCustomToken && !isAdmininstrator) { + appStore.displayAlert(t('errors:transferLocked')); + return; + } + if (isIdentical || isAdmininstrator) { + membersStore.setTransferStatus('input'); + const { dialogStore, id } = this.props; + this.setState({ selectedWallet }); + dialogStore.show(`transfer-token-${id}`); + } else { + // eslint-disable-next-line react/prop-types + appStore.displayAlert(t('errors:transferIfNotAdmin')); + } + } + + render() { + const { + id, + name, + fullBalance, + description, + wallet, + token, + list, + groupType, + textForEmptyState, + t, + membersStore: { transferStatus }, + } = this.props; + const { transferSteps } = this; + const { isOpen } = this.state; + console.log('list', list); + return ( + + + {`#${id}`} + + + {name} + + + {description} + + + + + + {fullBalance} + + + {token} + + + {wallet} + + + { + list && list.length + ? ( + + + + ) + : ( + + + + + + {t(textForEmptyState)} + + + ) + } + { + groupType === tokenTypes.ERC20 + ? ( + + + + + + {t('other:noDataAdmins')} + + + ) + : null + + } + + + {this.modalContentSwitch()} + + + ); + } +} + +export default MembersGroupComponent; diff --git a/src/components/Members/MembersGroupComponent.test.js b/src/components/Members/MembersGroupComponent.test.js new file mode 100644 index 00000000..3e9a6f0c --- /dev/null +++ b/src/components/Members/MembersGroupComponent.test.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import MembersGroupComponent from './MembersGroupComponent'; +import MemberItem from '../../stores/MembersStore/MemberItem'; +import MembersGroupTable from './MembersGroupTable'; + +describe('MembersGroupComponent', () => { + const defaultProps = { + id: 0, + name: 'Admins', + fullBalance: '0.1201 TKN', + description: 'description text', + wallet: '0xA234FA767ASD7F67HH34HF7DF7S', + token: 'ERC 20', + textForEmptyState: 'textForEmptyState', + list: [], + }; + + describe('With correct data, list is empty', () => { + let wrapper; + let instance; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + instance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.members__group-id').text()).toEqual('#0'); + expect(wrapper.find('.members__group-name').text()).toEqual('Admins'); + expect(wrapper.find('.members__group-description').text()).toEqual('description text'); + expect(wrapper.find('.members__group-wallet').text()).toEqual('0xA234FA767ASD7F67HH34HF7DF7S'); + expect(wrapper.find('.members__group-token').text()).toEqual('ERC 20'); + expect(wrapper.find('.members__group-no-data-text').text()).toEqual('textForEmptyState'); + expect(wrapper.find('.members__group-button').prop('onClick')).toEqual(instance.toggleOpen); + }); + + it('toggleOpen should change isOpen state', () => { + expect(instance.state.isOpen).toEqual(false); + instance.toggleOpen(); + expect(instance.state.isOpen).toEqual(true); + instance.toggleOpen(); + expect(instance.state.isOpen).toEqual(false); + }); + }); + + describe('With correct data, list have items', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render correct with correct prop data', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(MembersGroupTable).prop('list')).toEqual([ + new MemberItem({ + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + isAdmin: true, + }), + new MemberItem({ + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + isAdmin: false, + }), + ]); + }); + }); +}); diff --git a/src/components/Members/MembersGroupTable.js b/src/components/Members/MembersGroupTable.js new file mode 100644 index 00000000..c0e4acd4 --- /dev/null +++ b/src/components/Members/MembersGroupTable.js @@ -0,0 +1,132 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import MemberItem from '../../stores/MembersStore/MemberItem'; +import { AdminIcon, BorderArrowIcon } from '../Icons'; + +import styles from './Members.scss'; + +/** + * Component for displaying table with members + */ +@withTranslation('other') +@observer +class MembersGroupTable extends React.PureComponent { + static propTypes = { + /** List members */ + list: PropTypes.arrayOf(PropTypes.instanceOf(MemberItem)).isRequired, + /** Method for translate */ + t: PropTypes.func.isRequired, + /** Method for handle table row click */ + onRowClick: PropTypes.func.isRequired, + } + + render() { + const { + list, + t, + onRowClick, + } = this.props; + if (!list || !list.length) return null; + return ( + + + + {/* eslint-disable-next-line */} + + {/* eslint-disable-next-line */} + + + {t('other:walletAddress')} + + + {t('other:weightVote')} + + + {t('other:balance')} + + + { + list.map((item, index) => ( + + + {item.isAdmin ? : ''} + + + + + + {item.wallet} + + + {`${item.weight}%`} + + + { + onRowClick({ selectedWallet: item.wallet }); + }} + > + {item.fullBalance} + + + + + + + )) + } + + + ); + } +} + +export default MembersGroupTable; diff --git a/src/components/Members/MembersGroupTable.test.js b/src/components/Members/MembersGroupTable.test.js new file mode 100644 index 00000000..674c878a --- /dev/null +++ b/src/components/Members/MembersGroupTable.test.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { observable } from 'mobx'; +import MembersGroupTable from './MembersGroupTable'; +import MemberItem from '../../stores/MembersStore/MemberItem'; + +describe('MembersGroupTable', () => { + describe('List is empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + {}} + list={observable([])} + />, + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('List is not empty', () => { + let wrapper; + let mockRowClick; + + beforeEach(() => { + mockRowClick = jest.fn(); + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('should have correct text', () => { + expect( + wrapper.find('.members__group-table-tr').text(), + ).toEqual('0xA234FA767ASD7F67HH34HF7DF7S10%120 TKN'); + }); + + it('row onClick prop should call mockRowClick', () => { + wrapper.find('.members__group-table-tr').prop('onClick')(); + expect(mockRowClick).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/Members/MembersPage.js b/src/components/Members/MembersPage.js new file mode 100644 index 00000000..4cb00e61 --- /dev/null +++ b/src/components/Members/MembersPage.js @@ -0,0 +1,132 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import { observable, computed } from 'mobx'; +import Container from '../Container'; +import MembersTop from './MembersTop'; +import MembersGroupComponent from './MembersGroupComponent'; +import Dialog from '../Dialog/Dialog'; +import Loader from '../Loader'; +import Footer from '../Footer'; +import Notification from '../Notification/Notification'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import DialogStore from '../../stores/DialogStore'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; + +import styles from './Members.scss'; + +/** + * Component for page with members + */ +@withTranslation() +@inject('membersStore', 'projectStore', 'dialogStore') +@observer +class MembersPage extends React.Component { + @observable votingIsActive = false; + + static propTypes = { + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + t: PropTypes.func.isRequired, + } + + @computed + get loading() { + const { membersStore } = this.props; + return membersStore.loading; + } + + render() { + const { loading } = this; + const { + membersStore: { list }, projectStore, dialogStore, t, + } = this.props; + const { historyStore } = projectStore; + return ( + <> + + {/* FIXME remove comment */} + + { + !loading + ? ( + <> + + + { + list && list.length + ? ( + list.map((group, index) => ( + + )) + ) + : null + } + + > + ) + : ( + + + + ) + } + + + + + { dialogStore.hide(); }} /> + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + + > + ); + } +} + +export default MembersPage; diff --git a/src/components/Members/MembersPage.test.js b/src/components/Members/MembersPage.test.js new file mode 100644 index 00000000..da0fa06f --- /dev/null +++ b/src/components/Members/MembersPage.test.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { observable } from 'mobx'; +import { MembersPage } from '.'; +import MembersGroup from '../../stores/MembersStore/MembersGroup'; + +describe('MembersPage', () => { + describe('List is empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + {}, + list: observable([]), + }} + />, + ).dive().dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + }); + + describe('List not empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + {}, + list: observable([ + new MembersGroup({ + name: 'Admins', + description: 'short description for group', + customTokenName: 'TKN', + tokenName: 'ERC20', + wallet: '0xB210af05Bf82eF6C6BA034B22D18c89B5D23Cc90', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + }, + ], + }), + ]), + }} + />, + ).dive().dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + }); +}); diff --git a/src/components/Members/MembersTop.js b/src/components/Members/MembersTop.js new file mode 100644 index 00000000..9e23a383 --- /dev/null +++ b/src/components/Members/MembersTop.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { withTranslation, Trans } from 'react-i18next'; +import { PlayCircleIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './Members.scss'; +import { systemQuestionsId } from '../../constants'; + +/** + * Component in top members page + * + * @returns {Node} component + */ +const MembersTop = ({ + projectName, + votingIsActive, + history, +}) => ( + + )} + theme="with-play-icon" + onClick={ + () => history.push(`/votings?modal=start_new_vote&option=${systemQuestionsId.connectGroupUsers}`) + } + disabled={votingIsActive} + hint={ + votingIsActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + + + +); + +MembersTop.propTypes = { + projectName: PropTypes.string.isRequired, + votingIsActive: PropTypes.bool.isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, +}; + +export default withRouter(withTranslation('other')(MembersTop)); diff --git a/src/components/Members/MembersTop.test.js b/src/components/Members/MembersTop.test.js new file mode 100644 index 00000000..7141d08e --- /dev/null +++ b/src/components/Members/MembersTop.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Trans } from 'react-i18next'; +import { MembersTop } from '.'; + +describe('MembersTop', () => { + it('should render correct without onClick props', () => { + const wrapper = shallow().dive(); + expect(wrapper.length).toEqual(1); + expect(wrapper.find(Trans).props().values).toEqual({ project: 'test' }); + }); + + it('button onClick should call mockClick with onClick prop', () => { + const mockClick = jest.fn(); + const wrapper = shallow( + , + ).dive(); + expect(wrapper.length).toEqual(1); + expect(wrapper.find(Trans).props().values).toEqual({ project: 'test project' }); + const button = wrapper.find('button'); + button.prop('onClick')(); + expect(mockClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Members/index.js b/src/components/Members/index.js new file mode 100644 index 00000000..859b0564 --- /dev/null +++ b/src/components/Members/index.js @@ -0,0 +1,11 @@ +import MembersPage from './MembersPage'; +import MembersTop from './MembersTop'; +import MembersGroupComponent from './MembersGroupComponent'; + +export default MembersPage; + +export { + MembersPage, + MembersTop, + MembersGroupComponent, +}; diff --git a/src/components/Message/ERC20TokensUsed.js b/src/components/Message/ERC20TokensUsed.js new file mode 100644 index 00000000..57c363f9 --- /dev/null +++ b/src/components/Message/ERC20TokensUsed.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Message about agreed decision + */ +@withTranslation(['dialogs']) +class ERC20TokensUsed extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func, + buttonText: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + }; + + static defaultProps = { + onButtonClick: null, + buttonText: , + } + + render() { + const { + props: { + onButtonClick, + t, + buttonText, + }, + } = this; + return ( + + + + {t('other:erc20ListIsNotViewable')} + + + { + onButtonClick + ? ( + + + {buttonText} + + + ) + : null + } + + ); + } +} + +export default ERC20TokensUsed; diff --git a/src/components/Message/ErrorMessage.js b/src/components/Message/ErrorMessage.js new file mode 100644 index 00000000..510981bd --- /dev/null +++ b/src/components/Message/ErrorMessage.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other', 'headings']) +class ErrorMessage extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func, + buttonText: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + } + + static defaultProps = { + onButtonClick: null, + buttonText: , + } + + render() { + const { props: { t, onButtonClick, buttonText } } = this; + return ( + + + {t('headings:failedTransaction.subheading')} + + { + onButtonClick + ? ( + + + {buttonText} + + + ) + : null + } + + ); + } +} + +export default ErrorMessage; diff --git a/src/components/Message/Message.scss b/src/components/Message/Message.scss index e74b429a..d8b29553 100644 --- a/src/components/Message/Message.scss +++ b/src/components/Message/Message.scss @@ -9,7 +9,7 @@ color: #000; font-weight: 700; font-size: 24px; - font-family: "Grotesk"; + font-family: "Roboto"; line-height: 28px; text-align: center; } @@ -58,6 +58,58 @@ } } + &--transfer-error { + .subtext { + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + line-height: 16px; + } + + .message { + &__title { + margin-top: 57px; + margin-bottom: 24px; + } + } + + .footer { + padding-top: 73px; + padding-bottom: 51px; + } + } + + &--transfer-progress { + display: flex; + flex-flow: row nowrap; + &::after { + display: inline-block; + min-height: 200px; + vertical-align: middle; + content: ''; + } + } + + &--erc20 { + .message { + &__title { + margin-top: 58px; + white-space: pre-wrap; + } + } + + .subtext { + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 14px; + line-height: 16px; + white-space: pre-wrap; + } + + .footer { + padding-top: 40px; + } + } + &--progress { .message { &__title { @@ -78,4 +130,15 @@ text-align: center; } } + + &--agreed, + &--reject, + &--transfer-success, + &--transfer-error, + &--erc20 { + button { + width: 100%; + max-width: 309px; + } + } } diff --git a/src/components/Message/SuccessMessage.js b/src/components/Message/SuccessMessage.js new file mode 100644 index 00000000..946e370c --- /dev/null +++ b/src/components/Message/SuccessMessage.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other', 'headings']) +class SuccessMessage extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func.isRequired, + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + ]), + } + + static defaultProps = { + children: null, + }; + + render() { + const { props: { t, onButtonClick, children } } = this; + return ( + + + {children} + + + + {t('buttons:continue')} + + + + ); + } +} + +export default SuccessMessage; diff --git a/src/components/Message/TokenInProgressMessage.js b/src/components/Message/TokenInProgressMessage.js index ee5a02f4..815422e3 100644 --- a/src/components/Message/TokenInProgressMessage.js +++ b/src/components/Message/TokenInProgressMessage.js @@ -1,8 +1,8 @@ import React from 'react'; import { withTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; -import Loader from '../Loader'; import DefaultMessage from './DefaultMessage'; +import { TransactionLoader } from '../Progress'; import styles from './Message.scss'; @@ -27,7 +27,7 @@ class TokenInProgressMessage extends React.Component { {t('dialogs:someTimeText')} - + diff --git a/src/components/Message/TransactionProgress.js b/src/components/Message/TransactionProgress.js new file mode 100644 index 00000000..06eb95af --- /dev/null +++ b/src/components/Message/TransactionProgress.js @@ -0,0 +1,51 @@ +/* eslint-disable no-unused-vars */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { EMPTY_DATA_STRING } from '../../constants'; +import DefaultMessage from './DefaultMessage'; +import { TransactionLoader, DeployingProgress } from '../Progress'; +// import Loader from '../Loader'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other']) +class TransactionProgress extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + value: PropTypes.string, + deploy: PropTypes.bool, + type: PropTypes.string, + } + + static defaultProps = { + value: EMPTY_DATA_STRING, + deploy: false, + type: '', + } + + render() { + const { + props: { + t, + value, + deploy, + type, + }, + } = this; + return ( + + { + deploy + ? + : + } + + ); + } +} + +export default TransactionProgress; diff --git a/src/components/Message/TransferErrorMessage.js b/src/components/Message/TransferErrorMessage.js new file mode 100644 index 00000000..b9fd7357 --- /dev/null +++ b/src/components/Message/TransferErrorMessage.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import DefaultMessage from './DefaultMessage'; +import Button from '../Button/Button'; + +import styles from './Message.scss'; + +/** + * Dialog with message about success token transfer + */ +@withTranslation(['dialogs', 'other']) +class TransferErrorMessage extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + onButtonClick: PropTypes.func, + buttonText: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({}), + ]), + } + + static defaultProps = { + onButtonClick: null, + buttonText: , + } + + render() { + const { props: { t, onButtonClick, buttonText } } = this; + return ( + + + {t('other:notEnoughTokens')} + + { + onButtonClick + ? ( + + + {buttonText} + + + ) + : null + } + + ); + } +} + +export default TransferErrorMessage; diff --git a/src/components/Message/TransferErrorMessage.test.js b/src/components/Message/TransferErrorMessage.test.js new file mode 100644 index 00000000..3dd15293 --- /dev/null +++ b/src/components/Message/TransferErrorMessage.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import TransferErrorMessage from './TransferErrorMessage'; +import Button from '../Button/Button'; + +describe('TransferErrorMessage', () => { + let wrapper; + let mockOnClick; + + beforeEach(() => { + mockOnClick = jest.fn(); + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('should call mockOnClick on button onClick', () => { + const button = wrapper.find(Button); + expect(button.length).toEqual(1); + button.prop('onClick')(); + expect(mockOnClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Message/TransferSuccessMessage.js b/src/components/Message/TransferSuccessMessage.js index 936a2b1f..213905a9 100644 --- a/src/components/Message/TransferSuccessMessage.js +++ b/src/components/Message/TransferSuccessMessage.js @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withTranslation, Trans } from 'react-i18next'; -import { EMPTY_DATA_STRING } from '../../constants'; import DefaultMessage from './DefaultMessage'; import Button from '../Button/Button'; import styles from './Message.scss'; + /** * Dialog with message about success token transfer */ @@ -14,7 +14,6 @@ class TransferSuccessMessage extends React.Component { static propTypes = { t: PropTypes.func.isRequired, onButtonClick: PropTypes.func, - value: PropTypes.string, buttonText: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({}), @@ -26,7 +25,6 @@ class TransferSuccessMessage extends React.Component { } static defaultProps = { - value: EMPTY_DATA_STRING, buttonText: , } @@ -35,7 +33,6 @@ class TransferSuccessMessage extends React.Component { props: { onButtonClick, t, - value, buttonText, }, } = this; @@ -43,10 +40,7 @@ class TransferSuccessMessage extends React.Component { - {t('other:yourBalance')} - {value} - + /> { onButtonClick ? ( diff --git a/src/components/Message/index.js b/src/components/Message/index.js index 4e724aae..bc3ae3cb 100644 --- a/src/components/Message/index.js +++ b/src/components/Message/index.js @@ -3,6 +3,8 @@ import AgreedMessage from './AgreedMessage'; import RejectMessage from './RejectMessage'; import TransferSuccessMessage from './TransferSuccessMessage'; import TokenInProgressMessage from './TokenInProgressMessage'; +import TransferErrorMessage from './TransferErrorMessage'; +import ERC20TokensUsed from './ERC20TokensUsed'; export default DefaultMessage; @@ -11,4 +13,6 @@ export { RejectMessage, TransferSuccessMessage, TokenInProgressMessage, + TransferErrorMessage, + ERC20TokensUsed, }; diff --git a/src/components/Notification/Notification.js b/src/components/Notification/Notification.js new file mode 100644 index 00000000..a8e4589e --- /dev/null +++ b/src/components/Notification/Notification.js @@ -0,0 +1,137 @@ +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import NotificationStore from '../../stores/NotificationStore'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import NotificationItem from './NotificationItem'; +import TokensWithoutActiveVoting from '../Notifications/TokensWithoutActiveVoting'; +import TokensWithActiveVoting from '../Notifications/TokensWithActiveVoting'; + +import styles from './Notification.scss'; + +/** + * Class for render notification + */ +@inject('notificationStore', 'projectStore') +@observer +class Notification extends React.Component { + idTimer = null; + + static propTypes = { + notificationStore: PropTypes.instanceOf(NotificationStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + }; + + componentDidMount() { + const { projectStore: { rootStore } } = this.props; + const { configStore: { UPDATE_INTERVAL } } = rootStore; + this.updateReturnTokensNotification(); + this.idTimer = setInterval(() => { + this.updateReturnTokensNotification(); + }, UPDATE_INTERVAL); + } + + componentWillUnmount() { + clearInterval(this.idTimer); + } + + /** + * Method for remove notification + * + * @param {string} id id notification + */ + removeNotification = (id) => { + const { props } = this; + const { + notificationStore, + } = props; + notificationStore.remove(id); + } + + resetNotification = () => { + const { props } = this; + const { + notificationStore, + } = props; + notificationStore.reset(); + } + + updateReturnTokensNotification = async () => { + const { props } = this; + const { + projectStore: { + historyStore, + }, + notificationStore, + } = props; + const hasActiveVoting = await historyStore.isVotingActive; + + const userTokenReturns = await historyStore.fetchUserReturnTokens(); + const lastUserVoting = await historyStore.lastUserVoting(); + const countOfVoting = await historyStore.fetchVotingsCount(); + const lastVoteIndex = countOfVoting - 1; + + if (userTokenReturns === true) { + this.resetNotification(); + return; + } + if (Number(lastVoteIndex) !== Number(lastUserVoting) && userTokenReturns === false) { + this.resetNotification(); + // TODO maybe make other notification description? + notificationStore.add({ + isOpen: true, + content: , + }); + return; + } + if (hasActiveVoting === true && userTokenReturns === false) { + this.resetNotification(); + notificationStore.add({ + isOpen: true, + content: , + status: 'important', + }); + } + if (hasActiveVoting === false && userTokenReturns === false) { + this.resetNotification(); + notificationStore.add({ + isOpen: true, + content: , + }); + } + } + + render() { + const { props } = this; + const { + notificationStore: { + list, + }, + } = props; + return ( + <> + { + list && list.length + ? ( + + { + list.map((notification) => ( + this.removeNotification(notification.id)} + /> + )) + } + + ) + : null + } + > + ); + } +} + +export default Notification; diff --git a/src/components/Notification/Notification.scss b/src/components/Notification/Notification.scss new file mode 100644 index 00000000..1b0bf188 --- /dev/null +++ b/src/components/Notification/Notification.scss @@ -0,0 +1,65 @@ +.notification { + &__container { + padding-top: 40px; + padding-bottom: 40px; + } + + &__item { + position: relative; + width: 100%; + margin-bottom: 20px; + padding: 20px 40px; + color: #000; + font-weight: 700; + font-size: 13px; + line-height: 14px; + text-align: center; + background: #fff; + border: 1px solid #fff; + + &-close { + position: absolute; + top: 50%; + right: 15px; + display: none; + color: #c8c9ca; + background: transparent; + border: unset; + outline: none; + transform: translate(0, -50%); + cursor: pointer; + } + + .btn { + &__text { + font-weight: 700; + font-size: 12px; + line-height: 14px; + } + } + + &--info { + color: #000; + background: #fff; + border: 1px solid #000; + + .btn { + margin-left: 5px; + color: #000; + border-bottom-color: #000; + } + } + + &--important { + color: #fff; + background: #000; + border: 1px solid #000; + + .btn { + margin-left: 5px; + color: #fff; + border-bottom-color: #fff; + } + } + } +} \ No newline at end of file diff --git a/src/components/Notification/NotificationItem.js b/src/components/Notification/NotificationItem.js new file mode 100644 index 00000000..ec3a7661 --- /dev/null +++ b/src/components/Notification/NotificationItem.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { CloseIcon } from '../Icons'; + +import styles from './Notification.scss'; + +/** + * Class for render notification item + */ +class NotificationItem extends React.PureComponent { + static propTypes = { + isOpen: PropTypes.bool.isRequired, + content: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + ]).isRequired, + status: PropTypes.oneOf([ + 'info', + 'important', + ]).isRequired, + handleRemove: PropTypes.func.isRequired, + }; + + render() { + const { props } = this; + const { + isOpen, + content, + status, + handleRemove, + } = props; + return ( + <> + { + isOpen + ? ( + + {content} + + + + + ) + : null + } + > + ); + } +} + +export default NotificationItem; diff --git a/src/components/Notification/index.js b/src/components/Notification/index.js new file mode 100644 index 00000000..29a08c89 --- /dev/null +++ b/src/components/Notification/index.js @@ -0,0 +1,3 @@ +import Notification from './Notification'; + +export default Notification; diff --git a/src/components/Notifications/TokensWithActiveVoting.js b/src/components/Notifications/TokensWithActiveVoting.js new file mode 100644 index 00000000..d0a77da5 --- /dev/null +++ b/src/components/Notifications/TokensWithActiveVoting.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Button from '../Button/Button'; +import DialogStore from '../../stores/DialogStore'; + +@withTranslation() +@inject('dialogStore') +class TokensWithActiveVoting extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + }; + + handleClick = () => { + const { props } = this; + const { dialogStore } = props; + dialogStore.show('return_tokens'); + } + + render() { + const { props } = this; + const { t } = props; + return ( + <> + {t('other:youVotedAndTokensInContract')} + + {t('buttons:pickUpTokens')} + + > + ); + } +} + +export default TokensWithActiveVoting; diff --git a/src/components/Notifications/TokensWithoutActiveVoting.js b/src/components/Notifications/TokensWithoutActiveVoting.js new file mode 100644 index 00000000..321f5836 --- /dev/null +++ b/src/components/Notifications/TokensWithoutActiveVoting.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject } from 'mobx-react'; +import { withTranslation } from 'react-i18next'; +import Button from '../Button/Button'; +import DialogStore from '../../stores/DialogStore'; + +@withTranslation() +@inject('dialogStore') +class TokensWithoutActiveVoting extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + }; + + handleClick = () => { + const { props } = this; + const { dialogStore } = props; + dialogStore.show('return_tokens'); + } + + render() { + const { props } = this; + const { t } = props; + return ( + <> + {t('other:votingCompletedButTokensInContract')} + + {t('buttons:pickUpTokensCapital')} + + > + ); + } +} + +export default TokensWithoutActiveVoting; diff --git a/src/components/Pagination/Pagination.js b/src/components/Pagination/Pagination.js new file mode 100644 index 00000000..122334b0 --- /dev/null +++ b/src/components/Pagination/Pagination.js @@ -0,0 +1,172 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import ReactJsPagination from 'react-js-pagination'; +import { withTranslation } from 'react-i18next'; +import { ThinArrow } from '../Icons'; + +import './Pagination.scss'; + +/** + * Component for pagination + */ +@withTranslation() +@observer +class Pagination extends React.Component { + static propTypes = { + activePage: PropTypes.number.isRequired, + lastPage: PropTypes.number.isRequired, + handlePageChange: PropTypes.func.isRequired, + itemsCountPerPage: PropTypes.number.isRequired, + totalItemsCount: PropTypes.number.isRequired, + pageRangeDisplayed: PropTypes.number.isRequired, + t: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + this.state = { + value: props.activePage, + }; + } + + static getDerivedStateFromProps(props, state) { + if (props.activePage !== state.value) { + return { + value: props.activePage, + }; + } + return null; + } + + /** + * Handle input change + * + * @param {event} event event called on input + */ + handleChange = (event) => { + const { + lastPage, + } = this.props; + const minPage = 1; + let newValue = parseInt(event.target.value, 10) || 0; + // do not let us enter a value greater than the maximum + if (newValue >= lastPage) newValue = lastPage; + if (newValue <= minPage) newValue = minPage; + this.setPaginationValue(newValue); + event.target.select(); + } + + /** + * Method for setting input value + * + * @param {number} newValue new value input + */ + setPaginationValue = (newValue) => { + this.setState({ value: newValue }); + } + + /** + * Handle click on button chained to input + * + * @param {number} page page from input + */ + handleButtonClick = (page) => { + this.onPageChange(page); + } + + /** + * Method called onChange event in + * ReactJsPagination component + * + * @param {number} page page from input + */ + onPageChange = (page) => { + const { + handlePageChange, + } = this.props; + handlePageChange(page); + this.setPaginationValue(page); + } + + /** + * Handle focus on input element + * + * @param {event} event event called on input + */ + handleFocus = (event) => { + event.target.select(); + } + + render() { + const { + activePage, + lastPage, + itemsCountPerPage, + totalItemsCount, + pageRangeDisplayed, + t, + } = this.props; + const { + value, + } = this.state; + return ( + <> + { + totalItemsCount === 0 + ? null + : ( + <> + )} + nextPageText={()} + firstPageText={( + <> + + + > + )} + lastPageText={( + <> + + + > + )} + itemClass="pagination__item" + itemClassFirst="pagination__item--first" + itemClassLast="pagination__item--last" + itemClassPrev="pagination__item--prev" + itemClassNext="pagination__item--next" + /> + + {t('other:page')} + + {`${t('other:outOf')} ${lastPage}`} + this.handleButtonClick(value)} + > + {t('other:goTo')} + + + > + ) + } + > + ); + } +} + +export default Pagination; diff --git a/src/components/Pagination/Pagination.scss b/src/components/Pagination/Pagination.scss new file mode 100644 index 00000000..616506d9 --- /dev/null +++ b/src/components/Pagination/Pagination.scss @@ -0,0 +1,122 @@ +.pagination { + margin-top: 27px; + margin-bottom: 18px; + padding: 0; + text-align: center; + list-style: none; + + &__item { + display: inline-block; + margin: 0 10px; + color: #808080; + font-size: 14px; + line-height: 16px; + vertical-align: middle; + + a { + text-decoration: unset; + } + + &.active { + a { + color: #000; + font-weight: 700; + } + } + + &--first, + &--last, + &--next, + &--prev { + margin: 0 4px; + + a { + display: inline-block; + width: 21px; + height: 21px; + color: #808080; + background-color: transparent; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0); + transition: background-color 0.3s, box-shadow 0.3s; + + svg { + width: 6px; + height: 18px; + } + } + } + + &--prev { + margin-right: 20px; + } + + &--next { + margin-left: 20px; + } + + &--last, + &--next { + a { + padding-right: 2px; + transform: rotate(180deg); + } + } + } + + &__footer { + margin-bottom: 65px; + text-align: center; + + span { + color: rgba(128, 128, 128, 0.4); + font-size: 11px; + line-height: 16px; + } + + input { + max-width: 50px; + margin: 0 9px; + padding: 3px; + color: #000; + font-size: 11px; + line-height: 16px; + text-align: center; + background: transparent; + border: unset; + border-bottom: 1px solid #c8c9ca; + outline: none; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0); + transition: background-color 0.3s, box-shadow 0.3s; + + &[type=number]::-webkit-inner-spin-button, + &[type=number]::-webkit-outer-spin-button { + margin: 0; + -webkit-appearance: none; + } + } + + button { + margin-left: 10px; + padding: 5px 15px; + color: rgba(128, 128, 128, 0.4); + font-size: 11px; + line-height: 16px; + background: #fff; + border: 1px solid #e1e4e8; + border-radius: 2px; + outline: none; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0); + cursor: pointer; + transition: background-color 0.3s, box-shadow 0.3s; + + &:hover, + &:active { + background-color: #f4fbfc; + } + + &:active { + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1); + } + } + } +} diff --git a/src/components/Pagination/Pagination.stories.js b/src/components/Pagination/Pagination.stories.js new file mode 100644 index 00000000..9d8b16af --- /dev/null +++ b/src/components/Pagination/Pagination.stories.js @@ -0,0 +1,26 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import Pagination from '.'; + +storiesOf('Pagination', module) + .add('Default', () => ( + {}} + itemsCountPerPage={10} + totalItemsCount={100} + pageRangeDisplayed={5} + /> + )) + .add('Short', () => ( + {}} + itemsCountPerPage={10} + totalItemsCount={21} + pageRangeDisplayed={5} + /> + )); diff --git a/src/components/Pagination/Pagination.test.js b/src/components/Pagination/Pagination.test.js new file mode 100644 index 00000000..716cf9cb --- /dev/null +++ b/src/components/Pagination/Pagination.test.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Pagination from '.'; + +describe('Pagination', () => { + let wrapper; + let mockHandlePageChange; + let wrapperInstance; + + beforeEach(() => { + mockHandlePageChange = jest.fn(); + wrapper = shallow( + , + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error & elements have correct prop', () => { + expect(wrapper.length).toEqual(1); + const inputElement = wrapper.find('input'); + expect(inputElement.prop('onChange')).toEqual(wrapperInstance.handleChange); + expect(inputElement.prop('onFocus')).toEqual(wrapperInstance.handleFocus); + }); + + it('handleChange with value 11 should set value to 10', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.handleChange({ target: { value: 11, select: () => {} } }); + expect(wrapperInstance.state.value).toEqual(10); + }); + + it('handleChange with value 5 should set value to 5', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.handleChange({ target: { value: 5, select: () => {} } }); + expect(wrapperInstance.state.value).toEqual(5); + }); + + it('handleChange with value -5 should set value to 1', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.handleChange({ target: { value: -5, select: () => {} } }); + expect(wrapperInstance.state.value).toEqual(1); + }); + + it('handleButtonClick with 5 should call mockHandlePageChange with 5', () => { + wrapperInstance.handleButtonClick(5); + expect(mockHandlePageChange).toHaveBeenCalledWith(5); + }); + + it('onPageChange with 6 should call mockHandlePageChange with 6 & set value to 6', () => { + // init state to equal activePage + expect(wrapperInstance.state.value).toEqual(1); + wrapperInstance.onPageChange(6); + expect(mockHandlePageChange).toHaveBeenCalledWith(6); + expect(wrapperInstance.state.value).toEqual(6); + }); + + it('handleFocus should call mockFocus', () => { + const mockSelect = jest.fn(); + wrapperInstance.handleFocus({ target: { select: mockSelect } }); + expect(mockSelect).toHaveBeenCalled(); + }); + + it('button onClick should call mockHandlePageChange with 1', () => { + wrapper.find('button').prop('onClick')(); + expect(mockHandlePageChange).toHaveBeenCalledWith(1); + }); +}); diff --git a/src/components/Pagination/index.js b/src/components/Pagination/index.js new file mode 100644 index 00000000..9ed530b1 --- /dev/null +++ b/src/components/Pagination/index.js @@ -0,0 +1,3 @@ +import Pagination from './Pagination'; + +export default Pagination; diff --git a/src/components/Progress/DeployingProgress.js b/src/components/Progress/DeployingProgress.js new file mode 100644 index 00000000..49467685 --- /dev/null +++ b/src/components/Progress/DeployingProgress.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import ProgressBlock from '../ProjectUploading/ProgressBlock'; +import { + SendingIcon, TxHashIcon, TxRecieptIcon, CompilingIcon, QuestionUploadingIcon, +} from '../Icons'; + +import styles from './progress.scss'; + +const DeployingProgress = withTranslation()(inject('appStore')(observer(({ appStore, type, t }) => { + const stagesToken = [ + [t('other:compiling'), ], + [t('other:sending'), ], + [t('other:txHash'), ], + [t('other:txReceipt'), ], + ]; + + const stagesZeroOne = [ + [t('other:compiling'), ], + [t('other:sending'), ], + [t('other:txHash'), ], + [t('other:txReceipt'), ], + [t('other:questionsUploading'), [ + , + + {appStore.uploadedQuestion} + {'/'} + {appStore.countOfQuestions} + ], + ], + ]; + + return ( + + { + type === 'ZeroOne' + ? stagesZeroOne.map((stage, index) => ( + + {stage[1]} + + )) + : stagesToken.map((stage, index) => ( + + {stage[1]} + + )) + } + + ); +}))); + +export default DeployingProgress; diff --git a/src/components/Progress/TransactionLoader.js b/src/components/Progress/TransactionLoader.js new file mode 100644 index 00000000..d084e479 --- /dev/null +++ b/src/components/Progress/TransactionLoader.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { withTranslation } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import ProgressBlock from '../ProjectUploading/ProgressBlock'; +import { + SigningIcon, SendingIcon, TxHashIcon, TxRecieptIcon, +} from '../Icons'; + +import styles from './progress.scss'; + +const TransactionLoader = withTranslation()(inject('appStore')(observer(({ appStore: { transactionStep }, t }) => { + const stages = [ + [t('other:txSigning'), ], + [t('other:sending'), ], + [t('other:txHash'), ], + [t('other:txReceipt'), ], + ]; + + return ( + + { + stages.map((stage, index) => ( + + {stage[1]} + + )) + } + + ); +}))); + +export default TransactionLoader; diff --git a/src/components/Progress/index.js b/src/components/Progress/index.js new file mode 100644 index 00000000..af3dec47 --- /dev/null +++ b/src/components/Progress/index.js @@ -0,0 +1,7 @@ +import TransactionLoader from './TransactionLoader'; +import DeployingProgress from './DeployingProgress'; + +export { + TransactionLoader, + DeployingProgress, +}; diff --git a/src/components/Progress/progress.scss b/src/components/Progress/progress.scss new file mode 100644 index 00000000..99311250 --- /dev/null +++ b/src/components/Progress/progress.scss @@ -0,0 +1,24 @@ +.transaction-progress { + display: flex; + justify-content: space-between; + width: 602px; + margin: 0 auto; + padding-top: 40px; + text-align: center; + &--zeroone { + .progress { + &-block { + .progress-line { + left: -51px; + width: 52px; + } + &.success { + .progress-line { + left: -51px; + width: 52px; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/components/ProgressBar/ProgressBar.js b/src/components/ProgressBar/ProgressBar.js new file mode 100644 index 00000000..411da328 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.js @@ -0,0 +1,65 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import styles from './ProgressBar.scss'; + +class ProgressBar extends React.PureComponent { + static propTypes = { + progress: PropTypes.number.isRequired, + countIndicator: PropTypes.number, + className: PropTypes.string, + }; + + static defaultProps = { + countIndicator: 10, + className: '', + }; + + /** + * Method for getting dimension scale + * for indicator + * + * @returns {number} dimension value + */ + getDimensionIndicator = () => { + const { props } = this; + const { countIndicator } = props; + return 100 / countIndicator; + } + + /** + * Method for detect filling indicator + * + * @param {number} index index indicator + * @returns {boolean} indicator filled state + */ + indicatorIsFilled = (index) => { + const { props } = this; + const { progress } = props; + const dimension = this.getDimensionIndicator(); + return (dimension * (index + 1)) <= progress; + } + + render() { + const { props } = this; + const { countIndicator, className } = props; + const arrIndicator = new Array(countIndicator).fill(''); + return ( + + { + arrIndicator.map((item, index) => ( + + )) + } + + ); + } +} + +export default ProgressBar; diff --git a/src/components/ProgressBar/ProgressBar.scss b/src/components/ProgressBar/ProgressBar.scss new file mode 100644 index 00000000..b0c90361 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.scss @@ -0,0 +1,15 @@ +.progress-bar { + &__indicator { + display: inline-block; + width: 8px; + height: 8px; + margin-right: 1px; + margin-bottom: 1px; + background: #fff; + border: 1px solid #000; + + &--filled { + background-color: #000; + } + } +} \ No newline at end of file diff --git a/src/components/ProgressBar/ProgressBar.test.js b/src/components/ProgressBar/ProgressBar.test.js new file mode 100644 index 00000000..991aa708 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.test.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ProgressBar from '.'; + +describe('ProgressBar', () => { + describe('countIndicator 10, progress 21', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + , + ); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.progress-bar__indicator').length).toEqual(10); + expect(wrapper.find('.progress-bar__indicator--filled').length).toEqual(2); + }); + + it('getDimensionIndicator should be equal 10', () => { + expect(wrapperInstance.getDimensionIndicator()).toEqual(10); + }); + + it('indicatorIsFilled should be equal correct state', () => { + // do not forget that the index counts from 0 + expect(wrapperInstance.indicatorIsFilled(0)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(1)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(2)).toEqual(false); + }); + }); + + describe('countIndicator 20, progress 49', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + , + ); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.progress-bar__indicator').length).toEqual(20); + expect(wrapper.find('.progress-bar__indicator--filled').length).toEqual(9); + }); + + it('getDimensionIndicator should be equal 5', () => { + expect(wrapperInstance.getDimensionIndicator()).toEqual(5); + }); + + it('indicatorIsFilled should be equal correct state', () => { + // do not forget that the index counts from 0 + expect(wrapperInstance.indicatorIsFilled(0)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(8)).toEqual(true); + expect(wrapperInstance.indicatorIsFilled(9)).toEqual(false); + }); + }); +}); diff --git a/src/components/ProgressBar/index.js b/src/components/ProgressBar/index.js new file mode 100644 index 00000000..26a10f67 --- /dev/null +++ b/src/components/ProgressBar/index.js @@ -0,0 +1,3 @@ +import ProgressBar from './ProgressBar'; + +export default ProgressBar; diff --git a/src/components/ProjectList/index.js b/src/components/ProjectList/index.js index 08f606f2..dd2de92b 100644 --- a/src/components/ProjectList/index.js +++ b/src/components/ProjectList/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import propTypes from 'prop-types'; -import { NavLink } from 'react-router-dom'; +import { NavLink, Redirect } from 'react-router-dom'; import { inject, observer } from 'mobx-react'; import { withTranslation } from 'react-i18next'; import Button from '../Button/Button'; @@ -15,9 +15,23 @@ import styles from '../Login/Login.scss'; @inject('appStore') @observer class ProjectList extends Component { + static propTypes = { + appStore: propTypes.shape({ + readProjectList: propTypes.func.isRequired, + projectList: propTypes.arrayOf(propTypes.object).isRequired, + checkIsQuestionsUploaded: propTypes.func.isRequired, + gotoProject: propTypes.func.isRequired, + setProjectAddress: propTypes.func.isRequired, + }).isRequired, + t: propTypes.func.isRequired, + }; + constructor(props) { super(props); - this.state = {}; + this.state = { + redirectToProject: false, + redirectToUploading: false, + }; } componentDidMount() { @@ -25,16 +39,51 @@ class ProjectList extends Component { appStore.readProjectList(); } + gotoProject = ({ address, name }) => { + const { appStore } = this.props; + appStore.gotoProject({ address, name }); + this.setState({ redirectToProject: true }); + } + + startUploading = (address) => { + const { appStore } = this.props; + appStore.setProjectAddress(address); + this.setState({ redirectToUploading: true }); + } + + checkProject = async ({ + address, + name, + }) => { + const { appStore } = this.props; + // eslint-disable-next-line no-unused-vars + const isQuestionsUploaded = await appStore.checkIsQuestionsUploaded(address); + // eslint-disable-next-line no-unused-expressions + isQuestionsUploaded + ? this.gotoProject({ address, name }) + : this.startUploading(address); + } + render() { const { appStore: { projectList }, t } = this.props; + const { redirectToProject, redirectToUploading } = this.state; const projects = projectList.map((project, index) => ( { + this.checkProject({ + address: project.address, + name: project.name, + }); + }} > {project.name.replace(/([!@#$%^&*()_+\-=])+/g, ' ')} )); + + if (redirectToProject) return ; + if (redirectToUploading) return ; return ( @@ -58,12 +107,4 @@ class ProjectList extends Component { } } -ProjectList.propTypes = { - appStore: propTypes.shape({ - readProjectList: propTypes.func.isRequired, - projectList: propTypes.arrayOf(propTypes.object).isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; - export default ProjectList; diff --git a/src/components/ProjectUploading/ProgressBlock/index.js b/src/components/ProjectUploading/ProgressBlock/index.js index e1426c9b..a85e76ea 100644 --- a/src/components/ProjectUploading/ProgressBlock/index.js +++ b/src/components/ProjectUploading/ProgressBlock/index.js @@ -6,10 +6,11 @@ import styles from '../../Login/Login.scss'; const ProgressBlock = ({ children, text, index, state, noline, }) => ( - index ? 'success' : ''}`} > + {!noline ? : ''} @@ -23,7 +24,6 @@ const ProgressBlock = ({ {text} {children[1] ? children[1] : ''} - {!noline ? : ''} ); diff --git a/src/components/ProjectUploading/index.js b/src/components/ProjectUploading/index.js index c323c6e6..4170262f 100644 --- a/src/components/ProjectUploading/index.js +++ b/src/components/ProjectUploading/index.js @@ -31,19 +31,44 @@ class ProjectUploading extends Component { this.state = { step: this.steps.compiling, uploading: true, + contractAddress: '', + projectName: '', }; } componentDidMount() { const { steps } = this; const { - appStore, appStore: { deployArgs, name }, userStore: { password }, t, + appStore, appStore: { deployArgs, projectAddress }, userStore: { password }, type, } = this.props; - this.setState({ - step: steps.sending, - }); - appStore.deployContract('project', deployArgs, password) + switch (type) { + case ('project'): + this.setState({ + step: steps.sending, + }); + this.deployProject(deployArgs, password); + break; + case ('question'): + this.setState({ + step: steps.questions, + }); + appStore.deployQuestions(projectAddress).then(() => { + this.setState({ + uploading: false, + }); + }); + break; + default: + break; + } + } + + deployProject(deployArgs, password) { + const { steps } = this; + const { appStore, appStore: { name }, t } = this.props; + + appStore.deployContract('ZeroOne', deployArgs, password) .then((txHash) => { this.setState({ step: steps.receipt, @@ -55,23 +80,33 @@ class ProjectUploading extends Component { this.setState({ step: steps.questions, }); + appStore.setProjectAddress(receipt.contractAddress); appStore.addProjectToList({ name, address: receipt.contractAddress }); appStore.deployQuestions(receipt.contractAddress).then(() => { this.setState({ uploading: false, + contractAddress: receipt.contractAddress, + projectName: name, }); }); } - }).catch(() => { appStore.displayAlert(t('errors:hostUnreachable'), 3000); }); + }).catch((err) => { + alert(err); + appStore.displayAlert(t('errors:hostUnreachable'), 3000); + }); } render() { - const { step, uploading } = this.state; + const { + step, uploading, contractAddress, projectName, + } = this.state; return ( { - uploading ? : + uploading + ? + : } @@ -106,7 +141,7 @@ const Progress = withTranslation()(inject('appStore')(observer(({ t, appStore, s text={item[0]} index={index} state={step} - noline={index === 4} + noline={index === 0} > {item[1]} @@ -116,7 +151,9 @@ const Progress = withTranslation()(inject('appStore')(observer(({ t, appStore, s ); }))); -const AlertBlock = withTranslation()(({ t }) => ( +const AlertBlock = withTranslation()(inject('appStore')(observer(({ + t, appStore, address, name, +}) => ( {t('headings:projectCreated.heading')} @@ -126,16 +163,24 @@ const AlertBlock = withTranslation()(({ t }) => ( {t('headings:projectCreated.subheading.1')} - } type="submit"> - {t('buttons:toCreatedProject')} - + + } + type="button" + onClick={() => { appStore.gotoProject({ address, name }); }} + > + {t('buttons:toCreatedProject')} + + - + {t('buttons:otherProject')} -)); +)))); ProjectUploading.propTypes = { appStore: propTypes.shape({ @@ -147,11 +192,14 @@ ProjectUploading.propTypes = { addProjectToList: propTypes.func.isRequired, deployQuestions: propTypes.func.isRequired, displayAlert: propTypes.func.isRequired, + projectAddress: propTypes.func.isRequired, + setProjectAddress: propTypes.func.isRequired, }).isRequired, userStore: propTypes.shape({ password: propTypes.string.isRequired, }).isRequired, t: propTypes.func.isRequired, + type: propTypes.string.isRequired, }; Progress.propTypes = { diff --git a/src/components/Questions/FullQuestion/index.js b/src/components/Questions/FullQuestion/index.js new file mode 100644 index 00000000..2edc0e23 --- /dev/null +++ b/src/components/Questions/FullQuestion/index.js @@ -0,0 +1,44 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import Container from '../../Container'; +import Question from '../Question'; +import Button from '../../Button/Button'; + +import styles from '../Questions.scss'; + +const FullQuestion = withTranslation()(inject( + 'projectStore', +)(observer(({ + t, + projectStore, +}) => { + const { id: pageid } = useParams(); + const { goBack } = useHistory(); + const { questionStore, historyStore } = projectStore; + const [question] = questionStore.getQuestionById(pageid); + return ( + + + + + + {t('buttons:back')} + + + + + + + + + ); +}))); + +export default FullQuestion; diff --git a/src/components/Questions/Question/Question.scss b/src/components/Questions/Question/Question.scss new file mode 100644 index 00000000..5c38fd8e --- /dev/null +++ b/src/components/Questions/Question/Question.scss @@ -0,0 +1,90 @@ +@import '../../../assets/styles/partials/variables'; +.question { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: start; + width: 100%; + height: 125px; + background-color: $white; + border: 1px solid $lightGrey; + &--extended { + align-items: flex-start; + height: auto; + .question__left { + width: 70%; + border: none; + } + .question__right { + padding-top: 28px; + } + } + &--short-name { + .question__left { + width: 50%; + } + .question__right { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + width: 50%; + .question__parameter { + &-heading { + display: block; + width: 100%; + margin-bottom: 10px; + } + width: 50%; + } + } + } + &__left { + align-self: flex-start; + width: 80%; + height: 100%; + padding: 10px 30px; + border-right: 1px solid $lightGrey; + cursor: pointer; + } + &__right { + width: 20%; + text-align: center; + } + &__id { + color: $lightGrey; + font-size: 11px; + } + &__caption { + max-width: 70%; + margin: 5px 0 10px; + font-weight: 700; + font-size: 18px; + } + &__description { + max-width: 70%; + font-size: 11px; + } + &__parameter { + margin: 10px 0; + text-align: left; + &-heading { + font-weight: 700; + font-size: 14px; + text-align: left; + } + &-label { + margin-bottom: 5px; + color: $primary; + font-size: 11px; + } + &-text { + color: $border; + font-size: 11px; + } + } + &__formula { + padding: 10px 30px 30px; + color: $border; + font-size: 11px; + } +} \ No newline at end of file diff --git a/src/components/Questions/Question/index.js b/src/components/Questions/Question/index.js new file mode 100644 index 00000000..f35e5231 --- /dev/null +++ b/src/components/Questions/Question/index.js @@ -0,0 +1,187 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import propTypes from 'prop-types'; +import { NavLink, withRouter } from 'react-router-dom'; +import { withTranslation, Trans } from 'react-i18next'; +import uniqKey from 'react-id-generator'; +import { inject, observer } from 'mobx-react'; +import { StartIcon } from '../../Icons'; +import Button from '../../Button/Button'; + +import styles from './Question.scss'; + +/** + * Component for render start button + * + * @param {object} param0 data + * @param {Function} param0.t method for translate text + * @param {*} param0.id id question + * @param {*} param0.history id question + * @returns {Node} component start button + */ +const startBlock = ({ + t, + id, + history, + votingIsActive, +}) => ( + + )} + onClick={() => history.push(`/votings?modal=start_new_vote&option=${id}`)} + disabled={votingIsActive} + hint={ + votingIsActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:startNewVote')} + + +); + +startBlock.propTypes = { + t: propTypes.func.isRequired, + id: propTypes.oneOfType([ + propTypes.number, + propTypes.string, + ]).isRequired, + history: propTypes.shape({ + push: propTypes.func.isRequired, + }).isRequired, + votingIsActive: propTypes.bool.isRequired, +}; + +const startBlockWithRouter = withRouter(startBlock); + +const ParametersBlock = (paramNames, paramTypes, t) => ( + + {t('other:parameters')} + {paramNames.map((param, index) => ( + + {param} + {paramTypes[index]} + + ))} + +); + +const ShortDescription = (text) => ( + + {text.slice(0, 250)} + +); + +const FullDescription = (text) => ( + + {text} + +); + +const FormulaBlock = (formula, t) => ( + + {`${t('other:votingFormula')}: ${formula}`} + +); + +const Content = inject('projectStore')(observer(({ + projectStore: { questionStore }, + id, + caption, + text, + extended, + groupId, +}) => { + const [group] = questionStore.getQuestionGroupById(groupId); + return ( + + {`#${id} - ${group.name}`} + {caption} + {extended ? FullDescription(text) : ShortDescription(text)} + + ); +})); + +// eslint-disable-next-line no-unused-vars +const Question = withTranslation()(({ + t, + extended, id, + name, + description, + formula, + paramNames, + paramTypes, + votingIsActive, + groupId, +}) => ( + 3 && extended) ? styles['question--short-name'] : ''} + `} + > + { + !extended + ? ( + + + + ) + : ( + + + + ) + } + { + extended + ? ParametersBlock(paramNames, paramTypes, t) + : startBlockWithRouter({ t, id, votingIsActive }) + } + + {extended ? FormulaBlock(formula, t) : null} + +)); + +Question.propTypes = { + id: propTypes.number.isRequired, + extended: propTypes.bool, + votingIsActive: propTypes.bool.isRequired, + name: propTypes.string.isRequired, + description: propTypes.string.isRequired, + formula: propTypes.string.isRequired, + paramNames: propTypes.arrayOf(propTypes.string).isRequired, + paramTypes: propTypes.arrayOf(propTypes.string).isRequired, +}; + +Question.defaultProps = { + extended: false, +}; + +export default Question; diff --git a/src/components/Questions/Questions.scss b/src/components/Questions/Questions.scss new file mode 100644 index 00000000..a7bef217 --- /dev/null +++ b/src/components/Questions/Questions.scss @@ -0,0 +1,83 @@ +@import '../../assets/styles/partials/variables'; +.questions { + margin: 0 auto; + + &__head { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + margin-bottom: 30px; + &-create { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + width: 45%; + .btn { + svg { + path { + fill: $white; + } + } + &:active { + path { + stroke: $white; + } + } + } + } + &-filters { + display: flex; + flex-flow: row nowrap; + align-items: center; + width: 25%; + .dropdown { + width: 100%; + } + } + } + + &__loader { + text-align: center; + } + + &__wrapper { + display: grid; + grid-template-columns:1fr; + row-gap: 10px; + justify-items: center; + margin-bottom: 30px; + } + + &__list-empty { + position: relative; + height: 40vh; + min-height: 200px; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + p { + display: inline-block; + margin: 0; + padding: 30px 0; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + text-align: center; + vertical-align: middle; + } + } +} + +.question { + &--back { + width: 105px; + padding: 7px 25px; + } +} \ No newline at end of file diff --git a/src/components/Questions/Questions.stories.js b/src/components/Questions/Questions.stories.js new file mode 100644 index 00000000..f6c1f71d --- /dev/null +++ b/src/components/Questions/Questions.stories.js @@ -0,0 +1,8 @@ +import React from 'react'; +import Questions from '.'; + +export default ({ title: 'Questions' }); + +export const Wrapper = () => ( + +); diff --git a/src/components/Questions/QuestionsHead.js b/src/components/Questions/QuestionsHead.js new file mode 100644 index 00000000..e5f2b628 --- /dev/null +++ b/src/components/Questions/QuestionsHead.js @@ -0,0 +1,111 @@ +import React from 'react'; +import { Trans, withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import uniqKey from 'react-id-generator'; +import ProjectStore from '../../stores/ProjectStore'; +import DialogStore from '../../stores/DialogStore'; +import { CreateToken, QuestionIcon } from '../Icons'; +import Button from '../Button/Button'; +import SimpleDropdown from '../SimpleDropdown'; + +import styles from './Questions.scss'; + +@withTranslation() +@inject('projectStore', 'dialogStore') +@observer +class QuestionsHead extends React.Component { + static propTypes = { + t: PropTypes.func.isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + }; + + /** + * Method for handle sort + * + * @param {object} selected new sort data + * @param {string|number} selected.value new sort value + * @param {string} selected.label new sort label + */ + handleSortSelect = (selected) => { + const { projectStore } = this.props; + const { + questionStore: { + addFilterRule, + }, + } = projectStore; + addFilterRule({ groupId: selected.value }); + } + + render() { + const { t, projectStore, dialogStore } = this.props; + const { + questionStore: { + questionGroups, + }, + historyStore, + } = projectStore; + return ( + + + } + onClick={() => { dialogStore.show('create_group_question'); }} + disabled={historyStore.isVotingActive} + hint={ + historyStore.isVotingActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:createQuestionGroup')} + + } + onClick={() => { dialogStore.show('create_question'); }} + disabled={historyStore.isVotingActive} + hint={ + historyStore.isVotingActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:createQuestion')} + + + + + + + + + ); + } +} + +export default QuestionsHead; diff --git a/src/components/Questions/QuestionsList.js b/src/components/Questions/QuestionsList.js new file mode 100644 index 00000000..40efb79f --- /dev/null +++ b/src/components/Questions/QuestionsList.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { computed } from 'mobx'; +import { inject, observer } from 'mobx-react'; +import { Trans } from 'react-i18next'; +import Question from './Question'; +import ProjectStore from '../../stores/ProjectStore'; + +import styles from './Questions.scss'; + +@inject('projectStore') +@observer +class QuestionsList extends React.Component { + static propTypes = { + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + }; + + @computed + get paginatedQuestions() { + const { props } = this; + const { + projectStore: { questionStore }, + } = props; + return questionStore.paginatedList; + } + + render() { + const { paginatedQuestions, props } = this; + const { + projectStore: { historyStore }, + } = props; + return ( + <> + { + paginatedQuestions + && paginatedQuestions.length + ? ( + paginatedQuestions.map((question) => ( + + )) + ) + : ( + + + + No questions have been + + created in this group yet + + + + ) + } + > + ); + } +} + +export default QuestionsList; diff --git a/src/components/Questions/QuestionsRoute.js b/src/components/Questions/QuestionsRoute.js new file mode 100644 index 00000000..3db31039 --- /dev/null +++ b/src/components/Questions/QuestionsRoute.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { + Route, Switch, +} from 'react-router-dom'; +import { inject } from 'mobx-react'; +import PropTypes from 'prop-types'; +import Questions from '.'; +import FullQuestion from './FullQuestion'; + +@inject('projectStore') +class QuestionsRoute extends React.Component { + static propTypes = { + projectStore: PropTypes.shape({ + questionStore: PropTypes.shape({ + resetFilter: PropTypes.func.isRequired, + }), + }).isRequired, + }; + + componentDidMount() { + const { projectStore } = this.props; + const { + questionStore: { + resetFilter, + }, + } = projectStore; + resetFilter(); + } + + render() { + return ( + + + + + ); + } +} + +export default QuestionsRoute; diff --git a/src/components/Questions/index.js b/src/components/Questions/index.js new file mode 100644 index 00000000..f5282c24 --- /dev/null +++ b/src/components/Questions/index.js @@ -0,0 +1,244 @@ +/* eslint-disable no-unused-vars */ +import React, { Component } from 'react'; +import propTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import { observable, computed } from 'mobx'; +import { withRouter } from 'react-router-dom'; +import ProjectStore from '../../stores/ProjectStore'; +import DialogStore from '../../stores/DialogStore'; +import UserStore from '../../stores/UserStore/UserStore'; +import Container from '../Container'; +import Footer from '../Footer'; +import Dialog from '../Dialog/Dialog'; +import CreateGroupQuestions from '../CreateGroupQuestions/CreateGroupQuestions'; +import CreateNewQuestion from '../CreateNewQuestion/CreateNewQuestion'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import Pagination from '../Pagination'; +import Loader from '../Loader'; +import Notification from '../Notification/Notification'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; +import QuestionsList from './QuestionsList'; +import QuestionsHead from './QuestionsHead'; + +import styles from './Questions.scss'; + +@withRouter +@withTranslation() +@inject('projectStore', 'dialogStore', 'userStore') +@observer +class Questions extends Component { + @observable votingIsActive = false; + + @observable _loading = false; + + passwordForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { props } = this; + const { + history, + dialogStore, + projectStore: { + historyStore, + rootStore: { Web3Service, contractService, appStore }, + votingData, + votingQuestion, + votingGroupId, + }, + userStore, + } = props; + dialogStore.show('progress_modal_questions'); + const { password } = form.values(); + userStore.setPassword(password); + appStore.setTransactionStep('compileOrSign'); + return userStore.readWallet(password) + .then(() => { + // eslint-disable-next-line max-len + const transaction = contractService.createVotingData(Number(votingQuestion), Number(votingGroupId), votingData); + return transaction; + }) + .then((tx) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + appStore.setTransactionStep('success'); + userStore.getEthBalance(); + dialogStore.hide(); + history.push('/votings'); + historyStore.getActualState(); + }) + .catch((error) => { + dialogStore.show('error_modal_questions'); + console.error(error); + })); + }, + onError: () => Promise.reject(), + }, + }); + + static propTypes = { + t: propTypes.func.isRequired, + projectStore: propTypes.instanceOf(ProjectStore).isRequired, + dialogStore: propTypes.instanceOf(DialogStore).isRequired, + userStore: propTypes.instanceOf(UserStore).isRequired, + history: propTypes.shape({ + push: propTypes.func.isRequired, + }).isRequired, + }; + + constructor(props) { + super(props); + this.state = {}; + } + + async componentDidMount() { + const { projectStore: { historyStore, questionStore } } = this.props; + this._loading = true; + this.votingIsActive = historyStore.isVotingActive; + questionStore.fetchActualQuestionGroups(); + this._loading = false; + } + + @computed + get loading() { + const { projectStore: { questionStore } } = this.props; + if (this._loading === true) return true; + return questionStore.loading; + } + + /** + * Method for getting init index + * for dropdown sort option + * + * @returns {number} index number + */ + get initIndex() { + const { projectStore } = this.props; + const { + questionStore: { + questionGroups, + filter: { rules }, + }, + } = projectStore; + let initIndex = 0; + questionGroups.forEach((option, index) => { + if (option.value === rules.groupId) { + initIndex = index; + } + }); + return initIndex; + } + + render() { + const { loading } = this; + const { + t, + projectStore, + dialogStore, + } = this.props; + const { + questionStore: { + pagination, + }, + rootStore: { contractService: { transactionStep } }, + } = projectStore; + return ( + <> + + {/* FIXME remove comment */} + + + { + !loading + ? ( + <> + + + + + { + pagination + ? ( + + ) + : null + } + > + ) + : ( + + + + ) + } + + + + + + + + + + + + + + + { dialogStore.hide(); }} /> + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + + > + ); + } +} + +export default Questions; diff --git a/src/components/ReturnTokens/ReturnTokens.js b/src/components/ReturnTokens/ReturnTokens.js new file mode 100644 index 00000000..60c84e46 --- /dev/null +++ b/src/components/ReturnTokens/ReturnTokens.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject } from 'mobx-react'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import UserStore from '../../stores/UserStore'; +import DialogStore from '../../stores/DialogStore'; +import Dialog from '../Dialog/Dialog'; +import TransactionProgress from '../Message/TransactionProgress'; +import HistoryStore from '../../stores/HistoryStore'; +import NotificationStore from '../../stores/NotificationStore'; +import ErrorMessage from '../Message/ErrorMessage'; +import SuccessMessage from '../Message/SuccessMessage'; + +@withTranslation() +@inject('userStore', 'dialogStore', 'projectStore', 'notificationStore') +class ReturnTokens extends React.Component { + form = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { + userStore, + dialogStore, + notificationStore, + projectStore: { + historyStore, + }, + } = this.props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.toggle('progress_modal_return_tokens'); + return historyStore.returnTokens() + .then(() => { + const notificationId = notificationStore.list[0].id; + notificationStore.remove(notificationId); + dialogStore.toggle('success_modal_return_tokens'); + historyStore.getActualState(); + }) + .catch((error) => { + console.error(error); + dialogStore.toggle('error_modal_return_tokens'); + }); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + notificationStore: PropTypes.instanceOf(NotificationStore).isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.instanceOf(HistoryStore), + }).isRequired, + }; + + render() { + const { props, form } = this; + const { t, dialogStore } = props; + return ( + <> + + + + + + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + { dialogStore.hide(); }} /> + + > + ); + } +} + +export default ReturnTokens; diff --git a/src/components/ReturnTokens/index.js b/src/components/ReturnTokens/index.js new file mode 100644 index 00000000..ccb7b6fe --- /dev/null +++ b/src/components/ReturnTokens/index.js @@ -0,0 +1,3 @@ +import ReturnTokens from './ReturnTokens'; + +export default ReturnTokens; diff --git a/src/components/Router/SimpleRouter.js b/src/components/Router/SimpleRouter.js index 92eea738..22361425 100644 --- a/src/components/Router/SimpleRouter.js +++ b/src/components/Router/SimpleRouter.js @@ -16,10 +16,16 @@ import ProjectUploading from '../ProjectUploading'; import CreationAlert from '../CreationAlert'; import DisplayUserInfo from '../DisplayUserInfo'; import Header from '../Header'; +import Members from '../Members'; +import Settings from '../Settings'; +import VotingRoute from '../Voting/VotingRoute'; +import QuestionsRoute from '../Questions/QuestionsRoute'; +import ReturnTokens from '../ReturnTokens/ReturnTokens'; const SimpleRouter = () => ( + @@ -36,8 +42,14 @@ const SimpleRouter = () => ( + ()} /> + ()} /> + + ()} /> ()} /> + + ); diff --git a/src/components/Settings/Settings.scss b/src/components/Settings/Settings.scss new file mode 100644 index 00000000..6edbbac0 --- /dev/null +++ b/src/components/Settings/Settings.scss @@ -0,0 +1,109 @@ +@import '../../assets/styles/partials/variables'; + +.settings { + display: flex; + flex-flow: row nowrap; + align-self: center; + justify-content: space-around; + padding-top: 50px; + + &__container { + display: block; + } + + &__block { + display: flex; + flex-flow: column nowrap; + justify-content: space-between; + width: 38%; + padding: 45px 40px; + text-align: center; + background-color: transparent; + //border: 1px solid $lightGrey; + &-heading { + margin-bottom: 35px; + font-weight: 700; + font-size: 18px; + } + &-content { + display: flex; + flex-flow: row wrap; + justify-content: space-around; + form { + width: 100%; + } + .field { + width: 100%; + margin-bottom: 35px; + &__label{ + z-index: 0; + } + } + .btn--white { + width: 45%; + margin: 5px 0; + } + } + &--contracts { + width: 22%; + padding: 45px 15px 25px; + .settings__block-content{ + .btn { + width: 80%; + } + } + } + &--settings { + .settings__block-group { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + .field { + width: 45%; + } + } + .field { + &::before { + content: unset; + } + &__input{ + width: 100%; + margin: 0; + } + &__label{ + left: 10px; + font-size: 12px; + } + } + } + } +} +.modal-buttons { + margin-top: 20px; + .btn { + &:first-child { + margin-bottom: 15px; + } + } + & > p { + margin-top: 5px; + color: #000000; + font-weight: 300; + font-size: 11px; + text-align: center; + opacity: 0.7; + } +} + +#dialog-project_modal_contract_uploading, +#dialog-token_modal_contract_uploading { + .dialog { + &__header { + padding: 20px; + padding-top: 55px; + } + &__body { + padding-bottom: 50px; + } + } +} diff --git a/src/components/Settings/entities/ContractUploading.js b/src/components/Settings/entities/ContractUploading.js new file mode 100644 index 00000000..410cacca --- /dev/null +++ b/src/components/Settings/entities/ContractUploading.js @@ -0,0 +1,203 @@ +/* eslint-disable react/no-unused-state */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer, inject } from 'mobx-react'; +import Button from '../../Button/Button'; +import CreateTokenForm from '../../../stores/FormsStore/CreateToken'; +import Dialog from '../../Dialog/Dialog'; +import TokenInputForm from '../../Forms/TokenInputForm'; +import TransactionProgress from '../../Message/TransactionProgress'; +import SuccessMessage from '../../Message/SuccessMessage'; +import ErrorMessage from '../../Message/ErrorMessage'; +import CreateProjectInSettings from '../../../stores/FormsStore/CreateProjectInSettings'; +import ProjectInputForm from '../../Forms/ProjectInputForm'; + +import styles from '../Settings.scss'; + +@withTranslation() +@inject('appStore', 'userStore', 'dialogStore') +@observer +class ContractUploading extends Component { + tokenForm = new CreateTokenForm({ + hooks: { + onSuccess: (form) => { + const { contractType } = this.state; + const { + appStore, userStore, dialogStore, appStore: { rootStore: { Web3Service } }, + } = this.props; + const { + name, count, password, symbol, + } = form.values(); + userStore.setPassword(password); + const deployArgs = [name, symbol, Number(count)]; + dialogStore.toggle('progress_modal_contract_uploading'); + return appStore.deployContract(contractType, deployArgs, password) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then((receipt) => { + appStore.setTransactionStep('success'); + dialogStore.toggle('success_modal_contract_uploading'); + this.setState({ address: receipt.contractAddress }); + form.clear(); + }) + .catch(() => { + dialogStore.toggle('error_modal_contract_uploading'); + }); + }, + onError: () => {}, + }, + }) + + projectForm = new CreateProjectInSettings({ + hooks: { + onSuccess: (form) => { + const { + appStore, userStore, dialogStore, appStore: { rootStore: { Web3Service } }, + } = this.props; + const { + name, address, password, + } = form.values(); + userStore.setPassword(password); + const deployArgs = [address]; + dialogStore.toggle('progress_modal_contract_uploading'); + return appStore.deployContract('ZeroOne', deployArgs, password) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then((receipt) => { + appStore.addProjectToList({ name, address: receipt.contractAddress }); + this.setState({ address: receipt.contractAddress }); + form.clear(); + return receipt.contractAddress; + }) + .then((contractAddress) => { + appStore.setTransactionStep('questionsUploading'); + return appStore.deployQuestions(contractAddress).then(() => { + }); + }) + .then(() => { + appStore.setTransactionStep('success'); + dialogStore.toggle('success_modal_contract_uploading'); + }) + .catch(() => { + dialogStore.toggle('error_modal_contract_uploading'); + }); + }, + onError: () => {}, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + dialogStore: PropTypes.shape().isRequired, + appStore: PropTypes.shape().isRequired, + userStore: PropTypes.shape().isRequired, + } + + constructor() { + super(); + this.state = { + contractType: '', + address: '', + }; + } + + // componentDidMount() { + // const { dialogStore } = this.props; + // this.setState({ contractType: 'token' }); + // dialogStore.toggle('progress_modal_contract_uploading'); + // } + + + triggerModal = async (contractType) => { + const { dialogStore } = this.props; + this.setState({ contractType }); + // eslint-disable-next-line react/destructuring-assignment + dialogStore.show('token_modal_contract_uploading'); + } + + triggerProjectModal = () => { + const { dialogStore } = this.props; + this.setState({ contractType: 'ZeroOne' }); + dialogStore.show('project_modal_contract_uploading'); + } + + changeFormLang =() => { + this.projectForm.fireHook('onLangChangeHook'); + } + + render() { + const { address, contractType } = this.state; + const { t, dialogStore } = this.props; + const modalFooter = () => ( + + + {t('explanations:freeze')} + + + ); + return ( + + {t('headings:creatingAndUpload')} + + { this.triggerModal('ERC20'); }}>ERC20 + { this.triggerModal('CustomToken'); }}>Custom tokens + { this.triggerProjectModal('ZeroOne'); }}>Project + + + + + + + + + + + + + { dialogStore.hide(); }}> + {`Contract address = ${address}`} + + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + ); + } +} + +export default ContractUploading; diff --git a/src/components/Settings/entities/LangChanger.js b/src/components/Settings/entities/LangChanger.js new file mode 100644 index 00000000..8754958e --- /dev/null +++ b/src/components/Settings/entities/LangChanger.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { withTranslation } from 'react-i18next'; +import i18n from '../../../i18n'; +import Button from '../../Button/Button'; +import LangSwitcher from '../../LangSwitcher'; +import { getCorrectMomentLocale } from '../../../utils/Date'; + + +import styles from '../Settings.scss'; + +@withTranslation() +class LangChanger extends Component { + static propTypes = { + t: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + this.state = { + language: i18n.language, + }; + window.ipcRenderer.on('change-language:confirm', (event, language) => { + this.changeLanguage(language); + }); + } + + componentWillUnmount() { + window.ipcRenderer.removeListener('change-language:confirm', (event, language) => { + this.changeLanguage(language); + }); + } + + handleSelect = (language) => { + this.setState({ language }); + } + + changeLanguage = (language) => { + i18n.changeLanguage(language); + moment.locale(getCorrectMomentLocale(i18n.language)); + } + + setLanguage = () => { + const { language } = this.state; + window.ipcRenderer.send('change-language:request', language); + } + + render() { + const { props: { t } } = this; + return ( + + {t('headings:interfaceLanguage')} + + + {t('buttons:apply')} + + + ); + } +} + + +export default LangChanger; diff --git a/src/components/Settings/entities/NodeConnection.js b/src/components/Settings/entities/NodeConnection.js new file mode 100644 index 00000000..266c84d9 --- /dev/null +++ b/src/components/Settings/entities/NodeConnection.js @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import Web3 from 'web3'; +import Input from '../../Input'; +import { Address } from '../../Icons'; +import NodeChangeForm from '../../../stores/FormsStore/NodeChangeForm'; +import Button from '../../Button/Button'; + +import styles from '../Settings.scss'; + + +@withTranslation() +@inject('appStore') +@observer +class NodeConnection extends Component { + nodeChange = new NodeChangeForm({ + hooks: { + onSuccess: (form) => { + // eslint-disable-next-line no-unused-vars + const { appStore } = this.props; + const { url } = form.values(); + const web3 = new Web3(url); + return web3.eth.getNodeInfo() + .then(() => { + this.setState({ success: true }); + return appStore.nodeChange(url); + }) + // eslint-disable-next-line consistent-return + .catch(() => { + // eslint-disable-next-line no-restricted-globals + if (confirm('Node is unreachable, continue anyway?')) { + this.setState({ success: true }); + return appStore.nodeChange(url); + } + }); + }, + onError: () => {}, + }, + }); + + static propTypes = { + appStore: PropTypes.shape().isRequired, + t: PropTypes.func.isRequired, + } + + constructor() { + super(); + this.state = { + success: false, + }; + } + + + render() { + const { nodeChange, state, props } = this; + const { t } = props; + const { success } = state; + return ( + + {t('headings:nodeConnection')} + + + + + + {success ? t('buttons:success') : t('buttons:continue')} + + + + ); + } +} + + +export default NodeConnection; diff --git a/src/components/Settings/entities/SettingsBlock.js b/src/components/Settings/entities/SettingsBlock.js new file mode 100644 index 00000000..d41ef69f --- /dev/null +++ b/src/components/Settings/entities/SettingsBlock.js @@ -0,0 +1,118 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import Button from '../../Button/Button'; +import ConfigForm from '../../../stores/FormsStore/ConfigForm'; +import Input from '../../Input'; +import Dialog from '../../Dialog/Dialog'; + +import styles from '../Settings.scss'; + +@withTranslation() +@inject('configStore', 'dialogStore') +@observer +class SettingsBlock extends Component { + settingsForm = new ConfigForm({ + hooks: { + onSuccess: (form) => { + // eslint-disable-next-line no-unused-vars + const { configStore, dialogStore } = this.props; + configStore.updateValues(form.values()); + dialogStore.show('apply_notification'); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + configStore: PropTypes.shape().isRequired, + dialogStore: PropTypes.shape({ + show: PropTypes.func.isRequired, + hide: PropTypes.func.isRequired, + }).isRequired, + }; + + constructor(props) { + super(props); + this.state = { + isChanged: false, + }; + } + + handleInputChange = () => { + const { settingsForm, props } = this; + const { configStore: { config } } = props; + const formValues = settingsForm.values(); + const formKeys = Object.keys(settingsForm.values()); + let changed = []; + formKeys.forEach((key) => { + if (Number(formValues[key]) !== config[key]) { + changed.push(key); + } else { + changed = changed.filter((e) => e !== key); + } + }); + if (changed.length !== 0) { + this.setState({ isChanged: true }); + } else { + this.setState({ isChanged: false }); + } + } + + reloadApp = () => { + window.location.reload(); + } + + render() { + const { props, settingsForm, state: { isChanged } } = this; + const { t, dialogStore, configStore: { config } } = props; + return ( + + {t('headings:other')} + + + + + + + + { + isChanged + ? {t('buttons:apply')} + : null +} + + + + + { this.reloadApp(); }}>{t('buttons:saveAndReload')} + { dialogStore.hide(); }}>{t('buttons:saveWithoutReload')} + + {t('other:reloadNotificaion')} + + + + + ); + } +} + +export default SettingsBlock; diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js new file mode 100644 index 00000000..7224b28c --- /dev/null +++ b/src/components/Settings/index.js @@ -0,0 +1,25 @@ +import React from 'react'; +import Container from '../Container'; +import Footer from '../Footer'; +import Contractuploading from './entities/ContractUploading'; +import NodeConnection from './entities/NodeConnection'; +import SettingsBlock from './entities/SettingsBlock'; + +import styles from './Settings.scss'; + +const Settings = () => ( + <> + + + + + + + + + + + > +); + +export default Settings; diff --git a/src/components/ShowSeed/index.js b/src/components/ShowSeed/index.js index 2462075e..45cd04e7 100644 --- a/src/components/ShowSeed/index.js +++ b/src/components/ShowSeed/index.js @@ -10,6 +10,7 @@ import Heading from '../Heading'; import Explanation from '../Explanation'; import Button from '../Button/Button'; import { BackIcon, EyeIcon, CrossedEyeIcon } from '../Icons'; +import UserStore from '../../stores/UserStore/UserStore'; import styles from '../Login/Login.scss'; @@ -17,6 +18,11 @@ import styles from '../Login/Login.scss'; @inject('userStore', 'appStore') @observer class ShowSeed extends Component { + static propTypes = { + userStore: propTypes.instanceOf(UserStore).isRequired, + t: propTypes.func.isRequired, + }; + constructor(props) { super(props); this.state = { @@ -67,12 +73,15 @@ class ShowSeed extends Component { - + {t('explanations:seed.0')} {t('explanations:seed.1')} : } onClick={this.toggleWords} > @@ -92,13 +101,6 @@ const SeedWord = ({ word, id, visible }) => ( ); -ShowSeed.propTypes = { - userStore: propTypes.shape({ - mnemonic: propTypes.arrayOf(propTypes.string).isRequired, - }).isRequired, - t: propTypes.func.isRequired, -}; - SeedWord.propTypes = { id: propTypes.number.isRequired, word: propTypes.string.isRequired, diff --git a/src/components/SimpleDropdown/index.js b/src/components/SimpleDropdown/index.js new file mode 100644 index 00000000..a95ebb1c --- /dev/null +++ b/src/components/SimpleDropdown/index.js @@ -0,0 +1,164 @@ +import React, { Component } from 'react'; +import propTypes from 'prop-types'; +import nextId from 'react-id-generator'; +import { withTranslation } from 'react-i18next'; +import { DropdownArrowIcon } from '../Icons'; +import DropdownOption from '../SimpleDropdownOption'; + +import styles from '../Dropdown/Dropdown.scss'; + +@withTranslation() +class SimpleDropdown extends Component { + static propTypes = { + children: propTypes.oneOfType([ + propTypes.element, + propTypes.string, + ]), + options: propTypes.arrayOf(propTypes.shape({ + value: propTypes.oneOfType([ + propTypes.string, + propTypes.number, + ]), + label: propTypes.string, + })).isRequired, + onSelect: propTypes.func, + field: propTypes.shape({ + set: propTypes.func, + validate: propTypes.func, + error: propTypes.string, + }), + initIndex: propTypes.number, + t: propTypes.func.isRequired, + placeholder: propTypes.string, + isNewQuestion: propTypes.bool, + }; + + static defaultProps = { + children: '', + onSelect: () => false, + field: { + set: () => {}, + validate: () => {}, + error: null, + }, + initIndex: null, + placeholder: null, + isNewQuestion: false, + } + + constructor(props) { + super(props); + const { + initIndex, + options, + } = props; + const initOption = options[initIndex] || {}; + this.state = { + opened: false, + selectedValue: initOption.value || '', + selectedLabel: initOption.label || '', + }; + this.setWrapperRef = this.setWrapperRef.bind(this); + this.handleClickOutside = this.handleClickOutside.bind(this); + } + + componentDidMount() { + document.addEventListener('mousedown', this.handleClickOutside); + } + + componentWillUnmount() { + document.removeEventListener('mousedown', this.handleClickOutside); + } + + setWrapperRef(node) { + this.wrapperRef = node; + } + + + toggleOptions = () => { + const { opened } = this.state; + this.setState({ opened: !opened }); + } + + closeOptions = () => { + this.setState({ opened: false }); + } + + handleSelect = async (selected) => { + const { onSelect, field } = this.props; + field.set(selected.value); + field.validate(); + onSelect(selected); + this.setState({ + selectedLabel: selected.label, + selectedValue: selected.value, + }); + this.toggleOptions(); + } + + handleClickOutside(event) { + if (this.wrapperRef && !this.wrapperRef.contains(event.target)) { + this.closeOptions(); + } + } + + render() { + const { + children, + options, + t, + field, + placeholder, + isNewQuestion, + } = this.props; + const { opened, selectedLabel, selectedValue } = this.state; + const getOptions = options.map((option) => ( + + )); + return ( + + + + {children ? {children} : ''} + + {selectedLabel || placeholder || t('other:select') } + { + (isNewQuestion && selectedLabel !== '') + ? ( + + {placeholder} + + ) + : '' + } + + + + + + + + {getOptions} + + + {field.error} + + + ); + } +} + +export default SimpleDropdown; diff --git a/src/components/SimpleDropdownOption/index.js b/src/components/SimpleDropdownOption/index.js new file mode 100644 index 00000000..3151bd94 --- /dev/null +++ b/src/components/SimpleDropdownOption/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import propTypes from 'prop-types'; + +import styles from '../Dropdown/Dropdown.scss'; + +const DropdownOption = ({ + value, label, select, +}) => ( + { select({ value, label }); }} + > + {label} + +); + +DropdownOption.propTypes = { + value: propTypes.oneOfType([ + propTypes.string, + propTypes.number, + ]).isRequired, + label: propTypes.string.isRequired, + select: propTypes.func.isRequired, +}; + + +export default DropdownOption; diff --git a/src/components/StartNewVote/StartNewVote.js b/src/components/StartNewVote/StartNewVote.js new file mode 100644 index 00000000..845c5021 --- /dev/null +++ b/src/components/StartNewVote/StartNewVote.js @@ -0,0 +1,317 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import { observer, inject } from 'mobx-react'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { withTranslation, Trans } from 'react-i18next'; +import { observable } from 'mobx'; +import nextId from 'react-id-generator'; +import SimpleDropdown from '../SimpleDropdown'; +import { Address, QuestionIcon } from '../Icons'; +import Input from '../Input'; +import Button from '../Button/Button'; +import StarNewVoteForm from '../../stores/FormsStore/StartNewVoteForm'; +import { systemQuestionsId } from '../../constants'; + +import styles from './StartNewVote.scss'; + +@withRouter +@withTranslation() +@inject('projectStore', 'dialogStore') +@observer +class StartNewVote extends React.Component { + @observable initIndex = null; + + votingData = ''; + + form = new StarNewVoteForm({ + hooks: { + onSuccess: (form) => { + const { + projectStore: { + rootStore: { + Web3Service, + }, + questionStore, + }, + projectStore, + dialogStore, + } = this.props; + const data = form.values(); + const keys = Object.keys(data); + keys.forEach((key) => { + const text = String(data[key]); + data[key] = text.trim(); + }); + const { question: questionId } = data; + delete data.question; + const values = Object.values(data); + const [question] = questionStore.getQuestionById(questionId); + const { paramTypes, groupId } = question; + const encodedParams = question.id === 1 + ? Web3Service.web3.eth.abi + .encodeParameters( + ['tuple(uint256,uint256,uint256,uint256,uint256)', `tuple(${paramTypes.join(',')})`], + [[0, 0, 0, 0, 0], values], + ) + : Web3Service.web3.eth.abi + .encodeParameters( + ['tuple(uint256,uint256,uint256,uint256,uint256)', `${paramTypes.join(',')}`], + [[0, 0, 0, 0, 0], values.join(',')], + ); + projectStore.setVotingData(questionId, groupId, encodedParams); + dialogStore.toggle('password_form'); + return Promise.resolve(); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }); + + static propTypes = { + t: PropTypes.func.isRequired, + projectStore: PropTypes.shape().isRequired, + dialogStore: PropTypes.shape().isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + }; + + constructor() { + super(); + this.state = { + isSelected: false, + description: '', + }; + } + + componentDidMount() { + const { props } = this; + const { projectStore: { rootStore: { eventEmitterService } } } = props; + eventEmitterService.subscribe('new_vote:toggle', this.handleNewVoteToggle); + eventEmitterService.subscribe('new_vote:closed', this.handleNewVoteClose); + } + + componentWillUnmount() { + const { props } = this; + const { projectStore: { rootStore: { eventEmitterService } } } = props; + eventEmitterService.off('new_vote:toggle'); + eventEmitterService.off('new_vote:closed'); + } + + handleNewVoteToggle = (selected) => { + const { form } = this; + this.initIndex = Number(selected.value); + form.$('question').set(selected.value); + this.handleSelect(selected); + } + + handleNewVoteClose = () => { + this.initIndex = null; + this.setState({ isSelected: false }); + } + + // eslint-disable-next-line consistent-return + handleSelect = (selected) => { + const { form } = this; + const { projectStore, dialogStore, history } = this.props; + const { questionStore } = projectStore; + const [question] = questionStore.getQuestionById(selected.value); + const { paramTypes, paramNames, text: description } = question; + this.initIndex = Number(selected.value); + this.setState({ description }); + // @ Clearing fields, except question selection dropdown + // eslint-disable-next-line array-callback-return + form.map((field) => { + if (field.name === 'question') return; + form.del(field.name); + }); + // @ If Question have dedicated modal, then toggle them, else create fields + switch (selected.value) { + case systemQuestionsId.addingNewQuestion: + history.push('/questions'); + dialogStore.toggle('create_question'); + break; + case systemQuestionsId.connectGroupQuestions: + history.push('/questions'); + dialogStore.toggle('create_group_question'); + break; + default: + this.createFields(paramTypes, paramNames); + } + this.setState({ isSelected: true }); + } + + // eslint-disable-next-line class-methods-use-this + createFields(paramTypes, paramNames) { + if ( + paramTypes + && paramNames + && paramTypes.length + && paramNames.length + && paramTypes.length === paramNames.length + ) { + paramNames.forEach((name, index) => { + this.form.add({ + name, + type: 'text', + label: 'parameter', + placeholder: name, + rules: `required|${paramTypes[index]}`, + }); + }); + } + } + + render() { + const { form, initIndex } = this; + const { isSelected, description } = this.state; + const { props } = this; + const { t, projectStore } = props; + const { + historyStore, + questionStore: { newVotingOptions }, + questionStore: { rootStore: { membersStore: { nonERC } } }, + } = projectStore; + const groupTypes = [ + { label: 'ERC20', value: 0 }, + { label: 'Custom', value: 1 }, + ]; + return ( + + + + {t('other:startANewVote')} + + + + + + { + isSelected + ? ( + + {/* TODO add correct description text */} + { + description + && description.length + && description.length > 150 + ? description.substr(0, 130) + : description + } + + ) + : null + } + + + + { + isSelected + ? ( + + + {form.map((field, index) => { + if (field.name === 'question') return null; + if (field.placeholder === 'Group' || field.placeholder === 'Group address') { + return ( + + + + + + ); + } if (field.placeholder === 'Type' && this.initIndex === 1) { + return ( + + + + + + ); + } + return ( + + + + ); + })} + + + + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:start')} + + + + + ) + : ( + + + {t('other:newVoteEmptyStateText')} + + + ) + } + + + ); + } +} + +export default StartNewVote; diff --git a/src/components/StartNewVote/StartNewVote.scss b/src/components/StartNewVote/StartNewVote.scss new file mode 100644 index 00000000..711e3d54 --- /dev/null +++ b/src/components/StartNewVote/StartNewVote.scss @@ -0,0 +1,99 @@ +.new-vote { + width: 100%; + padding: 40px 61px; + + &__top { + display: inline-block; + width: 100%; + margin: 0 -15px; + + .dropdown { + width: 100%; + } + } + + &__title, + &__dropdown { + display: inline-block; + width: 50%; + padding: 0 15px; + vertical-align: top; + } + + &__title { + color: #000; + font-weight: 700; + font-size: 36px; + line-height: 42px; + text-align: left; + } + + &__description { + margin-top: 15px; + margin-bottom: 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: left; + } + + &__content { + min-height: 324px; + padding: 10px 0; + + &--empty { + height: 100%; + max-height: 324px; + text-align: center; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + &-text { + display: inline-block; + max-width: 261px; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + vertical-align: middle; + } + } + } + + &__form { + padding-top: 45px; + padding-bottom: 15px; + + &-row { + display: inline-block; + width: 100%; + margin: 0 -15px; + text-align: left; + } + + &-col { + display: inline-block; + width: 50%; + padding: 0 15px; + } + + .field { + width: 100%; + margin-bottom: 30px; + } + .dropdown { + width: 100%; + margin-bottom: 30px; + } + + button { + width: 100%; + margin-top: 15px; + } + } +} diff --git a/src/components/StartNewVote/StartNewVote.test.js b/src/components/StartNewVote/StartNewVote.test.js new file mode 100644 index 00000000..25ecc5a9 --- /dev/null +++ b/src/components/StartNewVote/StartNewVote.test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import StartNewVote from '.'; + +jest.mock('../../utils/Validator'); + +describe('StartNewVote', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow().dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('should by default with empty state', () => { + expect( + wrapper.find('.new-vote__content--empty-text').length, + ).toEqual(1); + expect( + wrapper.find('.new-vote__content--empty-text').text(), + ).toEqual('other:newVoteEmptyStateText'); + }); + + it('handleSelect should show form instead empty state', () => { + wrapperInstance.handleSelect(); + expect(wrapper.find('form').length).toEqual(1); + expect( + wrapper.find('.new-vote__content--empty-text').length, + ).toEqual(0); + }); +}); diff --git a/src/components/StartNewVote/index.js b/src/components/StartNewVote/index.js new file mode 100644 index 00000000..072abaeb --- /dev/null +++ b/src/components/StartNewVote/index.js @@ -0,0 +1,3 @@ +import StartNewVote from './StartNewVote'; + +export default StartNewVote; diff --git a/src/components/StepIndicator/index.js b/src/components/StepIndicator/index.js index b518eae3..6ec789e9 100644 --- a/src/components/StepIndicator/index.js +++ b/src/components/StepIndicator/index.js @@ -19,7 +19,16 @@ const StepIndicator = withTranslation()(({ t, currentStep, stepCount }) => { - {arr.map((item, index) => = index + 1} />)} + { + arr.map( + (item, index) => ( + = index + 1} + key={`step-indicator--${index + 1}`} + /> + ), + ) + } ); diff --git a/src/components/TokenTransfer/TokenTransfer.js b/src/components/TokenTransfer/TokenTransfer.js new file mode 100644 index 00000000..c558d76b --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.js @@ -0,0 +1,215 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import { withRouter } from 'react-router-dom'; +import TransferTokenForm from '../../stores/FormsStore/TransferTokenForm'; +import Dialog from '../Dialog/Dialog'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import UserStore from '../../stores/UserStore'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import DialogStore from '../../stores/DialogStore'; +import Input from '../Input'; +import { Password, Address, TokenCount } from '../Icons'; +import Button from '../Button/Button'; +import { EMPTY_DATA_STRING, tokenTypes } from '../../constants'; + +import styles from './TokenTransfer.scss'; + +/** + * Component form for transfer token + */ +@withRouter +@withTranslation() +@inject('membersStore', 'userStore', 'dialogStore', 'projectStore') +@observer +class TokenTransfer extends React.Component { + votingIsActive = false; + + passwordForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { + wallet, + groupAddress, + userStore, + dialogStore, + membersStore: { + rootStore: { + Web3Service, + contractService, + projectStore: { historyStore }, + }, + }, + history, + } = this.props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.show('progress_modal'); + const votingData = Web3Service.web3.eth.abi + .encodeParameters( + ['tuple(uint,uint,uint,uint,uint)', 'address', 'address'], + [[0, 0, 0, 0, 0], groupAddress, wallet], + ); + + return userStore.readWallet(password) + .then(() => { + // eslint-disable-next-line max-len + const transaction = contractService.createVotingData(3, 0, votingData); + return transaction; + }) + .then((tx) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash))) + .then(() => { + userStore.getEthBalance(); + dialogStore.show('success_modal'); + historyStore.getActualState(); + history.push('/votings'); + }) + .catch((error) => { + dialogStore.show('error_modal'); + console.error(error); + }); + }, + onError: (form) => { + console.error(form.error); + }, + }, + }) + + static propTypes = { + t: PropTypes.func.isRequired, + wallet: PropTypes.string, + groupAddress: PropTypes.string.isRequired, + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + groupId: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + groupType: PropTypes.string.isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + form: PropTypes.instanceOf(TransferTokenForm).isRequired, + } + + static defaultProps = { + wallet: EMPTY_DATA_STRING, + } + + async componentDidMount() { + const { + projectStore: { + historyStore, + }, + } = this.props; + this.votingIsActive = historyStore.isVotingActive; + } + + handleClick = () => { + const { + groupId, dialogStore, + } = this.props; + dialogStore.show(`password_form-${groupId}`); + } + + render() { + const { props } = this; + const { + t, + wallet, + groupId, + groupType, + projectStore: { historyStore }, + form, + } = props; + return ( + + + {t('dialogs:tokenTransfer')} + + + + + + + + + + + + + + + + + + + + {t('buttons:transfer')} + + + {wallet} + + { + groupType !== tokenTypes.ERC20 + ? ( + + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:designateGroupAdministrator')} + + + ) + : null + } + { + groupType !== tokenTypes.ERC20 + ? ( + + + + ) + : null + } + + + ); + } +} + +export default TokenTransfer; diff --git a/src/components/TokenTransfer/TokenTransfer.scss b/src/components/TokenTransfer/TokenTransfer.scss new file mode 100644 index 00000000..f0941416 --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.scss @@ -0,0 +1,55 @@ +@import '../../assets/styles/includes/mixin'; + +.token-transfer { + text-align: center; + + .input__wrapper { + margin-bottom: 32px; + + .field { + width: 100%; + max-width: 309px; + } + } + + .button__wrapper { + margin-top: 48px; + margin-bottom: 16px; + + .btn { + width: 100%; + max-width: 298px; + } + } + + .wallet__wrapper { + margin-top: 16px; + margin-bottom: 24px; + color: #808080; + font-size: 14px; + line-height: 16px; + } + + &__title { + @include title; + + margin-top: 72px; + margin-bottom: 73px; + } + + &__button { + // compensate padding in dialog + width: calc(100% + 80px); + margin: 0px -40px -10px -40px; + padding: 15px 20px; + color: #c8c9ca; + font-size: 14px; + line-height: 16px; + text-decoration: underline; + background-color: transparent; + border: unset; + border-top: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + } +} \ No newline at end of file diff --git a/src/components/TokenTransfer/TokenTransfer.stories.js b/src/components/TokenTransfer/TokenTransfer.stories.js new file mode 100644 index 00000000..4de57cb9 --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.stories.js @@ -0,0 +1,12 @@ +import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { storiesOf } from '@storybook/react'; +import TokenTransfer from './TokenTransfer'; + +storiesOf('TokenTransfer', module) + .add('Without wallet', () => ( + + )) + .add('With wallet', () => ( + + )); diff --git a/src/components/TokenTransfer/TokenTransfer.test.js b/src/components/TokenTransfer/TokenTransfer.test.js new file mode 100644 index 00000000..62a9d0f9 --- /dev/null +++ b/src/components/TokenTransfer/TokenTransfer.test.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import TokenTransfer from './TokenTransfer'; + +jest.mock('../../utils/Validator'); + +describe('TokenTransfer', () => { + it('should render correct without "wallet" prop', () => { + const wrapper = shallow().dive(); + expect(wrapper.length).toEqual(1); + }); + + it('should render correct with props', () => { + const wrapper = shallow( + , + ).dive(); + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.wallet__wrapper').text()).toEqual('0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54'); + }); +}); diff --git a/src/components/TokenTransfer/index.js b/src/components/TokenTransfer/index.js new file mode 100644 index 00000000..2bd64e07 --- /dev/null +++ b/src/components/TokenTransfer/index.js @@ -0,0 +1,3 @@ +import TokenTransfer from './TokenTransfer'; + +export default { TokenTransfer }; diff --git a/src/components/User/User.scss b/src/components/User/User.scss index 28733112..0797bfe4 100644 --- a/src/components/User/User.scss +++ b/src/components/User/User.scss @@ -1,6 +1,8 @@ @import '../../assets/styles/partials/variables'; +@import '../../assets/styles/includes/mixin'; .user { + position: relative; display: inline-block; font-size: 0; border: 1px solid $primary; @@ -10,22 +12,116 @@ vertical-align: middle; } + &__info { + position: absolute; + top: calc(100% + 1px); + right: -1px; + left: -1px; + display: none; + padding: 8px 10px; + background-color: #fff; + border: 1px solid $primary; + border-top: unset; + cursor: default; + } + + &__copy { + padding-bottom: 7px; + color: rgba($color: $primary, $alpha: 0.3); + font-size: 12px; + line-height: 14px; + text-align: center; + border-bottom: 1px solid #e6e6e6; + } + &__wallet { margin: 0 10px; font-size: 12px; vertical-align: middle; background-color: $white; + &--full { display: none; } } + + &__balances { + padding-top: 12px; + padding-bottom: 20px; + border-bottom: 1px solid #e6e6e6; + + &-top { + padding-bottom: 12px; + color: $primary; + font-size: 12px; + line-height: 100%; + + span { + font-weight: 700; + + + &:last-child { + float: right; + } + } + } + } + + &__balance { + &-item { + display: block; + padding-bottom: 8px; + overflow: hidden; + color: $primary; + font-size: 12px; + line-height: 100%; + letter-spacing: 0.01em; + + @include clearfix; + + span { + &:first-child { + position: relative; + float: left; + + &::after { + position: absolute; + left: calc(100% + 5px); + color: #c8c9ca; + content: '.....................................................................................................................'; + } + } + + &:last-child { + position: relative; + float:right; + padding-left: 5px; + background-color: #fff; + } + } + } + } + + &__button { + padding-top: 16px; + padding-bottom: 9px; + text-align: center; + } + &:hover { - .user__wallet { - &--full { - display: inline-block; + .user { + &__wallet { + &--full { + display: inline-block; + } + + &--half { + display: none; + } } - &--half { - display: none; + + &__info { + display: block; } } } diff --git a/src/components/User/index.js b/src/components/User/index.js index 93457481..0f8a4d54 100644 --- a/src/components/User/index.js +++ b/src/components/User/index.js @@ -1,17 +1,177 @@ import React from 'react'; -import propTypes from 'prop-types'; +import PropTypes from 'prop-types'; +import { withRouter } from 'react-router-dom'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { withTranslation } from 'react-i18next'; +import { inject, observer } from 'mobx-react'; +import uniqKey from 'react-id-generator'; +import AppStore from '../../stores/AppStore'; +import UserStore from '../../stores/UserStore/UserStore'; +import { MembersStore } from '../../stores/MembersStore'; +import ProjectStore from '../../stores/ProjectStore'; +import NotificationStore from '../../stores/NotificationStore'; +import Button from '../Button/Button'; + import styles from './User.scss'; -const User = ({ children }) => ( - - - {children} - {`${children.substr(0, 8)}...${children.substr(35, 41)}`} - -); - -User.propTypes = { - children: propTypes.string.isRequired, -}; +@withRouter +@withTranslation() +@inject( + 'userStore', + 'membersStore', + 'projectStore', + 'appStore', + 'notificationStore', +) +@observer +class User extends React.Component { + timeoutCopy = 2000; + + timerId; + + static propTypes = { + children: PropTypes.string.isRequired, + t: PropTypes.func.isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + appStore: PropTypes.instanceOf(AppStore).isRequired, + notificationStore: PropTypes.instanceOf(NotificationStore).isRequired, + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + }).isRequired, + }; + + constructor() { + super(); + this.state = { + isCopied: false, + }; + } + + componentDidMount() { + const { props } = this; + const { userStore } = props; + userStore.getEthBalance(); + } + + /** + * Method for handle copy event + */ + handleCopy = () => { + if (this.timerId) clearTimeout(this.timerId); + this.setState({ isCopied: true }); + this.timerId = setTimeout(() => { + this.setState({ isCopied: false }); + }, this.timeoutCopy); + } + + handleToggleUser = () => { + const { props } = this; + const { + appStore, + userStore, + projectStore, + projectStore: { + historyStore, + questionStore, + }, + membersStore, + notificationStore, + history, + } = props; + history.push('/'); + appStore.reset(); + userStore.reset(); + historyStore.reset(); + projectStore.reset(); + membersStore.reset(); + notificationStore.reset(); + questionStore.reset(); + } + + render() { + const { isCopied } = this.state; + const { props } = this; + const { + children, + t, + userStore, + projectStore, + membersStore: { + groups, + }, + } = props; + return ( + + + + {children} + + {`${children.substr(0, 8)}...${children.substr(35, 41)}`} + + + { + isCopied + ? t('other:copied') + : t('other:clickOnAddressForCopy') + } + + + + {t('other:groups')} + {t('other:tokens')} + + + + {t('other:privateBalance')} + + + {userStore.userBalance} + + + { + groups + && groups.length + ? ( + groups.map((item) => ( + + + {item.name} + + + {item.fullUserBalance} + + + )) + ) + : null + } + + + + {t('other:toggleUser')} + + + + + ); + } +} export default User; diff --git a/src/components/VoterList/VoterList.js b/src/components/VoterList/VoterList.js new file mode 100644 index 00000000..cc8dbeb4 --- /dev/null +++ b/src/components/VoterList/VoterList.js @@ -0,0 +1,71 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import { + Tab, + Tabs, + TabList, + TabPanel, +} from 'react-tabs'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import VoterListTable from './VoterListTable'; + +import 'react-tabs/style/react-tabs.css'; +import styles from './VoterList.scss'; + +@withTranslation() +@observer +class VoterList extends React.PureComponent { + static propTypes = { + t: PropTypes.func.isRequired, + data: PropTypes.shape({ + positive: PropTypes.arrayOf(PropTypes.shape()).isRequired, + negative: PropTypes.arrayOf(PropTypes.shape()).isRequired, + }).isRequired, + }; + + render() { + const { props } = this; + const { t, data } = props; + return ( + + + + + {t('other:voterList')} + + + + {t('other:agree')} + + + {t('other:against')} + + + + + + + + + + + + + + ); + } +} + +export default VoterList; diff --git a/src/components/VoterList/VoterList.scss b/src/components/VoterList/VoterList.scss new file mode 100644 index 00000000..104edceb --- /dev/null +++ b/src/components/VoterList/VoterList.scss @@ -0,0 +1,143 @@ +@import '../../assets/styles/includes/mixin'; + +.voter-list { + // compensate dialog padding + width: calc(100% + 80px); + margin: -10px -40px; + + &__top { + padding: 12px 0 12px 30px; + background-color: #fff; + border-bottom: 1px solid #e1e4e8; + } + + &__tab-list, + &__title { + display: inline-block; + width: 50%; + vertical-align: middle; + } + + &__tab { + display: inline-block; + padding: 15px; + color: #000; + font-size: 16px; + line-height: 100%; + letter-spacing: 0.01em; + vertical-align: middle; + border: unset; + outline: unset; + cursor: pointer; + + &--selected { + font-weight: 700; + background-image: url('../../assets/images/activeTab.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + } + + &-list { + padding-right: 45px; + padding-left: 15px; + text-align: right; + } + } + + &__title { + font-weight: 700; + font-size: 24px; + line-height: 28px; + text-align: left; + } + + &__content { + height: 350px; + padding-top: 10px; + overflow-y: auto; + background-color: #fff; + + @include scrollbar; + } + + &__table { + width: 100%; + background-color: #fff; + border-collapse: collapse; + + &-th { + padding: 8px; + color: #808080; + font-size: 11px; + line-height: 13px; + text-align: left; + + &--weight { + text-align: center; + } + } + + &-td { + padding: 10px; + + &--is { + min-width: 20px; + padding: 0; + text-align: center; + } + + &--img { + width: 24px; + padding: 0; + } + + &--wallet { + padding: 10px 8px; + color: #000; + font-size: 14px; + line-height: 16px; + } + + &--weight { + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-align: center; + } + } + + &-tr { + background-color: #fff; + cursor: pointer; + transition: background-color 0.5s; + + &:hover { + background: #e1e4e8; + } + } + } + + &__no-data { + width: 100%; + padding: 18px 32px; + background: #fff; + + &-icon, + &-text { + display: inline-block; + vertical-align: middle; + } + + &-icon { + margin-right: 10px; + } + + &-text { + color: #4d4d4d; + font-size: 11px; + line-height: 13px; + white-space: pre-line; + } + } +} diff --git a/src/components/VoterList/VoterList.test.js b/src/components/VoterList/VoterList.test.js new file mode 100644 index 00000000..9a31d900 --- /dev/null +++ b/src/components/VoterList/VoterList.test.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VoterList from './VoterList'; + +describe('VoterList', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow().dive(); + }); + + it('should render without error', () => { + console.log(wrapper.debug()); + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/VoterList/VoterListTable.js b/src/components/VoterList/VoterListTable.js new file mode 100644 index 00000000..aea98766 --- /dev/null +++ b/src/components/VoterList/VoterListTable.js @@ -0,0 +1,125 @@ +/* eslint-disable react/static-property-placement */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { AdminIcon, Pudding } from '../Icons'; + +import styles from './VoterList.scss'; + +@withTranslation() +class VoterListTable extends React.PureComponent { + static propTypes = { + t: PropTypes.func.isRequired, + list: PropTypes.arrayOf( + PropTypes.shape({ + isAdmin: PropTypes.bool, + wallet: PropTypes.string.isRequired, + weight: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + }; + + renderTable = () => { + const { props } = this; + const { t, list } = props; + return ( + + + + {/* eslint-disable-next-line */} + + {/* eslint-disable-next-line */} + + + {t('other:walletAddress')} + + + {t('other:weightVote')} + + + { + list.map((item, index) => ( + + + {item.isAdmin ? : ''} + + + + + + {item.wallet} + + + {`${item.weight}%`} + + + )) + } + + + ); + } + + render() { + const { props } = this; + const { t, list } = props; + return ( + <> + { + list && list.length + ? this.renderTable() + : ( + + + + + + {t('other:noData')} + + + ) + } + > + ); + } +} + +export default VoterListTable; diff --git a/src/components/VoterList/VoterListTable.test.js b/src/components/VoterList/VoterListTable.test.js new file mode 100644 index 00000000..ee7339ec --- /dev/null +++ b/src/components/VoterList/VoterListTable.test.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VoterListTable from './VoterListTable'; + +describe('VoterListTable', () => { + describe('List is empty', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + }); + + describe('List with correct data', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find('.voter-list__table-tr').length).toEqual(2); + expect( + wrapper.find('.voter-list__table-tr').at(0).text(), + ).toEqual('0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc542%'); + }); + }); +}); diff --git a/src/components/VoterList/index.js b/src/components/VoterList/index.js new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Voting/Voting.js b/src/components/Voting/Voting.js new file mode 100644 index 00000000..c35389c7 --- /dev/null +++ b/src/components/Voting/Voting.js @@ -0,0 +1,274 @@ +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import { computed, observable } from 'mobx'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import queryString from 'query-string'; +import VotingTop from './VotingTop'; +import VotingFilter from './VotingFilter'; +import Container from '../Container'; +import Footer from '../Footer'; +import Pagination from '../Pagination'; +import Dialog from '../Dialog/Dialog'; +import StartNewVote from '../StartNewVote'; +import FinPassFormWrapper from '../FinPassFormWrapper/FinPassFormWrapper'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; +import Notification from '../Notification/Notification'; +import ProjectStore from '../../stores/ProjectStore'; +import DialogStore from '../../stores/DialogStore'; +import VotingList from './VotingList'; +import Loader from '../Loader'; + +import styles from './Voting.scss'; + +@withTranslation() +@inject('dialogStore', 'projectStore', 'userStore') +@observer +class Voting extends React.Component { + @observable votingIsActive = false; + + @observable _loading = false; + + passwordForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { props } = this; + const { + // eslint-disable-next-line no-unused-vars + dialogStore, + projectStore: { + historyStore, + rootStore: { Web3Service, contractService, appStore }, + votingData, + votingQuestion, + votingGroupId, + }, + userStore, + } = props; + dialogStore.toggle('progress_modal_voting'); + const { password } = form.values(); + userStore.setPassword(password); + appStore.setTransactionStep('compileOrSign'); + return userStore.readWallet(password) + .then(() => { + // eslint-disable-next-line max-len + const transaction = contractService.createVotingData(Number(votingQuestion), Number(votingGroupId), votingData); + return transaction; + }) + .then((tx) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + })) + .then(() => { + appStore.setTransactionStep('success'); + dialogStore.show('success_modal_voting'); + userStore.getEthBalance(); + historyStore.getActualState(); + }) + .catch((error) => { + dialogStore.show('error_modal_voting'); + console.error(error); + }); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }); + + voteStatus = { + inProgress: 0, + success: 1, + failed: 2, + } + + static propTypes = { + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + userStore: PropTypes.shape().isRequired, + t: PropTypes.func.isRequired, + location: PropTypes.shape({ + search: PropTypes.string.isRequired, + }).isRequired, + }; + + constructor(props) { + super(props); + this.state = { + // eslint-disable-next-line react/no-unused-state + status: this.voteStatus.inProgress, + }; + } + + async componentDidMount() { + const { props } = this; + const { + location, + dialogStore, + projectStore: { + historyStore, + rootStore: { + eventEmitterService, + }, + questionStore: { + newVotingOptions, + }, + }, + } = props; + const parsed = queryString.parse(location.search); + if (parsed.modal && parsed.option) { + dialogStore.show(parsed.modal); + const targetOption = newVotingOptions[Number(parsed.option)]; + eventEmitterService.emit('new_vote:toggle', targetOption); + } + this.votingIsActive = historyStore.isVotingActive; + } + + @computed + get loading() { + const { projectStore: { historyStore } } = this.props; + return historyStore.loading; + } + + closeModal = (name) => { + const { dialogStore } = this.props; + dialogStore.hide(name); + } + + onCloseNewVote = () => { + const { props } = this; + const { + projectStore: { + rootStore: { + eventEmitterService, + }, + }, + } = props; + eventEmitterService.emit('new_vote:closed'); + } + + render() { + const { + props, + voteStatus, + state, + loading, + } = this; + const { status } = state; + const { + t, + dialogStore, + projectStore: { + historyStore: { + pagination, + isVotingActive, + }, + }, + } = props; + return ( + <> + + + {/* FIXME remove comment */} + + { + !loading + ? ( + <> + + { dialogStore.show('start_new_vote'); }} + votingIsActive={isVotingActive} + /> + + { + pagination + ? ( + + ) + : null + } + > + ) + : ( + + + + ) + } + + + + + + + + + + + + { dialogStore.hide(); }} /> + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + + > + ); + } +} + +export default Voting; diff --git a/src/components/Voting/Voting.scss b/src/components/Voting/Voting.scss new file mode 100644 index 00000000..f20122b6 --- /dev/null +++ b/src/components/Voting/Voting.scss @@ -0,0 +1,600 @@ +@import '../../assets/styles/includes/mixin'; +@import '../../assets/styles/partials/variables'; + +.voting { + &-page { + margin-bottom: 40px; + + &__list { + text-align: center; + + &-empty { + position: relative; + height: 40vh; + min-height: 200px; + + &::after { + display: inline-block; + height: 100%; + vertical-align: middle; + content: ''; + } + + p { + display: inline-block; + margin: 0; + padding: 30px 0; + color: rgba(0, 0, 0, 0.7); + font-weight: 300; + font-size: 18px; + line-height: 21px; + text-align: center; + vertical-align: middle; + } + } + + .loader { + margin-top: 50px; + }; + } + + &__loader { + text-align: center; + } + } + + &-info { + margin-bottom: 40px; + + &__back { + display: inline-block; + vertical-align: middle; + } + + &__date { + float: right; + padding: 5px 0; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + vertical-align: middle; + + span { + &:last-child { + padding-left: 24px; + } + } + } + + &__card { + margin-top: 7px; + background-color: #fff; + + &-inner { + padding: 20px 45px 20px; + border: 1px solid #e1e4e8; + } + } + + &__index { + margin-bottom: 16px; + color: rgba(0, 0, 0, 0.2); + font-size: 11px; + line-height: 13px; + } + + &__title { + margin-bottom: 8px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + &__main { + margin-bottom: 32px; + font-size: 0; + } + + &--long-params { + .voting-info__description { + width: 40%; + } + + .voting-info__data { + display: inline-flex; + flex-flow: row wrap; + width: 60%; + & > div { + width: 50%; + &.voting-info__block { + width: 100%; + } + } + } + } + + &__description { + display: inline-block; + width: 50%; + padding-right: 50px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + vertical-align: top; + } + + &__data { + display: inline-block; + width: 50%; + padding-left: 50px; + color: rgba(0, 0, 0, 1); + font-size: 11px; + line-height: 13px; + vertical-align: top; + + &-title { + margin-bottom: 4px; + } + + &-value { + margin-bottom: 16px; + font-weight: 700; + } + } + + &__formula { + margin-bottom: 16px; + color: #808080; + font-size: 11px; + line-height: 13px; + } + + &__buttons { + + button { + float: left; + width: 50%; + padding: 28px 30px 23px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + background-color: #fff; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + + &:first-child { + position: relative; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + // &::after { + // position: absolute; + // top: 0; + // right: 0; + // width: 1px; + // height: 100%; + // background-color: #e1e4e8; + // content: ''; + // } + } + &:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + @include clearfix; + } + + &__button { + &-icon { + margin-bottom: 8px; + text-align: center; + + svg { + width: 32px; + height: 32px; + } + } + + &--close { + display: inline-block; + width: 100%; + padding: 46px; + color: #000; + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + background: transparent; + border: unset; + border: 1px solid #e1e4e8; + outline: none; + cursor: pointer; + transition: .2s linear; + + &:hover { + border: 1px solid $primary + } + + &:active { + color: $white; + background-color: $primary; + } + + &:disabled { + cursor: default; + opacity: 0.6; + } + } + } + + &__stats { + margin-top: 16px; + + svg { + width: auto; + height: auto; + } + + button { + padding: 11px 15px; + + svg path { + fill: #fff; + } + + &:hover { + svg path { + fill: #fff; + } + } + + &:active { + svg path { + fill: #000; + } + } + } + + &-button { + margin-bottom: 16px; + text-align: center; + } + + &-content { + margin-bottom: 40px; + padding: 40px 60px 53px; + background: #fff; + border: 1px solid #e1e4e8; + } + } + + &__progress { + display: inline-block; + margin-left: 5px; + + &-container { + padding-right: 24px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + } + + .progress-bar__indicator { + width: 4px; + height: 4px; + margin-right: 2px; + + &--filled { + background-color: rgba(0, 0, 0, 0.7); + } + } + } + + &__decision { + padding: 49px 20px; + text-align: center; + border: 1px solid #e1e4e8; + + &-text { + display: inline-block; + margin-right: 8px; + color: #000; + font-size: 11px; + line-height: 13px; + vertical-align: middle; + } + + &-icon { + display: inline-block; + color: #000; + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + vertical-align: middle; + + svg { + display: inline-block; + width: auto; + height: auto; + margin-right: 4px; + vertical-align: middle; + } + } + } + + &__result { + display: inline-block; + width: 100%; + padding: 33px 20px 44px; + text-align: center; + border: 1px solid #e1e4e8; + + &-item { + display: inline-block; + margin: 0 32px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: left; + vertical-align: top; + + &-value { + margin-top: 3px; + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 14px; + line-height: 16px; + } + } + + .voting-info__decision-icon { + margin-top: 3px; + } + } + } + + &__filter { + &-dropdown { + display: inline-flex; + flex-flow: row nowrap; + justify-content: space-between; + width: 500px; + margin-right: 29px; + + .dropdown { + width: 220px; + } + } + + &-date { + float: right; + } + } + + &__top { + width: 100%; + margin-top: 36px; + margin-bottom: 16px; + } + + &__item { + min-height: 126px; + margin-bottom: 8px; + padding: 10px 8px 16px 30px; + font-size : 0; + background: #fff; + border: 1px solid #e1e4e8; + transition: border-color 0.3s; + + &-date, + &-progress { + display: inline-block; + margin-top: 8px; + vertical-align: middle; + } + + &-info { + display: inline-block; + width: 54%; + text-align: left; + vertical-align: top; + } + + &-date { + width: 24%; + padding: 10px 0; + text-align: center; + border-right: 1px solid rgba(0, 0, 0, 0.1); + border-left: 1px solid rgba(0, 0, 0, 0.1); + + &-block { + display: inline-block; + padding: 0 39px; + + &:first-child { + margin-bottom: 16px; + } + } + + &-text { + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 15px; + text-align: left; + } + } + + &-progress { + width: 22%; + padding: 0 36px; + } + + &-index { + margin-bottom: 5px; + color: rgba(0, 0, 0, 0.2); + font-size: 11px; + line-height: 13px; + } + + &-title { + margin-bottom: 8px; + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 18px; + line-height: 21px; + } + + &-description { + margin-bottom: 10px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + } + + &:hover { + border: 1px solid #000; + } + + &--new { + position: relative; + border: 1px dashed #000; + + &::before { + left: -20px; + } + + &::after { + right: -20px; + transform: rotate(180deg); + } + + &::after, + &::before { + position: absolute; + top: 12%; + width: 12px; + height: 76%; + background-image: url('../../assets/images/activeVoting.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + content: ''; + } + } + } + + &__decision { + &--progress { + .voting__decision { + &-title { + margin-bottom: 8px; + color: #000; + font-size: 14px; + line-height: 16px; + } + } + } + + &-progress-bar { + text-align: center; + } + + &-state { + color: rgba(0, 0, 0, 1); + font-weight: 700; + font-size: 14px; + line-height: 16px; + text-align: center; + text-transform: uppercase; + } + + &-icon { + margin-top: 16px; + text-align: center; + + svg { + width: 32px; + height: 32px; + } + } + + &-title { + margin-bottom: 4px; + color: rgba(0, 0, 0, 0.7); + font-weight: 700; + font-size: 11px; + line-height: 13px; + text-align: center; + } + + &-text { + margin-top: 16px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + + &--remained { + text-align: left; + } + } + } + + &__back { + display: inline-block; + padding: 5px 35px; + color: #000; + font-size: 14px; + line-height: 107.5%; + background: #fff; + border: 1px solid #000; + border-radius: 2px; + transition: .2s linear; + &:hover { + color: #fff; + background: #000; + } + &:active { + color: #000; + background: #fff; + } + } + + &__stats { + text-align: center; + + &-col { + display: inline-block; + width: 33.3333%; + vertical-align: top; + + .recharts-wrapper { + margin: 0 auto; + } + + &-title { + margin-top: 17px; + color: #000; + font-weight: 700; + font-size: 18px; + line-height: 21px; + text-align: center; + } + + &-subtitle { + margin-top: 9px; + color: rgba(0, 0, 0, 0.7); + font-size: 11px; + line-height: 13px; + text-align: center; + } + } + + @include clearfix; + } +} \ No newline at end of file diff --git a/src/components/Voting/Voting.test.js b/src/components/Voting/Voting.test.js new file mode 100644 index 00000000..c4c18f18 --- /dev/null +++ b/src/components/Voting/Voting.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Voting from '.'; + +describe('Voting', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingDecision.js b/src/components/Voting/VotingDecision.js new file mode 100644 index 00000000..32d6e780 --- /dev/null +++ b/src/components/Voting/VotingDecision.js @@ -0,0 +1,106 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { VerifyIcon, RejectIcon, NoQuorum } from '../Icons'; + +import styles from './Voting.scss'; + +const Decision = ({ + prosState, + t, +}) => { + switch (prosState) { + case true: + return ( + + + {t('other:pros')} + + + + + + ); + case false: + return ( + + + {t('other:cons')} + + + + + + ); + case null: + return ( + + + {t('other:notAccepted')} + + + + + + ); + default: + return null; + } +}; + +Decision.propTypes = { + prosState: PropTypes.oneOfType([ + PropTypes.bool, + () => null, + ]), + t: PropTypes.func.isRequired, +}; + +Decision.defaultProps = { + prosState: null, +}; + +const DecisionTranslated = withTranslation()(Decision); + +/** + * Voting decision for pros & cons state + * + * @param {object} param0 data for component + * @param {boolean} param0.prosState decision is pros state + * @returns {Node} ready component + */ +const VotingDecision = ({ + prosState, + t, +}) => ( + + { + prosState === null + ? ( + + {t('other:decision')} + + ) + : ( + + {t('other:decisionIsMade')} + + ) + } + + +); + +VotingDecision.propTypes = { + prosState: PropTypes.oneOfType([ + PropTypes.bool, + () => null, + ]), + t: PropTypes.func.isRequired, +}; + +VotingDecision.defaultProps = { + prosState: null, +}; + +export default withTranslation()(VotingDecision); diff --git a/src/components/Voting/VotingDecision.test.js b/src/components/Voting/VotingDecision.test.js new file mode 100644 index 00000000..a2c4b5f7 --- /dev/null +++ b/src/components/Voting/VotingDecision.test.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingDecision from './VotingDecision'; +import { VerifyIcon, RejectIcon } from '../Icons'; + +describe('VotingDecision', () => { + it('should render without error with true prosState', () => { + const wrapper = shallow().dive(); + expect(wrapper.find('.voting__decision-state').text()).toEqual('PROS'); + expect(wrapper.find(VerifyIcon).length).toEqual(1); + expect(wrapper.length).toEqual(1); + }); + + it('should render without error with true prosState', () => { + const wrapper = shallow().dive(); + expect(wrapper.find('.voting__decision-state').text()).toEqual('CONS'); + expect(wrapper.find(RejectIcon).length).toEqual(1); + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingDecisionProgress.js b/src/components/Voting/VotingDecisionProgress.js new file mode 100644 index 00000000..ba0b1ffd --- /dev/null +++ b/src/components/Voting/VotingDecisionProgress.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import ProgressBar from '../ProgressBar/ProgressBar'; + +import styles from './Voting.scss'; + +/** + * Component render voting decision + * in progress state + * + * @param {object} param0 data for component + * @param {number} param0.progress progress in percent + * @param {string} param0.remained time remained + * @returns {Node} ready component + */ +const VotingDecisionProgress = ({ + progress, + remained, + t, +}) => ( + + + Voting + + + { + progress >= 100 + ? ( + + {t('other:endOfVoteRequired')} + + ) + : ( + + {t('other:timeLeft')} + + {`~${remained}`} + + ) + } + +); + +VotingDecisionProgress.propTypes = { + progress: PropTypes.number.isRequired, + remained: PropTypes.string.isRequired, + t: PropTypes.func.isRequired, +}; + +export default withTranslation()(VotingDecisionProgress); diff --git a/src/components/Voting/VotingDecisionProgress.test.js b/src/components/Voting/VotingDecisionProgress.test.js new file mode 100644 index 00000000..f40c6964 --- /dev/null +++ b/src/components/Voting/VotingDecisionProgress.test.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingDecisionProgress from './VotingDecisionProgress'; + +describe('VotingDecisionProgress', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingFilter.js b/src/components/Voting/VotingFilter.js new file mode 100644 index 00000000..34092e56 --- /dev/null +++ b/src/components/Voting/VotingFilter.js @@ -0,0 +1,155 @@ +import React from 'react'; +import { observer, inject } from 'mobx-react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import nextId from 'react-id-generator'; +import { withTranslation } from 'react-i18next'; +import { computed } from 'mobx'; +import SimpleDropdown from '../SimpleDropdown'; +import { QuestionIcon, DescisionIcon } from '../Icons'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import DatePicker from '../DatePicker'; + +import styles from './Voting.scss'; + +@withTranslation() +@inject('projectStore') +@observer +class VotingFilter extends React.PureComponent { + static propTypes = { + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + t: PropTypes.func.isRequired, + }; + + getStatusOptions() { + const { t } = this.props; + return [{ + label: 'All', + value: '*', + }, { + label: t('other:notAccepted'), + value: '0', + }, { + label: t('other:pros'), + value: '1', + }, { + label: t('other:cons'), + value: '2', + }]; + } + + @computed + get dateInit() { + const { + projectStore: { + historyStore: { filter: { rules } }, + }, + } = this.props; + if (!rules.date) { + return { + startDate: null, + endDate: null, + }; + } + return { + startDate: moment(rules.date.start * 1000), + endDate: moment(rules.date.end * 1000), + }; + } + + @computed + get indexForDescision() { + const { + projectStore: { + historyStore: { filter: { rules } }, + }, + } = this.props; + const options = this.getStatusOptions(); + const [element] = options.filter((item) => item.value === rules.descision); + return options.indexOf(element); + } + + /** + * Method for handle sort + * + * @param {object} selected new sort data + * @param {string|number} selected.value new sort value + * @param {string|number} selected.label new sort label + */ + handleQuestionSelect = (selected) => { + const { projectStore: { historyStore: { addFilterRule } } } = this.props; + addFilterRule({ questionId: selected.value.toString() }); + } + + handleStatusSelect = (selected) => { + const { projectStore: { historyStore: { addFilterRule } } } = this.props; + addFilterRule({ descision: selected.value.toString() }); + } + + /** + * Method for handle date change + */ + handleDateSelect = ({ + startDate, + endDate, + }) => { + const { projectStore: { historyStore: { addFilterRule } } } = this.props; + addFilterRule({ + date: { + // Convert to vote time format + start: startDate.valueOf() / 1000, + end: endDate.valueOf() / 1000, + }, + }); + } + + /** + * Method for handle clear date + */ + handleDateClear = () => { + const { projectStore: { historyStore } } = this.props; + historyStore.removeFilterRule('date'); + } + + render() { + const { + projectStore: { + questionStore: { options }, + historyStore: { filter: { rules } }, + }, + } = this.props; + + return ( + <> + + {/* Is not work correctly without key */} + + + + + + + + + + + > + ); + } +} + +export default VotingFilter; diff --git a/src/components/Voting/VotingInfo.js b/src/components/Voting/VotingInfo.js new file mode 100644 index 00000000..974f1b80 --- /dev/null +++ b/src/components/Voting/VotingInfo.js @@ -0,0 +1,363 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import uniqKey from 'react-id-generator'; +import { withTranslation } from 'react-i18next'; +import { Collapse } from 'react-collapse'; +import { observer } from 'mobx-react'; +import { computed, action, observable } from 'mobx'; +import { + statusStates, + votingStates, + userVotingStates, +} from '../../constants'; +import { + Stats, +} from '../Icons'; +import { getDateString } from './utils'; +import Button from '../Button/Button'; +import VotingStats from './VotingStats'; +import ProgressBar from '../ProgressBar/ProgressBar'; +import { progressByDateRange } from '../../utils/Date'; +import VotingInfoButtons from './VotingInfoButtons'; +import VotingInfoResult from './VotingInfoResult'; +import VotingInfoUserDecision from './VotingInfoUserDecision'; + +import styles from './Voting.scss'; + +@withTranslation() +@observer +class VotingInfo extends React.PureComponent { + @observable progress; + + intervalProgress = 5000; + + static propTypes = { + t: PropTypes.func.isRequired, + /** Index voting in list */ + index: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + /** Description voting */ + description: PropTypes.string.isRequired, + /** Title voting */ + title: PropTypes.string.isRequired, + /** Formula */ + formula: PropTypes.string.isRequired, + /** All needed date for voting */ + date: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + }).isRequired, + voting: PropTypes.shape({ + id: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + status: PropTypes.string.isRequired, + descision: PropTypes.string.isRequired, + userVote: PropTypes.number.isRequired, + closeVoteInProgress: PropTypes.bool, + }).isRequired, + params: PropTypes.arrayOf(PropTypes.array).isRequired, + dataStats: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + pros: PropTypes.number.isRequired, + cons: PropTypes.number.isRequired, + abstained: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + onVerifyClick: PropTypes.func.isRequired, + onRejectClick: PropTypes.func.isRequired, + onCompleteVoteClick: PropTypes.func.isRequired, + onBarClick: PropTypes.func.isRequired, + isUserReturnTokensActual: PropTypes.bool.isRequired, + }; + + constructor() { + super(); + this.state = { + isOpen: false, + }; + } + + componentDidMount() { + const { props } = this; + const { date } = props; + const initProgress = progressByDateRange(date); + this.setProgress(initProgress); + if (initProgress !== 100) { + const intervalId = setInterval(() => { + this.setProgress(progressByDateRange(date)); + if (this.progress === 100) { + clearInterval(intervalId); + } + }, this.intervalProgress); + } + } + + @action + setProgress = (progress) => { + this.progress = progress; + } + + /** + * Method for render dynamic content + * based on voting status + * + * @returns {Node} user decision Node element + */ + @computed + get renderDynamicContent() { + const { props, progress } = this; + const { + voting, + voting: { + userVote, + status, + descision, + closeVoteInProgress, + }, + isUserReturnTokensActual, + date, + onVerifyClick, + onRejectClick, + onCompleteVoteClick, + t, + } = props; + // TODO refactor this switch + switch (true) { + case ( + status === statusStates.active + && descision === votingStates.default + && userVote === userVotingStates.notAccepted + && progress < 100 + ): + return ( + + ); + case ( + status === statusStates.active + && descision === votingStates.default + && userVote !== userVotingStates.notAccepted + && progress < 100 + ): + return ( + + ); + case ( + status === statusStates.active + && descision === votingStates.default + && userVote === userVotingStates.notAccepted + && progress >= 100 + ): + return ( + + {t('buttons:completeTheVote')} + + ); + case ( + status === statusStates.active + && descision === votingStates.default + && userVote !== userVotingStates.notAccepted + && progress >= 100 + ): + return ( + + {t('buttons:completeTheVote')} + + ); + case ( + status === statusStates.closed + ): + return ( + + ); + default: + return null; + } + } + + /** + * Method for change isOpen state + */ + toggleOpen = () => { + this.setState((prevState) => ({ + isOpen: !prevState.isOpen, + })); + } + + render() { + const { isOpen } = this.state; + const { props, progress } = this; + const { + t, + date, + description, + index, + title, + formula, + params, + voting, + onBarClick, + dataStats, + } = props; + return ( + + + + {t('buttons:back')} + + + + { + voting.status === statusStates.active + && voting.descision === votingStates.default + && progress < 100 + ? ( + + {t('other:votingInProgress')} + : + + + ) + : ( + + {t('other:votingDone')} + + ) + } + + {t('other:start')} + : + {getDateString(date.start)} + + + {t('other:end')} + : + {getDateString(date.end)} + + + + + + # + {index} + + + {title} + + 3 ? styles['voting-info--long-params'] : ''}`} + > + + {description} + + + {params.map((item) => ( + 3 + && (new RegExp(/(0x)+([0-9 a-f A-F]){40}/g)).test(item[1])) + ? styles['voting-info__block'] + : '' + } + > + + {item[0]} + + + {item[1]} + + + ))} + + + + {`${t('other:votingFormula')}: ${formula}`} + + + {this.renderDynamicContent} + + + + )} + onClick={this.toggleOpen} + > + {t('other:statistics')} + + + + + + + + + + ); + } +} + +export default VotingInfo; diff --git a/src/components/Voting/VotingInfo.test.js b/src/components/Voting/VotingInfo.test.js new file mode 100644 index 00000000..9c1e948d --- /dev/null +++ b/src/components/Voting/VotingInfo.test.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingInfo from './VotingInfo'; +import { EMPTY_DATA_STRING } from '../../constants'; + +describe('VotingInfo', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow( + { console.log('onVerifyClick'); }} + /* eslint-disable-next-line */ + onRejectClick={() => { console.log('onRejectClick'); }} + />, + ).dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('toggleOpen should change isOpen state', () => { + expect(wrapper.state('isOpen')).toEqual(false); + wrapperInstance.toggleOpen(); + expect(wrapper.state('isOpen')).toEqual(true); + wrapperInstance.toggleOpen(); + expect(wrapper.state('isOpen')).toEqual(false); + }); + + it('getDateString should ', () => { + const dateString = wrapperInstance.getDateString(); + expect(dateString).toEqual(EMPTY_DATA_STRING); + }); +}); diff --git a/src/components/Voting/VotingInfoButtons.js b/src/components/Voting/VotingInfoButtons.js new file mode 100644 index 00000000..3f6e3629 --- /dev/null +++ b/src/components/Voting/VotingInfoButtons.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { VerifyIcon, RejectIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './Voting.scss'; + +/** + * Component for render decision buttons + * + * @returns {Node} element with decision buttons + */ +const VotingInfoButtons = ({ + onVerifyClick, + onRejectClick, + t, + disabled, +}) => ( + + )} + iconPosition="top" + onClick={onVerifyClick} + disabled={disabled} + hint={ + disabled + ? ( + + Return tokens first + + ) + : null + } + > + {t('other:iAgree')} + + )} + iconPosition="top" + onClick={onRejectClick} + disabled={disabled} + hint={ + disabled + ? ( + + Return tokens first + + ) + : null + } + > + {t('other:iAmAgainst')} + + +); + +VotingInfoButtons.propTypes = { + onVerifyClick: PropTypes.func.isRequired, + onRejectClick: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, + disabled: PropTypes.bool.isRequired, +}; + +export default withTranslation()(VotingInfoButtons); diff --git a/src/components/Voting/VotingInfoResult.js b/src/components/Voting/VotingInfoResult.js new file mode 100644 index 00000000..e79420ea --- /dev/null +++ b/src/components/Voting/VotingInfoResult.js @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import moment from 'moment'; +import { EMPTY_DATA_STRING } from '../../constants'; +import renderDecisionIcon from './utils'; +import { getTimeLeftString } from '../../utils/Date'; + +import styles from './Voting.scss'; + +const VotingInfoResult = ({ + voting: { userVote, descision }, + date, + t, +}) => { + const startDate = moment(date.start * 1000); + const endDate = moment(date.end * 1000); + return ( + + + {t('other:decisionWasMade')} + + {renderDecisionIcon({ state: Number(descision) })} + + + + {t('other:yourDecision')} + + {renderDecisionIcon({ state: Number(userVote) })} + + + + {t('other:totalVoted')} + + {/* TODO add total from stats */} + {EMPTY_DATA_STRING} + + + + {t('other:theVoteLasted')} + + { + getTimeLeftString({ + startDate, + endDate, + }) + } + + + + ); +}; + +VotingInfoResult.propTypes = { + t: PropTypes.func.isRequired, + date: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + }).isRequired, + voting: PropTypes.shape({ + descision: PropTypes.string.isRequired, + userVote: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + }).isRequired, +}; + +export default withTranslation()(VotingInfoResult); diff --git a/src/components/Voting/VotingInfoUserDecision.js b/src/components/Voting/VotingInfoUserDecision.js new file mode 100644 index 00000000..b46d8cc1 --- /dev/null +++ b/src/components/Voting/VotingInfoUserDecision.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from 'react-i18next'; +import { userVotingStates } from '../../constants'; +import renderDecisionIcon from './utils'; + +import styles from './Voting.scss'; + +/** + * Component for render user decision + * + * @returns {Node} user decision Node element + */ +const VotingInfoUserDecision = ({ + voting: { userVote }, + t, +}) => { + switch (userVote) { + case userVotingStates.decisionFor: + return ( + + + {t('other:youVoted')} + + {renderDecisionIcon({ state: userVotingStates.decisionFor })} + + ); + case userVotingStates.decisionAgainst: + return ( + + + {t('other:youVoted')} + + {renderDecisionIcon({ state: userVotingStates.decisionAgainst })} + + ); + default: + return null; + } +}; + +VotingInfoUserDecision.propTypes = { + t: PropTypes.func.isRequired, + voting: PropTypes.shape({ + userVote: PropTypes.number.isRequired, + }).isRequired, +}; + +export default withTranslation()(VotingInfoUserDecision); diff --git a/src/components/Voting/VotingInfoWrapper.js b/src/components/Voting/VotingInfoWrapper.js new file mode 100644 index 00000000..cc7d0bb1 --- /dev/null +++ b/src/components/Voting/VotingInfoWrapper.js @@ -0,0 +1,537 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { inject, observer } from 'mobx-react'; +import { action, observable } from 'mobx'; +import { withTranslation } from 'react-i18next'; +import VotingInfo from './VotingInfo'; +import Container from '../Container'; +import Dialog from '../Dialog/Dialog'; +import DecisionAgree from '../Decision/DecisionAgree'; +import DecisionClose from '../Decision/DecisionClose'; +import DecisionReject from '../Decision/DecisionReject'; +import VoterList from '../VoterList/VoterList'; +import Footer from '../Footer'; +import FinPassForm from '../../stores/FormsStore/FinPassForm'; +import { AgreedMessage, RejectMessage, ERC20TokensUsed } from '../Message'; +import TransactionProgress from '../Message/TransactionProgress'; +import SuccessMessage from '../Message/SuccessMessage'; +import ErrorMessage from '../Message/ErrorMessage'; +import ProjectStore from '../../stores/ProjectStore/ProjectStore'; +import { + systemQuestionsId, + statusStates, + userVotingStates, + tokenTypes, + votingDecisionStates, +} from '../../constants'; +import MembersStore from '../../stores/MembersStore/MembersStore'; +import UserStore from '../../stores/UserStore/UserStore'; +import DialogStore from '../../stores/DialogStore'; +import AsyncInterval from '../../utils/AsyncUtils'; + +@withTranslation() +@inject( + 'dialogStore', + 'projectStore', + 'userStore', + 'membersStore', + 'appStore', +) +@observer +class VotingInfoWrapper extends React.PureComponent { + question; + + @observable dataStats = []; + + @observable dataVotes = { + 0: { + positive: [], + negative: [], + }, + }; + + @observable selectedGroup = 0 + + votingId = 0; + + votingForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { votingId } = this; + const { decision } = this.state; + const { + userStore, + dialogStore, + userStore: { + rootStore: { + contractService, + }, + }, + projectStore: { + historyStore, + }, + } = this.props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.toggle('progress_modal_voting_info_wrapper'); + return contractService.sendVote(votingId, decision) + .then(async () => { + await historyStore.fetchAndUpdateLastVoting(); + const [voting] = historyStore.getVotingById(Number(votingId)); + this.getVotingStats(); + this.getVotes(); + if (String(voting.status) === statusStates.closed) { + dialogStore.toggle('success_modal_voting_info_wrapper'); + this.updateAfterCompleteVoting(voting); + return; + } + switch (Number(voting.userVote)) { + case (userVotingStates.decisionFor): + dialogStore.toggle('decision_agree_voting_info_wrapper_message'); + break; + case (userVotingStates.decisionAgainst): + dialogStore.toggle('decision_reject_voting_info_wrapper_message'); + break; + default: + dialogStore.toggle('success_modal_voting_info_wrapper'); + break; + } + }) + .catch((error) => { + console.log(error); + dialogStore.toggle('error_modal_voting_info_wrapper'); + }); + }, + onError: (form) => { + console.log(form.error); + }, + }, + }) + + closingForm = new FinPassForm({ + hooks: { + onSuccess: (form) => { + const { props } = this; + const { + match: { params: { id } }, + userStore, + projectStore: { + historyStore, + rootStore: { + contractService, + }, + }, + dialogStore, + } = props; + const { password } = form.values(); + userStore.setPassword(password); + dialogStore.toggle('progress_modal_voting_info_wrapper'); + const [voting] = historyStore.getVotingById(Number(id)); + voting.update({ + closeVoteInProgress: true, + }); + return contractService.closeVoting() + .then(() => { + historyStore.fetchAndUpdateLastVoting(); + this.updateAfterCompleteVoting(voting); + this.getVotingStats(); + this.getVotes(); + dialogStore.toggle('success_modal_voting_info_wrapper'); + }) + .catch((e) => { + console.error(e); + dialogStore.toggle('error_modal_voting_info_wrapper'); + }) + .finally(() => { + voting.update({ + closeVoteInProgress: false, + }); + }); + }, + onError: () => {}, + }, + }) + + static propTypes = { + dialogStore: PropTypes.instanceOf(DialogStore).isRequired, + membersStore: PropTypes.instanceOf(MembersStore).isRequired, + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + userStore: PropTypes.instanceOf(UserStore).isRequired, + t: PropTypes.func.isRequired, + }; + + constructor() { + super(); + this.state = { + decision: votingDecisionStates.default, + }; + } + + componentDidMount() { + const { props } = this; + const { + match: { params: { id } }, + projectStore: { + historyStore, + questionStore, + rootStore: { + configStore: { UPDATE_INTERVAL }, + }, + }, + } = props; + const [voting] = historyStore.getVotingById(Number(id)); + const [question] = questionStore.getQuestionById(Number(voting.questionId)); + this.question = question; + this.getVotingStats(); + this.getVotes(); + this.interval = new AsyncInterval({ + timeoutInterval: UPDATE_INTERVAL, + cb: this.updateData, + }); + } + + componentWillUnmount() { + this.interval.cancel(); + this.interval = null; + } + + /** + * Method for checking whether the type +of voting is ERC20 + * + * @returns {boolean} is ERC20 or not + * @param address + */ + isERC20Type = (address) => { + const { props } = this; + const { membersStore } = props; + const targetGroup = membersStore.getMemberGroupByAddress(address); + if (!targetGroup || !targetGroup.groupType) return false; + return targetGroup.groupType === tokenTypes.ERC20; + } + + /** + * Method for update question list + * if close voting have questionId=1 + * + * @param {object} voting voting object + * @param {string} voting.questionId voting question id + */ + updateAfterCompleteVoting = (voting) => { + const { props } = this; + const { + projectStore: { + questionStore, + historyStore, + }, + membersStore, + } = props; + const { + addingNewQuestion, + connectGroupUsers, + connectGroupQuestions, + assignGroupAdmin, + } = systemQuestionsId; + historyStore.getActualState(); + switch (Number(voting.questionId)) { + case addingNewQuestion: + questionStore.getActualQuestions(); + break; + case connectGroupUsers: + membersStore.fetchUserGroups(); + break; + case connectGroupQuestions: + questionStore.fetchActualQuestionGroups(); + break; + case assignGroupAdmin: + membersStore.getAddressesForAdminDesignate(voting.data) + .then((result) => { + membersStore.updateAdmin(result['1']); + }); + break; + default: + break; + } + } + + onVerifyClick = () => { + const { dialogStore } = this.props; + this.setState({ + decision: votingDecisionStates.agree, + }); + dialogStore.toggle('decision_agree_voting_info_wrapper'); + } + + onRejectClick = () => { + const { dialogStore } = this.props; + this.setState({ + decision: votingDecisionStates.reject, + }); + dialogStore.toggle('decision_reject_voting_info_wrapper'); + } + + onClosingClick = () => { + const { dialogStore } = this.props; + dialogStore.toggle('descision_close_voting_info_wrapper'); + } + + updateData = () => { + this.getVotes(); + this.getVotingStats(); + } + + @action getVotes = async () => { + const { props } = this; + const { + match: { params: { id } }, + projectStore: { + historyStore, + }, + } = props; + const votes = await historyStore.getVoterList(Number(id)); + this.dataVotes = { ...this.dataVotes, ...votes }; + } + + @action + getVotingStats = async () => { + const { props } = this; + const { + match: { params: { id } }, + membersStore, + projectStore: { + historyStore, + rootStore: { + contractService: { + _contract: { + methods, + }, + }, + }, + }, + } = props; + const [voting] = historyStore.getVotingById(Number(id)); + + const { allowedGroups } = voting; + if (allowedGroups.length === 0) { + return; + } + + allowedGroups.forEach(async (group) => { + this.dataStats = []; + const memberGroup = membersStore.getMemberGroupByAddress(group); + let { + positive, + negative, + totalSupply, + } = await methods.getGroupVotes(id, memberGroup.wallet).call(); + positive = parseInt(positive, 10); + negative = parseInt(negative, 10); + totalSupply = parseInt(totalSupply, 10); + const decimalPercent = totalSupply / 100; + const abstained = (totalSupply - (positive + negative)) / decimalPercent; + const duplicateStat = this.dataStats.find((item) => item.name === memberGroup.name); + if (!duplicateStat) { + this.dataStats.push({ + name: memberGroup.name, + address: group, + pros: positive / decimalPercent, + cons: negative / decimalPercent, + abstained, + }); + } + }); + } + + /** + * Method for opening previous dialog + */ + openPreviousDialog = () => { + const { props } = this; + const { dialogStore } = props; + dialogStore.back(3); + } + + // eslint-disable-next-line class-methods-use-this + prepareParameters(voting, question) { + const { projectStore: { rootStore: { Web3Service } } } = this.props; + const { data } = voting; + const { paramTypes, paramNames, id } = question; + // const votingData = `0x${data.slice(10)}`; + let decodedRawParams; + let decodedParams; + if (id !== 0) { + decodedRawParams = data !== '0x' + ? Web3Service.web3.eth.abi.decodeParameters(['tuple(uint,uint,uint,uint,uint)', `tuple(${paramTypes.join(',')})`], data) + : []; + delete decodedRawParams[0]; + decodedParams = paramNames.map((param, index) => [param, decodedRawParams[1][index]]); + } else { + const parameters = [ + 'tuple(uint,uint,uint,uint,uint)', + 'tuple(bool, string, string, uint, uint, string[], string[], address, bytes4, string, bytes)', + ]; + decodedRawParams = Web3Service.web3.eth.abi.decodeParameters(parameters, data); + delete decodedRawParams[0]; + // eslint-disable-next-line no-unused-vars + const [active, name, text, + groupId, time, parametersNames, + parametersTypes, target, method, formula, + ] = decodedRawParams[1]; + const decoded = [ + groupId, name, text, + time, method, formula, + parametersNames.join(','), parametersTypes.join(','), target, + ]; + decodedParams = paramNames.map((param, index) => [param, decoded[index]]); + } + return decodedParams; + } + + render() { + const { props, dataStats } = this; + const { + dialogStore, + projectStore: { historyStore, questionStore, rootStore: { contractService } }, + match: { params: { id } }, + t, + } = props; + this.votingId = Number(id); + const { isUserReturnTokensActual } = historyStore; + const [voting] = historyStore.getVotingById(Number(id)); + const [question] = questionStore.getQuestionById(voting.questionId); + const params = this.prepareParameters(voting, question); + return ( + <> + + { this.onVerifyClick(); }} + onRejectClick={() => { this.onRejectClick(); }} + onCompleteVoteClick={() => { this.onClosingClick(); }} + onBarClick={ + (group) => { + this.selectedGroup = group; + if (this.isERC20Type(group) === true) { + dialogStore.show('is_erc20_modal_voting_info_wrapper'); + return; + } + dialogStore.show('voter_list_voting_info_wrapper'); + } + } + /> + + + + + + + + + + + + + + + + + + + dialogStore.hide()} /> + + + + dialogStore.hide()} /> + + + + + + + + { dialogStore.hide(); }} /> + + + + { dialogStore.back(3); }} + buttonText={t('buttons:retry')} + /> + + + { dialogStore.hide(); }} /> + + + + > + ); + } +} + +export default VotingInfoWrapper; diff --git a/src/components/Voting/VotingInfoWrapper.test.js b/src/components/Voting/VotingInfoWrapper.test.js new file mode 100644 index 00000000..0099d552 --- /dev/null +++ b/src/components/Voting/VotingInfoWrapper.test.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingInfoWrapper from './VotingInfoWrapper'; + +describe('VotingInfoWrapper', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow().dive().dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); +}); diff --git a/src/components/Voting/VotingItem.js b/src/components/Voting/VotingItem.js new file mode 100644 index 00000000..4d142e56 --- /dev/null +++ b/src/components/Voting/VotingItem.js @@ -0,0 +1,217 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; +import { Link } from 'react-router-dom'; +import { computed, observable, action } from 'mobx'; +import { withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; +import VotingDecisionProgress from './VotingDecisionProgress'; +import { statusStates, votingStates } from '../../constants'; +import VotingDecision from './VotingDecision'; +import { progressByDateRange, getTimeLeftString } from '../../utils/Date'; +import { getDateString } from './utils'; + +import styles from './Voting.scss'; + +@withTranslation() +@observer +class VotingItem extends React.PureComponent { + @observable progress = 0; + + intervalId = null; + + intervalProgress = 5000; + + static propTypes = { + index: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + t: PropTypes.func.isRequired, + actualStatus: PropTypes.string.isRequired, + actualDecisionStatus: PropTypes.string.isRequired, + newForUser: PropTypes.bool.isRequired, + date: PropTypes.shape({ + start: PropTypes.number.isRequired, + end: PropTypes.number.isRequired, + }).isRequired, + onMouseEnter: PropTypes.func, + }; + + static defaultProps = { + onMouseEnter: () => {}, + } + + componentDidMount() { + const { props, ref } = this; + const { date, onMouseEnter } = props; + const initProgress = progressByDateRange(date); + this.setProgress(initProgress); + if (initProgress !== 100) { + this.intervalId = setInterval(() => { + this.setProgress(progressByDateRange(date)); + if (this.progress === 100) { + clearInterval(this.intervalId); + } + }, this.intervalProgress); + } + ref.addEventListener('mouseenter', () => { + onMouseEnter(); + }); + } + + @action + setProgress = (progress) => { + this.progress = progress; + } + + /** + * Method for render decision state + * + * @returns {Node} element for actual + * state + */ + @computed + get renderDecisionState() { + const { props, progress } = this; + const { date } = props; + switch (true) { + case (progress < 100): + return ( + + ); + case (progress >= 100): + return this.getVotingDecision(); + default: + return null; + } + } + + getVotingDecision = () => { + const { props } = this; + const { actualStatus, actualDecisionStatus } = props; + switch (true) { + case ( + actualStatus === statusStates.active + && actualDecisionStatus === votingStates.default + ): + return ( + + ); + case ( + actualStatus === statusStates.closed + && actualDecisionStatus === votingStates.decisionFor + ): + return (); + case ( + actualStatus === statusStates.closed + && actualDecisionStatus === votingStates.decisionAgainst + ): + return (); + case ( + actualStatus === statusStates.closed + && actualDecisionStatus === votingStates.default + ): + return (); + default: + return null; + } + } + + render() { + const { props } = this; + const { + index, + title, + description, + t, + date, + newForUser, + actualStatus, + } = props; + return ( + + { + this.ref = element; + } + } + > + + + {`#${index}`} + + + {title} + + + {description} + + + + + + {t('other:start')} + + {getDateString(date.start)} + + + + + {t('other:end')} + + {getDateString(date.end)} + + + + + {this.renderDecisionState} + + + + ); + } +} + +export default VotingItem; diff --git a/src/components/Voting/VotingItem.test.js b/src/components/Voting/VotingItem.test.js new file mode 100644 index 00000000..e89459ed --- /dev/null +++ b/src/components/Voting/VotingItem.test.js @@ -0,0 +1,79 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingItem from './VotingItem'; +import VotingDecision from './VotingDecision'; +import VotingDecisionProgress from './VotingDecisionProgress'; + +describe('VotingItem', () => { + describe('actualStatus is cons', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(VotingDecision).props()).toEqual({ prosState: false }); + }); + }); + + describe('actualStatus is pros', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(VotingDecision).props()).toEqual({ prosState: true }); + }); + }); + + describe('actualStatus in progress', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow( + , + ).dive(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + expect(wrapper.find(VotingDecisionProgress).length).toEqual(1); + }); + }); +}); diff --git a/src/components/Voting/VotingList.js b/src/components/Voting/VotingList.js new file mode 100644 index 00000000..540b1647 --- /dev/null +++ b/src/components/Voting/VotingList.js @@ -0,0 +1,120 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { computed } from 'mobx'; +import { inject, observer } from 'mobx-react'; +import uniqKey from 'react-id-generator'; +import { Trans } from 'react-i18next'; +import VotingItem from './VotingItem'; +import ProjectStore from '../../stores/ProjectStore'; + +import styles from './Voting.scss'; +import { statusStates } from '../../constants'; + +/** + * Component for render list of voting + */ +@inject('projectStore') +@observer +class VotingList extends React.Component { + static propTypes = { + projectStore: PropTypes.instanceOf(ProjectStore).isRequired, + }; + + @computed + get paginatedVotings() { + const { props } = this; + const { + projectStore: { + historyStore, + }, + } = props; + return historyStore.paginatedList; + } + + handleVotingMouseEnter = (item) => { + const { props } = this; + const { + projectStore: { + historyStore, + }, + } = props; + if ( + item.status === statusStates.active + && item.newForUser === true + ) { + const [voting] = historyStore.getVotingById(item.id); + if (voting) { + voting.update({ + newForUser: false, + }); + historyStore.writeVotingsToFile(); + } + } + } + + render() { + const { + paginatedVotings, + props, + } = this; + const { + projectStore: { + historyStore: { + votings, + }, + }, + } = props; + if (!votings || !votings.length) { + return ( + + + + + No polls created + + They will be displayed here later + + + + + ); + } + return ( + + { + paginatedVotings && paginatedVotings.length + ? paginatedVotings.map((item) => ( + this.handleVotingMouseEnter(item)} + /> + )) + : ( + + + + No voting matches + + the selected filter + + + + ) + } + + ); + } +} + +export default VotingList; diff --git a/src/components/Voting/VotingRoute.js b/src/components/Voting/VotingRoute.js new file mode 100644 index 00000000..67c906d7 --- /dev/null +++ b/src/components/Voting/VotingRoute.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { + Route, + withRouter, + Switch, +} from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { inject } from 'mobx-react'; +import Voting from '.'; +import VotingInfoWrapper from './VotingInfoWrapper'; + +@inject('projectStore') +class VotingRoute extends React.Component { + static propTypes = { + match: PropTypes.shape({ + path: PropTypes.string.isRequired, + }).isRequired, + projectStore: PropTypes.shape({ + historyStore: PropTypes.shape({ + resetFilter: PropTypes.func.isRequired, + }).isRequired, + }).isRequired, + }; + + componentDidMount() { + const { projectStore } = this.props; + const { + historyStore: { + resetFilter, + }, + } = projectStore; + resetFilter(); + } + + render() { + const { props } = this; + const { match } = props; + return ( + + + + + ); + } +} + +export default VotingRoute; diff --git a/src/components/Voting/VotingStats.js b/src/components/Voting/VotingStats.js new file mode 100644 index 00000000..3ad29bd9 --- /dev/null +++ b/src/components/Voting/VotingStats.js @@ -0,0 +1,147 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import uniqKey from 'react-id-generator'; +import { BarChart, Bar, LabelList } from 'recharts'; +import { Trans, withTranslation } from 'react-i18next'; +import { observer } from 'mobx-react'; + +import styles from './Voting.scss'; + +@withTranslation() +@observer +class VotingStats extends React.PureComponent { + static propTypes = { + t: PropTypes.func.isRequired, + onBarClick: PropTypes.func.isRequired, + data: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + pros: PropTypes.number.isRequired, + cons: PropTypes.number.isRequired, + abstained: PropTypes.number.isRequired, + }).isRequired, + ).isRequired, + }; + + /** + * Method for render label in BarChart + * + * @param {object} props property label from BarChart + * @param {string} option custom property for label + * @returns {Node} ready label + */ + renderLabel = (props, option) => { + const { + x, y, width, value, + } = props; + return ( + + + {`${value} %`} + + + { + option === 'pros' + ? () + : () + } + + + ); + } + + render() { + const { props } = this; + const { t, onBarClick, data } = props; + return ( + + { + data && data.length + ? data.map((item) => ( + + + onBarClick(item.address)} + > + this.renderLabel(contentProps, 'pros')} + /> + + onBarClick(item.address)} + > + this.renderLabel(contentProps, 'cons')} + /> + + + + { + item.abstained === 0 + ? t('other:everyoneVoted') + : `${t('other:didNotVote')} ${item.abstained || 0}%` + } + + + {item.name} + + + )) + : ('Empty state') + } + + ); + } +} + +export default VotingStats; diff --git a/src/components/Voting/VotingStats.test.js b/src/components/Voting/VotingStats.test.js new file mode 100644 index 00000000..6754581c --- /dev/null +++ b/src/components/Voting/VotingStats.test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingStats from './VotingStats'; + +describe('VotingStats', () => { + let wrapper; + let wrapperInstance; + + beforeEach(() => { + wrapper = shallow().dive(); + wrapperInstance = wrapper.instance(); + }); + + it('should render without error', () => { + expect(wrapper.length).toEqual(1); + }); + + it('renderLabel should return correct data', () => { + const label = wrapperInstance.renderLabel({ + x: 20, + y: 100, + width: 200, + value: 60, + }, 'pros'); + expect(label.props.x).toEqual(120); + expect(label.props.y).toEqual(90); + }); +}); diff --git a/src/components/Voting/VotingTop.js b/src/components/Voting/VotingTop.js new file mode 100644 index 00000000..9bb1d0c3 --- /dev/null +++ b/src/components/Voting/VotingTop.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation, Trans } from 'react-i18next'; +import { PlayCircleIcon } from '../Icons'; +import Button from '../Button/Button'; + +import styles from './Voting.scss'; + +/** + * Component in top voting page + * + * @returns {Node} component + */ +const VotingTop = ({ + onClick, + votingIsActive, + t, +}) => ( + + )} + theme="with-play-icon" + onClick={onClick} + disabled={votingIsActive} + hint={ + votingIsActive + ? ( + + During active voting, this + + functionality is not available. + + ) + : null + } + > + {t('buttons:startNewVote')} + + +); + +VotingTop.propTypes = { + onClick: PropTypes.func, + t: PropTypes.func.isRequired, + votingIsActive: PropTypes.bool.isRequired, +}; + +VotingTop.defaultProps = { + onClick: () => {}, +}; + +export default withTranslation('other')(VotingTop); diff --git a/src/components/Voting/VotingTop.test.js b/src/components/Voting/VotingTop.test.js new file mode 100644 index 00000000..7942c19c --- /dev/null +++ b/src/components/Voting/VotingTop.test.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import VotingTop from './VotingTop'; + +describe('VotingTop', () => { + it('should render correct without onClick props', () => { + const wrapper = shallow().dive(); + expect(wrapper.length).toEqual(1); + }); + + it('button onClick should call mockClick with onClick prop', () => { + const mockClick = jest.fn(); + const wrapper = shallow( + , + ).dive(); + expect(wrapper.length).toEqual(1); + const button = wrapper.find('button'); + button.prop('onClick')(); + expect(mockClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/Voting/index.js b/src/components/Voting/index.js new file mode 100644 index 00000000..79646671 --- /dev/null +++ b/src/components/Voting/index.js @@ -0,0 +1,8 @@ +import Voting from './Voting'; +import VotingTop from './VotingTop'; + +export default Voting; + +export { + VotingTop, +}; diff --git a/src/components/Voting/utils.js b/src/components/Voting/utils.js new file mode 100644 index 00000000..1fec25fc --- /dev/null +++ b/src/components/Voting/utils.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { withTranslation, Trans } from 'react-i18next'; +import moment from 'moment'; +import { EMPTY_DATA_STRING } from '../../constants'; +import { RejectIcon, NoQuorum, VerifyIcon } from '../Icons'; + +import styles from './Voting.scss'; + +/** + * Method for render icon with text + * + * @param {string} state state for icon + * @returns {Node} ready node element + */ +const renderDecisionIcon = ({ + state, + t, +}) => { + switch (state) { + case 0: + return ( + + + {t('other:notAccepted')} + + ); + case 1: + return ( + + + {t('other:pros')} + + ); + case 2: + return ( + + + {t('other:cons')} + + ); + default: + return EMPTY_DATA_STRING; + } +}; + + +/** + * Method for getting formatted date string + * + * @param {Date} date date for formatting + * @returns {string} formatted date + */ +const getDateString = (date) => { + if ( + !date + || typeof date !== 'number' + ) return EMPTY_DATA_STRING; + return ( + + ); +}; + +export default withTranslation()(renderDecisionIcon); + +export { + getDateString, +}; diff --git a/src/config.json b/src/config.json index c19978c1..d8984d9c 100644 --- a/src/config.json +++ b/src/config.json @@ -1,17 +1,14 @@ { "host": "https://ropsten.infura.io/v3/14b2319f08e24f3aadfe1aa933301b38", + "hostBackup": "http://hive3.thexproject.ru:21595", + "minGasPrice": 40, + "maxGasPrice": 70, + "interval": 30, + "gasLimit": 7900000, "projects": [ { - "name": "tetete", - "address": "0x1Df6AdA9f170D0FdFb075140333A44c0C651f4AC" - }, - { - "name": "test", - "address": "0x1Cd9D97EC3f3283cD564bB82f7d0Ee2737D6F352" - }, - { - "name": "T3st", - "address": "0xACA55c33D67d549CacA4f700D0319B865D8E0FC5" + "name": "Test1", + "address": "0x0a021E388c66Acf96a9e2a8DEbdf51446573Cd3e" } ] } \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js index 0da98827..78a7c428 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,13 +1,59 @@ /* eslint-disable no-useless-escape */ +export const statusStates = { + active: '1', + closed: '0', +}; + export const votingStates = { + default: '0', + decisionFor: '1', + decisionAgainst: '2', +}; + +export const userVotingStates = { + notAccepted: 0, + decisionFor: 1, + decisionAgainst: 2, +}; + +export const systemQuestionsId = { + addingNewQuestion: 0, + connectGroupUsers: 1, + connectGroupQuestions: 2, + assignGroupAdmin: 3, +}; + +export const tokenTypes = { + ERC20: '0', + Custom: '1', +}; + +export const votingDecisionStates = { default: 0, - prepared: 1, - active: 2, + agree: 1, + reject: 2, +}; + +export const languages = { + RUS: 'ru', + ENG: 'en', }; -export const SOL_PATH_REGEXP = new RegExp(/(\"|\')((\.{1,2}\/){1,})(\w+\/){0,}?(\w+\.(?:sol))(\"|\')/g); -export const SOL_IMPORT_REGEXP = new RegExp(/(import)*.(\"|\')((\.{1,2}\/){1,})(\w+\/){0,}?(\w+\.(?:sol))(\"|\')(;)/g); -export const SOL_VERSION_REGEXP = new RegExp(/(pragma).(solidity).((\^)?)([0-9](.)?){1,}/g); +export const transactionSteps = { + compileOrSign: 0, + sending: 1, + txHash: 2, + txReceipt: 3, + questionsUploading: 4, + success: 5, +}; + +export const SOL_PATH_REGEXP = new RegExp(/(\"|\')(((\.{1,2}\/){1,})||(zeroone-voting-vm\/))(\w+\/){0,}?(\w+\.(?:sol))(\"|\')/g); +export const VM_IMPORT_REGEXP = new RegExp(/(zeroone-voting-vm)([\/\\]\w+[\/\\]).{1,}(\w+\.(?:sol))/g); +export const SOL_IMPORT_REGEXP = new RegExp(/(import)*.(\"|\')((\.{1,}\/)||(zeroone-voting-vm\/))+((\w+\/*())+(\w+\.(?:sol)))(\"|\')(;)/g); +export const SOL_ENCODER_REGEXP = new RegExp(/(pragma experimental ABIEncoderV2;)/g); +export const SOL_VERSION_REGEXP = new RegExp(/(pragma).(solidity).((\^)?)([0-9](.)?){1,}.(;)/g); + export const EMPTY_DATA_STRING = '-/-'; export const walletHdPath = "m/44'/60'/0'/0/0"; diff --git a/src/constants/windowModules.js b/src/constants/windowModules.js index e588d5e0..f132938b 100644 --- a/src/constants/windowModules.js +++ b/src/constants/windowModules.js @@ -5,16 +5,24 @@ const ENV = process.env.NODE_ENV || 'development'; window.__ENV = ENV; const devPath = window.process.env.INIT_CWD; -const prodPath = window.process.env.PORTABLE_EXECUTABLE_DIR || path.join(window.__dirname, '../src'); +const prodPath = window.process.env.PORTABLE_EXECUTABLE_DIR || path.join(window.__dirname, ''); export const ROOT_DIR = window.__ENV === 'production' ? prodPath : path.join(devPath, './src/'); +export const PATH_TO_CONFIG = window.__ENV === 'production' + ? path.join(prodPath, '../../build/config.json') + : path.join(devPath, './src/config.json'); + export const PATH_TO_WALLETS = window.__ENV === 'production' - ? path.join(prodPath, './wallets/') + ? path.join(prodPath, '../../build/wallets') : path.join(devPath, './src/wallets/'); export const PATH_TO_CONTRACTS = window.__ENV === 'production' - ? path.join(prodPath, './contracts/') + ? path.join(prodPath, '../../build/contracts') : path.join(devPath, './src/contracts/'); + +export const PATH_TO_DATA = window.__ENV === 'production' + ? path.join(prodPath, '../../build/data') + : path.join(devPath, './src/data/'); diff --git a/src/contracts/CustomToken.abi b/src/contracts/CustomToken.abi new file mode 100644 index 00000000..d4a42fa6 --- /dev/null +++ b/src/contracts/CustomToken.abi @@ -0,0 +1,403 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "holder", + "type": "address" + } + ], + "name": "HolderAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "holder", + "type": "address" + } + ], + "name": "HolderRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + } + ], + "name": "ProjectAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + } + ], + "name": "ProjectRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "TokensLocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "project", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "TokensUnlocked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_project", + "type": "address" + } + ], + "name": "addToProjects", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getHolders", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getProjects", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isOwner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "isProjectAddress", + "outputs": [ + { + "internalType": "bool", + "name": "isProject", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_project", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "isTokenLocked", + "outputs": [ + { + "internalType": "bool", + "name": "isLocked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_project", + "type": "address" + } + ], + "name": "removeFromProjects", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "revoke", + "outputs": [ + { + "internalType": "bool", + "name": "isUnlocked", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_sender", + "type": "address" + }, + { + "internalType": "address", + "name": "_reciepient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_count", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/contracts/ERC20.abi b/src/contracts/ERC20.abi index 6e054f53..6921ea43 100644 --- a/src/contracts/ERC20.abi +++ b/src/contracts/ERC20.abi @@ -1 +1,280 @@ -[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"sender","type":"address"},{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"name","type":"string"},{"name":"symbol","type":"string"},{"name":"totalSupply","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"}] \ No newline at end of file +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/contracts/ERC20.sol b/src/contracts/ERC20.sol index c7dd2d5b..abc2465a 100644 --- a/src/contracts/ERC20.sol +++ b/src/contracts/ERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "./IERC20.sol"; import "./SafeMath.sol"; @@ -39,7 +39,7 @@ contract ERC20 is IERC20 { string private _symbol; - constructor (string name, string symbol, uint256 totalSupply) public { + constructor (string memory name, string memory symbol, uint256 totalSupply) public { _name = name; _symbol = symbol; _totalSupply = totalSupply; @@ -63,13 +63,13 @@ contract ERC20 is IERC20 { /** * @dev Get the symbol of token. */ - function symbol() public view returns (string) { + function symbol() public view returns (string memory) { return _symbol; } /** * @dev Get the name of token. */ - function name() public view returns (string) { + function name() public view returns (string memory) { return _name; } diff --git a/src/contracts/IERC20.sol b/src/contracts/IERC20.sol index 0501a2f9..29b8ff9a 100644 --- a/src/contracts/IERC20.sol +++ b/src/contracts/IERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; /** * @dev Interface of the ERC20 standard as defined in the EIP. Does not include @@ -12,11 +12,11 @@ interface IERC20 { /** * @dev Returns the amount of tokens in existence. */ - function name() external view returns (string); + function name() external view returns (string memory); /** * @dev Returns the amount of tokens in existence. */ - function symbol() external view returns (string); + function symbol() external view returns (string memory); /** * @dev Returns the amount of tokens owned by `account`. diff --git a/src/contracts/MERC20.abi b/src/contracts/MERC20.abi index 8f465543..a0e33850 100644 --- a/src/contracts/MERC20.abi +++ b/src/contracts/MERC20.abi @@ -1,235 +1,289 @@ [ - { - "constant": true, - "inputs": [], - "name": "owner", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "who", - "type": "address" - } - ], - "name": "balanceOfERC", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "admin", - "outputs": [ - { - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "name", - "type": "string" - }, - { - "name": "symbol", - "type": "string" - }, - { - "name": "decimals", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "constant": true, - "inputs": [], - "name": "symbol", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "name", - "outputs": [ - { - "name": "", - "type": "string" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getUsers", - "outputs": [ - { - "name": "userList", - "type": "address[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "who", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "user", - "type": "address" - } - ], - "name": "findUser", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [], - "name": "findEmptyUser", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "user", - "type": "address" - }, - { - "name": "balance", - "type": "uint256" - } - ], - "name": "_addUser", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_who", - "type": "address" - }, - { - "name": "_to", - "type": "address" - }, - { - "name": "value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_newAdmin", - "type": "address" - } - ], - "name": "setAdmin", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "uint256", + "name": "decimals", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "name": "_addUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "admin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "who", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "findEmptyUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "findUser", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "getAdmin", + "outputs": [ + { + "internalType": "address", + "name": "adminitstrator", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "getUsers", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_newAdmin", + "type": "address" + } + ], + "name": "setAdmin", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferBeetweenUsers", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "_who", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } ] \ No newline at end of file diff --git a/src/contracts/MERC20.sol b/src/contracts/MERC20.sol index 70cc00be..f2d05c70 100644 --- a/src/contracts/MERC20.sol +++ b/src/contracts/MERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; contract MERC20 { string private _name; @@ -11,7 +11,7 @@ contract MERC20 { mapping (uint => mapping (address => uint256)) userBalances; address[] users; - constructor (string name, string symbol, uint256 decimals ) public { + constructor (string memory name, string memory symbol, uint256 decimals ) public { _name = name; _symbol = symbol; _decimals = decimals; @@ -23,17 +23,17 @@ contract MERC20 { users.push(msg.sender); } - function symbol() external returns(string) { + function symbol() external returns(string memory) { return _symbol; } - function name() external returns(string) { + function name() external returns(string memory) { return _name; } function totalSupply() external returns(uint256) { return _decimals; } - function getUsers() external returns (address[]) { + function getUsers() external returns (address[] memory) { return users; } @@ -56,7 +56,7 @@ contract MERC20 { uint usersLength = users.length; uint matched = 0; for (uint i = 0; i < usersLength; i++) { - if (users[i] == 0) { + if (users[i] == address(0)) { matched = i; } } @@ -74,9 +74,13 @@ contract MERC20 { return balances[user]; } + function transferBeetweenUsers(address sender, address recipient, uint256 amount) public returns (bool) { + require((msg.sender == admin) || (msg.sender == sender)); + this.transferFrom(sender, recipient, amount); + } + - function transferFrom(address _who, address _to, uint256 value) external { - require(msg.sender == admin); + function transferFrom(address _who, address _to, uint256 value) public returns (bool) { require(_who != address(0), "MERC20: transfer from the zero address"); require(_to != address(0), "MERC20: transfer to the zero address"); require(balances[_who] >= value, "MERC20: Token value must be lower or equal"); @@ -99,5 +103,9 @@ contract MERC20 { function setAdmin(address _newAdmin) external { admin = _newAdmin; } + + function getAdmin() external returns (address adminitstrator) { + return admin; + } } \ No newline at end of file diff --git a/src/contracts/MERCInterface.sol b/src/contracts/MERCInterface.sol index b9f4a773..f678b36c 100644 --- a/src/contracts/MERCInterface.sol +++ b/src/contracts/MERCInterface.sol @@ -1,11 +1,11 @@ -pragma solidity 0.5; + pragma solidity ^0.5.15; interface MERCInterface { - function symbol() external returns (string); + function symbol() external returns (string memory); - function name() external returns (string); + function name() external returns (string memory); function totalSupply() external returns (uint256); diff --git a/src/contracts/Migrations.sol b/src/contracts/Migrations.sol new file mode 100644 index 00000000..d25f2c23 --- /dev/null +++ b/src/contracts/Migrations.sol @@ -0,0 +1,24 @@ +pragma solidity ^0.5.15; + + +contract Migrations { + address public owner; + uint public lastCompletedMigration; + + modifier restricted() { + if (msg.sender == owner) _; + } + + constructor() public { + owner = msg.sender; + } + + function setCompleted(uint completed) public restricted { + lastCompletedMigration = completed; + } + + function upgrade(address _newAddress) public restricted { + Migrations upgraded = Migrations(_newAddress); + upgraded.setCompleted(lastCompletedMigration); + } +} \ No newline at end of file diff --git a/src/contracts/Project/Project.sol b/src/contracts/Project/Project.sol index b27720b0..42b79287 100644 --- a/src/contracts/Project/Project.sol +++ b/src/contracts/Project/Project.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; contract Project { diff --git a/src/contracts/SafeMath.sol b/src/contracts/SafeMath.sol index 62cf6358..305cdd38 100644 --- a/src/contracts/SafeMath.sol +++ b/src/contracts/SafeMath.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; /** * @title SafeMath diff --git a/src/contracts/Voter.abi b/src/contracts/Voter.abi index 0efeb78e..becda8e6 100644 --- a/src/contracts/Voter.abi +++ b/src/contracts/Voter.abi @@ -1,609 +1,618 @@ [ { - "constant": true, "inputs": [ { - "name": "_id", - "type": "uint256" - } - ], - "name": "getVotingDescision", - "outputs": [ - { - "name": "result", - "type": "uint256" + "internalType": "address", + "name": "_address", + "type": "address" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x0d24ef38" + "stateMutability": "nonpayable", + "type": "constructor" }, { - "constant": true, + "anonymous": false, "inputs": [ { - "name": "_id", - "type": "uint256" - } - ], - "name": "question", - "outputs": [ - { + "indexed": false, + "internalType": "uint256", "name": "groupId", "type": "uint256" }, { + "indexed": false, + "internalType": "enum Questions.Status", "name": "status", "type": "uint8" }, { + "indexed": false, + "internalType": "string", "name": "caption", "type": "string" }, { + "indexed": false, + "internalType": "string", "name": "text", "type": "string" }, { + "indexed": false, + "internalType": "uint256", "name": "time", "type": "uint256" }, { + "indexed": false, + "internalType": "address", "name": "target", "type": "address" }, { + "indexed": false, + "internalType": "bytes4", "name": "methodSelector", "type": "bytes4" + } + ], + "name": "NewQuestion", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" }, { - "name": "_formula", - "type": "uint256[]" + "indexed": false, + "internalType": "uint256", + "name": "questionId", + "type": "uint256" }, { - "name": "_parameters", - "type": "bytes32[]" + "indexed": false, + "internalType": "enum Votings.Status", + "name": "status", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "starterGroup", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "starterAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "startblock", + "type": "uint256" } ], - "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x1e16f68b" + "name": "NewVoting", + "type": "event" }, { "constant": true, "inputs": [], - "name": "votings", + "name": "ERC20", "outputs": [ { - "name": "votingIdIndex", - "type": "uint256" + "internalType": "contract IERC20", + "name": "", + "type": "address" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x352485b7" + "type": "function" }, { "constant": true, - "inputs": [ - { - "name": "_id", - "type": "uint256" - } - ], - "name": "getUserGroup", + "inputs": [], + "name": "External", "outputs": [ { - "name": "name", - "type": "string" - }, - { - "name": "groupType", - "type": "string" - }, - { - "name": "status", - "type": "uint8" - }, - { - "name": "groupAddress", + "internalType": "contract ExternalContract", + "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x352704b7" + "type": "function" }, { "constant": true, "inputs": [], - "name": "getQuestionGroupsLength", + "name": "addresses", "outputs": [ { - "name": "length", - "type": "uint256" + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "address", + "name": "instance", + "type": "address" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x392e1c84" + "type": "function" }, { "constant": false, "inputs": [ { + "internalType": "uint256", "name": "votingId", "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "votingData", + "type": "bytes" } ], - "name": "returnTokens", + "name": "applyVotingData", "outputs": [ { - "name": "status", + "internalType": "bool", + "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0x3ae1786f" + "type": "function" }, { "constant": false, - "inputs": [ - { - "name": "group", - "type": "address" - }, - { - "name": "admin", - "type": "address" - } - ], - "name": "setCustomGroupAdmin", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], + "inputs": [], + "name": "closeVoting", + "outputs": [], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0x3cc0a953" + "type": "function" }, { "constant": true, - "inputs": [], - "name": "getUserWeight", + "inputs": [ + { + "internalType": "address", + "name": "_address", + "type": "address" + } + ], + "name": "findLastUserVoting", "outputs": [ { - "name": "weight", + "internalType": "uint256", + "name": "votingId", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x42429139" + "type": "function" }, { "constant": false, "inputs": [ { - "name": "votingId", - "type": "uint256" - }, - { + "internalType": "address", "name": "user", "type": "address" } ], - "name": "isUserReturnTokens", + "name": "findUserGroup", "outputs": [ { - "name": "result", - "type": "bool" + "internalType": "uint256", + "name": "", + "type": "uint256" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0x4a1e82a0" + "type": "function" }, { - "constant": true, + "constant": false, "inputs": [], - "name": "getERCTotal", + "name": "getCount", "outputs": [ { - "name": "balance", + "internalType": "uint256", + "name": "length", "type": "uint256" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x5b1bb4a2" + "stateMutability": "nonpayable", + "type": "function" }, { "constant": true, - "inputs": [], - "name": "groups", - "outputs": [ + "inputs": [ { - "name": "groupIdIndex", + "internalType": "uint256", + "name": "_id", "type": "uint256" } ], - "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0x5bf89d9e" - }, - { - "constant": false, - "inputs": [ - { - "name": "_idsAndTime", - "type": "uint256[]" - }, - { - "name": "_status", - "type": "uint8" - }, - { - "name": "_caption", - "type": "string" - }, + "name": "getQuestionGroup", + "outputs": [ { - "name": "_text", + "internalType": "string", + "name": "name", "type": "string" }, { - "name": "_target", - "type": "address" - }, - { - "name": "_methodSelector", - "type": "bytes4" - }, - { - "name": "_formula", - "type": "uint256[]" - }, - { - "name": "_parameters", - "type": "bytes32[]" - } - ], - "name": "saveNewQuestion", - "outputs": [ - { - "name": "_saved", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0x686c52c4" - }, - { - "constant": true, - "inputs": [], - "name": "getERCSymbol", - "outputs": [ - { - "name": "symbol", - "type": "string" + "internalType": "enum QuestionGroups.GroupType", + "name": "groupType", + "type": "uint8" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x7264420a" + "type": "function" }, { "constant": true, "inputs": [], - "name": "questions", + "name": "getQuestionGroupsLength", "outputs": [ { - "name": "questionIdIndex", + "internalType": "uint256", + "name": "length", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x77a49821" + "type": "function" }, { "constant": true, "inputs": [ { + "internalType": "uint256", "name": "_id", "type": "uint256" } ], - "name": "getQuestionGroup", + "name": "getUserGroup", "outputs": [ { + "internalType": "string", "name": "name", "type": "string" }, { + "internalType": "string", "name": "groupType", + "type": "string" + }, + { + "internalType": "enum UserGroups.GroupStatus", + "name": "status", "type": "uint8" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getUserGroupsLength", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x7fd60dfd" + "type": "function" }, { "constant": true, "inputs": [ { + "internalType": "uint256", "name": "_voteId", "type": "uint256" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" } ], "name": "getUserVote", "outputs": [ { + "internalType": "uint256", "name": "vote", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0x86194c19" + "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_name", - "type": "string" + "internalType": "uint256", + "name": "_voteId", + "type": "uint256" }, { - "name": "_address", + "internalType": "address", + "name": "_user", "type": "address" - }, - { - "name": "_type", - "type": "string" } ], - "name": "saveNewUserGroup", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0x952b627c" - }, - { - "constant": false, - "inputs": [], - "name": "getCount", + "name": "getUserVoteWeight", "outputs": [ { - "name": "length", + "internalType": "uint256", + "name": "tokenCount", "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xa87d942c" + "stateMutability": "view", + "type": "function" }, { "constant": true, "inputs": [], - "name": "getUserBalance", + "name": "getUserWeight", "outputs": [ { - "name": "balance", + "internalType": "uint256", + "name": "weight", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xb7013dc1" + "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_address", - "type": "address" + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" } ], - "name": "setERC20", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc29a6fda" - }, - { - "constant": true, - "inputs": [], - "name": "isActiveVoting", + "name": "getVotes", "outputs": [ { - "name": "", - "type": "bool" + "internalType": "uint256[3]", + "name": "_votes", + "type": "uint256[3]" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xc39c89df" - }, - { - "constant": false, - "inputs": [], - "name": "closeVoting", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc631b292" + "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_questionId", - "type": "uint256" - }, - { - "name": "_status", - "type": "uint8" - }, - { - "name": "_starterGroup", + "internalType": "uint256", + "name": "_id", "type": "uint256" - }, - { - "name": "_data", - "type": "bytes" } ], - "name": "startNewVoting", + "name": "getVotingDescision", "outputs": [ { - "name": "", - "type": "bool" + "internalType": "uint256", + "name": "result", + "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc7ce7f10" + "stateMutability": "view", + "type": "function" }, { - "constant": false, - "inputs": [ - { - "name": "_choice", - "type": "uint256" - } - ], - "name": "sendVote", + "constant": true, + "inputs": [], + "name": "getVotingsCount", "outputs": [ { - "name": "result", - "type": "uint256" - }, - { - "name": "votePos", - "type": "uint256" - }, - { - "name": "voteNeg", + "internalType": "uint256", + "name": "count", "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "function", - "signature": "0xc8f5714e" + "stateMutability": "view", + "type": "function" }, { "constant": true, "inputs": [], - "name": "ERC20", + "name": "groups", "outputs": [ { - "name": "", - "type": "address" + "internalType": "uint256", + "name": "groupIdIndex", + "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xcc4aa204" + "type": "function" }, { "constant": true, "inputs": [], - "name": "getERCAddress", + "name": "isActiveVoting", "outputs": [ { - "name": "_address", - "type": "address" + "internalType": "bool", + "name": "", + "type": "bool" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xd094b706" + "type": "function" }, { "constant": true, - "inputs": [], - "name": "userGroups", + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isUserReturnTokens", "outputs": [ { - "name": "groupIdIndex", - "type": "uint256" + "internalType": "bool", + "name": "result", + "type": "bool" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xd7843f54" + "type": "function" }, { "constant": true, - "inputs": [], - "name": "addresses", + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "question", "outputs": [ { - "name": "user", - "type": "address" + "internalType": "uint256", + "name": "groupId", + "type": "uint256" }, { - "name": "instance", + "internalType": "enum Questions.Status", + "name": "status", + "type": "uint8" + }, + { + "internalType": "string", + "name": "caption", + "type": "string" + }, + { + "internalType": "string", + "name": "text", + "type": "string" + }, + { + "internalType": "uint256", + "name": "time", + "type": "uint256" + }, + { + "internalType": "address", + "name": "target", "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "uint256[]", + "name": "_formula", + "type": "uint256[]" + }, + { + "internalType": "bytes32[]", + "name": "_parameters", + "type": "bytes32[]" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xda0321cd" + "type": "function" }, { "constant": true, "inputs": [], - "name": "getVotingsCount", + "name": "questions", "outputs": [ { - "name": "count", + "internalType": "uint256", + "name": "questionIdIndex", "type": "uint256" } ], "payable": false, "stateMutability": "view", - "type": "function", - "signature": "0xdae7c92c" + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "returnTokens", + "outputs": [ + { + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" }, { "constant": false, "inputs": [ { + "internalType": "string", "name": "_name", "type": "string" } @@ -611,229 +620,309 @@ "name": "saveNewGroup", "outputs": [ { + "internalType": "uint256", "name": "id", "type": "uint256" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0xdf831328" + "type": "function" }, { - "constant": true, - "inputs": [], - "name": "getUserGroupsLength", + "constant": false, + "inputs": [ + { + "internalType": "uint256[]", + "name": "_idsAndTime", + "type": "uint256[]" + }, + { + "internalType": "enum Questions.Status", + "name": "_status", + "type": "uint8" + }, + { + "internalType": "string", + "name": "_caption", + "type": "string" + }, + { + "internalType": "string", + "name": "_text", + "type": "string" + }, + { + "internalType": "address", + "name": "_target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "_methodSelector", + "type": "bytes4" + }, + { + "internalType": "uint256[]", + "name": "_formula", + "type": "uint256[]" + }, + { + "internalType": "bytes32[]", + "name": "_parameters", + "type": "bytes32[]" + } + ], + "name": "saveNewQuestion", "outputs": [ { - "name": "length", - "type": "uint256" + "internalType": "bool", + "name": "_saved", + "type": "bool" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0xec4d819c" + "stateMutability": "nonpayable", + "type": "function" }, { "constant": false, "inputs": [ { - "name": "user", + "internalType": "string", + "name": "_name", + "type": "string" + }, + { + "internalType": "address", + "name": "_address", "type": "address" + }, + { + "internalType": "string", + "name": "_type", + "type": "string" } ], - "name": "findUserGroup", + "name": "saveNewUserGroup", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "_choice", + "type": "uint256" + } + ], + "name": "sendVote", "outputs": [ { - "name": "", + "internalType": "uint256", + "name": "result", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "votePos", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "voteNeg", "type": "uint256" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0xf0c4c3e6" + "type": "function" }, { "constant": false, "inputs": [ { - "name": "_who", + "internalType": "address", + "name": "group", "type": "address" }, { - "name": "_value", - "type": "uint256" + "internalType": "address", + "name": "admin", + "type": "address" } ], - "name": "transferERC20", + "name": "setCustomGroupAdmin", "outputs": [ { - "name": "newBalance", - "type": "uint256" + "internalType": "bool", + "name": "", + "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", - "type": "function", - "signature": "0xf7448a31" + "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_id", - "type": "uint256" + "internalType": "address", + "name": "_address", + "type": "address" } ], - "name": "voting", - "outputs": [ + "name": "setERC20", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ { - "name": "id", + "internalType": "uint256", + "name": "_questionId", "type": "uint256" }, { - "name": "status", + "internalType": "enum Votings.Status", + "name": "_status", "type": "uint8" }, { - "name": "caption", - "type": "string" - }, - { - "name": "text", - "type": "string" - }, - { - "name": "startTime", - "type": "uint256" - }, - { - "name": "endTime", + "internalType": "uint256", + "name": "_starterGroup", "type": "uint256" }, { - "name": "data", + "internalType": "bytes", + "name": "_data", "type": "bytes" } ], + "name": "startNewVoting", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0xfd4a77f1" + "stateMutability": "nonpayable", + "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_votingId", + "internalType": "address", + "name": "_who", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", "type": "uint256" } ], - "name": "getVotes", + "name": "transferERC20", "outputs": [ { - "name": "_votes", - "type": "uint256[3]" + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" } ], "payable": false, - "stateMutability": "view", - "type": "function", - "signature": "0xff981099" + "stateMutability": "nonpayable", + "type": "function" }, { - "inputs": [ + "constant": true, + "inputs": [], + "name": "userGroups", + "outputs": [ { - "name": "_address", - "type": "address" + "internalType": "uint256", + "name": "groupIdIndex", + "type": "uint256" } ], "payable": false, - "stateMutability": "nonpayable", - "type": "constructor", - "signature": "constructor" + "stateMutability": "view", + "type": "function" }, { - "anonymous": false, + "constant": true, "inputs": [ { - "indexed": false, - "name": "groupId", + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "voting", + "outputs": [ + { + "internalType": "uint256", + "name": "id", "type": "uint256" }, { - "indexed": false, + "internalType": "enum Votings.Status", "name": "status", "type": "uint8" }, { - "indexed": false, + "internalType": "string", "name": "caption", "type": "string" }, { - "indexed": false, + "internalType": "string", "name": "text", "type": "string" }, { - "indexed": false, - "name": "time", + "internalType": "uint256", + "name": "startTime", "type": "uint256" }, { - "indexed": false, - "name": "target", - "type": "address" + "internalType": "uint256", + "name": "endTime", + "type": "uint256" }, { - "indexed": false, - "name": "methodSelector", - "type": "bytes4" + "internalType": "bytes", + "name": "data", + "type": "bytes" } ], - "name": "NewQuestion", - "type": "event", - "signature": "0xf74c837bcb7177c5fc07298a351f91ebaf73da24a2665c4dd592976c4e1780c9" + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "id", - "type": "uint256" - }, - { - "indexed": false, - "name": "questionId", - "type": "uint256" - }, - { - "indexed": false, - "name": "status", - "type": "uint8" - }, - { - "indexed": false, - "name": "starterGroup", - "type": "uint256" - }, - { - "indexed": false, - "name": "starterAddress", - "type": "address" - }, + "constant": true, + "inputs": [], + "name": "votings", + "outputs": [ { - "indexed": false, - "name": "startblock", + "internalType": "uint256", + "name": "votingIdIndex", "type": "uint256" } ], - "name": "NewVoting", - "type": "event", - "signature": "0x20cd0a5d2be6a115945cf28e1511206f69d34a385fb4b9c8ee18f39fa52471c7" + "payable": false, + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file diff --git a/src/contracts/Voter/ExternalInterface.sol b/src/contracts/Voter/ExternalInterface.sol new file mode 100644 index 00000000..2a8b02b6 --- /dev/null +++ b/src/contracts/Voter/ExternalInterface.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.5.15; + +interface ExternalContract { + function applyVotingData(uint votingId, uint questionId, bytes calldata data) external returns (bool); +} \ No newline at end of file diff --git a/src/contracts/Voter/Voter.sol b/src/contracts/Voter/Voter.sol index eefe9132..e929cfae 100644 --- a/src/contracts/Voter/Voter.sol +++ b/src/contracts/Voter/Voter.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "./VoterBase.sol"; diff --git a/src/contracts/Voter/VoterBase.sol b/src/contracts/Voter/VoterBase.sol index 8297ee95..e60b3704 100644 --- a/src/contracts/Voter/VoterBase.sol +++ b/src/contracts/Voter/VoterBase.sol @@ -1,10 +1,11 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "../libs/QuestionGroups.sol"; import "../libs/UserGroups.sol"; import "../libs/Questions.sol"; import "../libs/Votings.sol"; import "./VoterInterface.sol"; +import "./ExternalInterface.sol"; import "../IERC20.sol"; @@ -21,6 +22,7 @@ contract VoterBase is VoterInterface { UserGroups.List public userGroups; IERC20 public ERC20; + ExternalContract public External; constructor() public { questions.init(); @@ -30,7 +32,6 @@ contract VoterBase is VoterInterface { // METHODS function setERC20(address _address) public { - ERC20 = IERC20(_address); userGroups.init(_address); } @@ -55,7 +56,7 @@ contract VoterBase is VoterInterface { address _target, bytes4 _methodSelector, uint[] memory _formula, - bytes32[] memory _parameters + bytes32[] memory _parameters ) private returns (Questions.Question memory _question) { Questions.Question memory question = Questions.Question({ groupId: _idsAndTime[1], @@ -88,14 +89,14 @@ contract VoterBase is VoterInterface { * @return new question id */ function saveNewQuestion( - uint[] _idsAndTime, + uint[] calldata _idsAndTime, Questions.Status _status, - string _caption, - string _text, + string calldata _caption, + string calldata _text, address _target, bytes4 _methodSelector, - uint[] _formula, - bytes32[] _parameters + uint[] calldata _formula, + bytes32[] calldata _parameters ) external returns (bool _saved){ Questions.Question memory question = createNewQuestion( @@ -118,7 +119,7 @@ contract VoterBase is VoterInterface { * @return new question id */ function saveNewGroup( - string _name + string calldata _name ) external returns (uint id) { QuestionGroups.Group memory group = QuestionGroups.Group({ name: _name, @@ -164,7 +165,7 @@ contract VoterBase is VoterInterface { } function getQuestionGroup(uint _id) public view returns ( - string name, + string memory name, QuestionGroups.GroupType groupType ) { return ( @@ -177,8 +178,8 @@ contract VoterBase is VoterInterface { return groups.groupIdIndex ; } function getUserGroup(uint _id) public view returns ( - string name, - string groupType, + string memory name, + string memory groupType, UserGroups.GroupStatus status, address groupAddress ) { @@ -210,7 +211,7 @@ contract VoterBase is VoterInterface { uint _questionId, Votings.Status _status, uint _starterGroup, - bytes _data + bytes calldata _data ) external returns (bool) { bool canStart; uint votingId = votings.votingIdIndex - 1; @@ -253,7 +254,7 @@ contract VoterBase is VoterInterface { string memory text, uint startTime, uint endTime, - bytes data + bytes memory data ){ uint votingId = _id; uint questionId = votings.voting[_id].questionId; @@ -319,7 +320,7 @@ contract VoterBase is VoterInterface { if (quorumPercent >= percent) { if (positiveVotes > negativeVotes) { votings.descision[votingId] = 1; - address(this).call(votings.voting[votingId].data); + callExternal(votingId, questionId); } else if (positiveVotes < negativeVotes) { votings.descision[votingId] = 2; } else if (positiveVotes == negativeVotes) { @@ -331,7 +332,7 @@ contract VoterBase is VoterInterface { if (quorumPercent <= percent) { if (positiveVotes > negativeVotes) { votings.descision[votingId] = 1; - address(this).call(votings.voting[votingId].data); + callExternal(votingId, questionId); } else if (positiveVotes < negativeVotes) { votings.descision[votingId] = 2; } else if (positiveVotes == negativeVotes) { @@ -344,6 +345,18 @@ contract VoterBase is VoterInterface { votings.voting[votingId].status = Votings.Status.ENDED; } + function callExternal(uint votingId, uint questionId) internal { + address target = questions.question[questionId].target; + ExternalContract controlled = ExternalContract(target); + controlled.applyVotingData(votingId, questionId, votings.voting[votingId].data); + } + + + function applyVotingData(uint votingId, uint questionId, bytes calldata votingData) external returns (bool) { + address(this).call(votingData); + return true; + } + function getVotes(uint _votingId) external view returns (uint256[3] memory _votes) { uint questionId = votings.voting[_votingId].questionId; @@ -357,32 +370,41 @@ contract VoterBase is VoterInterface { return votes; } - function returnTokens(uint votingId) public returns (bool status){ + function returnTokens() public returns (bool status){ + uint votingId = this.findLastUserVoting(msg.sender); uint questionId = votings.voting[votingId].questionId; uint groupId = questions.question[questionId].groupId; string memory groupType = userGroups.group[groupId].groupType; + string memory groupName = userGroups.names[groupId]; IERC20 group = IERC20(userGroups.group[groupId].groupAddr); uint256 weight = votings.voting[votingId].voteWeigths[address(group)][msg.sender]; - bool isReturned = this.isUserReturnTokens(votingId, msg.sender); + bool isReturned = this.isUserReturnTokens(msg.sender); + uint userVote = this.getUserVote(votingId, msg.sender); if (!isReturned) { - if( bytes4(keccak256(groupType)) == bytes4(keccak256("ERC20"))) { + if(keccak256(abi.encodePacked((groupType))) == keccak256(abi.encodePacked(("ERC20")))) { group.transfer(msg.sender, weight); } else { group.transferFrom(address(this), msg.sender, weight); } + if (votings.voting[votingId].status != Votings.Status.ENDED) { + votings.voting[votingId].votes[address(group)][msg.sender] = 0; + votings.voting[votingId].voteWeigths[address(group)][msg.sender] = 0; + votings.voting[votingId].descisionWeights[userVote][groupName] -= weight; + } votings.voting[votingId].tokenReturns[address(group)][msg.sender] = weight; } return true; } - function isUserReturnTokens(uint votingId, address user) returns (bool result) { + function isUserReturnTokens(address user) external view returns (bool result) { + uint votingId = this.findLastUserVoting(user); uint questionId = votings.voting[votingId].questionId; uint groupId = questions.question[questionId].groupId; string memory groupType = userGroups.group[groupId].groupType; IERC20 group = IERC20(userGroups.group[groupId].groupAddr); uint256 returnedTokens = votings.voting[votingId].tokenReturns[address(group)][user]; - return returnedTokens > 0; + return votingId == 0 ? true : returnedTokens > 0; } @@ -423,36 +445,41 @@ contract VoterBase is VoterInterface { this.closeVoting(); } return ( - votings.voting[_voteId].votes[address(group)][msg.sender] = _choice, + votings.voting[_voteId].votes[address(group)][msg.sender], votings.voting[_voteId].descisionWeights[1][groupName], votings.voting[_voteId].descisionWeights[2][groupName] ); } - function getERCAddress() external view returns (address _address) { - return address(ERC20); - } - - function getUserBalance() external view returns (uint256 balance) { - uint256 _balance = ERC20.balanceOf(msg.sender); - return _balance; - } - - function getERCTotal() external view returns (uint256 balance) { - return ERC20.totalSupply(); + function getUserVote(uint _voteId, address _user) external view returns (uint vote) { + uint questionId = votings.voting[_voteId].questionId; + uint groupId = questions.question[questionId].groupId; + IERC20 group = IERC20(userGroups.group[groupId].groupAddr); + return votings.voting[_voteId].votes[address(group)][_user]; } - function getERCSymbol() external view returns (string symbol) { - return ERC20.symbol(); + function getUserVoteWeight(uint _voteId, address _user) external view returns (uint tokenCount) { + uint questionId = votings.voting[_voteId].questionId; + uint groupId = questions.question[questionId].groupId; + IERC20 group = IERC20(userGroups.group[groupId].groupAddr); + return votings.voting[_voteId].voteWeigths[address(group)][_user]; } - function getUserVote(uint _voteId) external view returns (uint vote) { - uint questionId = votings.voting[_voteId].questionId; + function findLastUserVoting(address _address) external view returns (uint votingId){ + uint maxVoteId = votings.votingIdIndex - 1; + uint questionId = votings.voting[maxVoteId].questionId; uint groupId = questions.question[questionId].groupId; IERC20 group = IERC20(userGroups.group[groupId].groupAddr); - return votings.voting[_voteId].votes[address(group)][msg.sender]; + while((votings.voting[maxVoteId].votes[address(group)][_address] == 0) && (maxVoteId >= 1)) { + maxVoteId--; + questionId = votings.voting[maxVoteId].questionId; + groupId = questions.question[questionId].groupId; + group = IERC20(userGroups.group[groupId].groupAddr); + } + return maxVoteId; } + function getUserWeight() external view returns (uint256 weight) { uint _voteId = votings.votingIdIndex - 1; return votings.voting[_voteId].voteWeigths[address(ERC20)][msg.sender]; @@ -470,7 +497,7 @@ contract VoterBase is VoterInterface { ); } - function saveNewUserGroup (string _name, address _address, string _type) external { + function saveNewUserGroup (string calldata _name, address _address, string calldata _type) external { UserGroups.UserGroup memory userGroup = UserGroups.UserGroup({ name: _name, groupType: _type, @@ -480,8 +507,8 @@ contract VoterBase is VoterInterface { userGroups.save(userGroup); } - function setCustomGroupAdmin(address group, address admin) external returns (bool) { - require(group.call( bytes4( keccak256("setAdmin(address)")), admin)); + function setCustomGroupAdmin(address group, address admin) external returns (bool) { + group.call(abi.encodeWithSignature("setAdmin(address)", admin)); return true; } } diff --git a/src/contracts/Voter/VoterInterface.sol b/src/contracts/Voter/VoterInterface.sol index 51a10280..eab75094 100644 --- a/src/contracts/Voter/VoterInterface.sol +++ b/src/contracts/Voter/VoterInterface.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; import "../libs/QuestionGroups.sol"; import "../libs/Questions.sol"; @@ -53,14 +53,14 @@ interface VoterInterface { * return new question id */ function saveNewQuestion( - uint[] _idsAndTime, + uint[] calldata _idsAndTime, Questions.Status _status, - string _caption, - string _text, + string calldata _caption, + string calldata _text, address _target, bytes4 _methodSelector, - uint[] _formula, - bytes32[] _parameters + uint[] calldata _formula, + bytes32[] calldata _parameters ) external returns (bool _saved); /** @@ -69,7 +69,7 @@ interface VoterInterface { * @return new question id */ function saveNewGroup( - string _name + string calldata _name ) external returns (uint id); /** @@ -97,7 +97,7 @@ interface VoterInterface { uint questionId, Votings.Status status, uint starterGroup, - bytes data + bytes calldata data ) external returns (bool); function voting(uint id) external view returns ( @@ -107,7 +107,7 @@ interface VoterInterface { string memory text, uint startTime, uint endTime, - bytes data + bytes memory data ); function getVotingsCount() external view returns (uint length); } diff --git a/src/contracts/ZeroOne.abi b/src/contracts/ZeroOne.abi new file mode 100644 index 00000000..55d99c07 --- /dev/null +++ b/src/contracts/ZeroOne.abi @@ -0,0 +1,1271 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "owners", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes4", + "name": "selector", + "type": "bytes4" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bool", + "name": "result", + "type": "bool" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "name": "Call", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + } + ], + "name": "QuestionAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "name": "QuestionGroupAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "group", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum VM.Vote", + "name": "userVote", + "type": "uint8" + } + ], + "name": "UpdatedUserVote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "address", + "name": "groupAddress", + "type": "address" + } + ], + "name": "UserGroupAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "enum VM.Vote", + "name": "descision", + "type": "uint8" + } + ], + "name": "UserVote", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "votingId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "enum VM.Vote", + "name": "descision", + "type": "uint8" + } + ], + "name": "VotingEnded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "votingId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + } + ], + "name": "VotingStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct IZeroOne.MetaData", + "name": "_meta", + "type": "tuple" + } + ], + "name": "ZeroOneCall", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timeLimit", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "paramNames", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "paramTypes", + "type": "string[]" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "string", + "name": "rawFormula", + "type": "string" + }, + { + "internalType": "bytes", + "name": "formula", + "type": "bytes" + } + ], + "internalType": "struct QuestionType.Question", + "name": "_question", + "type": "tuple" + } + ], + "name": "addQuestion", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timeLimit", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "paramNames", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "paramTypes", + "type": "string[]" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "string", + "name": "rawFormula", + "type": "string" + }, + { + "internalType": "bytes", + "name": "formula", + "type": "bytes" + } + ], + "internalType": "struct QuestionType.Question", + "name": "_question", + "type": "tuple" + } + ], + "name": "addQuestion", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct GroupType.Group", + "name": "_questionGroup", + "type": "tuple" + } + ], + "name": "addQuestionGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct GroupType.Group", + "name": "_questionGroup", + "type": "tuple" + } + ], + "name": "addQuestionGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + }, + { + "internalType": "enum UserGroup.Type", + "name": "groupType", + "type": "uint8" + } + ], + "internalType": "struct UserGroup.Group", + "name": "_group", + "type": "tuple" + } + ], + "name": "addUserGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + }, + { + "internalType": "enum UserGroup.Type", + "name": "groupType", + "type": "uint8" + } + ], + "internalType": "struct UserGroup.Group", + "name": "_group", + "type": "tuple" + } + ], + "name": "addUserGroup", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "didUserVote", + "outputs": [ + { + "internalType": "bool", + "name": "confirm", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "findLastUserVoting", + "outputs": [ + { + "internalType": "uint256", + "name": "votingId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + } + ], + "name": "getGroupVotes", + "outputs": [ + { + "internalType": "uint256", + "name": "positive", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "negative", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getQuestion", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "groupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "timeLimit", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "paramNames", + "type": "string[]" + }, + { + "internalType": "string[]", + "name": "paramTypes", + "type": "string[]" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "methodSelector", + "type": "bytes4" + }, + { + "internalType": "string", + "name": "rawFormula", + "type": "string" + }, + { + "internalType": "bytes", + "name": "formula", + "type": "bytes" + } + ], + "internalType": "struct QuestionType.Question", + "name": "question", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getQuestionGroup", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + } + ], + "internalType": "struct GroupType.Group", + "name": "questionGroup", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getQuestionGroupsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getQuestionsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getUserGroup", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "address", + "name": "groupAddress", + "type": "address" + }, + { + "internalType": "enum UserGroup.Type", + "name": "groupType", + "type": "uint8" + } + ], + "internalType": "struct UserGroup.Group", + "name": "group", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getUserGroupsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getUserVote", + "outputs": [ + { + "internalType": "enum VM.Vote", + "name": "descision", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getUserVoteWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "weight", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + } + ], + "name": "getVoting", + "outputs": [ + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTime", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "starterGroupId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "starterAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "enum BallotType.BallotStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "votingData", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_votingId", + "type": "uint256" + } + ], + "name": "getVotingResult", + "outputs": [ + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVotingsAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isProject", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "isUserReturnTokens", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revoke", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_status", + "type": "bool" + } + ], + "name": "setActiveStatus", + "outputs": [ + { + "internalType": "bool", + "name": "changed", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endBlock", + "type": "uint256" + }, + { + "internalType": "enum VM.Vote", + "name": "result", + "type": "uint8" + } + ], + "internalType": "struct IZeroOne.MetaData", + "name": "_metaData", + "type": "tuple" + }, + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "setGroupAdmin", + "outputs": [ + { + "internalType": "uint256", + "name": "ballotId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_id", + "type": "uint256" + }, + { + "internalType": "string", + "name": "_name", + "type": "string" + } + ], + "name": "setQuestionGroupName", + "outputs": [ + { + "internalType": "bool", + "name": "changed", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum VM.Vote", + "name": "_descision", + "type": "uint8" + } + ], + "name": "setVote", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "starterGroupId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTime", + "type": "uint256" + }, + { + "internalType": "address", + "name": "starterAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "questionId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct BallotList.BallotSimple", + "name": "_votingPrimary", + "type": "tuple" + } + ], + "name": "startVoting", + "outputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "submitVoting", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_group", + "type": "address" + }, + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_newVoteWeight", + "type": "uint256" + } + ], + "name": "updateUserVote", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/src/contracts/libs/QuestionGroups.sol b/src/contracts/libs/QuestionGroups.sol index 06012284..4b37c8d6 100644 --- a/src/contracts/libs/QuestionGroups.sol +++ b/src/contracts/libs/QuestionGroups.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library QuestionGroups { diff --git a/src/contracts/libs/Questions.sol b/src/contracts/libs/Questions.sol index 6d66d600..8242f04b 100644 --- a/src/contracts/libs/Questions.sol +++ b/src/contracts/libs/Questions.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library Questions { diff --git a/src/contracts/libs/UserGroups.sol b/src/contracts/libs/UserGroups.sol index d283237e..41408b3c 100644 --- a/src/contracts/libs/UserGroups.sol +++ b/src/contracts/libs/UserGroups.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library UserGroups { diff --git a/src/contracts/libs/Votings.sol b/src/contracts/libs/Votings.sol index cba423c8..7658f1aa 100644 --- a/src/contracts/libs/Votings.sol +++ b/src/contracts/libs/Votings.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5; +pragma solidity ^0.5.15; library Votings { diff --git a/src/contracts/project.sol b/src/contracts/project.sol index 85f98ad3..57653a11 100644 --- a/src/contracts/project.sol +++ b/src/contracts/project.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.5; +pragma solidity ^0.5.15; contract UsingERC20 { diff --git a/src/contracts/sysQuestions.json b/src/contracts/sysQuestions.json index 67343d14..3c62e032 100644 --- a/src/contracts/sysQuestions.json +++ b/src/contracts/sysQuestions.json @@ -1,128 +1,80 @@ { - "1": { - "id": 1, - "group": 1, + "0": { + "groupId": 0, "name": "Добавить Вопрос", - "caption": "Добавление нового вопроса", - "time": 5, - "method": "0x686c52c4", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ + "description": "Добавление нового вопроса", + "timeLimit": 300, + "methodSelector": "0x5fe495c3", + "paramNames": [ "GroupId", - "uint", "Name", - "string", "Caption", - "string", "Time", - "uint", "MethodSelector", - "bytes4", "Formula", + "paramNames", + "paramTypes", + "Target" + ], + "paramTypes": [ + "uint", + "string", + "string", + "uint", + "bytes4", "string", - "parameters", - "bytes32[]" + "string[]", + "string[]", + "address" ], - "hints": { - "0": { - "desc": "Id группы вопросов, к которому он будет принадлежать", - "example": "1 - системные вопросы, 2 - вопросы для группы владельцев кастомных токенов" - }, - "1": { - "desc": "Название вопроса", - "example": "Уничтожить проект" - }, - "2": { - "desc": "Описание вопроса", - "example": "Уничтожение всего проекта, так как дальнейшее существование потеряло смысл" - }, - "3": { - "desc": "Время, в течении которого можно будет проголосовать после того, как будет начато голосование по данному вопросу (в минутах)", - "example": "10" - }, - "4": { - "desc": "Селектор метода в контракте, который будет вызываться в случае положительного исхода голосования", - "example": "0xcaF1deb4" - }, - "5": { - "desc": "Формула, по которой будут подсчитываться результаты голосования", - "example": "(group (Owners) => condition (positive >= 20% of all)) \r\n (group (Designers) => condition (quorum >= 20%))" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, - "2": { - "id": 2, - "group": 1, + "1": { + "groupId": 0, "name": "Подключить группу пользователей", - "caption": "Подключить новую группу пользователей для участия в голосованиях", - "time": 5, - "method": "0x952b627c", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ + "description": "Подключить новую группу пользователей для участия в голосованиях", + "timeLimit": 300, + "methodSelector": "0x6b13e1e0", + "paramNames": [ "Name", - "string", "Address", + "Type" + ], + "paramTypes": [ + "string", "address", - "Type", - "string" + "uint8" ], - "hints": { - "0": { - "desc": "Название группы пользователей", - "example": "Дизайнеры" - }, - "1": { - "desc": "Адрес контракта, в котором находятся токены, распределенные среди участников группы", - "example": "0х0000000000000000000000000000000000000000" - }, - "2": { - "desc": "Тип контракта данной группы (ERC20 для контракта токенов ERC20, Custom для контракта токенов, созданного в этом приложении)", - "example": "ERC20, Custom" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, - "3": { - "id": 3, - "group": 1, + "2": { + "groupId": 0, "name": "Добавить группу вопросов", - "caption": "Добавить новую группу вопросов", - "time": 5, - "method": "0xdf831328", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ - "Name", + "description": "Добавить новую группу вопросов", + "timeLimit": 300, + "methodSelector": "0xb9253b2b", + "paramNames": [ + "Name" + ], + "paramTypes": [ "string" ], - "hints": { - "0": { - "desc": "Имя для группы вопросов", - "example": "Административные" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" }, - "4": { - "id": 4, - "group": 1, + "3": { + "groupId": 0, "name": "Установить администратора группы", - "caption": "Установка администратора в группе кастомных токенов", - "time": 5, - "method": "0x3cc0a953", - "formula": "(group (Owners) => condition (positive >= 20% of all))", - "parameters": [ - "Group", + "description": "Установка администратора в группе кастомных токенов", + "timeLimit": 300, + "methodSelector": "0xa589e384", + "paramNames": [ + "Group Address", + "New Admin Address" + ], + "paramTypes": [ "address", - "Admin address", "address" ], - "hints": { - "0": { - "desc": "Aдрес группы, в которой произойдет установка администратора", - "example": "0х0000000000000000000000000000000000000000" - }, - "1": { - "desc": "Адрес пользователя, который получит администраторские привелегии", - "example": "0х0000000000000000000000000000000000000000" - } - } + "rawFormula": "erc20{%s}->conditions{quorum>50%, positive>50% of all}" } } \ No newline at end of file diff --git a/src/electron.js b/src/electron.js index 59301c66..07d8a400 100644 --- a/src/electron.js +++ b/src/electron.js @@ -1,10 +1,52 @@ -const { app, BrowserWindow } = require('electron'); -const electronLocalshortcut = require('electron-localshortcut'); +const { + app, BrowserWindow, shell, ipcMain, dialog, +} = require('electron'); +const electronLocalShortcut = require('electron-localshortcut'); const isDev = require('electron-is-dev'); const path = require('path'); +const fs = require('fs'); +const solc = require('solc'); +const linker = require('solc/linker'); + +require.extensions['.sol'] = function (module, filename) { + module.exports = fs.readFileSync(filename, 'utf8'); +}; + let mainWindow; +let loadingScreen; +/** + * + */ +function createLoadingScreen() { + loadingScreen = new BrowserWindow({ + minWidth: 539, + minHeight: 539, + width: 539, + height: 539, + center: true, + backgroundColor: '#fff', + webPreferences: { + nodeIntegration: true, + webSecurity: false, + }, + frame: false, + skipTaskbar: true, + resizable: false, + alwaysOnTop: false, + }); + loadingScreen.setResizable(false); + loadingScreen.loadURL(`file://${__dirname}/splash.html`); + // eslint-disable-next-line no-return-assign + loadingScreen.on('closed', () => (loadingScreen = null)); + loadingScreen.webContents.on('did-finish-load', () => { + loadingScreen.show(); + }); +} +/** + * + */ function createWindow() { mainWindow = new BrowserWindow({ useContentSize: true, @@ -15,19 +57,86 @@ function createWindow() { webPreferences: { nodeIntegration: true, }, + // show to false mean than the window will proceed with its + // lifecycle, but will not render until we will show it up + show: false, }); mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`); - // eslint-disable-next-line no-unused-expressions - isDev - ? process.env.NODE_ENV = 'production' - : process.env.NODE_ENV = 'development'; + + process.env.NODE_ENV = isDev + ? 'production' + : 'development'; + + mainWindow.setMenu(null); + + // eslint-disable-next-line no-return-assign mainWindow.on('closed', () => mainWindow = null); - electronLocalshortcut.register(mainWindow, 'F12', () => { + + mainWindow.webContents.on('new-window', (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }); + + electronLocalShortcut.register(mainWindow, 'F12', () => { mainWindow.webContents.toggleDevTools(); }); + + // keep listening on the did-finish-load event, when the mainWindow content has loaded + mainWindow.webContents.on('did-finish-load', () => { + // then close the loading screen window and show the main window + if (loadingScreen) { + loadingScreen.close(); + } + mainWindow.show(); + }); + + ipcMain.on('config-problem', (event, filePath) => { + dialog.showErrorBox('File reading error', `File ${filePath} is corrupted, please check it`); + loadingScreen.close(); + }); + + ipcMain.on('change-language:request', ((event, value) => { + mainWindow.webContents.send('change-language:confirm', value); + })); + + ipcMain.on('compile-request', ((event, input) => { + const { contract, type } = input; + const data = { + language: 'Solidity', + sources: { + 'test.sol': { + content: contract, + }, + }, + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + outputSelection: { + '*': { + '*': ['*'], + }, + }, + }, + }; + const output = JSON.parse(solc.compile(JSON.stringify(data))); + if (type === 'ZeroOne') { + const contracts = { + ZeroOne: output.contracts['test.sol'][type], + ZeroOneVM: output.contracts['test.sol'].ZeroOneVM, + }; + mainWindow.webContents.send('contract-compiled', contracts); + } else { + mainWindow.webContents.send('contract-compiled', output.contracts['test.sol'][type]); + } + })); } -app.on('ready', createWindow); +app.on('ready', () => { + createLoadingScreen(); + createWindow(); +}); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { diff --git a/src/i18n.js b/src/i18n.js index e6c87b28..ad3326da 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,5 +1,6 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; +import moment from 'moment'; import headingsEn from './locales/ENG/headings'; import headingsRu from './locales/RUS/headings'; import explanationsEn from './locales/ENG/explanations'; @@ -14,6 +15,9 @@ import errorsEn from './locales/ENG/errors'; import errorsRu from './locales/RUS/errors'; import dialogsEn from './locales/ENG/dialogs'; import dialogsRu from './locales/RUS/dialogs'; +import 'moment/locale/ru'; +import 'moment/locale/en-gb'; +import { getCorrectMomentLocale } from './utils/Date'; const resources = { ENG: { @@ -49,6 +53,8 @@ i18n nsMode: 'default', useSuspense: false, }, + }, () => { + moment.locale(getCorrectMomentLocale(i18n.language)); }); window.i18n = i18n; export default i18n; diff --git a/src/index.js b/src/index.js index 1bb212a8..240cc737 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable no-unused-vars */ import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'mobx-react'; @@ -8,13 +10,28 @@ import './i18n'; import './assets/styles/style.scss'; -const { userStore, appStore, dialogStore } = rootStore; +const { ipcRenderer } = window.require('electron'); +window.ipcRenderer = ipcRenderer; + +const { + userStore, + appStore, + dialogStore, + membersStore, + projectStore, + notificationStore, + configStore, +} = rootStore; render( diff --git a/src/locales/ENG/buttons.js b/src/locales/ENG/buttons.js index 7303b775..9c5299f8 100644 --- a/src/locales/ENG/buttons.js +++ b/src/locales/ENG/buttons.js @@ -17,5 +17,23 @@ const buttons = { withTokens: 'Connect contract and create project', withoutTokens: 'Create new contract and project', toWallets: 'To wallets', + createQuestion: 'Create question', + createQuestionGroup: 'Create group of questions', + startNewVoting: 'Start new voting', + transfer: 'Transfer', + designateGroupAdministrator: 'start voting for appointment as group administrator', + vote: 'Vote', + startNewVote: 'Start a new vote', + start: 'Start', + nextStep: 'Next step', + addParameter: '+ Add parameter', + apply: 'Apply', + completeTheVote: 'Complete the vote', + pickUpTokens: 'pick up tokens', + pickUpTokensCapital: 'Pick up tokens', + clear: 'Clear', + retry: 'Retry', + saveAndReload: 'Save and reload app', + saveWithoutReload: 'Save without reload', }; export default buttons; diff --git a/src/locales/ENG/dialogs.js b/src/locales/ENG/dialogs.js index bd2146a9..fba093c2 100644 --- a/src/locales/ENG/dialogs.js +++ b/src/locales/ENG/dialogs.js @@ -1,10 +1,16 @@ const dialog = { definetelyAgree: 'Do you definitely agree?', + definetelyReject: 'Do you definitely reject?', agreedMessage: 'You agreed', rejectMessage: 'You voted against', transferInProgress: 'Token transfer in progress', someTimeText: 'It will take some time', tokenTransferSuccess: 'Tokens successfully transferred!', + tokenTransfer: 'Transfer token', + tokenTransferError: 'Transfer error', + createAGroupOfQuestions: 'Create a group of questions', + completionOfVoting: 'Completion of voting', + ERC20TokensUsed: 'This vote uses ERC20 tokens', }; export default dialog; diff --git a/src/locales/ENG/errors.js b/src/locales/ENG/errors.js index 73888097..7e341be9 100644 --- a/src/locales/ENG/errors.js +++ b/src/locales/ENG/errors.js @@ -6,6 +6,8 @@ const errors = { emptyFields: 'Form have empty fields', lowBalance: 'Your balance is to low for this', hostUnreachable: 'Host is unreachable, please check your internet connection and try again', + transferIfNotAdmin: 'You cannot tranfer tokens from other wallets', + transferLocked: 'You cannot send custom tokens', }; export default errors; diff --git a/src/locales/ENG/explanations.js b/src/locales/ENG/explanations.js index 6dc9bd22..557ebd8d 100644 --- a/src/locales/ENG/explanations.js +++ b/src/locales/ENG/explanations.js @@ -6,20 +6,21 @@ const explanations = { symbol: 'a special character', length: 'minimum 6 char length', }, - seed: ['The phrase gives you complete control over your account', 'Be sure to write down and do’t tell it to anyone'], + seed: ['The phrase gives you complete control over your account', 'Be sure to write down and don’t tell it to anyone'], project: { name: 'The project title is set by you and appears in the project selection page', address: 'The address is provided by the creator of the project' }, token: { left: { wallet: ['The contract will be uploaded to the network', ' by a wallet:'], balance: 'Balance: ', - tokens: ['Tokens will be credited to this wallet', ' Тhey can be distributed later'], + tokens: ['Tokens will be credited to this wallet', ' They can be distributed later'], }, right: { symbol: 'A token symbol is its abbreviated name. For example: ETH, BTC, etc.', count: 'The total number of tokens is set by you. They can be distributed among the project participants later.', }, }, + freeze: 'On contract compiling app will freeze on several seconds, please be patient', }; export default explanations; diff --git a/src/locales/ENG/fields.js b/src/locales/ENG/fields.js index 80666846..85dd92ef 100644 --- a/src/locales/ENG/fields.js +++ b/src/locales/ENG/fields.js @@ -8,6 +8,34 @@ const placeholders = { quantity: 'Quantity', projectTitle: 'Project title', contractAddress: 'Enter contract address', + address: 'Address', + countTokens: 'Count tokens', + question: 'Question', + status: 'Status', + date: 'Date', + descision: 'Descision', + durationInBlocks: 'Duration of circulation in blocks', + questionTitle: 'Question title', + questionLifeTime: 'Question lifetime', + methodSelector: 'Function selector', + parameter: 'Parameter', + enterNewParameterName: 'Enter new parameter name', + votingFormula: 'Voting formula', + questionDescription: 'Question description', + selectParameterType: 'Select parameter type', + titleGroupQuestions: 'Title group questions', + descriptionOrComment: 'Description or comment', + dateFrom: 'Date from', + dateTo: 'Date to', + questionGroup: 'Question group', + targetContractAddress: 'Target Contract Address', + functionSelector: 'Function selector', + nodeUrl: 'Node URL', + chooseTheQuestion: 'Choose the question', + minGasPrice: 'Min. gas price, Gwei', + maxGasPrice: 'Max. gas price, Gwei', + interval: 'Data update interval, sec', + selectQuestionGroup: 'Select group of questions', }; export default placeholders; diff --git a/src/locales/ENG/headings.js b/src/locales/ENG/headings.js index 9df808df..8c9452cb 100644 --- a/src/locales/ENG/headings.js +++ b/src/locales/ENG/headings.js @@ -2,25 +2,32 @@ const headings = { login: { heading: 'Sign In', subheading: 'Get ready for a new era of voting' }, logging: { heading: 'Sighning in', subheading: 'It does not take much time' }, projects: { heading: 'Project selection', subheading: 'Chose a project or create a new one' }, - addingProject: { heading: 'Adding a project', subheading: 'Create a new one or connect already existing' }, - passwordCreation: { heading: 'Password creation', subheading: 'Will be used to enter the wallet and transactions confirmation' }, + addingProject: { heading: 'Adding a project', subheading: ['Create a new one or connect', 'already existing'] }, + passwordCreation: { heading: 'Password creation', subheading: ['Will be used to enter the wallet', 'and transactions confirmation'] }, showSeed: { heading: 'Reserve phrase', subheading: 'Will be used for password recovery' }, seedCheck: { heading: 'Reserve phrase check', subheading: ['Checking the seed', 'Enter the phrase u wrote down'] }, - сonnectProject: { heading: 'Project connecting', subheading: 'Create a new one or connect already existing' }, + connectProject: { heading: 'Project connecting', subheading: 'Create a new one or connect already existing' }, projectChecking: { heading: 'Checking the project address', subheading: 'It does not take much time' }, projectConnected: { heading: 'Project is connected!', subheading: 'Now you can start working with it or choose another project' }, newProject: { heading: 'Creating a new project', subheading: 'Choose the suitable option ' }, - existingTokens: { heading: 'Connecting contarct of tokens', subheading: 'The owner of this contract will be considered the owner of the created project' }, + existingTokens: { heading: 'Connecting contract of tokens', subheading: 'The owner of this contract will be considered the owner of the created project' }, checkingTokens: { heading: 'Checking the project address', subheading: 'It does not take much time' }, - checkingTokensConfirm: { heading: 'The contract is checked', subheading: 'Verify the data before proceeding' }, - newTokens: { heading: 'Token creating', subheading: '' }, + checkingTokensConfirm: { heading: 'The contract is checked', subheading: ['Verify the data', 'before proceeding'] }, + newTokens: { heading: 'Creating token ', subheading: '' }, tokensCreating: { heading: 'Creating ERC20 tokens', subheading: 'It will take some time' }, tokensCreated: { heading: 'Tokens are created!', subheading: 'Now you need to create a project' }, - projectCreating: { heading: 'Creating a project', subheading: 'The project contract will be uploaded to the network by a wallet' }, + projectCreating: { heading: 'Creating a project', subheading: ['The project contract will be uploaded', 'to the network by a wallet'] }, uploadingProject: { heading: 'Uploading a contract', subheading: 'It can take up to 5 minutes' }, - projectCreated: { heading: 'Contract is created!', subheading: 'Now you can start working with it or choose another project' }, - walletRestored: { heading: 'Wallet restoring', subheading: 'Wallet is succesfully restored' }, - walletCreated: { heading: 'Wallet creating', subheading: 'Wallet is succesfully created' }, - walletRestoring: { heading: 'Wallet restoring', subheading: 'Check the data accuracy before continuing' }, + projectCreated: { heading: 'Contract is created!', subheading: ['Now you can start working with', 'it or choose another project'] }, + walletRestored: { heading: 'Wallet restoring', subheading: 'Wallet is successfully restored' }, + walletCreated: { heading: 'Wallet creating', subheading: 'Wallet is successfully created' }, + walletRestoring: { heading: 'Wallet restoring', subheading: ['Check the data accuracy', 'before continuing'] }, + nodeConnection: 'Node connection', + creatingAndUpload: 'Create and upload contract', + interfaceLanguage: 'Interface language', + sendingTransaction: 'Sending transaction', + successfullTransaction: 'Transaction sended successfully', + failedTransaction: { heading: 'Transaction sending failed', subheading: 'Please, try again' }, + other: 'Other', }; export default headings; diff --git a/src/locales/ENG/other.js b/src/locales/ENG/other.js index 8395020a..eec6c710 100644 --- a/src/locales/ENG/other.js +++ b/src/locales/ENG/other.js @@ -6,6 +6,7 @@ const other = { sending: 'Sendind', txHash: 'Awaiting hash', txReceipt: 'Awaiting receipt', + txSigning: 'Transaction signing', questionsUploading: 'Uploading questions', walletAddress: 'Wallet', balance: 'Balance', @@ -14,7 +15,79 @@ const other = { withTokens: 'If you have ERC20 tokens', withoutTokens: "If you don't have ERC20 tokens", yourBalance: 'Your balance', + notEnoughTokens: 'Perhaps there are not enough tokens', + enterPassForConfirm: 'Enter your password to confirm your decision.', + connectOuterGroupToProject: 'Connect an external group of participants to the "{{project}}" project', + noData: 'No data', + noDataAdmins: 'Unfortunately, no one can see the administrators. \nSuch is the secret design and limitation of the technologies used.', + weightVote: 'Weight vote', + page: 'Page', + goTo: 'Go to', + voterList: 'Voter list', + agree: 'Agree', + against: 'Against', + startANewVote: 'Start a new vote', + newVoteEmptyStateText: 'As soon as you select a question, all information on it will appear here.', + selectQuestionGroup: 'Select a question group to start creating a new question.', + createANewQuestion: 'Create a new question', + basicInfo: 'Basic information', + stepProgress: 'Step {{current}} from {{total}}', RUS: 'Русский', ENG: 'English', + start: 'Start', + end: 'End', + timeLeft: 'Time left', + dateInFormat: '{{date}} in {{time}}', + decision: 'Decision', + decisionIsMade: 'Decision is made', + dateOfApplication: 'Date of application', + durationInBlocks: 'Duration of circulation in blocks', + newAddressContract: 'New address contract', + votingFormula: 'Voting formula', + iAgree: 'I agree', + iAmAgainst: 'I\'m against', + statistics: 'Statistics', + didNotVote: 'Did not vote', + everyoneVoted: 'Everyone voted', + voteLaunchDescription: 'A vote will be launched to create a question; if the decision is positive, a question will be created', + voteLaunchAdminDescription: 'A vote will be launched among administrators to create a group, if the decision is positive, the group will be created', + createGroupQuestionsDescription: 'Project participants can then be divided into groups \n \nFor example: Designers who are only in the Design group will be able to vote on issues of only this group', + createNameForTheGroupQuestions: 'Come up with a name that best reflects the essence of the group', + endOfVoteRequired: 'End \nof vote \nrequired', + pros: 'Pros', + cons: 'Cons', + notAccepted: 'Not accepted', + votingInProgress: 'Voting in progress', + youVoted: 'You voted', + votingDone: 'Voting done', + decisionWasMade: 'The decision was made', + yourDecision: 'Your decision', + totalVoted: 'Total voted', + theVoteLasted: 'The vote lasted', + voting: 'Voting', + questions: 'Questions', + members: 'Members', + votingCompletedButTokensInContract: 'Voting is completed, but your tokens are still in contract.', + youVotedAndTokensInContract: 'You voted and your tokens are in the contract. To cancel the voice', + pickUpTokens: 'Pick up tokens', + sendingTransaction: 'Sending transaction', + parameters: 'Parameters', + select: 'Select', + hintFunctionalityNotAvailable: 'During active voting, this <1/> functionality is not available.', + outOf: 'out of', + clickOnAddressForCopy: 'Click on address for copy', + copied: 'Copied', + groups: 'Groups', + tokens: 'Tokens', + privateBalance: 'Private balance', + toggleUser: 'Toggle user', + returnTokensFirst: 'Return tokens first', + selectorNonexistentFunctionDescription: 'If you specify the selector of a nonexistent function, the voting results will not be applied', + erc20ListIsNotViewable: 'ERC20 tokens are arranged so \n that the voter list is not viewable', + votingListIsEmpty: 'No polls created <1/> They will be displayed here later', + noVotingFilterMatches: 'No voting matches <1/> the selected filter', + noQuestionsInThisGroup: 'No questions have been <1/> created in this group yet', + reloadNotification: 'Will apply on next launch', + loadingToggleDisabled: "While all data is not loaded, you can't change user", }; export default other; diff --git a/src/locales/RUS/buttons.js b/src/locales/RUS/buttons.js index 0dba007a..7713407b 100644 --- a/src/locales/RUS/buttons.js +++ b/src/locales/RUS/buttons.js @@ -17,5 +17,24 @@ const buttons = { withTokens: 'Подключить контракт и создать проект', withoutTokens: 'Создать новые токены и проект', toWallets: 'К выбору кошелька', + createQuestion: 'Создать вопрос', + createQuestionGroup: 'Создать группу вопросов', + startNewVoting: 'Начать новое голосование', + transfer: 'Перевести', + designateGroupAdministrator: 'начать голосование за назначение администратором группы', + vote: 'Голосовать', + startNewVote: 'Начать новое голосование', + start: 'Начать', + nextStep: 'Следующий шаг', + addParameter: '+ Добавить параметр', + apply: 'Применить', + completeTheVote: 'Завершить голосование', + pickUpTokens: 'заберите токены', + pickUpTokensCapital: 'Заберите токены', + clear: 'Понятно', + retry: 'Попробовать снова', + saveAndReload: 'Сохранить и перезапустить', + saveWithoutReload: 'Сохранить без перезагрузки', + }; export default buttons; diff --git a/src/locales/RUS/dialogs.js b/src/locales/RUS/dialogs.js index 4a7ff65b..ed2d0720 100644 --- a/src/locales/RUS/dialogs.js +++ b/src/locales/RUS/dialogs.js @@ -1,10 +1,16 @@ const dialog = { definetelyAgree: 'Вы точно согласны?', + definetelyReject: 'Вы точно против?', agreedMessage: 'Вы выразили согласие', rejectMessage: 'Вы проголосовали против', transferInProgress: 'Переводим токены', someTimeText: 'Это займет некоторое время', tokenTransferSuccess: 'Токены успешно переведены!', + tokenTransfer: 'Перевести токены', + tokenTransferError: 'Ошибка перевода', + createAGroupOfQuestions: 'Создать группу вопросов', + completionOfVoting: 'Завершение голосования', + ERC20TokensUsed: 'В этом голосовании\n используются токены ERC20', }; export default dialog; diff --git a/src/locales/RUS/errors.js b/src/locales/RUS/errors.js index 6c54fb3d..d782ecd2 100644 --- a/src/locales/RUS/errors.js +++ b/src/locales/RUS/errors.js @@ -6,5 +6,7 @@ const errors = { emptyFields: 'Вы ввели не все данные, пожалуйста, введите все данные', lowBalance: 'Ваш баланс слишком мал для этого действия', hostUnreachable: 'Соединение с хостом потеряно, проверьте доступ к интернету и повторите еще раз', + transferIfNotAdmin: 'Вы не можете отправлять токены с других кошельков', + transferLocked: 'Вы не можете отправлять кастомные токены', }; export default errors; diff --git a/src/locales/RUS/explanations.js b/src/locales/RUS/explanations.js index bb55eede..d05aa5f2 100644 --- a/src/locales/RUS/explanations.js +++ b/src/locales/RUS/explanations.js @@ -23,6 +23,7 @@ const explanations = { count: 'Общее число токенов задаете вы. В дальнейшем их можно будет распределить между участниками проекта', }, }, + freeze: 'При загрузке контракта приложение зависнет на несколько секунд, будьте терпеливы', }; export default explanations; diff --git a/src/locales/RUS/fields.js b/src/locales/RUS/fields.js index 81c14fb6..f0df92b2 100644 --- a/src/locales/RUS/fields.js +++ b/src/locales/RUS/fields.js @@ -8,6 +8,34 @@ const placeholders = { quantity: 'Количество', projectTitle: 'Придумайте название проекта', contractAddress: 'Введите адрес контракта', + address: 'Адрес кошелька', + countTokens: 'Количество токенов', + question: 'Вопрос', + status: 'Статус', + date: 'Дата', + descision: 'Решение', + durationInBlocks: 'Продолжительность тиража в блоках', + questionTitle: 'Название вопроса', + questionLifeTime: 'Время жизни вопроса', + methodSelector: 'Селектор функции', + parameter: 'Параметр', + enterNewParameterName: 'Введите название нового параметра', + votingFormula: 'Формула голосования', + questionDescription: 'Описание вопроса', + selectParameterType: 'Выберите тип параметра', + titleGroupQuestions: 'Название группы вопросов', + descriptionOrComment: 'Описание или комментарий', + dateFrom: 'Дата с', + dateTo: 'Дата по', + questionGroup: 'Группа вопросов', + targetContractAddress: 'Адрес целевого контракта', + functionSelector: 'Function selector', + nodeUrl: 'URL ноды', + chooseTheQuestion: 'Выберите вопрос', + minGasPrice: 'Цена газа MIN, Gwei', + maxGasPrice: 'Цена газа MAX, Gwei', + selectQuestionGroup: 'Выберите группу вопросов', + }; export default placeholders; diff --git a/src/locales/RUS/headings.js b/src/locales/RUS/headings.js index c8abc999..bee57029 100644 --- a/src/locales/RUS/headings.js +++ b/src/locales/RUS/headings.js @@ -1,12 +1,12 @@ const headings = { login: { heading: 'Вход в систему', subheading: 'Приготовьтесь к новой эре в сфере голосования' }, logging: { heading: 'Выполняется вход', subheading: 'Это не займет много времени' }, + projects: { heading: 'Выбор проекта', subheading: 'Выберите проект или создайте новый' }, + addingProject: { heading: 'Добавление проекта', subheading: ['Cоздайте новый или подключите', 'уже существующий'] }, passwordCreation: { heading: 'Создание пароля', subheading: ['Будет использоваться для входа в', 'кошелек и подтверждения транзакций'] }, showSeed: { heading: 'Резервная фраза', subheading: 'Нужна для восстановления пароля' }, seedCheck: { heading: 'Проверка резервной фразы', subheading: ['Проверяется фраза', 'Введите фразу, которую вы записали'] }, - projects: { heading: 'Выбор проекта', subheading: 'Выберите проект или создайте новый' }, - addingProject: { heading: 'Добавление проекта', subheading: ['Cоздайте новый или подключите', 'уже существующий'] }, - сonnectProject: { heading: 'Подключить проект', subheading: 'Подключите уже существующий проект' }, + connectProject: { heading: 'Подключить проект', subheading: 'Подключите уже существующий проект' }, projectChecking: { heading: 'Проверяем адрес проекта', subheading: 'Это не займет много времени' }, projectConnected: { heading: 'Проект успешно подключен!', subheading: 'Теперь можно начать работу с ним или выбрать другой проект' }, newProject: { heading: 'Создание нового проекта', subheading: 'Выберите подходящий вам вариант' }, @@ -22,5 +22,12 @@ const headings = { walletRestored: { heading: 'Восстановление кошелька', subheading: 'Кошелек успешно восстанолен' }, walletCreated: { heading: 'Создание кошелька', subheading: 'Кошелек успешно создан' }, walletRestoring: { heading: 'Процесс восстановления кошелька', subheading: ['Проверьте правильность данных', ' перед тем, как продолжить'] }, + nodeConnection: 'Подключение ноды', + creatingAndUpload: 'Создание или загрузка контрактов', + interfaceLanguage: 'Язык интерфейса', + sendingTransaction: 'Отправка транзакции', + successfullTransaction: 'Транзакция успешно отправлена', + failedTransaction: { heading: 'Ошибка отправки транзакции', subheading: 'Пожалуйста, повторите попытку' }, + other: 'Прочее', }; export default headings; diff --git a/src/locales/RUS/other.js b/src/locales/RUS/other.js index 6234e3ae..512cc5d6 100644 --- a/src/locales/RUS/other.js +++ b/src/locales/RUS/other.js @@ -6,6 +6,7 @@ const other = { sending: 'Отправка', txHash: 'Получение хэша', txReceipt: 'Получение чека', + txSigning: 'Подпись транзакции', questionsUploading: 'Загрузка вопросов', walletAddress: 'Кошелек', balance: 'Баланс', @@ -14,7 +15,79 @@ const other = { withTokens: 'Если есть токены ERC20', withoutTokens: 'Если токенов ERC20 нет', yourBalance: 'Ваш баланс', + notEnoughTokens: 'Возможно не хватает токенов', + enterPassForConfirm: 'Введите пароль, чтобы подтвердить свое решеине', + connectOuterGroupToProject: 'Подключить внешнюю группу участников к проекту "{{project}}"', + noData: 'Нет данных', + noDataAdmins: 'К сожалению никто не может увидеть администраторов. \nТаков тайный замысел и ограничение, используемых технологий.', + weightVote: 'Вес голоса', + page: 'Страница', + goTo: 'Перейти', + voterList: 'Список проголосавших ', + agree: 'Согласны', + against: 'Против', + startANewVote: 'Начать новое голосование', + newVoteEmptyStateText: 'Как только вы выберите вопрос здесь появится вся информация по нему', + selectQuestionGroup: 'Выберите группу вопросов, чтобы приступить к созданию нового вопроса', + createANewQuestion: 'Создать вопрос', + basicInfo: 'Основная информация', + stepProgress: 'Шаг {{current}} из {{total}}', RUS: 'Русский', ENG: 'English', + start: 'Начало', + end: 'Конец', + timeLeft: 'Осталось', + dateInFormat: '{{date}} в {{time}}', + decision: 'Решение', + decisionIsMade: 'Решение принято', + dateOfApplication: 'Дата применения', + durationInBlocks: 'Продолжительность тиража в блоках', + newAddressContract: 'Новый адрес контракта', + votingFormula: 'Формула голосования', + iAgree: 'Я согласен', + iAmAgainst: 'Я против', + statistics: 'Статистика', + didNotVote: 'Не проголосовали', + everyoneVoted: 'Проголосовали все', + voteLaunchDescription: 'Будет запущено голосование по созданию вопроса, при положительном решении вопрос будет создан', + voteLaunchAdminDescription: 'Будет запущено голосование среди администраторов по созданию группы, при положительном решении группа будет создана', + createGroupQuestionsDescription: 'Участников проекта затем можно распределить по группам \n \nНапример: Дизайнеры, находящиеся только в группе “Дизайн” смогут голосовать по вопросам только этой группы', + createNameForTheGroupQuestions: 'Придумайте название, которое лучше всего отражает суть группы', + endOfVoteRequired: 'Требуется \nзавершение \nголосования', + pros: 'За', + cons: 'Против', + notAccepted: 'Не принято', + votingInProgress: 'Идет голосование', + youVoted: 'Вы проголосовали', + votingDone: 'Голосование проведено', + decisionWasMade: 'Принято решение', + yourDecision: 'Ваш голос', + totalVoted: 'Проголосовало', + theVoteLasted: 'Голосование длилось', + voting: 'Голосования', + questions: 'Вопросы', + members: 'Участники', + votingCompletedButTokensInContract: 'Голосование завершено, но ваши токены все еще находятся в контракте.', + youVotedAndTokensInContract: 'Вы проголосовали и ваши токены находятся в контракте. Для отмены голоса', + pickUpTokens: 'Забрать токены', + sendingTransaction: 'Отправка транзакции', + parameters: 'Параметры', + select: 'Выберите', + hintFunctionalityNotAvailable: 'Во время активного голосования <1/> этот функционал недоступен', + outOf: 'из', + clickOnAddressForCopy: 'Нажмите на адрес кошелька, чтобы скопировать', + copied: 'Скопировано', + groups: 'Группы', + tokens: 'Токены', + privateBalance: 'Личный баланс', + toggleUser: 'Сменить пользователя', + returnTokensFirst: 'Сначала верните токены', + selectorNonexistentFunctionDescription: 'Если укажете селектор несуществующей функции, то результаты голосования не будут применены', + erc20ListIsNotViewable: 'ERC20 токены устроены так, что список\n проголосовавших недоступен для просмотра', + votingListIsEmpty: 'Не создано ни одного голосования <1/> В дальнейшем они будут отображены здесь', + noVotingFilterMatches: 'Выбранному фильтру не <1/> соответствует ни одно голосование', + noQuestionsInThisGroup: 'В этой группе пока не <1/> создано ни одного вопроса', + reloadNotificaion: 'Применятся при следующем запуске', + loadingToggleDisabled: 'Вы не можете сменить пользователя, пока не загруженны данные', }; export default other; diff --git a/src/models/FormModel/index.js b/src/models/FormModel/index.js index f1e4bd9a..316ce621 100644 --- a/src/models/FormModel/index.js +++ b/src/models/FormModel/index.js @@ -2,15 +2,28 @@ import { extendObservable, action } from 'mobx'; import { Form } from 'mobx-react-form'; import dvr from 'mobx-react-form/lib/validators/DVR'; import plugins from '../../utils/Validator'; +import i18n from '../../i18n'; +import { languages } from '../../constants'; class ExtendedForm extends Form { constructor(data) { const { hooks } = data || {}; super(); extendObservable(this, { loading: false }); + Object.keys(hooks).forEach((hook) => { this.addHook(hook, hooks[hook]); }); + + this.addHook('onLangChange', () => { + this.fields.forEach((field) => { + field.set('placeholder', i18n.t(`fields:${field.label}`)); + }); + }); + window.ipcRenderer.on('change-language:confirm', (event, value) => { + this.fireHook('onLangChangeHook'); + window.validator.useLang(languages[value]); + }); } // eslint-disable-next-line class-methods-use-this diff --git a/src/services/ContractService/ContractService.js b/src/services/ContractService/ContractService.js index dd2a68e0..eaed616f 100644 --- a/src/services/ContractService/ContractService.js +++ b/src/services/ContractService/ContractService.js @@ -1,12 +1,17 @@ +/* eslint-disable no-console */ /* eslint-disable no-unused-vars */ -import browserSolc from 'browser-solc'; -import { BN } from 'ethereumjs-util'; -import { SOL_IMPORT_REGEXP, SOL_PATH_REGEXP, SOL_VERSION_REGEXP } from '../../constants'; +import * as linker from 'solc/linker'; +import { compile } from 'zeroone-translator'; +import { + SOL_IMPORT_REGEXP, + SOL_VERSION_REGEXP, + tokenTypes, +} from '../../constants'; import { fs, PATH_TO_CONTRACTS, path, } from '../../constants/windowModules'; -import Question from './entities/Question'; import readSolFile from '../../utils/fileUtils/index'; +import UserStore from '../../stores/UserStore/UserStore'; /** * Class for work with contracts @@ -15,12 +20,25 @@ class ContractService { constructor(rootStore) { this._contract = {}; this.rootStore = rootStore; - this.sysQuestions = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './sysQuestions.json'), 'utf8')); - this.ercAbi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ERC20.abi'))); + const pathToQuestions = path.join(PATH_TO_CONTRACTS, './sysQuestions.json'); + const pathToErcAbi = path.join(PATH_TO_CONTRACTS, './ERC20.abi'); + + try { + this.sysQuestions = JSON.parse(fs.readFileSync(pathToQuestions), 'utf8'); + } catch (e) { + alert(`Error while reading file ${pathToQuestions}, please check this.`); + } + + try { + this.ercAbi = JSON.parse(fs.readFileSync(pathToErcAbi)); + } catch (e) { + alert(`Error while reading file ${pathToErcAbi}, please check this.`); + } } /** * sets instance of contract to this._contract + * * @param {object} instance instance of contract created by Web3Service */ // eslint-disable-next-line consistent-return @@ -32,56 +50,90 @@ class ContractService { /** * compiles contracts and returning type of compiled contract, bytecode & abi + * * @param {string} type - ERC20 - if compiling ERC20 token contract, project - if project contract + * @param {string} password password * @returns {object} contains type of compiled contract, his bytecode and abi for deploying */ - compileContract(type) { + // eslint-disable-next-line class-methods-use-this + compileContract(type, password) { + const { rootStore: { Web3Service, userStore } } = this; + const { address } = userStore; + window.linker = linker; return new Promise((resolve, reject) => { - window.BrowserSolc.getVersions((sources, releases) => { - const version = releases['0.4.24']; - const contract = this.combineContract(type); - const contractName = type === 'ERC20' - ? ':ERC20' - : ':Voter'; - window.BrowserSolc.loadVersion(version, (compiler) => { - const compiledContract = compiler.compile(contract); - const contractData = compiledContract.contracts[contractName]; - if (contractData.interface !== '') { - const { bytecode, metadata } = contractData; - const { output: { abi } } = JSON.parse(metadata); - resolve({ type, bytecode, abi }); - } else reject(new Error('Something went wrong on contract compiling')); - }); + let bytecode; + let abi; + + const contract = this.combineContract(type); + window.ipcRenderer.send('compile-request', { contract, type }); + window.ipcRenderer.once('contract-compiled', async (event, compiledContract) => { + console.log(compiledContract); + if (type === 'ZeroOne') { + const { ZeroOne, ZeroOneVM } = compiledContract; + const { evm: { bytecode: { object: libraryBytecode } } } = ZeroOneVM; + const { evm: { bytecode: { object: ZeroOneBytecode } }, abi: ZeroOneABI } = ZeroOne; + const libTX = { data: `0x${libraryBytecode}` }; + await Web3Service.createTxData(address, libTX) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then(({ contractAddress }) => { + console.log(`library at ${contractAddress}`); + abi = ZeroOneABI; + const [link] = Object.keys(linker.findLinkReferences(ZeroOneBytecode)); + bytecode = linker.linkBytecode(ZeroOneBytecode, { [link]: contractAddress }); + }); + } else if (compiledContract.abi !== '') { + const { evm: { bytecode: { object } }, abi: contractAbi } = compiledContract; + abi = contractAbi; + bytecode = object; + } else reject(new Error('Something went wrong on contract compiling')); + + fs.writeFileSync(path.join(PATH_TO_CONTRACTS, `${type}.abi`), JSON.stringify(abi, null, '\t')); + resolve({ type, bytecode, abi }); }); }); } /** * reading all imports in main contract file and importing all files in one output file + * * @param {string} type type of project - ERC20 for ERC-20 tokens, Project for project contract * @returns {string} combined contracts */ // eslint-disable-next-line class-methods-use-this combineContract(type) { - const dir = type === 'ERC20' ? './' : './Voter/'; - const compiler = 'pragma solidity ^0.4.24;'; - const pathToMainFile = type === 'ERC20' - ? path.join(PATH_TO_CONTRACTS, `${dir}ERC20.sol`) - : path.join(PATH_TO_CONTRACTS, `${dir}Voter.sol`); + let dir; + const compiler = 'pragma solidity 0.6.1;'; + switch (type) { + case ('ERC20'): + dir = '../../node_modules/zeroone-contracts/contracts/__vendor__/'; + break; + case ('CustomToken'): + dir = '../../node_modules/zeroone-contracts/contracts/Token/'; + break; + case ('ZeroOne'): + dir = '../../node_modules/zeroone-contracts/contracts/ZeroOne/'; + break; + default: + break; + } + const pathToMainFile = path.join(PATH_TO_CONTRACTS, `${dir}${type}.sol`); const importedFiles = {}; let output = readSolFile(pathToMainFile, importedFiles); - output = output.replace(SOL_VERSION_REGEXP, compiler); - output = output.replace(/(calldata)/g, ''); + output = output.replace(SOL_VERSION_REGEXP, compiler).replace((SOL_IMPORT_REGEXP), ''); + // output = output.replace(/(calldata)/g, ''); return output; } /** - * Sendind transaction with contract to blockchain + * Sending transaction with contract to blockchain + * * @param {object} params parameters for deploying - * @param {array} params.deployArgs ERC20 - [Name, Symbol, Count], Project - [tokenAddress] + * @param {Array} params.deployArgs ERC20 - [Name, Symbol, Count], Project - [tokenAddress] * @param {string} params.bytecode bytecode of contract * @param {JSON} params.abi JSON interface of contract * @param {string} params.password password of user wallet @@ -92,31 +144,36 @@ class ContractService { }) { const { rootStore: { Web3Service, userStore } } = this; const { address } = userStore; - const maxGasPrice = 30000000000; const contract = Web3Service.createContractInstance(abi); - const txData = contract.deploy({ + const data = contract.deploy({ data: `0x${bytecode}`, arguments: deployArgs, }).encodeABI(); + const tx = { - data: txData, - gasLimit: 8000000, - gasPrice: maxGasPrice, + data, + from: userStore.address, + value: '0x0', }; - return new Promise((resolve) => { - Web3Service.createTxData(address, tx, maxGasPrice) + return new Promise((resolve, reject) => { + Web3Service.createTxData(address, tx) .then((formedTx) => userStore.singTransaction(formedTx, password)) .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) - .then((txHash) => resolve(txHash)); + .then((txHash) => { + userStore.getEthBalance(); + resolve(txHash); + }) + .catch((err) => reject(err)); }); } /** * checks erc20 tokens contract on totalSupply and symbol + * * @param {string} address address of erc20 contract - * @return {object} {totalSypply, symbol} + * @returns {object} {totalSypply, symbol} */ async checkTokens(address) { const { rootStore: { Web3Service }, ercAbi } = this; @@ -129,117 +186,397 @@ class ContractService { /** * checks is the address of contract + * * @param {string} address address of contract - * @return {Promise} Promise with function which resolves, if address is contract + * @returns {Promise} Promise with function which resolves, if address is contract */ - // eslint-disable-next-line class-methods-use-this checkProject(address) { const { rootStore: { Web3Service } } = this; return new Promise((resolve, reject) => { - Web3Service.web3.eth.getCode(address).then((bytecode) => { - if (bytecode === '0x') reject(); - resolve(bytecode); - }); + const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'))); + const contract = Web3Service.createContractInstance(abi); + contract.options.address = address; + contract.methods.getQuestionGroupsAmount().call() + .then((data) => resolve()) + .catch((err) => reject(err)); }); } /** * calling contract method + * * @param {string} method method, which will be called - * @param {string} from address of caller - * @param params parameters for method + * @param {any} params parameters for method + * @returns {object} data from method */ async callMethod(method, ...params) { const data = await this._contract.methods[method](...params).call(); return data; } + // TODO add correct js doc + /** + * Method create data for voting + * + * @returns {object} voting data + * @param votingQuestion + * @param votingGroupId + * @param votingData + */ + createVotingData(votingQuestion, votingGroupId, votingData) { + const { rootStore: { userStore, Web3Service: { web3: { eth: { abi } } } }, _contract } = this; + const votingInfo = { + starterGroupId: votingGroupId, + endTime: 0, + starterAddress: userStore.address, + questionId: votingQuestion, + data: votingData, + }; + // eslint-disable-next-line max-len + const data = { + // eslint-disable-next-line max-len + data: _contract.methods.startVoting(votingInfo).encodeABI(), + from: userStore.address, + value: '0x0', + to: _contract.options.address, + }; + return data; + } + /** * checks count of uploaded to contract questions and total count of system questions + * * @function * @returns {object} {countOfUploaded, totalCount} */ async checkQuestions() { - const countOfUploaded = await this._contract.methods.getCount().call(); + const countOfUploaded = await this._contract.methods.getQuestionsAmount().call(); const totalCount = Object.keys(this.sysQuestions).length; return ({ countOfUploaded, totalCount }); } /** * send question to created contract + * * @param {number} idx id of question; - * @return {Promise} Promise, which resolves on transaction hash + * @returns {Promise} Promise, which resolves on transaction hash */ async sendQuestion(idx) { + console.log(`question id = ${idx}`); + const { _contract: contract, rootStore } = this; const { Web3Service, userStore, - } = this.rootStore; - const sysQuestion = this.sysQuestions[idx]; - await this.fetchQuestion(idx).then((result) => { - if (result.caption === '') { - const { address, password } = userStore; - const question = new Question(sysQuestion); - const contractAddr = this._contract.options.address; - const params = question.getUploadingParams(contractAddr); - - const dataTx = this._contract.methods.saveNewQuestion(...params).encodeABI(); - - const maxGasPrice = 30000000000; - const rawTx = { - to: contractAddr, - data: dataTx, - gasLimit: 8000000, - value: '0x0', - }; - - return new Promise((resolve) => { - Web3Service.createTxData(address, rawTx, maxGasPrice) - .then((formedTx) => userStore.singTransaction(formedTx, password)) - .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) - .then((txHash) => resolve(txHash)); + } = rootStore; + const question = this.sysQuestions[idx]; + const owners = await contract.methods.getUserGroup(0).call(); + + const { address, password } = userStore; + const contractAddr = contract.options.address; + question.target = contractAddr; + question.rawFormula = question.rawFormula.replace('%s', owners.groupAddress); + question.formula = compile(question.rawFormula); + question.active = true; + + console.log(question); + const dataTx = contract.methods.addQuestion(question).encodeABI(); + console.log(dataTx); + const rawTx = { + to: contractAddr, + data: dataTx, + value: '0x0', + }; + + return new Promise((resolve) => { + Web3Service.createTxData(address, rawTx) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then((receipt) => { + userStore.getEthBalance(); + resolve(receipt); }); - } - return Promise.reject(); }); } /** * Fetching one question from contract + * * @param {number} id id of question - * @returns {Object} Question data from contract + * @returns {object} Question data from contract */ fetchQuestion(id) { - return this.callMethod('question', [id]); + return this.callMethod('getQuestion', id); } /** * getting one voting + * * @param {number} id id of voting - * @param {string} from address who calls method + * @returns {object} Voting data */ async fetchVoting(id) { - return this.callMethod('getVoting', [id]); + return this.callMethod('getVoting', id); } + /** * getting votes weights for voting + * * @param {number} id id of voting - * @param {string} from address, who calls + * @returns {object} Voting stats data + * @deprecated */ + // TODO delete me after async fetchVotingStats(id) { return this.callMethod('getVotingStats', [id]); } + /** + * Fetch length of usergroups in contract + * + * @returns {number} amount groups + */ + fetchUserGroupsLength() { + return this._contract.methods.getUserGroupsAmount().call(); + } + /** * Starting the voting - * @param {id} id id of question + * + * @param {string|number} id id of question * @param {string} from address, who starts - * @param params parameters of voting + * @param {any} params parameters of voting + * @returns {Promise} promise + * @deprecated */ + // TODO delete me after async sendVotingStart(id, from, params) { return (this, id, from, params); } + /** + * creates transaction for sending decision about voting + * + * @param {number} votingId voting + * @param {number} decision 0 - negative, 1 - positive + * @returns {Promise} promise + */ + // eslint-disable-next-line consistent-return + async sendVote(votingId, decision) { + const { + ercAbi, + _contract, + rootStore: { + appStore, + Web3Service, + userStore, + membersStore, + projectStore: { + historyStore, + questionStore, + }, + }, + } = this; + appStore.setTransactionStep('compileOrSign'); + const [voting] = historyStore.getVotingById(votingId); + const { allowedGroups } = voting; + const { length: groupsLength } = allowedGroups; + + const data = _contract.methods.setVote(decision).encodeABI(); + + for (let i = 0; i < groupsLength; i += 1) { + const group = membersStore.getMemberGroupByAddress(allowedGroups[i]); + if (group.groupType === tokenTypes.ERC20) { + console.log('approving'); + // eslint-disable-next-line no-await-in-loop + await this.approveErc(group); + console.log('approved'); + } + } + + const tx = { + from: userStore.address, + to: _contract.options.address, + value: '0x0', + data, + }; + + // eslint-disable-next-line consistent-return + return new Promise((resolve, reject) => Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then((rec) => { + appStore.setTransactionStep('success'); + historyStore.updateVotingById({ + id: votingId, + newState: { + userVote: Number(decision), + }, + }); + for (let i = 0; i < groupsLength; i += 1) { + const group = membersStore.getMemberGroupByAddress(allowedGroups[i]); + group.updateUserBalance(); + } + userStore.getEthBalance(); + resolve(rec); + }) + .catch((err) => reject(err))); + } + + closeVoting() { + const { + _contract, + rootStore: { + Web3Service, + userStore, + contractService, + appStore, + }, + } = this; + + const tx = { + from: userStore.address, + data: _contract.methods.submitVoting().encodeABI(), + value: '0x0', + to: _contract.options.address, + }; + + console.log('sending TX'); + appStore.setTransactionStep('compileOrSign'); + return Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + userStore.getEthBalance(); + }); + } + + /** + * Method for start voting + * + * @returns {Promise} promise + * @deprecated + */ + // startVoting(questionId, params) { + // const { + // _contract, + // rootStore: { + // projectStore: { questionStore }, + // Web3Service, + // userStore, + // }, + // } = this; + // const [question] = questionStore.getQuestionById(questionId); + // const { parameters } = question; + // const data = Web3Service.web3.eth.abi.encodeParameters(parameters, params); + // const votingData = (data).replace('0x', question.methodSelector); + // const votingInfo = { + // starterGroupId: 0, + // endTime: 0, + // starterAddress: userStore.address, + // questionId, + // data: votingData, + // }; + // console.log('votingInfo', votingInfo); + // const tx = { + // data: _contract.methods.startVoting(votingInfo).encodeABI(), + // from: userStore.address, + // to: _contract.options.address, + // value: '0x0', + // }; + // console.log('tx', tx); + // return Web3Service.createTxData(userStore.address, tx) + // .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + // .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + // .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + // .then(() => { + // userStore.getEthBalance(); + // }); + // } + + returnTokens() { + const { + _contract, + rootStore: { + Web3Service, + userStore, + membersStore, + projectStore: { + historyStore, + questionStore, + }, + }, + } = this; + const data = _contract.methods.returnTokens().encodeABI(); + const tx = { + from: userStore.address, + to: _contract.options.address, + value: '0x0', + data, + }; + return Web3Service.createTxData(userStore.address, tx) + .then((formedTx) => userStore.singTransaction(formedTx, userStore.password)) + .then((signedTx) => Web3Service.sendSignedTransaction(`0x${signedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then(async (rec) => { + userStore.getEthBalance(); + const lastVotingId = await _contract.methods.findLastUserVoting().call(); + const [voting] = historyStore.getVotingById(Number(lastVotingId)); + const { questionId } = voting; + const [question] = questionStore.getQuestionById(Number(questionId)); + const { groupId } = question; + const [group] = membersStore.getMemberById(Number(groupId)); + group.updateUserBalance(); + }); + } + + /** + * approve token transfer from user to Voter contract + * + * @param {object} group group instance + */ + approveErc(group) { + const { + ercAbi, + _contract, + rootStore: { + Web3Service, + userStore, + }, + } = this; + const ercContract = Web3Service.createContractInstance(ercAbi); + ercContract.options.address = group.wallet; + // eslint-disable-next-line max-len + const txData = ercContract.methods.approve(_contract.options.address, userStore.address).encodeABI(); + const tx = { + data: txData, + from: userStore.address, + value: '0x0', + to: group.wallet, + }; + return Web3Service.createTxData(userStore.address, tx) + .then((createdTx) => userStore.singTransaction(createdTx, userStore.password)) + .then((formedTx) => Web3Service.sendSignedTransaction(`0x${formedTx}`)) + .then((txHash) => Web3Service.subscribeTxReceipt(txHash)) + .then(() => { + userStore.getEthBalance(); + }); + } + /** * Finishes the voting */ diff --git a/src/services/ContractService/entities/Question.js b/src/services/ContractService/entities/Question.js index a8a792c6..ecff0eb8 100644 --- a/src/services/ContractService/entities/Question.js +++ b/src/services/ContractService/entities/Question.js @@ -1,8 +1,17 @@ -import web3 from 'web3'; +import { compile } from 'zeroone-translator'; class Question { constructor({ - id, group, name, caption, time, method, formula, parameters, + id, + group, + name, + caption, + time, + method, + formula, + parameters, + paramTypes, + paramNames, }) { this.id = id; this.group = group; @@ -10,42 +19,44 @@ class Question { this.caption = caption; this.time = time; this.method = method; - this.formula = formula; + this.rawFormula = formula; this.parameters = parameters; - } - - /** - * convert simple formula of system question for contract - * @param {string} formula text implimentation of formula - * @returns {array} numeric implimentation of formula for smart contract - */ - getFormulaForContract() { - const FORMULA_REGEXP = new RegExp(/(group)|((?:[a-zA-Z0-9]{1,}))|((quorum|positive))|(>=|<=)|([0-9%]{1,})|(quorum|all)/g); - const matched = this.formula.match(FORMULA_REGEXP); - const convertedFormula = []; - convertedFormula.push(matched[0] === 'group' ? 0 : 1); - convertedFormula.push(matched[1] === 'Owners' ? 1 : 2); - convertedFormula.push(matched[3] === 'quorum' ? 0 : 1); - convertedFormula.push(matched[4] === '<=' ? 0 : 1); - convertedFormula.push(Number(matched[5])); - if (matched.length === 9) { - convertedFormula.push(matched[8] === 'quorum' ? 0 : 1); - } - return convertedFormula; + this.paramTypes = paramTypes; + this.paramNames = paramNames; + this.formula = compile(formula); } /** * getting formed parameters for contract + * * @param {string} contractAddr address of target contract - * @returns {array} formed data for encoding transaction + * @returns {Array} formed data for encoding transaction */ getUploadingParams(contractAddr) { const { - id, group, name, caption, time, method, parameters, + name, + caption, + group, + time, + paramNames, + paramTypes, + method, + rawFormula, + formula, } = this; - const convertedFormula = this.getFormulaForContract(); - const params = parameters.map((param) => web3.utils.utf8ToHex(param)); - return [[id, group, time], 0, name, caption, contractAddr, method, convertedFormula, params]; + return [ + true, + name, + caption, + group, + time, + paramNames, + paramTypes, + contractAddr, + method, + rawFormula, + formula, + ]; } } export default Question; diff --git a/src/services/EventEmitterService/index.js b/src/services/EventEmitterService/index.js index e60b8ee7..01a5076d 100644 --- a/src/services/EventEmitterService/index.js +++ b/src/services/EventEmitterService/index.js @@ -12,6 +12,10 @@ class EventEmitterService { if (!this.events[event]) this.events[event] = []; this.events[event].push(callback); } + + off(event) { + delete this.events[event]; + } } export default EventEmitterService; diff --git a/src/services/Web3Service/Web3Service.js b/src/services/Web3Service/Web3Service.js index 634a11bc..eeb99b65 100644 --- a/src/services/Web3Service/Web3Service.js +++ b/src/services/Web3Service/Web3Service.js @@ -6,7 +6,9 @@ import { BN } from 'ethereumjs-util'; */ class Web3Service { /** - * @constructor + * @class + * @param url + * @param rootStore * @param {string} provider - provider for this.web3 */ constructor(url, rootStore) { @@ -19,36 +21,38 @@ class Web3Service { return new Contract(abi); } - createTxData(address, tx, maxGasPrice) { - const { web3: { eth } } = this; + createTxData(address, tx) { + const { web3: { eth }, rootStore } = this; + // eslint-disable-next-line no-unused-vars + const { configStore: { MIN_GAS_PRICE, MAX_GAS_PRICE, GAS_LIMIT: gasLimit } } = rootStore; let transaction = { ...tx }; return eth.getTransactionCount(address, 'pending') .then((nonce) => { transaction = { ...tx, nonce }; - return eth.estimateGas(tx); + return eth.estimateGas(transaction); }) .then((gas) => { - if (!maxGasPrice) return (Promise.resolve(gas)); + transaction = { ...transaction, gas }; return this.getGasPrice() .then((gasPrice) => { - const minGasPrice = 10000000000; const gp = new BN(gasPrice); - const minGp = new BN(minGasPrice); - const maxGp = new BN(maxGasPrice); + const minGp = new BN(MIN_GAS_PRICE); + const maxGp = new BN(MAX_GAS_PRICE); transaction.gasPrice = (gp.gte(minGp) && gp.lte(maxGp)) ? gasPrice - : minGasPrice; + : MIN_GAS_PRICE; return Promise.resolve(transaction.gasPrice); }) .catch(Promise.reject); }) // eslint-disable-next-line no-unused-vars - .then((gasPrice) => (transaction)) + .then((gasPrice) => ({ ...transaction, gasPrice })) .catch((err) => Promise.reject(err)); } /** * getting gas price + * * @returns {number} gasPrice from network */ getGasPrice() { @@ -58,8 +62,10 @@ class Web3Service { /** * Sending transaction to contract + * + * @param rawTx * @param {string} txData Raw transaction (without 0x) - * @return {Promise} promise with web3 transaction PromiEvent + * @returns {Promise} promise with web3 transaction PromiEvent */ sendSignedTransaction(rawTx) { const { web3: { eth: { sendSignedTransaction } } } = this; @@ -76,8 +82,9 @@ class Web3Service { /** * checking transaction receipt by hash every 5 seconds + * * @param {string} txHash hash of transaction - * @return {Promise} Promise which resolves on successful receipt fetching + * @returns {Promise} Promise which resolves on successful receipt fetching */ subscribeTxReceipt(txHash) { const { web3 } = this; diff --git a/src/services/models/FormModel/index.js b/src/services/models/FormModel/index.js deleted file mode 100644 index f1e4bd9a..00000000 --- a/src/services/models/FormModel/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import { extendObservable, action } from 'mobx'; -import { Form } from 'mobx-react-form'; -import dvr from 'mobx-react-form/lib/validators/DVR'; -import plugins from '../../utils/Validator'; - -class ExtendedForm extends Form { - constructor(data) { - const { hooks } = data || {}; - super(); - extendObservable(this, { loading: false }); - Object.keys(hooks).forEach((hook) => { - this.addHook(hook, hooks[hook]); - }); - } - - // eslint-disable-next-line class-methods-use-this - plugins() { - return { dvr: dvr(plugins.dvr) }; - } - - hooks() { - const $this = this; - return { - onSubmit: (form) => { - $this.setLoading(true); - $this.fireHook('onSubmitHook', form); - // Trigger hide mobile keyboard - document.activeElement.blur(); - }, - onSuccess: (form) => { - const promise = $this.fireHook('onSuccessHook', form); - promise - .finally(() => { - $this.setLoading(false); - }); - }, - onError: (form) => { - $this.setLoading(false); - $this.fireHook('onErrorHook', form); - }, - }; - } - - @action addHook(hook, fn) { - this[`${hook}Hook`] = fn; - } - - @action setLoading(status) { - this.loading = status; - } - - fireHook(hook, form) { - const fire = this[hook]; - if (fire && typeof fire === 'function') { - return fire(form); - } - return null; - } -} - -export default ExtendedForm; diff --git a/src/splash.html b/src/splash.html new file mode 100644 index 00000000..70335ba8 --- /dev/null +++ b/src/splash.html @@ -0,0 +1,142 @@ + + + + + + ZeroOne + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/stores/AppStore/AppStore.js b/src/stores/AppStore/AppStore.js index 27400adc..6130f521 100644 --- a/src/stores/AppStore/AppStore.js +++ b/src/stores/AppStore/AppStore.js @@ -1,7 +1,8 @@ import { observable, action, computed } from 'mobx'; import { - fs, path, PATH_TO_WALLETS, ROOT_DIR, PATH_TO_CONTRACTS, + fs, path, PATH_TO_WALLETS, PATH_TO_CONTRACTS, PATH_TO_CONFIG, } from '../../constants/windowModules'; +import { transactionSteps } from '../../constants'; import Alert from './entities/Alert'; class AppStore { @@ -11,6 +12,8 @@ class AppStore { @observable projectList = []; + @observable transactionStep = 0; + @observable ERC = { }; @@ -27,13 +30,25 @@ class AppStore { @observable userInProject = false; + @observable _projectAddress = ''; + constructor(rootStore) { this.rootStore = rootStore; this.readWalletList(); } + /** + * set current transaction step for displaying if in modal + * + * @param {string} step key of step + */ + @action setTransactionStep(step) { + this.transactionStep = transactionSteps[step]; + } + /** * Getting list of url's for sending this to wallet service + * * @function */ @action readWalletList() { @@ -42,17 +57,23 @@ class AppStore { const files = fs.readdirSync(PATH_TO_WALLETS); files.forEach((file) => { - const wallet = JSON.parse(fs.readFileSync(path.join(PATH_TO_WALLETS, file), 'utf8')); - const walletObject = {}; - eth.getBalance(wallet.address) - .then((balance) => { this.balances[wallet.address] = utils.fromWei(balance); }); - walletObject[wallet.address] = wallet; - this.walletList = Object.assign(this.walletList, walletObject); + let wallet; + try { + wallet = JSON.parse(fs.readFileSync(path.join(PATH_TO_WALLETS, file), 'utf8')); + const walletObject = {}; + eth.getBalance(wallet.address) + .then((balance) => { this.balances[wallet.address] = utils.fromWei(balance); }); + walletObject[wallet.address] = wallet; + this.walletList = Object.assign(this.walletList, walletObject); + } catch { + alert(`Error occuried on reaing file ${path.join(PATH_TO_WALLETS, file)}. Please check it.`); + } }); } /** * selecting encrypted wallet and pushing this to userStore + * * @param {string} address address of wallet */ selectWallet = (address) => { @@ -63,31 +84,41 @@ class AppStore { /** * Reading list of projects for displaing them in project list + * * @function */ @action readProjectList() { - const config = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, './config.json'))); + const config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG)); this.projectList = config.projects; } /** * compile contract by given type and arguments + * * @param {string} type type of contract - ERC20 for erc tokens, project - for project contract + * @param deployArgs + * @param password * @returns {Promise} Function which compiles contract and deploy contract to network on success */ @action deployContract(type, deployArgs, password) { const { contractService } = this.rootStore; - return new Promise((resolve) => { - contractService.compileContract(type) - .then(({ bytecode, abi }) => contractService.deployContract({ - type, deployArgs, bytecode, abi, password, - })) - .then((txhash) => resolve(txhash)); + return new Promise((resolve, reject) => { + this.setTransactionStep('compileOrSign'); + contractService.compileContract(type, password) + .then(({ bytecode, abi }) => { + this.setTransactionStep('sending'); + return contractService.deployContract({ + type, deployArgs, bytecode, abi, password, + }); + }) + .then((txhash) => resolve(txhash)) + .catch((err) => reject(err)); }); } /** * checks given address on ERC20 tokens + * * @param {string} address address of ERC20 contract * @returns {Promise} resolves on success checking and set information about ERC token */ @@ -107,33 +138,33 @@ class AppStore { /** * checks if given address is contract in network + * * @param {string} address address, which will be ckecked on contract instance */ @action checkProject(address) { const { contractService } = this.rootStore; return contractService.checkProject(address) - .then((data) => { - Promise.resolve(data); - }) - .catch(() => { Promise.reject(); }); + .then((data) => Promise.resolve(data)) + .catch((e) => Promise.reject(e)); } /** * Upload questions to created project + * * @param {string} address address of smart contract, where will be uploaded questions - * @return Promise.resolve() + * @returns Promise.resolve() */ @action async deployQuestions(address) { const { Web3Service, contractService } = this.rootStore; - const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './Voter.abi'), 'utf8')); + const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'), 'utf8')); const contract = Web3Service.createContractInstance(abi); contract.options.address = address; contractService.setContract(contract); const { countOfUploaded, totalCount } = await contractService.checkQuestions(); this.countOfQuestions = Number(totalCount); this.uploadedQuestion = Number(countOfUploaded); - let idx = Number(countOfUploaded) === 0 ? 1 : Number(countOfUploaded); - for (idx; idx <= totalCount; idx += 1) { + let idx = Number(countOfUploaded) === 0 ? 0 : Number(countOfUploaded); + for (idx; idx < totalCount; idx += 1) { // eslint-disable-next-line no-await-in-loop await contractService.sendQuestion(idx); this.uploadedQuestion += 1; @@ -143,25 +174,73 @@ class AppStore { /** * add project to config and update config saved in file + * * @param {object} data data about project {name, address} */ // eslint-disable-next-line class-methods-use-this @action addProjectToList(data) { - const config = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, './config.json'), 'utf8')); + const config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG, 'utf8')); config.projects.push(data); - fs.writeFileSync(path.join(ROOT_DIR, './config.json'), JSON.stringify(config, null, '\t')); + fs.writeFileSync(PATH_TO_CONFIG, JSON.stringify(config, null, '\t')); + } + + /** + * checks count of uploaded Questions + * + * @param {string} address address of project + * @returns {boolean} countOfUploaded > totalQuestionCount + */ + async checkIsQuestionsUploaded(address) { + const { Web3Service, contractService } = this.rootStore; + const abi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'), 'utf8')); + const contract = Web3Service.createContractInstance(abi); + contract.options.address = address; + contractService.setContract(contract); + const { countOfUploaded, totalCount } = await contractService.checkQuestions(); + return countOfUploaded >= totalCount; + } + + // eslint-disable-next-line class-methods-use-this + parseFormula(rawFormula) { + if (!rawFormula || !rawFormula.length) return ''; + const f = rawFormula.map((text) => Number(text)); + const r = []; + let ready = '( )'; + r.push(f[0] === 0 ? 'group( ' : 'user(0x298e231fcf67b4aa9f41f902a5c5e05983e1d5f8) => condition('); + r.push(f[1] === 1 ? 'Owner) => condition(' : 'Custom) => condition('); + r.push(f[2] === 0 ? 'quorum ' : 'positive'); + r.push(f[3] === 0 ? ' <= ' : ' >= '); + + if (f.length === 6) { + r.push(`${f[4]} %`); + r.push(f[5] === 0 ? ' of quorum)' : ' of all)'); + } else { + r.push(`${f[4]} % )`); + } + const formula = r.join(''); + ready = ready.replace(' ', formula); + return ready; } /** * Check transaction receipt + * * @param {string} hash Transaction hash - * @return {Promise} Promise with interval, which resolves on succesfull receipt recieving + * @returns {Promise} Promise with interval, which resolves on succesfull receipt recieving */ @action checkReceipt(hash) { const { Web3Service } = this.rootStore; return Web3Service.subscribeTxReceipt(hash); } + // eslint-disable-next-line class-methods-use-this + nodeChange(url) { + const config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG, 'utf8')); + config.host = url; + fs.writeFileSync(PATH_TO_CONFIG, JSON.stringify(config, null, '\t'), 'utf8'); + window.location.reload(); + } + /** * * @param {string} text error text @@ -186,6 +265,10 @@ class AppStore { return this.userInProject; } + @computed get projectAddress() { + return this._projectAddress; + } + @action setProjectName(value) { this.name = value; } @@ -193,6 +276,30 @@ class AppStore { @action setDeployArgs(value) { this.deployArgs = value; } + + @action gotoProject({ address, name }) { + const { rootStore } = this; + this.setProjectAddress(address); + rootStore.initProject({ address, name }); + this.userInProject = true; + } + + @action setProjectAddress(value) { + this._projectAddress = value; + } + + @action + reset = () => { + this.projectList = []; + this.ERC = {}; + this.deployArgs = []; + this.name = ''; + this.alert = new Alert(); + this.uploadedQuestion = 0; + this.countOfQuestions = 0; + this.userInProject = false; + this._projectAddress = ''; + } } export default AppStore; diff --git a/src/stores/ConfigStore/index.js b/src/stores/ConfigStore/index.js new file mode 100644 index 00000000..aa225773 --- /dev/null +++ b/src/stores/ConfigStore/index.js @@ -0,0 +1,72 @@ +import { observable, action, computed } from 'mobx'; +import { + fs, PATH_TO_CONFIG, +} from '../../constants/windowModules'; + +class ConfigStore { + @observable config + + constructor() { + this.getConfig(); + } + + @action + getConfig() { + try { + this.config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG)); + this.updateValues(this.config); + // eslint-disable-next-line no-empty + } catch { + + } + } + + @action + updateValues({ + minGasPrice, maxGasPrice, interval, gasLimit, + }) { + const { config } = this; + console.log(PATH_TO_CONFIG); + config.minGasPrice = minGasPrice < 1 ? 1 : Number(minGasPrice); + config.maxGasPrice = maxGasPrice < 1 ? 1 : Number(maxGasPrice); + config.interval = interval < 10 ? 10 : Number(interval); + config.gasLimit = Number(gasLimit); + this.updateConfig(); + } + + @action + updateConfig() { + const { config } = this; + console.log(PATH_TO_CONFIG); + fs.writeFileSync(PATH_TO_CONFIG, JSON.stringify(config, null, '\t'), 'utf8'); + } + + @action + addProject(project) { + const { config } = this; + config.projects.push(project); + this.updateConfig(); + } + + @computed get MIN_GAS_PRICE() { + const { config } = this; + return config.minGasPrice * 1000000000; + } + + @computed get MAX_GAS_PRICE() { + const { config } = this; + return config.maxGasPrice * 1000000000; + } + + @computed get GAS_LIMIT() { + const { config } = this; + return config.gasLimit; + } + + @computed get UPDATE_INTERVAL() { + const { config } = this; + return config.interval * 1000; + } +} + +export default ConfigStore; diff --git a/src/stores/DialogStore/DialogStore.js b/src/stores/DialogStore/DialogStore.js index 73bf9304..a098c061 100644 --- a/src/stores/DialogStore/DialogStore.js +++ b/src/stores/DialogStore/DialogStore.js @@ -9,160 +9,175 @@ import DialogItem from './DialogItemModel'; * Class for manage all dialogs in app */ class DialogStore { - /** actual open state */ - @observable open = false; - - /** closing in progress state */ - @observable closing = false; - - /** current active name dialog */ - @observable dialog = null; - - /** history dialogs */ - history = []; - - /** list of all dialogs */ - list = {}; - - @computed - /** - * Actual open state - * - * @returns {string, boolean} name dialog or boolean state - */ - get isOpen() { - if (this.open && this.dialog) return this.dialog; - return false; - } + /** actual open state */ + @observable open = false; - /** - * Method for getting dialog by name in list - * - * @param {string} dialogName name dialog - * @return {object} dialog item model - */ - getDialog(dialogName) { - const { list } = this; - return list[dialogName]; - } + /** closing in progress state */ + @observable closing = false; - /** - * Method for checking the existence of a dialog - * - * @param {string} dialog dialog name - * @returns {boolean} dialog exists or does not exist - */ - doesExist(dialog) { - return this.getDialog(dialog) !== undefined; - } + /** current active name dialog */ + @observable dialog = null; - /** - * Method for adding new dialog in list - * - * @param {string} name name new dialog - * @param {object} options options for new dialog - * @param {boolean} options.history add to history or not - * @param {Function} options.onOpen method that is called - * on open dialog - * @param {Function} options.onClose method that is called - * on close dialog - */ - add(name, options) { - this.list[name] = new DialogItem(name, options); - } + /** history dialogs */ + history = []; + + /** list of all dialogs */ + list = {}; + + @computed + /** + * Actual open state + * + * @returns {string|boolean} name dialog or boolean state + */ + get isOpen() { + if (this.open && this.dialog) return this.dialog; + return false; + } - /** - * Method for removing dialog from list dialogs - * - * @param {string} name name dialog - */ - remove(name) { - delete this.list[name]; + /** + * Method for getting dialog by name in list + * + * @param {string} dialogName name dialog + * @returns {object} dialog item model + */ + getDialog(dialogName) { + const { list } = this; + return list[dialogName]; + } + + /** + * Method for checking the existence of a dialog + * + * @param {string} dialog dialog name + * @returns {boolean} dialog exists or does not exist + */ + doesExist(dialog) { + return this.getDialog(dialog) !== undefined; + } + + /** + * Method for adding new dialog in list + * + * @param {string} name name new dialog + * @param {object} options options for new dialog + * @param {boolean} options.history add to history or not + * @param {Function} options.onOpen method that is called + * on open dialog + * @param {Function} options.onClose method that is called + * on close dialog + */ + add(name, options) { + this.list[name] = new DialogItem(name, options); + } + + /** + * Method for removing dialog from list dialogs + * + * @param {string} name name dialog + */ + remove(name) { + delete this.list[name]; + } + + @action + /** + * Method for showing dialog by name + * + * @param {string} dialogName dialog name + */ + show(dialogName) { + const { open, dialog: currentDialogName } = this; + const dialog = this.getDialog(dialogName); + // not found + if (!dialog) return this.hide(); + // this dialog is opened next already + if (dialog.name === currentDialogName) return Promise.resolve(); + // save provided dialog as next to open + if (open) { + this.next = dialogName; + return this.hide(true); } + document.body.classList.add('dialog-overlay'); + this.open = true; + this.dialog = dialog.name; + dialog.open(); + this.addToHistory(dialog); + // this.emit(`${dialog.name}:open`); + return Promise.resolve(); + } - @action - /** - * Method for showing dialog by name - * - * @param {string} dialogName dialog name - */ - show(dialogName) { - const { open, dialog: currentDialogName } = this; - const dialog = this.getDialog(dialogName); - // not found - if (!dialog) return this.hide(); - // this dialog is opened next already - if (dialog.name === currentDialogName) return Promise.resolve(); - // save provided dialog as next to open - if (open) { - this.next = dialogName; - return this.hide(true); - } - document.body.classList.add('dialog-overlay'); - this.open = true; - this.dialog = dialog.name; - dialog.open(); - this.addToHistory(dialog); - // this.emit(`${dialog.name}:open`); + @action + /** + * Method for hiding dialog + */ + hide() { + const { dialog: dialogName } = this; + const dialog = this.getDialog(dialogName); + // closing right now + if (this.closing || !dialog) { return Promise.resolve(); } + return new Promise((resolve) => { + this.closing = true; + setTimeout(() => { + const { next } = this; + this.open = false; + this.closing = false; + this.dialog = false; + if (dialog) dialog.close(); + if (!next || typeof next !== 'boolean') { + document.body.classList.remove('dialog-overlay'); + } + if (next) { + this.next = false; + this.show(next) + .then(resolve); + } + // this.emit(`${dialog.name}:hidden`); + return resolve(); + }, 400); + }); + } - @action - /** - * Method for hiding dialog - */ - hide() { - const { dialog: dialogName } = this; - const dialog = this.getDialog(dialogName); - // closing right now - if (this.closing || !dialog) { - return Promise.resolve(); - } - return new Promise((resolve) => { - this.closing = true; - setTimeout(() => { - const { next } = this; - this.open = false; - this.closing = false; - this.dialog = false; - if (dialog) dialog.close(); - if (!next || typeof next !== 'boolean') { - document.body.classList.remove('dialog-overlay'); - } - if (next) { - this.next = false; - this.show(next) - .then(resolve); - } - // this.emit(`${dialog.name}:hidden`); - return resolve(); - }, 400); - }); - } + /** + * Method for adding dialog in history dialogs + * + * @param {object} dialog dialog item model + * @returns {number} length history + */ + addToHistory(dialog) { + if (dialog.history === false) return false; + return this.history.push(dialog.name); + } - /** - * Method for adding dialog in history dialogs - * - * @param {object} dialog dialog item model - */ - addToHistory(dialog) { - if (dialog.history === false) return false; - return this.history.push(dialog.name); + /** + * Method for toggle dialogs + * + * @param {string} dialogName dialog name for opening + */ + toggle(dialogName) { + const { open } = this; + if (!open || this.dialog !== dialogName) { + this.hide().then(() => { this.show(dialogName); }); + } else { + this.hide(); } + } - /** - * Method for toggle dialogs - * - * @param {string} dialogName dialog name for opening - */ - toggle(dialogName) { - const { open } = this; - if (!open || this.dialog !== dialogName) { - this.hide().then(() => { this.show(dialogName); }); - } else { - this.hide(); - } - } + /** + * Method for toggle dialog back + * by history + * + * @param {number} length count for + * return back by history + */ + back(length) { + const { history } = this; + const offset = length || 1; + if (history.length - offset < 0) return; + this.show(history[history.length - offset]); + } } export default DialogStore; diff --git a/src/stores/FilterStore/FilterStore.js b/src/stores/FilterStore/FilterStore.js new file mode 100644 index 00000000..3204a355 --- /dev/null +++ b/src/stores/FilterStore/FilterStore.js @@ -0,0 +1,126 @@ +import { + action, + set, + remove, + entries, + computed, + observable, +} from 'mobx'; + +class FilterStore { + /** List of _rules for filtering data */ + @observable _rules = {}; + + @computed + get rules() { + const result = {}; + const ruleEntries = entries(this._rules); + ruleEntries.forEach((key) => { + const ruleKey = key[0]; + const ruleValue = key[1]; + result[ruleKey] = ruleValue; + }); + return result; + } + + /** + * Method for adding (or rewrite) + * a list filter rule + * + * @param {object} rule filter rule + * @param {Function} cb callback + */ + @action + addFilterRule(rule, cb) { + Object.keys(rule).forEach((key) => { + set(this._rules, key, rule[key]); + }); + if (cb) cb(); + } + + /** + * Method for removing rule from list rules + * + * @param {string} rule rule name for removing + * @param {Function} cb callback function + */ + @action + removeFilterRule(rule, cb) { + remove(this._rules, rule); + if (cb) cb(); + } + + /** + * Method for reset state this store + */ + @action + reset() { + this._rules = {}; + } + + /** + * Method for filter by date + * + * @param {Array} rawList raw data + * @returns {Array} list from date range + */ + filteredByDateList(rawList) { + let resultList = []; + const rulesKeys = Object.keys(this.rules); + if (rulesKeys.length) { + if (!this.rules.date) return rawList; + rulesKeys.forEach((key) => { + if (key === 'date') { + const { start, end } = this.rules.date; + // Filter list with startTime by start & end date rule + const filtered = rawList.filter( + (item) => ( + parseInt(item.startTime, 10) >= start + && parseInt(item.startTime, 10) <= end + ), + ); + resultList = resultList.concat(filtered); + } + }); + } else { + resultList = rawList; + } + return resultList; + } + + /** + * Method for getting list filtered + * by filter rules + * + * @param {Array} data raw data + * @returns {Array} correct list + */ + filteredList(data) { + const rawList = data; + const listByDate = this.filteredByDateList(rawList); + const rulesKeys = Object.keys(this.rules); + // If _rules not exist return list by date + if (!rulesKeys.length) { + return listByDate; + } + let resultList = listByDate; + rulesKeys.forEach((key) => { + if (key !== 'date') { + if (this.rules[key] !== '*') { + resultList = this.filterByKey(resultList, key, this.rules[key]); + } + // If exist only date rule result is list by date + } else if (rulesKeys.length === 1) { + resultList = listByDate; + } + }); + return resultList; + } + + // eslint-disable-next-line class-methods-use-this + filterByKey(list, key, value) { + return list.filter((item) => item[key] === value); + } +} + +export default FilterStore; diff --git a/src/stores/FilterStore/FilterStore.test.js b/src/stores/FilterStore/FilterStore.test.js new file mode 100644 index 00000000..56fcaa32 --- /dev/null +++ b/src/stores/FilterStore/FilterStore.test.js @@ -0,0 +1,434 @@ +import FilterStore from '.'; + +describe('FilterStore', () => { + describe('data with date', () => { + let filter; + const dataList = [ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]; + + beforeEach(() => { + filter = new FilterStore(); + }); + + it('addFilterRule by date should change rules object & filteredByDateList should be correct', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.rules).toEqual({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + }); + + it('filteredList should be correct only with date rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.rules).toEqual({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + }); + + it('filteredList should be correct with date & other rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + filter.addFilterRule({ data: 1 }); + expect(filter.rules).toEqual({ + date: { + start: 1566383552, + end: 1566385552, + }, + data: 1, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('filteredList should be correct with different filter for one item', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: 1, test: 1 }); + expect(filter.rules).toEqual({ data: 1, test: 1 }); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('reset should clear filter rules', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: 1 }); + expect(filter.rules).toEqual({ data: 1 }); + filter.reset(); + expect(filter.rules).toEqual({}); + }); + + it('filteredByDateList & filteredList should be correct with date, then with other rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566385552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + filter.addFilterRule({ data: 1 }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('filteredByDateList & filteredList should be correct with rule, then with date rule', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: 1 }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566384552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + ]); + }); + + it('* key rules should work correct', () => { + expect(filter.rules).toEqual({}); + filter.addFilterRule({ data: '*' }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + { + data: 3, + test: 3, + startTime: '1566385552', + endTime: '1566385852', + }, + { + data: 4, + test: 4, + startTime: '1566386552', + endTime: '1566386852', + }, + ]); + filter.addFilterRule({ + date: { + start: 1566383552, + end: 1566384552, + }, + }); + expect(filter.filteredByDateList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + ]); + expect(filter.filteredList(dataList)).toEqual([ + { + data: 1, + test: 1, + startTime: '1566383552', + endTime: '1566383852', + }, + { + data: 2, + test: 2, + startTime: '1566384552', + endTime: '1566384852', + }, + ]); + }); + }); +}); diff --git a/src/stores/FilterStore/index.js b/src/stores/FilterStore/index.js new file mode 100644 index 00000000..4a7f7f79 --- /dev/null +++ b/src/stores/FilterStore/index.js @@ -0,0 +1,3 @@ +import FilterStore from './FilterStore'; + +export default FilterStore; diff --git a/src/stores/FormsStore/ConfigForm.js b/src/stores/FormsStore/ConfigForm.js new file mode 100644 index 00000000..82294c90 --- /dev/null +++ b/src/stores/FormsStore/ConfigForm.js @@ -0,0 +1,40 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class ConfigForm extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'minGasPrice', + type: 'text', + label: 'minGasPrice', + placeholder: i18n.t('fields:minGasPrice'), + rules: 'numeric|min:1|max:100', + }, { + name: 'maxGasPrice', + type: 'text', + label: 'maxGasPrice', + placeholder: i18n.t('fields:maxGasPrice'), + rules: 'numeric|min:1|max:100', + }, + { + name: 'gasLimit', + type: 'text', + label: 'gasLimit', + value: 7900000, + placeholder: i18n.t('fields:gasLimit'), + rules: 'numeric|min:25000|max:7900000', + }, + { + name: 'interval', + type: 'text', + label: 'interval', + placeholder: i18n.t('fields:interval'), + rules: 'numeric|min:10', + }], + }; + } +} + +export default ConfigForm; diff --git a/src/stores/FormsStore/ConnectProject.js b/src/stores/FormsStore/ConnectProject.js index 1fa25943..1a50868a 100644 --- a/src/stores/FormsStore/ConnectProject.js +++ b/src/stores/FormsStore/ConnectProject.js @@ -8,13 +8,13 @@ class ConnectProjectForm extends ExtendedForm { fields: [{ name: 'name', type: 'text', - label: 'Project Name', + label: 'projectTitle', placeholder: i18n.t('fields:projectTitle'), rules: 'required|string|between:3,20', }, { name: 'address', type: 'text', - label: 'Token Address', + label: 'contractAddress', placeholder: i18n.t('fields:contractAddress'), rules: 'required|string|address', }], diff --git a/src/stores/FormsStore/ConnectToken.js b/src/stores/FormsStore/ConnectToken.js index 3c247bb2..fd5771bb 100644 --- a/src/stores/FormsStore/ConnectToken.js +++ b/src/stores/FormsStore/ConnectToken.js @@ -8,7 +8,7 @@ class ConnectTokenForm extends ExtendedForm { fields: [{ name: 'address', type: 'text', - label: 'Token Address', + label: 'contractAddress', placeholder: i18n.t('fields:contractAddress'), rules: 'required|string|address', }], diff --git a/src/stores/FormsStore/CreateGroupQuestionsForm.js b/src/stores/FormsStore/CreateGroupQuestionsForm.js new file mode 100644 index 00000000..4c817bb4 --- /dev/null +++ b/src/stores/FormsStore/CreateGroupQuestionsForm.js @@ -0,0 +1,25 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateGroupQuestionsForm extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'name', + type: 'text', + label: 'titleGroupQuestions', + placeholder: i18n.t('fields:titleGroupQuestions'), + rules: 'required|string', + }, { + name: 'description', + type: 'text', + label: 'descriptionOrComment', + placeholder: i18n.t('fields:descriptionOrComment'), + rules: 'string', + }], + }; + } +} + +export default CreateGroupQuestionsForm; diff --git a/src/stores/FormsStore/CreateProject.js b/src/stores/FormsStore/CreateProject.js index a35ed028..86fab110 100644 --- a/src/stores/FormsStore/CreateProject.js +++ b/src/stores/FormsStore/CreateProject.js @@ -2,19 +2,19 @@ import i18n from 'i18next'; import ExtendedForm from '../../models/FormModel'; -class СreateProjectForm extends ExtendedForm { +class CreateProjectForm extends ExtendedForm { setup() { return { fields: [{ name: 'name', type: 'text', - label: 'Project name', + label: 'projectTitle', placeholder: i18n.t('fields:projectTitle'), rules: 'required|string|between:3,20', }, { name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }], @@ -22,4 +22,4 @@ class СreateProjectForm extends ExtendedForm { } } -export default СreateProjectForm; +export default CreateProjectForm; diff --git a/src/stores/FormsStore/CreateProjectInSettings.js b/src/stores/FormsStore/CreateProjectInSettings.js new file mode 100644 index 00000000..9ca8103e --- /dev/null +++ b/src/stores/FormsStore/CreateProjectInSettings.js @@ -0,0 +1,31 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateProjectInSettings extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'address', + type: 'text', + label: 'contractAddress', + placeholder: i18n.t('fields:contractAddress'), + rules: 'required|string|address', + }, { + name: 'name', + type: 'text', + label: 'projectTitle', + placeholder: i18n.t('fields:projectTitle'), + rules: 'required|string|between:3,20', + }, { + name: 'password', + type: 'password', + label: 'enterPassword', + placeholder: i18n.t('fields:enterPassword'), + rules: 'required|password', + }], + }; + } +} + +export default CreateProjectInSettings; diff --git a/src/stores/FormsStore/CreateQuestionBasicForm.js b/src/stores/FormsStore/CreateQuestionBasicForm.js new file mode 100644 index 00000000..7d54cd31 --- /dev/null +++ b/src/stores/FormsStore/CreateQuestionBasicForm.js @@ -0,0 +1,63 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateQuestionBasicForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'GroupId', + type: 'text', + label: 'questionGroup', + placeholder: i18n.t('fields:questionGroup'), + rules: '', + }, + { + name: 'question_title', + type: 'text', + label: 'questionTitle', + placeholder: i18n.t('fields:questionTitle'), + rules: 'required', + }, + { + name: 'question_life_time', + type: 'text', + label: 'questionLifeTime', + placeholder: i18n.t('fields:questionLifeTime'), + rules: 'required|numeric', + }, + { + name: 'target', + type: 'text', + label: 'targetContractAddress', + placeholder: i18n.t('fields:targetContractAddress'), + rules: 'required|string|address', + }, + { + name: 'methodSelector', + type: 'text', + label: 'methodSelector', + placeholder: i18n.t('fields:methodSelector'), + rules: 'string|bytes4', + }, + { + name: 'voting_formula', + type: 'text', + label: 'votingFormula', + placeholder: i18n.t('fields:votingFormula'), + rules: 'required', + }, + { + name: 'description', + type: 'text', + label: 'questionDescription', + placeholder: i18n.t('fields:questionDescription'), + rules: 'required', + }, + ], + }; + } +} + +export default CreateQuestionBasicForm; diff --git a/src/stores/FormsStore/CreateQuestionDynamicForm.js b/src/stores/FormsStore/CreateQuestionDynamicForm.js new file mode 100644 index 00000000..343ef347 --- /dev/null +++ b/src/stores/FormsStore/CreateQuestionDynamicForm.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class CreateQuestionDynamicForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'input--id0', + type: 'text', + label: 'enterNewParameterName', + placeholder: i18n.t('fields:enterNewParameterName'), + rules: 'required', + }, + { + name: 'select--id0', + type: 'text', + label: 'selectParameterType', + placeholder: i18n.t('fields:selectParameterType'), + rules: 'required', + }, + ], + }; + } +} + +export default CreateQuestionDynamicForm; diff --git a/src/stores/FormsStore/CreateToken.js b/src/stores/FormsStore/CreateToken.js index 6efd78d4..1c5988f8 100644 --- a/src/stores/FormsStore/CreateToken.js +++ b/src/stores/FormsStore/CreateToken.js @@ -7,26 +7,26 @@ class CreateTokenForm extends ExtendedForm { return { fields: [{ name: 'name', + label: 'tokenTitle', type: 'text', - label: 'Имя', placeholder: i18n.t('fields:tokenTitle'), rules: 'required|string', }, { name: 'symbol', + label: 'symbol', type: 'text', - label: 'Символ Токена', placeholder: i18n.t('fields:symbol'), rules: 'required|string|between:3,5', }, { name: 'count', + label: 'quantity', type: 'text', - label: 'Количество токенов', placeholder: i18n.t('fields:quantity'), rules: 'required|numeric|min:1|max:2147483647 ', }, { name: 'password', + label: 'enterPassword', type: 'password', - label: 'Password', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }], diff --git a/src/stores/FormsStore/CreateWalletForm.js b/src/stores/FormsStore/CreateWalletForm.js index e01a1760..b05e7a32 100644 --- a/src/stores/FormsStore/CreateWalletForm.js +++ b/src/stores/FormsStore/CreateWalletForm.js @@ -8,13 +8,13 @@ class CreateWalletForm extends ExtendedForm { fields: [{ name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }, { name: 'passwordConfirm', type: 'password', - label: 'Password Confirmation', + label: 'repeatPassword', placeholder: i18n.t('fields:repeatPassword'), rules: 'required|same:password', }], diff --git a/src/stores/FormsStore/FinPassForm.js b/src/stores/FormsStore/FinPassForm.js index ec5ff96a..5d225f68 100644 --- a/src/stores/FormsStore/FinPassForm.js +++ b/src/stores/FormsStore/FinPassForm.js @@ -8,7 +8,7 @@ class FinPassForm extends ExtendedForm { fields: [{ name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', }], diff --git a/src/stores/FormsStore/LoginForm.js b/src/stores/FormsStore/LoginForm.js index d96e27fa..c0e5351f 100644 --- a/src/stores/FormsStore/LoginForm.js +++ b/src/stores/FormsStore/LoginForm.js @@ -8,13 +8,13 @@ class LoginForm extends ExtendedForm { fields: [{ name: 'wallet', type: 'text', - label: 'Wallet', + label: 'wallet', placeholder: i18n.t('fields:wallet'), rules: 'required|string', }, { name: 'password', type: 'password', - label: 'Password', + label: 'enterPassword', placeholder: i18n.t('fields:enterPassword'), rules: 'required|password', diff --git a/src/stores/FormsStore/NodeChangeForm.js b/src/stores/FormsStore/NodeChangeForm.js new file mode 100644 index 00000000..72280c06 --- /dev/null +++ b/src/stores/FormsStore/NodeChangeForm.js @@ -0,0 +1,36 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable no-restricted-globals */ +/* eslint-disable class-methods-use-this */ +/* eslint-disable no-unused-vars */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; +import { fs, PATH_TO_CONFIG } from '../../constants/windowModules'; + +const { ipcRenderer } = window.require('electron'); + +let config; +try { + config = JSON.parse(fs.readFileSync(PATH_TO_CONFIG), 'utf8'); +} catch { + ipcRenderer.send('config-problem', PATH_TO_CONFIG); + // alert(`Something wrong with config file + // located in ${PATH_TO_CONFIG}. + // Please check it, without this you can't continue.`); +} + +class NodeChangeForm extends ExtendedForm { + setup() { + return { + fields: [{ + name: 'url', + type: 'text', + label: 'nodeUrl', + placeholder: i18n.t('fields:nodeUrl'), + rules: 'required|url', + value: config.host, + }], + }; + } +} + +export default NodeChangeForm; diff --git a/src/stores/FormsStore/StartNewVoteForm.js b/src/stores/FormsStore/StartNewVoteForm.js new file mode 100644 index 00000000..17b2f457 --- /dev/null +++ b/src/stores/FormsStore/StartNewVoteForm.js @@ -0,0 +1,23 @@ +/* eslint-disable no-alert */ +/* eslint-disable no-console */ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class StartNewVoteForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'question', + type: 'text', + label: 'chooseTheQuestion', + placeholder: i18n.t('fields:chooseTheQuestion'), + rules: 'required|integer|min:1', + }, + ], + }; + } +} + +export default StartNewVoteForm; diff --git a/src/stores/FormsStore/TransferTokenForm.js b/src/stores/FormsStore/TransferTokenForm.js new file mode 100644 index 00000000..61b75cf5 --- /dev/null +++ b/src/stores/FormsStore/TransferTokenForm.js @@ -0,0 +1,35 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class TransferTokenForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'address', + type: 'text', + label: 'address', + placeholder: i18n.t('fields:address'), + rules: 'required|string|address', + }, + { + name: 'count', + type: 'text', + label: 'countTokens', + placeholder: i18n.t('fields:countTokens'), + rules: 'required|numeric', + }, + { + name: 'password', + type: 'password', + label: 'password', + placeholder: i18n.t('fields:password'), + rules: 'required|password', + }, + ], + }; + } +} + +export default TransferTokenForm; diff --git a/src/stores/FormsStore/VotingFilterForm.js b/src/stores/FormsStore/VotingFilterForm.js new file mode 100644 index 00000000..f5287b34 --- /dev/null +++ b/src/stores/FormsStore/VotingFilterForm.js @@ -0,0 +1,32 @@ +/* eslint-disable class-methods-use-this */ +import i18n from 'i18next'; +import ExtendedForm from '../../models/FormModel'; + +class VotingFilterForm extends ExtendedForm { + setup() { + return { + fields: [ + { + name: 'question', + type: 'text', + label: 'question', + placeholder: i18n.t('fields:question'), + }, + { + name: 'date_before', + type: 'text', + label: 'dateBefore', + placeholder: i18n.t('fields:dateBefore'), + }, + { + name: 'date_after', + type: 'text', + label: 'dateAfter', + placeholder: i18n.t('fields:dateAfter'), + }, + ], + }; + } +} + +export default VotingFilterForm; diff --git a/src/stores/HistoryStore/HistoryStore.js b/src/stores/HistoryStore/HistoryStore.js index e96b6627..9f67593a 100644 --- a/src/stores/HistoryStore/HistoryStore.js +++ b/src/stores/HistoryStore/HistoryStore.js @@ -1,73 +1,600 @@ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-await-in-loop */ import { observable, action, computed } from 'mobx'; import Voting from './entities/Voting'; +import { PATH_TO_DATA } from '../../constants/windowModules'; +import { readDataFromFile, writeDataToFile } from '../../utils/fileUtils/data-manager'; +import FilterStore from '../FilterStore/FilterStore'; +import PaginationStore from '../PaginationStore'; +import { statusStates } from '../../constants'; +import AsyncInterval from '../../utils/AsyncUtils'; class HistoryStore { - @observable votings = []; + @observable pagination = null; - constructor(projectAddress) { - this.fetchVotings(projectAddress); + /** Voting list */ + @observable _votings = []; + + /** Voting data is loading, should be true only on first load */ + @observable loading = false; + + /** User tokens is return (actual state, updated by interval) */ + @observable isUserReturnTokensActual = false; + + /** Is there an active vote */ + @observable isActiveVoting = false; + + constructor(rootStore) { + this.rootStore = rootStore; + const { configStore: { UPDATE_INTERVAL } } = rootStore; + this.loading = false; + this.filter = new FilterStore(); + this.updateHistoryInterval = new AsyncInterval({ + cb: async () => { + await this.getActualState(() => { + this.returnToLastPage(); + }); + await this.setLoadingFinish(); + }, + timeoutInterval: UPDATE_INTERVAL, + }); + } + + @action setLoadingFinish() { + this.loading = false; + } + + /** + * Actual list of voting + * + * @returns {Array} list of voting + */ + @computed get votings() { + return this._votings; + } + + /** + * Get filtered list votings + * + * @returns {Array} filtered by rules + * votings list + */ + @computed + get list() { + return this.filter.filteredList(this.votings); + } + + /** + * Get paginated voting list + * + * @returns {Array} paginated voting list + */ + @computed + get paginatedList() { + let range; + if (!this.pagination || !this.pagination.paginationRange) { + range = [0, 5]; + } else { + range = this.pagination.paginationRange; + } + return this.list.slice(range[0], range[1] + 1); + } + + /** + * Get raw voting list + * + * @returns {Array} raw voting list + */ + get rawList() { + return this._votings.map((voting) => ({ + ...voting.raw, + })); + } + + /** + * Method for check that user return token + * + * @returns {boolean} user return tokens or not + */ + @action + fetchUserReturnTokens = async () => { + const isReturn = await this.isUserReturnTokens(); + this.isUserReturnTokensActual = isReturn; + return isReturn; + } + + /** + * Method for getting actual state for + * this store. This includes: current voting list, + * whether the user returned tokens, whether + * there is an active voting + * + * @param {Function} cb callback function + */ + @action + async getActualState(cb) { + await this.getActualVotingList(); + this.isActiveVoting = await this.hasActiveVoting(); + await this.fetchUserReturnTokens(); + if (cb) cb(); + } + + /** + * Method for returning on last page after votings + * list update + */ + @action + returnToLastPage() { + let currentPage = 1; + if (this.pagination !== null) { + currentPage = this.pagination.getCurrentPage(); + } + this.pagination = new PaginationStore({ + totalItemsCount: this.list.length, + }); + this.pagination.handleChange(currentPage); } /** * recieving voting length for fetching them from contract + * * @function - * @param {string} address user address * @returns {number} count of votings */ - @action fetchVotingsCount = (address) => address + @action fetchVotingsCount = async () => { + // eslint-disable-next-line no-unused-vars + const { contractService: { _contract } } = this.rootStore; + const votingsLength = await _contract.methods.getVotingsAmount().call(); + return Number(votingsLength); + } /** * recieving voting for local using + * * @function - * @param {string} address user address */ - @action fetchVotings = (address) => { - this.fetchVotingsCount(address); - this.votings.push(new Voting()); + @action fetchVotings = async () => { + let length = await this.fetchVotingsCount(); + length -= 1; + for (length; length >= 0; length -= 1) { + const voting = await this.getVotingFromContractById(length); + const duplicateVoting = this._votings.find((item) => item.id === voting.id); + if (!duplicateVoting) this._votings.push(new Voting(voting)); + } } /** - * Getting full info about one voting, selected by id - * @function - * @param {number} id id of voting - * @return {object} selected voting + * Method for update voting with active + * status state + */ + @action + async updateVotingWithActiveState() { + const { votings } = this; + votings.forEach(async (votingItem) => { + if (votingItem.status === statusStates.active) { + const { id } = votingItem; + const voting = await this.getVotingFromContractById(id); + votingItem.update(voting); + } + }); + } + + /** + * Method for getting actual question + * from the contract & file */ - @action getVotingsById = (id) => this.votings.filter((voting) => voting.id === id) + @action + getActualVotingList = async () => { + const votings = await this.getFilteredVotingListFromFile(); + this.writeVotingListToState(votings); + if (!votings || !votings.length) { + await this.getVotingListFromContract(); + return; + } + await this.getFilteredVotingList(); + await this.updateVotingWithActiveState(); + } /** - * Getting stats about votes in voting, selected by id + * Getting full info about one voting, selected by id + * * @function * @param {number} id id of voting - * @return {array} stats + * @returns {object} selected voting */ - @action getVotingStats = (id) => id + @action getVotingById = (id) => this._votings.filter((voting) => voting.id === id) /** - * filtering voting by given parameters - * @function - * @param {object} params parameters for filtering - * @param {number} params.questionId filter votings by questionId - * @param {number} params.descision filter voting by descision - * @param {string} params.dateFrom filter voting by startTime - * @param {string} params.dateTo filter voting by endTime - * @return {array} Filtered question + * Method for update specific data + * for voting by id + * + * @param {object} param0 data + * @param {string|number} param0.id id voting + * @param {object} param0.newState new state for data update */ - @action filterVotings = (params) => params + @action + updateVotingById = ({ + id, + newState, + }) => { + const [voting] = this.getVotingById(id); + voting.update(newState); + } /** - * @function - * @return {bool} True if project have not ended voting + * Method for update last voting from list. + * By default used for update voting after closed. */ + @action + fetchAndUpdateLastVoting = async () => { + const lastIndex = await this.fetchVotingsCount(); + const votingFromContract = await this.getVotingFromContractById(lastIndex - 1); + const [voting] = this.getVotingById(lastIndex - 1); + voting.update(votingFromContract); + this.writeVotingsToFile(); + } + + @action + reset = () => { + this.updateHistoryInterval.cancel(); + this.pagination = null; + this._votings = []; + this.loading = true; + } + @computed get isVotingActive() { - return this.votings; + return this.isActiveVoting; + } + + async getVotingListFromContract() { + await this.fetchVotings(); + this.writeVotingsToFile(); } /** - * @function - * @return {array} list of votings + * Method for getting list voting from file + * without duplicated item & with correct order + * + * @returns {Array} correct array of voting + */ + async getFilteredVotingListFromFile() { + const { contractService, userStore, projectStore: { questionStore } } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + const votings = []; + const votingsFromFile = await readDataFromFile({ + name: 'votings', + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + const votingsFromFileLength = votingsFromFile.data && votingsFromFile.data.length + ? votingsFromFile.data.length + : 0; + for (let i = 0; i < votingsFromFileLength; i += 1) { + let voting = votingsFromFile.data[i]; + if (voting) { + const { id } = voting; + let [question] = questionStore.getQuestionById(Number(voting.questionId)); + if (!question) { + question = await contractService.fetchQuestion(voting.questionId); + } + voting = await this.extendVotingInfo({ voting, question, id }); + const duplicateVoting = votings.find((item) => item.id === voting.id); + if (!duplicateVoting) votings.push(new Voting(voting)); + } + } + + votings.sort((a, b) => b.id - a.id); + return votings; + } + + /** + * Method for write voting list to + * state history + * + * @param {Array} votingList voting list + */ + writeVotingListToState(votingList) { + const votingListLength = votingList.length; + for (let i = 0; i < votingListLength; i += 1) { + const voting = votingList[i]; + const duplicateVoting = this._votings.find((item) => item.id === voting.id); + if (!duplicateVoting) this._votings.push(new Voting(voting)); + } + } + + /** Method for getting missing votings from contract */ + async getFilteredVotingList() { + const votingListFromFile = await this.getFilteredVotingListFromFile(); + const countOfVotings = await this.fetchVotingsCount(); + const votingListFromFileLength = votingListFromFile.length; + if (countOfVotings > votingListFromFileLength) { + for (let i = votingListFromFileLength; i < countOfVotings; i += 1) { + // eslint-disable-next-line no-await-in-loop + const voting = await this.getVotingFromContractById(i); + const duplicateVoting = this._votings.find((item) => item.id === voting.id); + if (!duplicateVoting) { + this._votings.unshift(new Voting(voting)); + } + } + this.writeVotingsToFile(); + } + } + + /** + * Add new filter rule + * + * @param {object} rule object with rules + */ + addFilterRule = (rule) => { + this.filter.addFilterRule(rule); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** + * Method for remove filter rule + * by name + * + * @param {string} rule name rule + */ + removeFilterRule = (rule) => { + this.filter.removeFilterRule(rule); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** + * Method for reset filter + * & update pagination + */ + resetFilter = () => { + this.filter.reset(); + if ( + this.pagination + && this.pagination.update + ) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** Write raw voting list data to file */ + writeVotingsToFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + writeDataToFile({ + name: 'votings', + data: { + data: this.rawList, + }, + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + } + + async getVoterList(votingId) { + const { + rootStore: { + projectStore: { + questionStore, + }, + contractService: { + _contract, + }, + membersStore, + }, + } = this; + const [voting] = this.getVotingById(votingId); + const { allowedGroups } = voting; + + const result = {}; + + for (let i = 0; i < allowedGroups.length; i += 1) { + const group = allowedGroups[i]; + const memberGroup = membersStore.getMemberGroupByAddress(group); + if (!memberGroup || !memberGroup.list) return []; + const { list, balance } = memberGroup; + result[memberGroup.wallet] = { + positive: [], + negative: [], + }; + for (let j = 0; j < list.length; j += 1) { + let info = {}; + const { wallet } = list[j]; + const vote = await _contract.methods + .getUserVote(votingId, group, wallet).call(); + const tokenCount = await _contract.methods + .getUserVoteWeight(votingId, group, wallet).call(); + const weight = ((tokenCount / Number(balance)) * 100).toFixed(2); + switch (vote) { + case ('1'): + info = { wallet, weight }; + if ( + result[memberGroup] + && result[memberGroup].positive + ) { + result[memberGroup].positive.push(info); + } + break; + case ('2'): + info = { wallet, weight }; + if ( + result[memberGroup.wallet] + && result[memberGroup.wallet].negative + ) { + result[memberGroup.wallet].negative.push(info); + } + break; + default: + break; + } + } + } + return result; + } + + /** + * Method for extending voting. Avoid + * duplicate code for @getVotingFromContractById method + * + * @returns {object} extended voting + */ + async extendVotingInfo({ + voting, + question, + id, + }) { + const resultVoting = voting; + resultVoting.caption = question && question.name; + resultVoting.text = question && question.description; + const formula = question.rawFormula ? question.rawFormula : question.formula; + resultVoting.allowedGroups = await this.getGroupsAllowedToVoting({ formula }); + const userVotes = await this.getUserVote(voting.allowedGroups, id); + resultVoting.userVote = userVotes.length === 1 ? userVotes[0] : 0; + return resultVoting; + } + + /** + * Method for getting actual voting + * from contract by id + * + * @param {string|number} id id voting + * @returns {object} actual voting form contract + */ + async getVotingFromContractById(id) { + const { contractService, projectStore: { questionStore } } = this.rootStore; + let voting = await contractService.fetchVoting(id); + const descision = await contractService.callMethod('getVotingResult', id); + voting.descision = descision; + let [question] = questionStore.getQuestionById(Number(voting.questionId)); + if (question) { + voting = await this.extendVotingInfo({ voting, question, id }); + } else { + // Get question from contract, for correct work! + question = await contractService.fetchQuestion(voting.questionId); + if (!question) throw new Error(`Question with id: ${voting.questionId}, not found!`); + voting = await this.extendVotingInfo({ voting, question, id }); + } + voting.data = voting.votingData; + delete voting.votingData; + voting.id = id; + for (let j = 0; j < 6; j += 1) { + delete voting[j]; + } + return voting; + } + + async getUserVote(allowedGroups, votingId) { + const { + contractService: { _contract }, + userStore: { address }, + } = this.rootStore; + const votes = []; + for (let i = 0; i < allowedGroups.length; i += 1) { + const vote = await _contract.methods.getUserVote(votingId, allowedGroups[i], address).call(); + votes.push(vote); + } + + const set = [...new Set(votes)]; + return set; + } + + /** + * Method for finding allowed groups from formula + * + * @param {object} question quiestion, which formula will be used */ - @computed get votingsList() { - return this.votings; + // eslint-disable-next-line class-methods-use-this + getGroupsAllowedToVoting({ formula }) { + const list = formula.match(/(erc20{((0x)+([0-9 a-f A-F]){40})})|(custom{((0x)+([0-9 a-f A-F]){40})})/g); + if (!list || !list.length) return []; + const groups = list.map((group) => group.replace(/(erc20({)|(}))|(custom({)|(}))/g, '')); + return groups; + } + + async lastUserVoting() { + const { contractService, userStore, membersStore } = this.rootStore; + const { list } = membersStore; + let lastVoting = 0; + // TODO fix empty list + if (list && list.length) { + const listLength = list.length; + for (let i = 0; i < listLength; i += 1) { + const lastVotingFromContract = await contractService._contract.methods.findLastUserVoting( + list[i].wallet, + userStore.address, + ).call(); + if (Number(lastVotingFromContract) > lastVoting) { + lastVoting = Number(lastVotingFromContract); + } + } + } + return lastVoting; + } + + /** + * Method for check active voting state + * + * @returns {boolean} has active voting state + */ + async hasActiveVoting() { + const countOfVotings = await this.fetchVotingsCount(); + const lastVote = countOfVotings - 1; + const voting = await this.getVotingFromContractById(lastVote); + return voting.status === statusStates.active; + } + + async isUserReturnTokens() { + const { contractService, userStore, membersStore } = this.rootStore; + const { list } = membersStore; + const listLength = list.length; + const isReturn = true; + for (let i = 0; i < listLength; i += 1) { + const isReturnTokens = await contractService._contract.methods + .isUserReturnTokens(list[i].wallet, userStore.address).call(); + if (isReturnTokens === false) return false; + } + return isReturn; + } + + async returnTokens() { + const { + contractService, Web3Service, userStore, appStore, + } = this.rootStore; + const { _contract } = contractService; + const { address, password } = userStore; + const tx = { + data: contractService._contract.methods.revoke().encodeABI(), + value: '0x0', + from: address, + to: _contract.options.address, + }; + + appStore.setTransactionStep('compileOrSign'); + return Web3Service.createTxData(address, tx) + .then((createdTx) => userStore.singTransaction(createdTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + appStore.setTransactionStep('success'); + userStore.getEthBalance(); + }); } } export default HistoryStore; diff --git a/src/stores/HistoryStore/entities/Voting.js b/src/stores/HistoryStore/entities/Voting.js index d2bff068..fffab676 100644 --- a/src/stores/HistoryStore/entities/Voting.js +++ b/src/stores/HistoryStore/entities/Voting.js @@ -1,17 +1,79 @@ +import { action, observable, computed } from 'mobx'; +import { statusStates } from '../../../constants'; + class Voting { + raw; + + @observable _userVote; + + @observable status; + + @observable descision; + + @observable closeVoteInProgress; + /** - * @constructor - * @param {Object} data object contains info adout voting - * @param {Number} data.votingId id of voting - * @param {Number} data.questionId id of question which selected for voting + * Voting is new for user. Used to highlight a new vote. + * It differs only for active voting. For closed, this is + * always false + */ + @observable newForUser = false; + + /** + * @class + * @param {object} data object contains info adout voting + * @param {number} data.id id of voting + * @param {number} data.questionId id of question which selected for voting * @param {Array} data.params parameters of voting */ constructor({ - votingId, questionId, params, + id, descision, questionId, + data, status, startTime, + endTime, caption, text, + userVote, newForUser, + allowedGroups, }) { - this.id = votingId; + this.raw = { + id, descision, questionId, data, status, startTime, endTime, caption, text, userVote, + }; + this.id = id; this.questionId = questionId; - this.params = params; + this.descision = descision; + this.data = data; + this.startTime = startTime; + this.endTime = endTime; + this.status = status; + this.caption = caption; + this.text = text; + this._userVote = Number(userVote); + this.closeVoteInProgress = false; + this.allowedGroups = allowedGroups; + if (status === statusStates.active) { + this.newForUser = newForUser !== undefined ? newForUser : true; + } + } + + @computed + get userVote() { + return Number(this._userVote); + } + + /** + * Method for update state voting + * + * @param {object} newState new state for voting + */ + @action + update = (newState) => { + Object.keys(newState).forEach((key) => { + if (key === 'userVote') { + this._userVote = newState.userVote; + this.raw.userVote = newState.userVote; + } else { + this[key] = newState[key]; + this.raw[key] = newState[key]; + } + }); } } diff --git a/src/stores/MembersStore/MemberItem.js b/src/stores/MembersStore/MemberItem.js new file mode 100644 index 00000000..f4924e51 --- /dev/null +++ b/src/stores/MembersStore/MemberItem.js @@ -0,0 +1,81 @@ +import { computed, action, observable } from 'mobx'; + +class MemberItem { + /** + * Class constructor + * + * @param {object} param0 data for constructor + * @param {string} param0.wallet wallet + * @param {number} param0.weight weight + * @param {number} param0.balance balance + * @param {string} param0.customTokenName custom token name + * @param {boolean} param0.isAdmin wallet is admin + */ + constructor({ + wallet, + weight, + balance, + customTokenName, + isAdmin, + }) { + if ( + !wallet + || !weight + || !customTokenName + || !balance + ) throw new Error('Incorrect data provided for MemberItem!'); + this.wallet = wallet; + this._weight = parseInt(weight, 10); + this.balance = parseInt(balance, 10); + this.customTokenName = customTokenName; + if (typeof isAdmin === 'boolean') this.isAdmin = isAdmin; + } + + /** Wallet address member */ + wallet = ''; + + /** Weight member in vote */ + @observable _weight = 0; + + /** Balance member */ + @observable balance = 0; + + /** Custom token name */ + customTokenName = ''; + + /** Wallet is admin */ + isAdmin = false; + + @computed + /** Method for getting full balance text */ + get fullBalance() { + return `${this.balance} ${this.customTokenName}`; + } + + @computed + get weight() { + return this._weight.toFixed(2); + } + + @action + setWeight(weight) { + this._weight = weight; + } + + @action + setTokenBalance(balance) { + this.balance = balance; + } + + @action + removeAdminPrivileges() { + this.isAdmin = false; + } + + @action + addAdminPrivileges() { + this.isAdmin = true; + } +} + +export default MemberItem; diff --git a/src/stores/MembersStore/MemberItem.test.js b/src/stores/MembersStore/MemberItem.test.js new file mode 100644 index 00000000..5d160bab --- /dev/null +++ b/src/stores/MembersStore/MemberItem.test.js @@ -0,0 +1,53 @@ +import MemberItem from './MemberItem'; + +describe('MemberItem', () => { + const defaultProps = { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 5, + balance: 120, + customTokenName: 'TKN', + }; + + describe('With correct data', () => { + let memberItem; + + beforeEach(() => { + memberItem = new MemberItem(defaultProps); + }); + + it('should has correct data', () => { + expect(memberItem.wallet).toEqual('0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54'); + expect(memberItem.weight).toEqual(5); + expect(memberItem.balance).toEqual(120); + expect(memberItem.customTokenName).toEqual('TKN'); + }); + + it('fullBalance should be correct', () => { + expect(memberItem.fullBalance).toEqual('120 TKN'); + }); + }); + + it('should cause error without wallet', () => { + expect( + () => (new MemberItem({ ...defaultProps, wallet: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); + + it('should cause error without weight', () => { + expect( + () => (new MemberItem({ ...defaultProps, weight: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); + + it('should cause error without balance', () => { + expect( + () => (new MemberItem({ ...defaultProps, balance: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); + + it('should cause error without customTokenName', () => { + expect( + () => (new MemberItem({ ...defaultProps, customTokenName: null })), + ).toThrow(new Error('Incorrect data provided for MemberItem!')); + }); +}); diff --git a/src/stores/MembersStore/MembersGroup.js b/src/stores/MembersStore/MembersGroup.js new file mode 100644 index 00000000..b070ed8e --- /dev/null +++ b/src/stores/MembersStore/MembersGroup.js @@ -0,0 +1,192 @@ +import { observable, action, computed } from 'mobx'; +import MemberItem from './MemberItem'; +import AsyncInterval from '../../utils/AsyncUtils'; +import { tokenTypes } from '../../constants'; + +class MembersGroup { + /** + * Class constructor + * + * @param {object} param0 data for constructor + * @param {string} param0.name name group (Example: Admins) + * @param {string} param0.description description for group + * @param {string} param0.wallet wallet for group + * @param {number} param0.balance balance for group + * @param {string} param0.customTokenName custom token name + * @param {string} param0.tokenName token name + * @param {string} param0.textForEmptyState text for state when list is empty + * @param {Array} param0.list members list + */ + constructor({ + name, + groupAddress, + contract, + totalSupply, + groupType, + tokenSymbol, + userAddress, + interval, + members, + textForEmptyState, + groupId, + }) { + if ( + !name + || !contract + || !groupType + || !tokenSymbol + || Array.isArray(members) === false + || !groupAddress + ) throw new Error('Incorrect data provided!'); + this.name = name; + this.wallet = groupAddress; + this.groupType = groupType; + this.balance = totalSupply; + this.contract = contract; + this.customTokenName = tokenSymbol; + this.userAddress = userAddress; + this.groupId = groupId; + if (textForEmptyState && textForEmptyState.length) { + this.textForEmptyState = textForEmptyState; + } + this.addToList(members); + this.getUserBalanceInGroup(); + this.updateInterval = 60000; + this.interval = new AsyncInterval({ + timeoutInterval: interval, + cb: this.updateUserBalanceAndGroupAdmin, + }); + } + + /** Name group (Example: Admins) */ + name = ''; + + /** Description for group */ + description = ''; + + /** Wallet for group */ + wallet = null; + + /** Custom token name */ + customTokenName = ''; + + /** Basic token name */ + tokenName = ''; + + /** Balance group */ + balance; + + /** User balance in group */ + @observable userBalance; + + /** Text for state when list is empty */ + textForEmptyState = 'other:noData'; + + @computed + get fullBalance() { + return `${this.balance} ${this.customTokenName}`; + } + + @computed + get fullUserBalance() { + return `${this.userBalance} ${this.customTokenName}`; + } + + @computed + get groupAdmin() { + return this.list.filter((user) => user.isAdmin === true); + } + + /** + * Method for getting balance in group + */ + getUserBalanceInGroup = async () => { + if (!this.contract || !this.contract.methods) return; + const balance = await this.contract.methods.balanceOf(this.userAddress).call(); + this.userBalance = balance; + } + + /** Members list */ + @observable list = []; + + @action + /** + * Method for adding new member to group + * + * @param {object} member all data about member + */ + addToList = (members) => { + members.forEach((member) => { + if (!this.list.find((item) => item.wallet === member.wallet)) { + this.list.push( + new MemberItem({ + ...member, + weight: member.weight.toString(), + }), + ); + } + }); + } + + /** + * Method for updating a member’s balance + * + * @param {number} address address wallet member + */ + @action + updateMemberBalanceAndWeight = async (address) => { + const userBalance = await this.contract.methods.balanceOf(address).call(); + this.getUserBalanceInGroup(); + const user = this.list.find((member) => ( + member.wallet.toUpperCase() === address.toUpperCase() + )); + if ((!user || !user.setTokenBalance || !user.setWeight) && userBalance !== 0) { + const admin = this.groupType === tokenTypes.Custom + ? await this.contract.methods.owner().call() + : null; + this.list.push(new MemberItem({ + wallet: address, + balance: userBalance, + weight: (userBalance / Number(this.balance)) * 100, + customTokenName: this.customTokenName, + isAdmin: admin !== null + ? address === admin + : false, + })); + } else { + const weight = (userBalance / Number(this.balance)) * 100; + user.setTokenBalance(userBalance); + user.setWeight(weight); + } + } + + @action + updateUserBalanceAndGroupAdmin = () => { + this.updateUserBalance(); + this.setNewAdmin(); + } + + @action + updateUserBalance = async () => { + await this.updateMemberBalanceAndWeight(this.userAddress); + } + + @action + setNewAdmin = async () => { + const admin = this.list.find((member) => member.isAdmin === true); + const newAdmin = this.groupType === tokenTypes.Custom + ? await this.contract.methods.owner().call() + : null; + const user = this.list.find((member) => member.wallet === newAdmin); + if (admin) admin.removeAdminPrivileges(); + if (user) user.addAdminPrivileges(); + } + + @action + stopInterval = () => { + this.interval.cancel(); + this.interval = null; + } +} + +export default MembersGroup; diff --git a/src/stores/MembersStore/MembersGroup.test.js b/src/stores/MembersStore/MembersGroup.test.js new file mode 100644 index 00000000..77298826 --- /dev/null +++ b/src/stores/MembersStore/MembersGroup.test.js @@ -0,0 +1,71 @@ +import MembersGroup from './MembersGroup'; + +describe('MembersGroup', () => { + const defaultProps = { + name: 'Admins', + description: 'short description for group', + customTokenName: 'TKN', + tokenName: 'ERC20', + wallet: '0xB210af05Bf82eF6C6BA034B22D18c89B5D23Cc90', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + }, + ], + }; + + describe('with correct data', () => { + let membersGroup; + + beforeEach(() => { + membersGroup = new MembersGroup(defaultProps); + }); + + it('should has correct data', () => { + expect(membersGroup.name).toEqual('Admins'); + expect(membersGroup.description).toEqual('short description for group'); + expect(membersGroup.customTokenName).toEqual('TKN'); + expect(membersGroup.tokenName).toEqual('ERC20'); + expect(membersGroup.list.length).toEqual(1); + }); + }); + + it('should cause error without name', () => { + expect( + () => (new MembersGroup({ ...defaultProps, name: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without description', () => { + expect( + () => (new MembersGroup({ ...defaultProps, description: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without customTokenName', () => { + expect( + () => (new MembersGroup({ ...defaultProps, customTokenName: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without tokenName', () => { + expect( + () => (new MembersGroup({ ...defaultProps, tokenName: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without list', () => { + expect( + () => (new MembersGroup({ ...defaultProps, list: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); + + it('should cause error without wallet', () => { + expect( + () => (new MembersGroup({ ...defaultProps, wallet: null })), + ).toThrow(new Error('Incorrect data provided!')); + }); +}); diff --git a/src/stores/MembersStore/MembersStore.js b/src/stores/MembersStore/MembersStore.js new file mode 100644 index 00000000..29e6c760 --- /dev/null +++ b/src/stores/MembersStore/MembersStore.js @@ -0,0 +1,335 @@ +/* eslint-disable no-await-in-loop */ +import { observable, computed, action } from 'mobx'; +import MembersGroup from './MembersGroup'; +import { + fs, + path, + PATH_TO_CONTRACTS, + PATH_TO_DATA, +} from '../../constants/windowModules'; +import { readDataFromFile, writeDataToFile } from '../../utils/fileUtils/data-manager'; +import AsyncInterval from '../../utils/AsyncUtils'; +import { tokenTypes } from '../../constants'; + +/** + * Store for manage Members groups + * + * @param id + * @param data + * @param groupAddress + * @param admin + * @param address + * @param group + */ +class MembersStore { + transferSteps = { + input: 0, + transfering: 1, + success: 2, + error: 3, + } + + /** + * Create a member store + * + * @param {object} rootStore rootStore + */ + constructor(rootStore) { + this.rootStore = rootStore; + } + + @observable groups = []; + + @observable _transferStatus = 0; + + @observable loading = false; + + @action init() { + const { rootStore: { configStore: { UPDATE_INTERVAL } } } = this; + this.groups = []; + this.loading = true; + this.fetchUserGroups(); + this.asyncUpdater = new AsyncInterval({ + timeoutInterval: UPDATE_INTERVAL, + cb: async () => { + await this.fetchUserGroups(); + }, + }); + } + + async fetchUserGroupsLength() { + const { contractService } = this.rootStore; + return contractService.fetchUserGroupsLength(); + } + + async fetchUserGroups() { + await this.fetchUserGroupsLength() + .then((length) => this.getActualUserGroups(length)) + .then((groups) => this.getPrimaryGroupsInfo(groups)) + .then((groups) => this.getUsersBalances(groups)) + .then((groups) => { + groups.forEach((group) => { + this.addToGroups(group); + }); + this.loading = false; + }) + .catch((err) => { + console.error(err); + this.loading = false; + }); + } + + async getUserGroups(length) { + const { contractService } = this.rootStore; + const groups = []; + for (let i = 0; i < length; i += 1) { + const group = await contractService.callMethod('getUserGroup', i); + groups.push(group); + } + return groups; + } + + /** + * Method for getting groups from file + * without duplicated item + * + * @returns {Array} correct array of groups + */ + async getGroupsFromFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + const groups = []; + try { + const groupsFromFile = await readDataFromFile({ + name: 'groups', + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + const groupsFromFileLength = groupsFromFile.data && groupsFromFile.data.length + ? groupsFromFile.data.length + : 0; + for (let i = 0; i < groupsFromFileLength; i += 1) { + const group = groupsFromFile.data[i]; + if (group) { + const duplicateGroup = groups.find((item) => item.name === group.name); + if (!duplicateGroup) groups.push(group); + } + } + } catch { + return groups; + } + return groups; + } + + /** + * Method for write groups to file + * + * @param {Array} groups array of groups + */ + writeGroupsToFile(groups) { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + writeDataToFile({ + name: 'groups', + data: { + data: groups, + }, + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + } + + /** + * Method for getting groups from file + * or from contract + * + * @param {number} length length groups + * @returns {Array} actual groups data + */ + async getActualUserGroups(length) { + // Groups FROM FILE + let groups = await this.getGroupsFromFile(); + // Groups FROM CONTRACT + if ( + !groups + || !groups.length + || !groups.length < length + ) { + groups = await this.getUserGroups(length); + this.writeGroupsToFile(groups); + return groups; + } + return groups; + } + + async getPrimaryGroupsInfo(groups) { + const { Web3Service, userStore } = this.rootStore; + for (let i = 0; i < groups.length; i += 1) { + const group = groups[i]; + const abi = fs.readFileSync( + path.join(PATH_TO_CONTRACTS, group.groupType === tokenTypes.ERC20 ? './ERC20.abi' : './CustomToken.abi'), + ); + const contract = Web3Service.createContractInstance(JSON.parse(abi)); + contract.options.address = await group.groupAddress; + group.contract = contract; + group.totalSupply = await contract.methods.totalSupply().call(); + group.tokenSymbol = await contract.methods.symbol().call(); + group.users = group.groupType === tokenTypes.ERC20 + ? [userStore.address] + : await contract.methods.getHolders().call(); + group.groupId = i; + // eslint-disable-next-line no-param-reassign + groups[i] = group; + } + return groups; + } + + // eslint-disable-next-line class-methods-use-this + async getUsersBalances(groups) { + for (let i = 0; i < groups.length; i += 1) { + const group = groups[i]; + const { contract, groupType } = group; + group.members = []; + const admin = groupType === tokenTypes.Custom + ? await contract.methods.owner().call() + : null; + + for (let j = 0; j < group.users.length; j += 1) { + const user = group.users[j]; + const balance = await contract.methods.balanceOf(user).call(); + group.members.push({ + wallet: user, + balance, + weight: (balance / Number(group.totalSupply)) * 100, + customTokenName: group.tokenSymbol, + isAdmin: admin !== null + ? user === admin + : false, + }); + } + } + return groups; + } + + @action + /** + * Method for adding new group + * + * @param {object} group data for group + */ + addToGroups = (group) => { + const { userStore, configStore: { UPDATE_INTERVAL } } = this.rootStore; + const duplicateMembersGroup = this.groups.find((item) => item.name === group.name); + if (!duplicateMembersGroup) { + this.groups.push(new MembersGroup({ + ...group, + interval: UPDATE_INTERVAL, + userAddress: userStore.address, + })); + } + } + + @action + isUserInGroup(groupId, address) { + // eslint-disable-next-line max-len + const memberItem = this.groups[groupId].list.filter((user) => (user.wallet).toUpperCase() === address.toUpperCase()); + return memberItem.length > 0 ? this.groups[groupId] : null; + } + + @action setTransferStatus(status) { + this._transferStatus = this.transferSteps[status]; + } + + @action + transferTokens(groupId, from, to, count) { + const { rootStore: { appStore } } = this; + const { contract, groupType } = this.list[groupId]; + window.contract = contract; + console.log('contract', contract); + // eslint-disable-next-line no-unused-vars + const { Web3Service, userStore: { address, password }, userStore } = this.rootStore; + const data = groupType === tokenTypes.ERC20 + ? contract.methods.transfer(to, Number(count)).encodeABI() + : contract.methods.transferFrom(from, to, Number(count)).encodeABI(); + const txData = { + data, + from: userStore.address, + to: contract.options.address, + value: '0x0', + }; + appStore.setTransactionStep('compileOrSign'); + return new Promise((resolve, reject) => { + Web3Service.createTxData(address, txData) + .then((formedTx) => userStore.singTransaction(formedTx, password)) + .then((signedTx) => { + appStore.setTransactionStep('sending'); + return Web3Service.sendSignedTransaction(`0x${signedTx}`); + }) + .then((txHash) => { + appStore.setTransactionStep('txReceipt'); + return Web3Service.subscribeTxReceipt(txHash); + }) + .then(() => { + appStore.setTransactionStep('success'); + userStore.getEthBalance(); + resolve(); + }) + .catch((error) => { + reject(error); + }); + }); + } + + @action getMemberById = (id) => { + if (!this.list) return {}; + const [group] = this.list.filter((groupItem) => groupItem.groupId === Number(id)); + return group; + } + + @action getMemberGroupByAddress = (address) => { + if (!this.list) return {}; + const [group] = this.list.filter((groupItem) => ( + groupItem.wallet.toLowerCase() === address.toLowerCase() + )); + return group; + } + + getAddressesForAdminDesignate = (data) => new Promise((resolve) => { + const { Web3Service: { web3: { eth } } } = this.rootStore; + const parameters = ['tuple(uint,uint,uint,uint,uint)', 'address', 'address']; + resolve(Object.values(eth.abi.decodeParameters(parameters, data))); + }); + + @action + updateAdmin = (groupAddress) => { + const groups = this.groups.filter((group) => group.wallet === groupAddress); + groups.forEach((group) => { group.setNewAdmin(); }); + } + + @action + reset = () => { + this.asyncUpdater.cancel(); + this.groups.forEach((group) => { group.stopInterval(); }); + this.groups = []; + this._transferStatus = 0; + this.loading = true; + } + + @computed + get transferStatus() { + return this._transferStatus; + } + + @computed + get nonERC() { + return this.groups.filter((group) => group.groupType !== tokenTypes.ERC20) + .map((group) => ({ label: group.name, value: group.wallet })); + } + + @computed + get list() { + return this.groups; + } +} + +export default MembersStore; diff --git a/src/stores/MembersStore/MembersStore.test.js b/src/stores/MembersStore/MembersStore.test.js new file mode 100644 index 00000000..99fbce7c --- /dev/null +++ b/src/stores/MembersStore/MembersStore.test.js @@ -0,0 +1,53 @@ +import MembersStore from './MembersStore'; + +describe('MembersStore', () => { + const defaultGroup = { + name: 'Admins', + description: 'short description for group', + customTokenName: 'TKN', + tokenName: 'ERC20', + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + list: [ + { + wallet: '0xD490af05Bf82eF6C6BA034B22D18c39B5D52Cc54', + weight: 20, + balance: 100, + customTokenName: 'TKN', + }, + ], + }; + + describe('correct input data', () => { + let memberStore; + + beforeEach(() => { + memberStore = new MembersStore([ + defaultGroup, + ]); + }); + + it('should create without error', () => { + expect(memberStore).toBeTruthy(); + }); + + it('list length should be equal 1', () => { + expect(memberStore.list.length).toEqual(1); + }); + }); + + it('should cause error with incorrect data', () => { + expect( + () => (new MembersStore({ data: 1 })), + ).toThrow(new Error('Incorrect groups provided')); + }); + + it('store with textEmptyForState should be correct', () => { + const memberStore = new MembersStore([ + { + ...defaultGroup, + textEmptyForState: 'text for empty', + }, + ]); + expect(memberStore).toBeTruthy(); + }); +}); diff --git a/src/stores/MembersStore/index.js b/src/stores/MembersStore/index.js new file mode 100644 index 00000000..82c4ced3 --- /dev/null +++ b/src/stores/MembersStore/index.js @@ -0,0 +1,7 @@ +import MembersStore from './MembersStore'; + +export default MembersStore; + +export { + MembersStore, +}; diff --git a/src/stores/NotificationStore/NotificationItem.js b/src/stores/NotificationStore/NotificationItem.js new file mode 100644 index 00000000..46bd2991 --- /dev/null +++ b/src/stores/NotificationStore/NotificationItem.js @@ -0,0 +1,50 @@ +import { observable, action } from 'mobx'; + +const defaultOpenState = false; +const defaultStatus = 'info'; + +/** Class for manage notifications */ +class NotificationItem { + constructor(props) { + const { + isOpen, + content, + id, + status, + } = props; + if (!id && id !== 0) throw Error('Incorrect NotificationItem "id" provided'); + this.id = id; + if (!content) throw Error('Incorrect NotificationItem "content" provided'); + this.content = content; + this.status = status || defaultStatus; + this.setIsOpen(isOpen || defaultOpenState); + } + + /** + * Id notification + */ + @observable id; + + /** Notification is open state */ + @observable isOpen = defaultOpenState; + + /** Notification content */ + @observable content; + + /** Notification status. [info, important] */ + @observable status; + + // TODO add notification name for simple manage notification + + /** + * Set new is open state + * + * @param {boolean} newState new is open state + */ + @action + setIsOpen(newState) { + this.isOpen = Boolean(newState); + } +} + +export default NotificationItem; diff --git a/src/stores/NotificationStore/NotificationItem.test.js b/src/stores/NotificationStore/NotificationItem.test.js new file mode 100644 index 00000000..e2f2a8de --- /dev/null +++ b/src/stores/NotificationStore/NotificationItem.test.js @@ -0,0 +1,39 @@ +import NotificationItem from './NotificationItem'; + +describe('NotificationItem', () => { + let notificationItem; + + beforeEach(() => { + notificationItem = new NotificationItem({ + id: 0, + content: 'test', + }); + }); + + it('should have correct init state', () => { + expect(notificationItem.id).toEqual(0); + expect(notificationItem.isOpen).toEqual(false); + expect(notificationItem.content).toEqual('test'); + expect(notificationItem.content).toEqual('test'); + }); + + it('setIsOpen should work correct', () => { + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen(true); + expect(notificationItem.isOpen).toEqual(true); + notificationItem.setIsOpen(false); + expect(notificationItem.isOpen).toEqual(false); + }); + + it('setIsOpen should work correct with incorrect input data', () => { + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen(null); + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen(1); + expect(notificationItem.isOpen).toEqual(true); + notificationItem.setIsOpen(undefined); + expect(notificationItem.isOpen).toEqual(false); + notificationItem.setIsOpen('true'); + expect(notificationItem.isOpen).toEqual(true); + }); +}); diff --git a/src/stores/NotificationStore/NotificationStore.js b/src/stores/NotificationStore/NotificationStore.js new file mode 100644 index 00000000..0e996312 --- /dev/null +++ b/src/stores/NotificationStore/NotificationStore.js @@ -0,0 +1,54 @@ +import { observable, action } from 'mobx'; +import uniqKey from 'react-id-generator'; +import NotificationItem from './NotificationItem'; + +/** Class for manage notifications */ +class NotificationStore { + /** Notification list */ + @observable list = []; + + /** + * Method for adding new notification + * + * @param {object} newNotification new notification data + * @param {boolean} newNotification.isOpen open state notification + * @param {string|Node} newNotification.content content notification + */ + @action + add(newNotification) { + this.list.push( + new NotificationItem({ + ...newNotification, + id: uniqKey(), + }), + ); + } + + getNotification(id) { + return [this.list.filter((notification) => ( + notification.id === id + ))]; + } + + // TODO add manage notification by name after refactor NotificationItem + + /** + * Method fore removing notification from list + * + * @param {string} id id notification + */ + @action + remove(id) { + const filtered = this.list.filter((value) => ( + value.id !== id + )); + this.list = filtered; + } + + @action + reset = () => { + this.list = []; + } +} + +export default NotificationStore; diff --git a/src/stores/NotificationStore/NotificationStore.test.js b/src/stores/NotificationStore/NotificationStore.test.js new file mode 100644 index 00000000..0aaebf1a --- /dev/null +++ b/src/stores/NotificationStore/NotificationStore.test.js @@ -0,0 +1,55 @@ +import NotificationStore from '.'; + +describe('NotificationStore', () => { + let notificationStore; + + beforeEach(() => { + notificationStore = new NotificationStore(); + }); + + it('should have correct init state', () => { + expect(notificationStore.list).toEqual([]); + }); + + it('add method should work correct', () => { + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(1); + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(2); + }); + + it('remove method should work correct', () => { + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(2); + const { id } = notificationStore.list[0]; + notificationStore.remove(id); + expect(notificationStore.list.length).toEqual(1); + }); + + it('remove for non-exist notification should work correct', () => { + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + notificationStore.add({ + content: 'Content', + isOpen: true, + }); + expect(notificationStore.list.length).toEqual(2); + notificationStore.remove('random_identificator_1'); + expect(notificationStore.list.length).toEqual(2); + }); +}); diff --git a/src/stores/NotificationStore/index.js b/src/stores/NotificationStore/index.js new file mode 100644 index 00000000..97db3ca5 --- /dev/null +++ b/src/stores/NotificationStore/index.js @@ -0,0 +1,3 @@ +import NotificationStore from './NotificationStore'; + +export default NotificationStore; diff --git a/src/stores/PaginationStore/PaginationStore.js b/src/stores/PaginationStore/PaginationStore.js new file mode 100644 index 00000000..bd32c7d1 --- /dev/null +++ b/src/stores/PaginationStore/PaginationStore.js @@ -0,0 +1,65 @@ +import { observable, action, computed } from 'mobx'; + +const DEFAULT_ITEMS_PER_PAGE = 5; +const DEFAULT_PAGE_RANGE = 5; + +class PaginationStore { + constructor({ + totalItemsCount, + itemsCountPerPage, + pageRangeDisplayed, + }) { + this.totalItemsCount = totalItemsCount; + this.itemsCountPerPage = itemsCountPerPage || DEFAULT_ITEMS_PER_PAGE; + this.pageRangeDisplayed = pageRangeDisplayed || DEFAULT_PAGE_RANGE; + } + + @observable activePage = 1; + + @computed + get lastPage() { + return Math.ceil(this.totalItemsCount / this.itemsCountPerPage); + } + + @observable itemsCountPerPage; + + @observable totalItemsCount; + + @observable pageRangeDisplayed; + + @action + update = (newState) => { + Object.keys(newState).forEach((key) => { + this[key] = newState[key]; + }); + } + + @action + handleChange = (page) => { + let activePage = page; + if (page > this.lastPage) activePage = this.lastPage; + if (page < 1) activePage = 1; + this.activePage = activePage; + } + + getCurrentPage() { + return this.activePage; + } + + /** + * Method for getting pagination range + * + * @returns {Array} [lowRange, highRange] + * lowRange is index first element for current activePage pagination + * highRange is index last element for current activePage pagination + */ + @computed + get paginationRange() { + const { activePage, itemsCountPerPage } = this; + const lowRange = (activePage - 1) * itemsCountPerPage; + const highRange = (activePage) * itemsCountPerPage - 1; + return [lowRange, highRange]; + } +} + +export default PaginationStore; diff --git a/src/stores/PaginationStore/PaginationStore.test.js b/src/stores/PaginationStore/PaginationStore.test.js new file mode 100644 index 00000000..004a25fa --- /dev/null +++ b/src/stores/PaginationStore/PaginationStore.test.js @@ -0,0 +1,37 @@ +import PaginationStore from '.'; + +describe('PaginationStore', () => { + it('should be with correct data for total 101 & items per page 10', () => { + const pagination = new PaginationStore({ + totalItemsCount: 101, + }); + expect(pagination.lastPage).toEqual(11); + expect(pagination.activePage).toEqual(1); + expect(pagination.pageRangeDisplayed).toEqual(5); + }); + + it('should be with correct data for total 21 & items per page 2', () => { + const pagination = new PaginationStore({ + totalItemsCount: 21, + itemsCountPerPage: 2, + }); + expect(pagination.lastPage).toEqual(11); + expect(pagination.activePage).toEqual(1); + expect(pagination.pageRangeDisplayed).toEqual(5); + }); + + it('handleChange should set correct page', () => { + const pagination = new PaginationStore({ + totalItemsCount: 100, + itemsCountPerPage: 10, + }); + expect(pagination.activePage).toEqual(1); + pagination.handleChange(0); + expect(pagination.activePage).toEqual(1); + pagination.handleChange(5); + expect(pagination.activePage).toEqual(5); + expect(pagination.lastPage).toEqual(10); + pagination.handleChange(11); + expect(pagination.activePage).toEqual(10); + }); +}); diff --git a/src/stores/PaginationStore/index.js b/src/stores/PaginationStore/index.js new file mode 100644 index 00000000..f058c5fb --- /dev/null +++ b/src/stores/PaginationStore/index.js @@ -0,0 +1,3 @@ +import PaginationStore from './PaginationStore'; + +export default PaginationStore; diff --git a/src/stores/ProjectStore/ProjectStore.js b/src/stores/ProjectStore/ProjectStore.js index 063499fc..5bf2fc17 100644 --- a/src/stores/ProjectStore/ProjectStore.js +++ b/src/stores/ProjectStore/ProjectStore.js @@ -1,28 +1,72 @@ +/* eslint-disable no-unused-vars */ import { observable, action } from 'mobx'; import UsergroupStore from '../UsergroupStore'; import QuestionStore from '../QuestionStore'; import HistoryStore from '../HistoryStore'; import { votingStates } from '../../constants'; +import { fs, path, PATH_TO_CONTRACTS } from '../../constants/windowModules'; /** * Class implements whole project */ class ProjectStore { - @observable projectAddress = '' + @observable projectAddress = ''; + + @observable name = ''; @observable prepared = 0; + @observable votingData = ''; + + @observable votingQuestion = ''; + + @observable votingGroupId = ''; + @observable userGrops = []; @observable questionStore; @observable historyStore; - constructor(projectAddress) { - this.projectAddress = projectAddress; - this.questionStore = new QuestionStore(projectAddress); - this.historyStore = new HistoryStore(projectAddress); - this.userGrops = this.fetchUserGroups(projectAddress); + @observable isInitiated = true; + + timer = null; + + constructor(rootStore) { + this.rootStore = rootStore; + try { + this.projectAbi = JSON.parse(fs.readFileSync(path.join(PATH_TO_CONTRACTS, './ZeroOne.abi'))); + } catch { + alert(`Error occuried when trying to read ${path.join(PATH_TO_CONTRACTS, './ZeroOne.abi')}. Please check it.`); + } + } + + @action init({ address, name }) { + const { contractService, Web3Service, membersStore } = this.rootStore; + const contract = Web3Service.createContractInstance(this.projectAbi); + this.name = name; + this.isInitiated = false; + contract.options.address = address; + contractService.setContract(contract); + this.questionStore = new QuestionStore(this.rootStore); + this.historyStore = new HistoryStore(this.rootStore); + membersStore.init(); + this.timer = setInterval(() => { + this.getInitStatus(); + }, 1000); + } + + @action getInitStatus() { + const { rootStore } = this; + if (this.questionStore && this.historyStore && rootStore.membersStore) { + const { membersStore } = rootStore; + const { questionStore, historyStore } = this; + this.isInitiated = !(questionStore.loading || historyStore.loading || membersStore.loading); + if (this.isInitiated) { + clearInterval(this.timer); + this.timer = null; + } + } } /** @@ -32,10 +76,17 @@ class ProjectStore { this.prepared = votingStates.active; } + @action setVotingData(questionId, groupId, data) { + this.votingQuestion = questionId; + this.votingGroupId = groupId; + this.votingData = data; + } + /** * Preparing app for start voting + * * @param {number} questionId - * @param {array} parameters + * @param {Array} parameters */ @action prepareVoting(questionId, parameters) { /** @@ -58,14 +109,29 @@ class ProjectStore { /** * getting usergroups from contract + * * @param {number} projectAddress address of project - * @return {array} list of usergroups */ @action fetchUserGroups = (projectAddress) => { this.fetchUserGroupsLength(projectAddress); const data = {}; this.userGrops.push(new UsergroupStore(data)); } + + @action + reset = () => { + clearInterval(this.timer); + this.timer = null; + this.projectAddress = ''; + this.name = ''; + this.prepared = 0; + this.votingData = ''; + this.votingQuestion = ''; + this.votingGroupId = ''; + this.userGrops = []; + this.questionStore = null; + this.historyStore = null; + } } export default ProjectStore; diff --git a/src/stores/QuestionStore/QuestionStore.js b/src/stores/QuestionStore/QuestionStore.js index aea16405..8a46a0f9 100644 --- a/src/stores/QuestionStore/QuestionStore.js +++ b/src/stores/QuestionStore/QuestionStore.js @@ -1,62 +1,348 @@ import { observable, action, computed } from 'mobx'; import Question from './entities/Question'; +import { readDataFromFile, writeDataToFile } from '../../utils/fileUtils/data-manager'; +import { PATH_TO_DATA } from '../../constants/windowModules'; +import FilterStore from '../FilterStore/FilterStore'; +import PaginationStore from '../PaginationStore'; +import AsyncInterval from '../../utils/AsyncUtils'; /** * Contains methods for working + * + * @param id */ class QuestionStore { + @observable pagination; + + /** List models Question */ @observable _questions; - constructor(projectAddress) { + @observable _questionGroups; + + @observable loading = false; + + @observable filter; + + constructor(rootStore) { this._questions = []; - this.fetchQuestionsCount(projectAddress); + this._questionGroups = []; + this.rootStore = rootStore; + const { configStore: { UPDATE_INTERVAL } } = rootStore; + this.loading = false; + this.filter = new FilterStore(); + this.interval = new AsyncInterval({ + cb: async () => { + await this.getActualState(() => { + this.pagination = new PaginationStore({ + totalItemsCount: this.list.length, + }); + }); + }, + timeoutInterval: UPDATE_INTERVAL, + }); } /** - * Recieving questions count for fetching them from contract + * Getting list of questions for displaying + * * @function - * @param {string} address user address - * @returns {number} count of questions + * @returns {Array} list of all questions + */ + @computed get questions() { + return this._questions; + } + + /** + * Get raw questions list + * + * @returns {Array} raw questions list + */ + get rawList() { + return this._questions.map((question) => ({ + ...question.raw, + })); + } + + /** + * Get list questions + * + * @returns {Array} filtered by rules + * questions list + */ + @computed + get list() { + return this.filter.filteredList(this._questions); + } + + /** + * Get paginated list questions + * + * @returns {Array} paginated questions list */ - @action fetchQuestionsCount = (address) => address + @computed + get paginatedList() { + let range; + if (!this.pagination || !this.pagination.paginationRange) { + range = [0, 5]; + } else { + range = this.pagination.paginationRange; + } + return this.list.slice(range[0], range[1] + 1); + } + + @computed get options() { + return this._questions.reduce((acc, question) => ([ + ...acc, + { + value: question.id, + label: question.name, + }, + ]), [{ + value: '*', + label: 'All', + }]); + } + + @computed get newVotingOptions() { + return this._questions.map((question) => ( + { + value: question.id, + label: question.name, + } + )); + } + + @computed get questionGroups() { + return this._questionGroups.reduce((acc, group) => ([ + ...acc, + { + value: group.groupId, + label: group.name, + }, + ]), [{ + value: '*', + label: 'All', + }]); + } + + @computed get questionGroupsForVoting() { + return this._questionGroups.map((group) => ( + { + value: group.groupId, + label: group.name, + })); + } /** - * Recieving question from contract + * Method for getting actual state for + * this store. + * + * @param {Function} cb callback function + */ + @action + async getActualState(cb) { + await this.fetchActualQuestionGroups(); + await this.getActualQuestions(); + this.loading = false; + if (cb) cb(); + } + + /** + * Method for getting question groups + * from contract + */ + @action + async fetchActualQuestionGroups() { + const { contractService } = this.rootStore; + const localGroupsLength = this._questionGroups.length; + const contractGroupsLength = Number(await contractService.callMethod('getQuestionGroupsAmount')); + if (localGroupsLength < contractGroupsLength) { + for (let i = localGroupsLength; i < contractGroupsLength; i += 1) { + // eslint-disable-next-line no-await-in-loop + const element = await contractService.callMethod('getQuestionGroup', i); + const [groupName] = element; + const groupId = i; + this._questionGroups[groupId] = { groupId, name: groupName }; + } + } + } + + /** + * fetching questions from smart contract + * * @function - * @param {string} address user address */ - @action fetchQuestions = (address) => { - this.fetchQuestionsCount(address); - /** - * gets the question - */ - this.addQuestion(); + @action + async fetchQuestions() { + const { contractService } = this.rootStore; + const data = await contractService.checkQuestions(); + const countOfUploaded = Number(data.countOfUploaded); + for (let i = 0; i < countOfUploaded; i += 1) { + // eslint-disable-next-line no-await-in-loop + const question = await contractService.fetchQuestion(i); + question.groupId = Number(question.groupId); + if (question.name !== '') this.addQuestion(i, question); + } + } + + /** + * Method for getting questions from contract + * & save then to json file + */ + async getQuestionsFromContract() { + await this.fetchQuestions(); + this.writeQuestionsToFile(); + } + + /** + * Method for getting & adding questions from file + * without duplicated item + * + * @returns {Array} correct array of questions + */ + async getQuestionsFromFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + const questions = []; + try { + const questionsFromFile = await readDataFromFile({ + name: 'questions', + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + const questionsFromFileLength = questionsFromFile.data && questionsFromFile.data.length + ? questionsFromFile.data.length + : 0; + for (let i = 0; i < questionsFromFileLength; i += 1) { + const question = questionsFromFile.data[i]; + if (question) { + const duplicateQuestion = questions.find((item) => item.caption === question.name); + if (!duplicateQuestion) questions.push(question); + } + } + } catch { + return questions; + } + return questions; + } + + /** + * Get & add questions that are not in the file, + * but are in the contract + * + * @param {Array} questions array of questions + */ + async getMissingQuestions(questions) { + const firstQuestionIndex = 0; + const { contractService } = this.rootStore; + const { countOfUploaded } = await contractService.checkQuestions(); + const questionsFromFileLength = questions.length; + const countQuestionFromContract = countOfUploaded - firstQuestionIndex; + if (countQuestionFromContract > questionsFromFileLength) { + this.getQuestionsFromContract(); + } + } + + /** Write raw voting list data to file */ + writeQuestionsToFile() { + const { contractService, userStore } = this.rootStore; + const userAddress = userStore.address; + const projectAddress = contractService._contract.options.address; + writeDataToFile({ + name: 'questions', + data: { + data: this.rawList, + }, + basicPath: `${PATH_TO_DATA}${userAddress}\\${projectAddress}`, + }); + } + + /** + * Method for getting actual question + * from the contract & file + */ + async getActualQuestions() { + const questions = await this.getQuestionsFromFile(); + this.writeQuestionsListToState(questions); + if (!questions || !questions.length) { + await this.getQuestionsFromContract(); + return; + } + await this.getMissingQuestions(questions); + } + + /** + * Method for write questions list to + * this state + * + * @param {Array} questionsList questions list + */ + writeQuestionsListToState(questionsList) { + const questionsListLength = questionsList.length; + for (let i = 0; i < questionsListLength; i += 1) { + const question = questionsList[i]; + this.addQuestion(i, question); + } + } + + /** + * Add new filter rule + * + * @param {object} rule object with rules + */ + addFilterRule = (rule) => { + this.filter.addFilterRule(rule); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } + } + + /** + * Method for reset filter + * & update pagination + */ + resetFilter = () => { + this.filter.reset(); + if (this.pagination) { + this.pagination.update({ + activePage: 1, + totalItemsCount: this.list.length, + }); + } } /** * Adding question to the list + * * @function + * @param {number} id id question * @param {object} question Question which will be added */ - @action addQuestion = (question) => { - this._questions.push(new Question(question)); + @action addQuestion = (id, question) => { + const duplicatedQuestion = this._questions.find((item) => item.name === question.name); + if (!duplicatedQuestion) this._questions.push(new Question(id, question)); } /** * Getting question by given id + * * @function * @param {number} id id of question - * @returns {object} question matched by id + * @returns {Array} array with lenght == 1, contains question matched by id */ - @action getQuestionById = (id) => this._questions.filter((question) => question.id === id) + @action getQuestionById = (id) => this._questions.filter((question) => question.id === Number(id)) - /** - * Getting list of questions for displaying - * @function - * @returns {Array} list of all questions - */ - @computed get questions() { - return this._questions; + @action getQuestionGroupById = (id) => ( + this._questionGroups.filter((group) => group.groupId === id) + ) + + @action reset = () => { + this._questions = []; + this._questionGroups = []; + this.interval.cancel(); } } diff --git a/src/stores/QuestionStore/entities/Question.js b/src/stores/QuestionStore/entities/Question.js index e5bbe555..914308ee 100644 --- a/src/stores/QuestionStore/entities/Question.js +++ b/src/stores/QuestionStore/entities/Question.js @@ -1,21 +1,78 @@ class Question { + raw; + + id; + + name; + + groupId; + + description; + + methodSelector; + + status; + + target; + + paramNames; + + paramTypes; + + formula; + /** - * @constructor - * @param {object} data data about question - * @param {number} data.id question id - * @param {number} data.groupId id of group, which can start voting for this question - * @param {string} data.caption question caption - * @param {string} data.text description of the question - * @param {Array} data.params parameters which will be used after voting + * @class + * @param {string} id id question + * @param {object} question data about question + * @param {number} question.groupId id of group, which can start voting for this question + * @param {string} question.name question name + * @param {string} question.description description of the question + * @param {Array} question.paramNames array contains parameters names which + * will be used after voting + * @param {Array} question.paramNames array contains parameters types which + * will be used after voting + * @param {string} question.rawFormula formula string + * @param {string} question.target address, which method will be called after end of the voting + * @param {string} question.methodSelector hex (4 bytes) - function signature of target contract + * @param {number} question.status status of question: 0 - can't start voting, + * 1 - can start voting */ - constructor({ - id, groupId, caption, text, params, - }) { + constructor(id, question) { + const { + groupId, + name, + description, + target, + timeLimit: time, + active: status, + methodSelector, + rawFormula, + paramNames, + paramTypes, + } = question; + + this.raw = question; + this.id = id; - this.caption = caption; - this.groupId = groupId; - this.text = text; - this.params = params; + this.name = name; + this.groupId = Number(groupId); + this.description = description; + this.time = time; + this.methodSelector = methodSelector; + this.status = status; + this.target = target; + this.paramNames = paramNames; + this.paramTypes = paramTypes; + this.formula = rawFormula; + } + + getParameters() { + return this.params.map((param) => param[1]); + } + + getFormula() { + return this.formula; } } diff --git a/src/stores/RootStore/RootStore.js b/src/stores/RootStore/RootStore.js index b564760c..c03e1888 100644 --- a/src/stores/RootStore/RootStore.js +++ b/src/stores/RootStore/RootStore.js @@ -3,11 +3,14 @@ import AppStore from '../AppStore'; import UserStore from '../UserStore'; import ProjectStore from '../ProjectStore'; import DialogStore from '../DialogStore'; +import NotificationStore from '../NotificationStore'; import Web3Service from '../../services/Web3Service'; import WalletService from '../../services/WalletService'; import ContractService from '../../services/ContractService'; +import { MembersStore } from '../MembersStore'; import EventEmitterService from '../../services/EventEmitterService'; -import { fs, path, ROOT_DIR } from '../../constants/windowModules'; +// import { fs, path, ROOT_DIR } from '../../constants/windowModules'; +import ConfigStore from '../ConfigStore'; class RootStore { // stores @@ -19,6 +22,8 @@ class RootStore { dialogStore; + notificationStore; + // services walletService; @@ -29,23 +34,26 @@ class RootStore { eventEmitterService; constructor() { - const configRaw = fs.readFileSync(path.join(ROOT_DIR, './config.json'), 'utf8'); - const config = JSON.parse(configRaw); - this.Web3Service = new Web3Service(config.host, this); + this.configStore = new ConfigStore(); + this.Web3Service = new Web3Service(this.configStore.config.host, this); this.appStore = new AppStore(this); this.userStore = new UserStore(this); + this.projectStore = new ProjectStore(this); this.walletService = new WalletService(); this.eventEmitterService = new EventEmitterService(); this.contractService = new ContractService(this); this.dialogStore = new DialogStore(); + this.membersStore = new MembersStore(this); + this.notificationStore = new NotificationStore(this); } /** * initiating project + * * @param {string} address adress of project */ - @action initProject(address) { - this.projectStore = new ProjectStore(address); + @action async initProject({ address, name }) { + this.projectStore.init({ address, name }); } } diff --git a/src/stores/UserStore/UserStore.js b/src/stores/UserStore/UserStore.js index 2c8c684c..56d14ee5 100644 --- a/src/stores/UserStore/UserStore.js +++ b/src/stores/UserStore/UserStore.js @@ -1,6 +1,8 @@ import { observable, action, computed } from 'mobx'; import { Transaction as Tx } from 'ethereumjs-tx'; import i18n from 'i18next'; +import weiToFixed from '../../utils/EthUtils/wei-to-fixed'; +import AsyncInterval from '../../utils/AsyncUtils'; /** * Describes store with user data */ @@ -23,20 +25,34 @@ class UserStore { @observable password = ''; + @observable currency = 'ETH'; + + @observable fullCurrencyName = 'ether'; + + updateBalanceInterval = null; + constructor(rootStore) { this.rootStore = rootStore; } + @computed + get userBalance() { + return `${weiToFixed(this.balance, this.fullCurrencyName)} ${this.currency}`; + } + /** * saves password to store for decoding wallet and transaction signing - *@param {string} value password from form - */ +param {string} value password from form + * + * @param {string} value new pass value + */ @action setPassword(value) { this.password = value; } /** * saves v3 keystore and wallet address to store + * * @param {object} wallet JSON Keystore V3 */ @action setEncryptedWallet(wallet) { @@ -45,6 +61,7 @@ class UserStore { /** * checking Ethereum balance for given address + * * @param {string} address wallet adddress * @returns {Promise} resolves with balance rounded to 5 decimal places */ @@ -59,8 +76,9 @@ class UserStore { /** * create wallet by given password + * * @param {string} password password which will be used for wallet decrypting - * @return {Promise} resolves on success with {v3wallet, mnemonic, privateKey, walletName} + * @returns {Promise} resolves on success with {v3wallet, mnemonic, privateKey, walletName} */ @action createWallet(password) { return new Promise((resolve, reject) => { @@ -83,13 +101,14 @@ class UserStore { /** * recovering wallet by mnemonic + * * @param {string} password * @returns {Promise} resolves with {v3wallet, privateKey} */ - @action recoverWallet() { + @action recoverWallet(password = undefined) { const seed = this._mnemonicRepeat.join(' '); return new Promise((resolve, reject) => { - this.rootStore.walletService.createWallet(undefined, seed).then((data) => { + this.rootStore.walletService.createWallet(password, seed).then((data) => { if (data.v3wallet) { const { v3wallet, mnemonic, privateKey, walletName, @@ -108,16 +127,22 @@ class UserStore { /** * method for authorize wallet for working with projects + * * @param {string} password password for wallet * @returns {Promise} resolve on success authorization */ @action login(password) { - const { appStore } = this.rootStore; + const { appStore, configStore: { UPDATE_INTERVAL } } = this.rootStore; return this.readWallet(password) .then((data) => { this.privateKey = data.privateKey; this.setEncryptedWallet(JSON.parse(data.wallet)); this.authorized = true; + this.setPassword(password); + this.updateBalanceInterval = new AsyncInterval({ + timeoutInterval: UPDATE_INTERVAL, + cb: this.getEthBalance, + }); Promise.resolve(); }).catch(() => { appStore.displayAlert(i18n.t('errors:wrongPassword'), 3000); @@ -127,6 +152,7 @@ class UserStore { /** * read wallet for any operations with it + * * @param {string} password password for wallet * @returns {Promise} resolves with object {v3wallet, privateKey} */ @@ -140,8 +166,8 @@ class UserStore { } else { reject(); } - }).catch(() => { - reject(); + }).catch((err) => { + reject(err); }); }); } @@ -157,6 +183,7 @@ class UserStore { /** * checks is seed valid with walletService + * * @param {string} mnemonic mnemonic * @returns {bool} is valid */ @@ -167,13 +194,16 @@ class UserStore { /** * Signing transactions with private key + * * @function * @param {string} data rawTx * @param {string} password password which was used to encode Keystore V3 - * @return Signed TX data + * @returns Signed TX data */ - @action singTransaction(data, password) { - return new Promise((resolve) => { + @action async singTransaction(data, password) { + const { rootStore: { Web3Service: { web3: { eth } } } } = this; + const chainId = await eth.net.getId(); + return new Promise((resolve, reject) => { // eslint-disable-next-line consistent-return this.readWallet(password).then((info) => { if (info instanceof Error) return false; @@ -181,16 +211,27 @@ class UserStore { info.privateKey, 'hex', ); - const tx = new Tx(data, { chain: 'ropsten' }); + // const customCommon = Common.forCustomChain( + // 'mainnet', + // { + // chainId, + // }, + // 'byzantium', + // ); + const tx = new Tx(data, { chain: chainId }); tx.sign(privateKey); const serialized = tx.serialize().toString('hex'); resolve(serialized); - }); + }) + .catch((err) => { + reject(err); + }); }); } /** * Sending transaction from user + * * @function * @param {string} txData Raw transaction */ @@ -200,11 +241,15 @@ class UserStore { /** * Getting user Ethereum balance - * @return {number} balance in ETH + * + * @returns {number} balance in ETH */ - @action async getEthBalance() { + @action getEthBalance = async () => { const { Web3Service: { web3 } } = this.rootStore; - this.balance = await web3.eth.getBalance(this.address); + web3.eth.getBalance(this.address) + .then((result) => { + this.balance = result; + }); } @action setMnemonic(value) { @@ -227,6 +272,22 @@ class UserStore { @computed get mnemonic() { return this._mnemonic; } + + @action + reset = () => { + this.authorized = false; + this.redirectToProjects = false; + this.encryptedWallet = ''; + this.walletName = ''; + this.privateKey = ''; + this._mnemonic = Array(12); + this._mnemonicRepeat = Array(12); + this.balance = 0; + this.password = ''; + this.currency = 'ETH'; + this.fullCurrencyName = 'ether'; + this.updateBalanceInterval.cancel(); + } } export default UserStore; diff --git a/src/utils/AsyncUtils/index.js b/src/utils/AsyncUtils/index.js new file mode 100644 index 00000000..947b0033 --- /dev/null +++ b/src/utils/AsyncUtils/index.js @@ -0,0 +1,41 @@ +/** + * Class for calling cb by interval + */ +class AsyncInterval { + /** Updating is disabled */ + disabled = false; + + /** Timeout timer id */ + timerId; + + /** Interval update period */ + timeoutInterval; + + /** Callback function for calling by interval */ + cb; + + constructor({ + cb, + timeoutInterval, + }) { + this.cb = cb; + this.timeoutInterval = timeoutInterval; + this.start(); + } + + start = async () => { + if (this.disabled) return; + await this.cb(); + if (this.disabled) return; + this.timerId = setTimeout(() => { + this.start(); + }, this.timeoutInterval); + } + + cancel = () => { + this.disabled = true; + clearTimeout(this.timerId); + } +} + +export default AsyncInterval; diff --git a/src/utils/Date/Date.test.js b/src/utils/Date/Date.test.js new file mode 100644 index 00000000..9a73c512 --- /dev/null +++ b/src/utils/Date/Date.test.js @@ -0,0 +1,44 @@ +import moment from 'moment'; +import progressByDateRange from '.'; + +describe('progressByDateRange', () => { + it('progressByDateRange should throw without date', () => { + expect(progressByDateRange).toThrow(); + }); + + it('progressByDateRange for yesterday & tomorrow should be 50', () => { + const tomorrow = moment(new Date()).add(1, 'days').valueOf() / 1000; + const yesterday = moment(new Date()).add(-1, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: yesterday, + end: tomorrow, + })).toEqual(50); + }); + + it('progressByDateRange for yesterday & now should be 100', () => { + const now = moment(new Date()).valueOf() / 1000; + const yesterday = moment(new Date()).add(-1, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: yesterday, + end: now, + })).toEqual(100); + }); + + it('progressByDateRange for -2 & -1 day should be 100', () => { + const day1 = moment(new Date()).add(-2, 'days').valueOf() / 1000; + const day2 = moment(new Date()).add(-1, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: day1, + end: day2, + })).toEqual(100); + }); + + it('progressByDateRange for +1 & +2 day should be 0', () => { + const day1 = moment(new Date()).add(1, 'days').valueOf() / 1000; + const day2 = moment(new Date()).add(2, 'days').valueOf() / 1000; + expect(progressByDateRange({ + start: day1, + end: day2, + })).toEqual(0); + }); +}); diff --git a/src/utils/Date/index.js b/src/utils/Date/index.js new file mode 100644 index 00000000..552709dd --- /dev/null +++ b/src/utils/Date/index.js @@ -0,0 +1,82 @@ +import moment from 'moment'; + +/** + * Method for get progress by date range + * + * @param {object} date date range in sec + * @param {string} date.start start range date in sec + * @param {string} date.end end range date in sec + * @returns {number} progress value NOW in % + */ +const progressByDateRange = (date) => { + if (!date || !date.start || !date.end) throw new Error('Incorrect date provided'); + const nowFormatted = moment().valueOf() / 1000; + const percentValue = (Number(date.end) - Number(date.start)) / 100; + const progressValue = Math.floor((nowFormatted - Number(date.start)) / percentValue); + if (progressValue < 0) return 0; + if (progressValue > 100) return 100; + return progressValue; +}; + +/** + * Method for getting correct moment + * locale name + * + * @param {string} locale locale for convert + * @returns {string} correct moment locale + */ +const getCorrectMomentLocale = (locale) => { + switch (locale) { + case 'RUS': + return 'ru'; + case 'ENG': + return 'en-gb'; + default: + return 'en-gb'; + } +}; + +/** + * Method for getting correct litepicker + * locale name + * + * @param {string} locale locale for convert + * @returns {string} correct locale + */ +const getCorrectPickerLocale = (locale) => { + switch (locale) { + case 'RUS': + return 'ru-RU'; + case 'ENG': + return 'en-US'; + default: + return 'en-US'; + } +}; + +/** + * Method for obtaining human-readable + * difference value for a given period + * of time + * + * @param {object} param0 date start & end + * @param {moment} param0.endDate moment js date object + * @param {moment} param0.startDate moment js date object + * @returns {string} readable period of time + */ +const getTimeLeftString = ({ + endDate, + startDate, +}) => { + const duration = endDate.diff(startDate); + return moment.duration(duration).humanize(); +}; + +export default progressByDateRange; + +export { + getTimeLeftString, + progressByDateRange, + getCorrectMomentLocale, + getCorrectPickerLocale, +}; diff --git a/src/utils/EthUtils/wei-to-fixed.js b/src/utils/EthUtils/wei-to-fixed.js new file mode 100644 index 00000000..49d5ed58 --- /dev/null +++ b/src/utils/EthUtils/wei-to-fixed.js @@ -0,0 +1,15 @@ +import { fromWei } from 'web3-utils'; + +export default (value = '0', currency = 'ether', options = {}) => { + const result = fromWei(value || '0', currency); + const floatPoint = result.indexOf('.'); + const zeros = floatPoint > -1 ? result.slice(floatPoint + 1, result.length) : null; + const maxFloats = { + ether: 4, + finney: 2, + szabo: 0, + ...options.maxFloats, + }; + const round = maxFloats[currency] || 0; + return zeros && zeros.length > round ? parseFloat(result).toFixed(round) : result; +}; diff --git a/src/utils/PasswordValidation/index.js b/src/utils/PasswordValidation/index.js index f2e2af90..92d0c469 100644 --- a/src/utils/PasswordValidation/index.js +++ b/src/utils/PasswordValidation/index.js @@ -1,8 +1,8 @@ const passwordValidation = (value) => { - const regexHigh = new RegExp(/^(?=[^A-Z]*[A-Z]).{1,}$/g); - const regexLow = new RegExp(/^(?=[^a-z]*[a-z]).{1,}$/g); - const regexNum = new RegExp(/^(?=[^0-9]*[0-9]).{1,}$/g); - const regexChar = new RegExp(/^(?=.*[!&$%&? "]).{1,}$/g); + const regexHigh = new RegExp(/[A-Z]/); + const regexLow = new RegExp(/[a-z]/); + const regexNum = new RegExp(/\d/g); + const regexChar = new RegExp(/[!&$%?"]/); const regexLength = new RegExp(/^.{6,}$/g); const values = { diff --git a/src/utils/Validator/index.js b/src/utils/Validator/index.js index cf5d328e..44e8b8b9 100644 --- a/src/utils/Validator/index.js +++ b/src/utils/Validator/index.js @@ -1,5 +1,6 @@ import validatorjs from 'validatorjs'; import i18n from 'i18next'; +import { languages } from '../../constants'; validatorjs.prototype.setAttributeNames = function setAttributeNames(attributes) { if (!attributes) return; @@ -14,22 +15,39 @@ validatorjs.prototype.setAttributeNames = function setAttributeNames(attributes) const rules = { password: { - function: (value) => value.match(/(?=.*[!@#$%^&*()_\-+=~])+(?=[a-z]*[A-Z]*[0-9]).{6,}/g), + function: (value) => value.match(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{6,}/), }, address: { function: (value) => value.match(/(0x)+([0-9 a-f A-F]){40}/g), }, + uint: { + // eslint-disable-next-line no-restricted-globals + function: (value) => !isNaN(Number(value)), + }, + uint8: { + // eslint-disable-next-line no-restricted-globals + function: (value) => !isNaN(Number(value)), + }, + bytes4: { + function: (value) => value.match(/(0x)+([0-9 a-f A-F]){8}/g), + }, + formula: { + function: (value) => value.match( + /\(\s*group\s*\(\s*[a-zA-Z0-9]{1,}\s*\)\s*=>\s*condition\s*\(\s*(quorum\s*(>=|<=)\s*[0-9]{1,} %\)\)|positive\s*(>=|<=)\s*[0-9]{1,}\s*% \s*of \s*(quorum|all)\s*\)\s*\))/, + ), + }, + url: { + // eslint-disable-next-line no-useless-escape + function: (value) => value.match(/^(?:(http(s)?|ws):\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/), + }, }; const plugins = { dvr: { package: validatorjs, extend: ({ validator }) => { + window.validator = validator; const { language } = i18n; - const languages = { - RUS: 'ru', - ENG: 'en', - }; Object.keys(rules).forEach( (key) => validator.register(key, rules[key].function, rules[key].message), ); @@ -39,12 +57,28 @@ const plugins = { same: 'Fields must be same', password: 'Field value not valid', address: 'Enter valid address', + numeric: 'Value is not numeric', + uint: 'Value is not numeric', + bytes4: 'Value is not bytes4 string', + between: 'Between :min and :max signs', + formula: 'Incorrect formula', + url: 'Not valid URL string', + min: 'Value less then :min', + max: 'Value larger then :max', }); validator.setMessages('ru', { required: 'Обязательное поле', same: 'Поля должны содержать одинаковые значения', password: 'Пароль не соответствует требованиям', address: 'Введите валидный адрес', + numeric: 'Значение не является числом', + uint: 'Значение не является числом', + bytes4: 'Значение не байтовая строка', + between: 'Между :min и :max знаками', + formula: 'Некорректная формула', + url: 'Неккоректный URL', + min: 'Значение меньше чем :min', + max: 'Значение больше чем :max', }); validator.stopOnError(true); }, diff --git a/src/utils/fileUtils/data-manager.js b/src/utils/fileUtils/data-manager.js new file mode 100644 index 00000000..12ae14b6 --- /dev/null +++ b/src/utils/fileUtils/data-manager.js @@ -0,0 +1,107 @@ +import { + fs, + path, + PATH_TO_DATA, +} from '../../constants/windowModules'; + +/** + * Method for creating directory with parents + * if needed + * + * @see https://stackoverflow.com/a/40686853/9965627 + * + * @param {string} targetDir target directory + * @returns {string} directory + */ +const mkDirByPathSync = (targetDir, { isRelativeToScript = false } = {}) => { + const { sep } = path; + const initDir = path.isAbsolute(targetDir) ? sep : ''; + const baseDir = isRelativeToScript ? __dirname : '.'; + + return targetDir.split(sep).reduce((parentDir, childDir) => { + const curDir = path.resolve(baseDir, parentDir, childDir); + try { + fs.mkdirSync(curDir); + } catch (err) { + if (err.code === 'EEXIST') { // curDir already exists! + return curDir; + } + + // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows. + if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure. + throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`); + } + + const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1; + if ( + !caughtErr + || (caughtErr && curDir === path.resolve(targetDir)) + ) { + throw err; // Throw if it's just the last created dir. + } + } + + return curDir; + }, initDir); +}; + +/** + * Method for write object data to file + * with some name + * + * @param {object} param0 data for writing + * @param {string} param0.name name to write to file + * @param {object} param0.data data object to write to a file + * @param {string} [param0.basicPath] basic path for write + */ +const writeDataToFile = async ({ + name, + data, + basicPath, +}) => { + const dataPath = path.join(basicPath || PATH_TO_DATA); + if (!fs.existsSync(path.join(PATH_TO_DATA))) { + await mkDirByPathSync(path.join(PATH_TO_DATA), { recursive: true }); + } + // Create folder for file, if folder does not exist + if (!fs.existsSync(dataPath)) { + await mkDirByPathSync(path.join(dataPath), { recursive: true }); + } + fs.writeFileSync( + path.join(basicPath || PATH_TO_DATA, `${name}.json`), + JSON.stringify(data, null, '\t'), + 'utf8', + ); +}; + +/** + * Method for reading file. In case error (file + * does not exist) return empty object. + * + * @param {object} param0 data for method + * @param {string} param0.name name file for reading + * @returns {object} JSON parsed data + * @param {string} [param0.basicPath] basic path for read + */ +const readDataFromFile = ({ + name, + basicPath, +}) => { + let dataFile; + try { + dataFile = fs.readFileSync( + path.join(basicPath || PATH_TO_DATA, `./${name}.json`), + 'utf8', + ); + } catch (err) { + return {}; + } + return JSON.parse(dataFile); +}; + +export default writeDataToFile; + +export { + writeDataToFile, + readDataFromFile, +}; diff --git a/src/utils/fileUtils/index.js b/src/utils/fileUtils/index.js index c336eb01..e1486175 100644 --- a/src/utils/fileUtils/index.js +++ b/src/utils/fileUtils/index.js @@ -1,22 +1,38 @@ -import { SOL_IMPORT_REGEXP, SOL_VERSION_REGEXP } from '../../constants'; +/* eslint-disable import/no-dynamic-require */ +/* eslint-disable global-require */ +/* eslint-disable no-param-reassign */ +// eslint-disable-next-line no-unused-vars +import { + SOL_IMPORT_REGEXP, SOL_VERSION_REGEXP, VM_IMPORT_REGEXP, SOL_ENCODER_REGEXP, +} from '../../constants'; import getImports from './get-sol-imports'; -import { fs, path } from '../../constants/windowModules'; +import { fs, path, ROOT_DIR } from '../../constants/windowModules'; const readSolFile = (src, importedFiles) => { let mainImport; + let pathToFile; + if (VM_IMPORT_REGEXP.test(src)) { + [src] = src.match(VM_IMPORT_REGEXP); + src = path.join(ROOT_DIR, `../node_modules/${src}`); + } + if (!fs.existsSync(src)) throw new Error(`${src} - file not exist`); + mainImport = fs.readFileSync(src, 'utf8'); const importList = getImports(mainImport); - const currentFolder = src.replace(/(((\.\/|\.\.\/)).{1,})*([a-zA-z0-9])*(\.sol)/g, ''); + const currentFolder = src.replace(/(((\.\/|\.\.\/)).{1,})*([a-zA-Z0-9])*(\.sol)/g, ''); + importList.forEach((file) => { - const pathToFile = path.join(currentFolder, file); - if (!importedFiles[pathToFile] && (pathToFile !== src)) { - const includedFile = (readSolFile(pathToFile, importedFiles)).replace(SOL_VERSION_REGEXP, ''); - if (mainImport.match(SOL_IMPORT_REGEXP)) { - mainImport = mainImport.replace(mainImport.match(SOL_IMPORT_REGEXP)[0], includedFile); + pathToFile = path.join(currentFolder, file); + const [fileName] = pathToFile.match(/(\w+\.(?:sol))/g); + if (!importedFiles[fileName] && (pathToFile !== src)) { + if (!importedFiles[fileName]) { + const includedFile = (readSolFile(pathToFile, importedFiles)).replace(SOL_VERSION_REGEXP, '').replace(SOL_ENCODER_REGEXP, ''); + if (mainImport.match(SOL_IMPORT_REGEXP)) { + mainImport = mainImport.replace(mainImport.match(SOL_IMPORT_REGEXP)[0], includedFile); + } + importedFiles[fileName] = true; } - // eslint-disable-next-line no-param-reassign - importedFiles[pathToFile] = true; } else { mainImport = mainImport.replace(mainImport.match(SOL_IMPORT_REGEXP)[0], ''); } diff --git a/src/wallets/213123.json b/src/wallets/213123.json new file mode 100644 index 00000000..224d2c8b --- /dev/null +++ b/src/wallets/213123.json @@ -0,0 +1,21 @@ +{ + "version": 3, + "id": "4ebe0dc4-9071-4798-91fe-33e013ef7f85", + "address": "90a2929bd230f0fbf9d8bec7996b6dfb00b19116", + "crypto": { + "ciphertext": "86b75e585d91d56ef13604ccb41909745d3d73164fbc25170f1a25f12b6ae7fd", + "cipherparams": { + "iv": "8abb18309e678f06919d3916cada29fe" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "4bf4f2f356533286a06ea293f53573adb19c473b3c9059d0ab9f9b597a73cad7", + "n": 262144, + "r": 8, + "p": 1 + }, + "mac": "30fa6c46afbe9b8fe503b62b374bda4b6fa92d7fe86c63b4205105bdc72735db" + } +} \ No newline at end of file diff --git a/src/wallets/UTC--2019-11-29T07-19-05.154Z--7f51b0660a89f459a46313f391b4521963a8e5b7.json b/src/wallets/UTC--2019-11-29T07-19-05.154Z--7f51b0660a89f459a46313f391b4521963a8e5b7.json deleted file mode 100644 index 08f07869..00000000 --- a/src/wallets/UTC--2019-11-29T07-19-05.154Z--7f51b0660a89f459a46313f391b4521963a8e5b7.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": 3, - "id": "fc76e19f-f0cd-443a-a275-869053e418c7", - "address": "7f51b0660a89f459a46313f391b4521963a8e5b7", - "crypto": { - "ciphertext": "56953182a01baba41d0b8c5bde3ea1056696ff8b748944a5738519aabfc1184b", - "cipherparams": { - "iv": "2536ee3ec7985c1b708f1b97c5fdb1af" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "258cd6b009cd9cf397b18040f4871ddeead4cf3c6c781ccfafec83b8ff3c5229", - "n": 262144, - "r": 8, - "p": 1 - }, - "mac": "9908077786372dcb21cffe114b92b942a2568e92c6c6b184d8eb1f7512414a86" - } -} \ No newline at end of file diff --git a/src/wallets/UTC--2019-14-4T2-19-16.914000000Z--f6676e5138576e61b058b36fb3d2de089edc39b9.json b/src/wallets/UTC--2019-14-4T2-19-16.914000000Z--f6676e5138576e61b058b36fb3d2de089edc39b9.json deleted file mode 100644 index bc543d96..00000000 --- a/src/wallets/UTC--2019-14-4T2-19-16.914000000Z--f6676e5138576e61b058b36fb3d2de089edc39b9.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": 3, - "id": "ef8113ac-160d-4504-ad90-a2357d017a2b", - "address": "f6676e5138576e61b058b36fb3d2de089edc39b9", - "crypto": { - "ciphertext": "05d6c307d57c210dd68bf85232d754127bf55420bd35f9d08454a20e08bcd759", - "cipherparams": { - "iv": "8de293d09e53249324e414c5ba3e38a7" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "ae15fb2ae0160cbaec8af14d832877955537720805385ffacc15fccea34b913c", - "n": 262144, - "r": 8, - "p": 1 - }, - "mac": "f07befdb5f801d71ff5d79874e1fae3b6937cfc47855a3c45a9d1ed428c1ed1c" - } -} \ No newline at end of file diff --git a/webpack.dev.js b/webpack.dev.js index 616ae052..1fdd3d82 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -15,6 +15,9 @@ module.exports = { }), new CopyWebpackPlugin([ { from: './src/assets', to: './build/assets' }, + { from: './src/wallets', to: './wallets' }, + { from: './src/contracts', to: './contracts' }, + { from: './src/config.json', to: './config.json' }, ]), ], output: { @@ -40,7 +43,7 @@ module.exports = { loader: 'eslint-loader', options: { failOnError: true, - failOnWarning: true, + failOnWarning: false, }, }], }, {
+ {t('explanations:project.name')} +
+ {t('explanations:freeze')} +
- {t('explanations:project.name')} -
+ {t('other:selectQuestionGroup')} +
+
+ {t('other:enterPassForConfirm')} +
+ {field.error} +
-
{children}
+ Формула голосования записывается в примерном виде: + + {'erc20{0x123...EF}->exclude{0x234...FE}->conditions{quorum>50%, positive>50% of all}, где:'} +
+ {'1) erc20{0x123...EF} / custom{0x123...EF}'} + {' '} + - тип токенов и адрес токенов необходимой группы +
+ {'2) exclude{0x234...FE}'} + {' '} + – пользователи, которые не должны голосовать (опционально) +
+ {'3) conditions{quorum>50%, positive>50% of all}'} + {' '} + - условия для принятия решения по голосованию +
+ {'3.1) quorum>50%'} + {' '} + min% голосов в общем +
+ {'3.2) positive>50%'} + {' '} + min% голосов «ЗА» +
+ 3.3 of quorum / of all + {' '} + – модификатор, от какого числа считать условие positive - от числа токенов, + которые учавствовали в голосовании, или от всех токенов из контракта группы +
+ Вы можете связывать несколько групп пользователей, объединяя их формулы операторами + and + или + or + .Например: + «Формула 1» + or + «Формула 2» + and + «Формула 3» +
Выглядит как 4 байта Keccak хэша от сигнатуры функции в ASCII кодировке
Пример:
+ bytes4(keccak256(baz(uint32,bool))) = + {' '} + 0xcdcd77c0 +
{field.error}
+ {t('other:erc20ListIsNotViewable')} +
{t('headings:failedTransaction.subheading')}
{t('other:notEnoughTokens')}
{t('other:yourBalance')}
{t('other:parameters')}
{param}
{paramTypes[index]}
+ {text.slice(0, 250)} +
+ {text} +
+ {`${t('other:votingFormula')}: ${formula}`} +
{`#${id} - ${group.name}`}
{caption}
+ + No questions have been + + created in this group yet + +
+ {t('other:reloadNotificaion')} +
{t('explanations:seed.0')}
{t('explanations:seed.1')}
+ {t('other:newVoteEmptyStateText')} +
- {arr.map((item, index) => = index + 1} />)} + { + arr.map( + (item, index) => ( + = index + 1} + key={`step-indicator--${index + 1}`} + /> + ), + ) + }
+ + No polls created + + They will be displayed here later + +
+ + No voting matches + + the selected filter + +