From 93d3ccbd3a9681b51d3f7e5acd39ec7a685c9de6 Mon Sep 17 00:00:00 2001 From: Vladimir Volek Date: Wed, 19 Sep 2018 16:55:09 +0200 Subject: [PATCH 01/28] Added separated dev build with sentry.io --- .eslintignore | 2 ++ .gitignore | 1 + package.json | 5 ++++- src/store.js | 21 +++++++++++---------- webpack/dev.babel.js | 1 - webpack/production.babel.js | 3 +++ yarn.lock | 9 ++++++++- 7 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.eslintignore b/.eslintignore index 340b57df..cb7239b8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,6 @@ public +build +build-devel coverage images node_modules diff --git a/.gitignore b/.gitignore index 495fb80e..32c7ed74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build +build-devel # Dependency directory node_modules diff --git a/package.json b/package.json index d920fb90..f06046d9 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "scripts": { "dev": "npx webpack-dev-server --config ./webpack/dev.babel.js", "dev:local": "npx webpack-dev-server --config ./webpack/local.babel.js", - "build:clean": "rm -rf build", + "build:clean": "rimraf build build-devel", "build:production": "npx webpack --config ./webpack/production.babel.js --progress --bail", + "build:dev": "cross-env BUILD=development npx webpack --config ./webpack/production.babel.js --output-path build-devel --progress --bail", "build": "run-s build:*", "flow": "flow check src", "lint": "run-s lint:*", @@ -27,6 +28,7 @@ "bignumber.js": "2.4.0", "color-hash": "^1.0.3", "copy-webpack-plugin": "^4.5.2", + "cross-env": "^5.2.0", "date-fns": "^1.29.0", "ethereumjs-tx": "^1.3.3", "ethereumjs-units": "^0.2.0", @@ -54,6 +56,7 @@ "redux-logger": "^3.0.6", "redux-raven-middleware": "^1.2.0", "redux-thunk": "^2.2.0", + "rimraf": "^2.6.2", "styled-components": "^3.3.3", "styled-media-query": "^2.0.2", "styled-normalize": "^8.0.0", diff --git a/src/store.js b/src/store.js index 30e78995..06257554 100644 --- a/src/store.js +++ b/src/store.js @@ -3,8 +3,6 @@ import { createStore, applyMiddleware, compose } from 'redux'; import { routerMiddleware } from 'react-router-redux'; import thunk from 'redux-thunk'; -// import createHistory from 'history/createBrowserHistory'; -// import { useRouterHistory } from 'react-router'; import createHistory from 'history/createHashHistory'; import { createLogger } from 'redux-logger'; import reducers from 'reducers'; @@ -17,17 +15,21 @@ import type { Action, GetState, Store } from 'flowtype'; export const history: History = createHistory({ queryKey: false }); -const RAVEN_KEY: string = 'https://497392c3ff6e46dc9e54eef123979378@sentry.io/294339'; -Raven.config(RAVEN_KEY).install(); - const initialState: any = {}; const enhancers = []; -const middleware = [ + +const middlewares = [ thunk, - RavenMiddleware(RAVEN_KEY), routerMiddleware(history), ]; +// sentry io middleware only in dev build +if (process.env.BUILD === 'development') { + const RAVEN_KEY = 'https://34b8c09deb6c4cd2a4dc3f0029cd02d8@sentry.io/1279550'; + const ravenMiddleware = RavenMiddleware(RAVEN_KEY); + Raven.config(RAVEN_KEY).install(); + middlewares.push(ravenMiddleware); +} let composedEnhancers: any; if (process.env.NODE_ENV === 'development') { @@ -50,13 +52,12 @@ if (process.env.NODE_ENV === 'development') { } composedEnhancers = compose( - applyMiddleware(logger, ...middleware, ...services), + applyMiddleware(logger, ...middlewares, ...services), ...enhancers, ); - } else { composedEnhancers = compose( - applyMiddleware(...middleware, ...services), + applyMiddleware(...middlewares, ...services), ...enhancers, ); } diff --git a/webpack/dev.babel.js b/webpack/dev.babel.js index 377bb513..af765229 100644 --- a/webpack/dev.babel.js +++ b/webpack/dev.babel.js @@ -7,7 +7,6 @@ import { SRC, BUILD, PORT, PUBLIC, } from './constants'; - module.exports = { watch: true, mode: 'development', diff --git a/webpack/production.babel.js b/webpack/production.babel.js index 39342d6b..ea20417a 100644 --- a/webpack/production.babel.js +++ b/webpack/production.babel.js @@ -60,6 +60,9 @@ module.exports = { hints: false, }, plugins: [ + new webpack.DefinePlugin({ + 'process.env.BUILD': JSON.stringify(process.env.BUILD), + }), new HtmlWebpackPlugin({ chunks: ['index'], template: `${SRC}index.html`, diff --git a/yarn.lock b/yarn.lock index 0aeaea58..46f01b6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2799,6 +2799,13 @@ create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-env@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" + dependencies: + cross-spawn "^6.0.5" + is-windows "^1.0.0" + cross-spawn@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -5551,7 +5558,7 @@ is-whitespace-character@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed" -is-windows@^1.0.1, is-windows@^1.0.2: +is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" From e12c44a34dd884786751bf21ffca351b9a749b9f Mon Sep 17 00:00:00 2001 From: Vladimir Volek Date: Wed, 19 Sep 2018 18:08:22 +0200 Subject: [PATCH 02/28] Added local prod servers --- cert.pem | 20 ++++++++++++ key.pem | 28 +++++++++++++++++ package.json | 5 ++- src/views/Landing/index.js | 7 ++--- yarn.lock | 64 +++++++++++++++++++++++++++++++++++--- 5 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 cert.pem create mode 100644 key.pem diff --git a/cert.pem b/cert.pem new file mode 100644 index 00000000..31a57cf6 --- /dev/null +++ b/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMjCCAhoCCQD/Ey0A0Ll1FjANBgkqhkiG9w0BAQsFADBbMQswCQYDVQQGEwJj +ejEKMAgGA1UECAwBczEKMAgGA1UEBwwBYTEKMAgGA1UECgwBczEKMAgGA1UECwwB +czEKMAgGA1UEAwwBczEQMA4GCSqGSIb3DQEJARYBczAeFw0xODA3MjUxMTUyMzla +Fw0xODA4MjQxMTUyMzlaMFsxCzAJBgNVBAYTAmN6MQowCAYDVQQIDAFzMQowCAYD +VQQHDAFhMQowCAYDVQQKDAFzMQowCAYDVQQLDAFzMQowCAYDVQQDDAFzMRAwDgYJ +KoZIhvcNAQkBFgFzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1sON +vf+ny+KASV8pCT83fJNyAV8uAvg/Ur0B/bf26onvhoUJ74w6lFfJTyw0WjOzXR/G +reXg4yO8aGHTwSuYifY0Uj1Lm4cZYz3n96X37cKyviNg8zp+QXKgESwnxJ/VyLf+ +rYc/DzTQsXFs7irDXsGCq+8z56ljZ788axd5wip2AGBAnTzH9OeHD8sRIYNqFKNm +S2Rx1Ev3QIIwqng3e6vX4/kk7Zz380z1MpB2wbEsUQxx27hT4Z2tZRyn0L/AXQBL +H3zxA5FNVxgQaUAFWoqS6cv8nJtIRbnvoiwNygDMyxhrB+CbUCtCVDm8W94TE+Qn ++BdZzfPg/fQbv4B6owIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC/g7yIDluDPOKP +mo37+hI4vQT90HnSzccMhelBoqHSw2OFN3O+eantCy9eNr9UNHeZP7KyTsvRW5nW +L48Ps1eAPC66QCNvkLmpJ3YRHa157722cPdgV0jYm5njV+9pdjNFFD/yF43nNm/r +IdiOwXak4qqajDeug9oeZh/sGPZ888gnZMaYhON8zzL+CR+xKv45ORVpHY+fQp4f +xv8195pqcehFy2RYniDzcU/cy3IfOJaxYYbV5mlQtFvY8plhC3QtcJebS0nei4J+ +oX2Wh1dxKUNGxqPUjwfyYxPtxPTUEsOJAtTtwcrQ2ShKCzFSP9xdZ9i4WviV84pU +9RGGV7yC +-----END CERTIFICATE----- diff --git a/key.pem b/key.pem new file mode 100644 index 00000000..587de4a2 --- /dev/null +++ b/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWw429/6fL4oBJ +XykJPzd8k3IBXy4C+D9SvQH9t/bqie+GhQnvjDqUV8lPLDRaM7NdH8at5eDjI7xo +YdPBK5iJ9jRSPUubhxljPef3pfftwrK+I2DzOn5BcqARLCfEn9XIt/6thz8PNNCx +cWzuKsNewYKr7zPnqWNnvzxrF3nCKnYAYECdPMf054cPyxEhg2oUo2ZLZHHUS/dA +gjCqeDd7q9fj+STtnPfzTPUykHbBsSxRDHHbuFPhna1lHKfQv8BdAEsffPEDkU1X +GBBpQAVaipLpy/ycm0hFue+iLA3KAMzLGGsH4JtQK0JUObxb3hMT5Cf4F1nN8+D9 +9Bu/gHqjAgMBAAECggEBAIbMgXgjMn/vcBQdjZVHP52KsoEX67pjdOOKzOgigvHd +mCE3+e+IdfBMVYfDOCzxzIAEBOF7qzcGZCikVpQlt/3IMjj4Ti+VkaLP5Xx0iPSM +Q0LC1AR2z25m8v80VtW8eSQeENV8UWFLBj6J8hRfdPdRwKIIZuzeTg19Y//X4U2z +0AQHc1askJc2Hu+FEm5GajurR4CEawrgHXtBaY8F96lGPRWXJThZhB8GemKkN8Ws +i8uMgdB4dcO7iGa1GpiSErCbJxC74/FA2XKf9eVYL7tRm1Bc/hojl1FxbWVnlC+W +RXqhdcKTHQ6LODYUovBCp5A5p5S/+zB24tnMgfIhYikCgYEA+1bzT4P2XuP4/A2N +yH3Da/fWlsoVo7/c9vy0AQhWo/ZH9mHiP1A7qC6b98T2DL0qsv0viGKBKPw0J1Cj +R4Tn3aKwPxsceHOEZ9zh1qApvaQH2kVMYrxbGCYGMIcj2Ps6HjPJGy3wH5T1+sco +u0qhIKs07TtoQe767HV+XWohxacCgYEA2r78nPhKpAQDiJ1uuqNPHybttnHX2OTv +f/LCM3B4pmORCSVostjqN6yUAUYBjvEczCkcyW3mTim7H9C92bePZmRrC8/7csYm +2ymJAzRMFlbnjIzF3EkBTcWermcrXwBchDQ6LlaKATT3qbxlewplFTE7h/3wa7da +kUPwHbJx+qUCgYAZCwbfS2TG+6wZYThZW76XCXDGQYh6cmmP6on8+Fm5qJZvBD3I +1TO8hDhiLavehRK2FugfjMEV1ltT94LtY16/BLDO+OKTVd9Bgg62lerSzH9DzlfY +FrB07YT8XNrDifS2ga5uGNuuKeeAf0udrcf0O1rgsGSo/SjfWq2mnSaUTQKBgHZl +iTUs7rl3srHvBE/gtKKX33IwjDPJNhh6vMI6zhLBMW9R4CltXthjgHhv+8fymTOn +zPz5jv4feDjwMtH0mJlDIO1z1RV6Su20vYQOemBdCVb5mt5wZVRC8nBTRxZUi77C +xfruvCOLF8G3RvYh2jRuQVqKB+dFhq+5pe1s+GRBAoGBAKX8oBMbm3e2eC3FzCKE +sB4BKLzGnQmoU4E/xUYLU+h5TTht4orsCil4Y9kkIE+jPP4Z84xqpfwN4QHw4Ls+ +SwBi+3C0HIo2Evss1D4ELs58N9nNnJRsGkqBqQkJIygpUVgYOTsCBOmjjm/5+Cd3 +RUE7p2ym8WCZmDkVkhFmZk1J +-----END PRIVATE KEY----- diff --git a/package.json b/package.json index f06046d9..e6fca698 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "lint:css": "npx stylelint './src/**/*.js'", "test": "run-s test:*", "test:unit": "npx jest", - "test-unit:watch": "npx jest -o --watch" + "test-unit:watch": "npx jest -o --watch", + "prod-server": "npx http-server ./build -a localhost --ssl", + "prod-server-dev": "npx http-server ./build-devel -a localhost --ssl" }, "dependencies": { "babel": "^6.23.0", @@ -35,6 +37,7 @@ "ethereumjs-util": "^5.1.4", "hdkey": "^0.8.0", "html-webpack-plugin": "^3.2.0", + "http-server": "^0.11.1", "npm-run-all": "^4.1.3", "prop-types": "^15.6.2", "raf": "^3.4.0", diff --git a/src/views/Landing/index.js b/src/views/Landing/index.js index ed2dd87b..3da5dd4a 100644 --- a/src/views/Landing/index.js +++ b/src/views/Landing/index.js @@ -1,6 +1,6 @@ /* @flow */ - import React from 'react'; +import CaseImage from 'images/case.png'; import styled from 'styled-components'; import Header from 'components/Header'; import Footer from 'components/Footer'; @@ -39,11 +39,10 @@ const LandingContent = styled.div` justify-content: center; `; -const LandingImage = styled.div` +const LandingImage = styled.img` width: 777px; min-height: 500px; margin: auto; - background-image: url('../images/case.png'); background-repeat: no-repeat; background-position: center 0px; background-size: contain; @@ -120,7 +119,7 @@ export default (props: Props) => { showDisconnect={shouldShowDisconnectDevice} /> - + {shouldShowConnectDevice && ( diff --git a/yarn.lock b/yarn.lock index 46f01b6c..099d91dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2732,6 +2732,10 @@ cors@^2.8.1: object-assign "^4" vary "^1" +corser@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" + cosmiconfig@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" @@ -3398,6 +3402,15 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecstatic@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.0.tgz#91cd417d152abf85b37b1ab3ebf3bd25cdc64e80" + dependencies: + he "^1.1.1" + mime "^1.6.0" + minimist "^1.1.0" + url-join "^2.0.5" + editions@^1.3.3: version "1.3.4" resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" @@ -4886,7 +4899,7 @@ hdkey@^0.8.0: safe-buffer "^5.1.1" secp256k1 "^3.0.1" -he@1.1.x: +he@1.1.x, he@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" @@ -5051,7 +5064,7 @@ http-proxy-middleware@~0.18.0: lodash "^4.17.5" micromatch "^3.1.9" -http-proxy@^1.16.2: +http-proxy@^1.16.2, http-proxy@^1.8.1: version "1.17.0" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" dependencies: @@ -5059,6 +5072,19 @@ http-proxy@^1.16.2: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-server@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.11.1.tgz#2302a56a6ffef7f9abea0147d838a5e9b6b6a79b" + dependencies: + colors "1.0.3" + corser "~2.0.0" + ecstatic "^3.0.0" + http-proxy "^1.8.1" + opener "~1.4.0" + optimist "0.6.x" + portfinder "^1.0.13" + union "~0.4.3" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -6729,6 +6755,10 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + mime@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" @@ -6780,7 +6810,7 @@ minimist@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -7281,13 +7311,17 @@ opener@^1.4.3: version "1.5.1" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" +opener@~1.4.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" + opn@^5.1.0: version "5.3.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" dependencies: is-wsl "^1.1.0" -optimist@^0.6.1: +optimist@0.6.x, optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -7647,6 +7681,14 @@ pn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" +portfinder@^1.0.13: + version "1.0.17" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a" + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + portfinder@^1.0.9: version "1.0.13" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" @@ -8151,6 +8193,10 @@ qs@6.5.2, qs@~6.5.1, qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" +qs@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404" + qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -10347,6 +10393,12 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^0.4.3" +union@~0.4.3: + version "0.4.6" + resolved "https://registry.yarnpkg.com/union/-/union-0.4.6.tgz#198fbdaeba254e788b0efcb630bc11f24a2959e0" + dependencies: + qs "~2.3.3" + uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" @@ -10444,6 +10496,10 @@ urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" +url-join@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" + url-join@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a" From 30e82f85fa2d0f3099904dec31253fcf38faa0ac Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Wed, 19 Sep 2018 18:56:38 +0200 Subject: [PATCH 03/28] flowconfig: add 'images' fix for import CaseImage from 'images/case.png'; in src/views/Landing/index.js --- .flowconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.flowconfig b/.flowconfig index 9a632b96..52287bb3 100644 --- a/.flowconfig +++ b/.flowconfig @@ -43,4 +43,5 @@ module.name_mapper='^data' -> '/src/data' module.name_mapper='^services' -> '/src/services' module.name_mapper='^support' -> '/src/support' module.name_mapper='^public' -> '/public' +module.name_mapper='^images' -> '/src/images' module.system=haste From 7269ca2715de5e647bf61396291a3d900e369247 Mon Sep 17 00:00:00 2001 From: Vladimir Volek Date: Thu, 20 Sep 2018 12:31:24 +0200 Subject: [PATCH 04/28] Moved certs to folder --- package.json | 4 ++-- cert.pem => server/cert.pem | 0 key.pem => server/key.pem | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename cert.pem => server/cert.pem (100%) rename key.pem => server/key.pem (100%) diff --git a/package.json b/package.json index e6fca698..c1e37723 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "test": "run-s test:*", "test:unit": "npx jest", "test-unit:watch": "npx jest -o --watch", - "prod-server": "npx http-server ./build -a localhost --ssl", - "prod-server-dev": "npx http-server ./build-devel -a localhost --ssl" + "prod-server": "npx http-server ./build -a localhost -S -C ./server/cert.pem -K ./server/key.pem -o", + "prod-server-dev": "npx http-server ./build-devel -a localhost -S -C ./server/cert.pem -K ./server/key.pem -o" }, "dependencies": { "babel": "^6.23.0", diff --git a/cert.pem b/server/cert.pem similarity index 100% rename from cert.pem rename to server/cert.pem diff --git a/key.pem b/server/key.pem similarity index 100% rename from key.pem rename to server/key.pem From bfbdb6b0e5d7e3a6eaf6b7f2e4f0f93a09e82c17 Mon Sep 17 00:00:00 2001 From: Vladimir Volek Date: Fri, 21 Sep 2018 12:00:12 +0200 Subject: [PATCH 05/28] Added OS notification when webpack failed to compile the project --- package.json | 2 +- webpack/dev.babel.js | 20 ++++++++++++-------- webpack/production.babel.js | 2 -- yarn.lock | 14 +++++++++----- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 27532f4c..39397433 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "ethereumjs-units": "^0.2.0", "ethereumjs-util": "^5.1.4", "git-revision-webpack-plugin": "^3.0.3", - "flow-webpack-plugin": "^1.2.0", "hdkey": "^0.8.0", "html-webpack-plugin": "^3.2.0", "jest-fetch-mock": "^1.6.5", @@ -63,6 +62,7 @@ "trezor-connect": "^5.0.32", "web3": "1.0.0-beta.35", "webpack": "^4.16.3", + "webpack-build-notifier": "^0.1.29", "webpack-bundle-analyzer": "^2.13.1", "whatwg-fetch": "^2.0.4", "yarn-run-all": "^3.1.1" diff --git a/webpack/dev.babel.js b/webpack/dev.babel.js index f39a2e6e..0d020d5f 100644 --- a/webpack/dev.babel.js +++ b/webpack/dev.babel.js @@ -1,9 +1,10 @@ import webpack from 'webpack'; import GitRevisionPlugin from 'git-revision-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; -import FlowWebpackPlugin from 'flow-webpack-plugin'; +import WebpackBuildNotifierPlugin from 'webpack-build-notifier'; -import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; +// turn on for bundle analyzing +// import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import { SRC, BUILD, PORT, PUBLIC, @@ -93,10 +94,13 @@ module.exports = { hints: false, }, plugins: [ + new WebpackBuildNotifierPlugin({ + title: 'Trezor Wallet', + suppressSuccess: true, + }), new webpack.DefinePlugin({ COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()), }), - new FlowWebpackPlugin(), new HtmlWebpackPlugin({ chunks: ['index'], template: `${SRC}index.html`, @@ -104,11 +108,11 @@ module.exports = { inject: true, favicon: `${SRC}images/favicon.ico`, }), - new BundleAnalyzerPlugin({ - openAnalyzer: false, - analyzerMode: false, // turn on to generate bundle pass 'static' - reportFilename: 'bundle-report.html', - }), + // new BundleAnalyzerPlugin({ + // openAnalyzer: false, + // analyzerMode: false, // turn on to generate bundle pass 'static' + // reportFilename: 'bundle-report.html', + // }), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.NoEmitOnErrorsPlugin(), new webpack.HotModuleReplacementPlugin(), diff --git a/webpack/production.babel.js b/webpack/production.babel.js index 95932ddd..3761cec3 100644 --- a/webpack/production.babel.js +++ b/webpack/production.babel.js @@ -3,7 +3,6 @@ import webpack from 'webpack'; import GitRevisionPlugin from 'git-revision-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import CopyWebpackPlugin from 'copy-webpack-plugin'; -import FlowWebpackPlugin from 'flow-webpack-plugin'; import { SRC, BUILD, PUBLIC } from './constants'; const gitRevisionPlugin = new GitRevisionPlugin(); @@ -67,7 +66,6 @@ module.exports = { new webpack.DefinePlugin({ COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()), }), - new FlowWebpackPlugin(), new HtmlWebpackPlugin({ chunks: ['index'], template: `${SRC}index.html`, diff --git a/yarn.lock b/yarn.lock index 2d274b77..75d14975 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4310,10 +4310,6 @@ flow-parser@^0.*: version "0.72.0" resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.72.0.tgz#6c8041e76ac7d0be1a71ce29c00cd1435fb6013c" -flow-webpack-plugin@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/flow-webpack-plugin/-/flow-webpack-plugin-1.2.0.tgz#1958821d16135028e391cad5ee2f3a4fa78197ec" - flush-write-stream@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" @@ -7017,7 +7013,7 @@ node-libs-browser@^2.0.0: util "^0.10.3" vm-browserify "0.0.4" -node-notifier@^5.2.1: +node-notifier@5.2.1, node-notifier@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea" dependencies: @@ -10900,6 +10896,14 @@ webpack-addons@^1.1.5: dependencies: jscodeshift "^0.4.0" +webpack-build-notifier@^0.1.29: + version "0.1.29" + resolved "https://registry.yarnpkg.com/webpack-build-notifier/-/webpack-build-notifier-0.1.29.tgz#d71f89bb94346c6b748e07aa3d117d2beb0a151f" + dependencies: + ansi-regex "^2.0.0" + node-notifier "5.2.1" + strip-ansi "^3.0.1" + webpack-bundle-analyzer@^2.13.1: version "2.13.1" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.1.tgz#07d2176c6e86c3cdce4c23e56fae2a7b6b4ad526" From 22c17f0e383faf9d48eeb5d4212c2547cfb3da43 Mon Sep 17 00:00:00 2001 From: Vladimir Volek Date: Fri, 21 Sep 2018 14:29:55 +0200 Subject: [PATCH 06/28] Clean folders properly before build --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c1e37723..d2960e9b 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,8 @@ "scripts": { "dev": "npx webpack-dev-server --config ./webpack/dev.babel.js", "dev:local": "npx webpack-dev-server --config ./webpack/local.babel.js", - "build:clean": "rimraf build build-devel", - "build:production": "npx webpack --config ./webpack/production.babel.js --progress --bail", - "build:dev": "cross-env BUILD=development npx webpack --config ./webpack/production.babel.js --output-path build-devel --progress --bail", + "build:prod": "rimraf build && npx webpack --config ./webpack/production.babel.js --progress --bail", + "build:dev": "rimraf build-devel && cross-env BUILD=development npx webpack --config ./webpack/production.babel.js --output-path build-devel --progress --bail", "build": "run-s build:*", "flow": "flow check src", "lint": "run-s lint:*", From 1e346380a09a08a3589c6ea076fb406cd62e83f1 Mon Sep 17 00:00:00 2001 From: Vladimir Volek Date: Fri, 21 Sep 2018 14:35:22 +0200 Subject: [PATCH 07/28] eslint fix --- webpack/dev.babel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/dev.babel.js b/webpack/dev.babel.js index 792835cb..8ad53418 100644 --- a/webpack/dev.babel.js +++ b/webpack/dev.babel.js @@ -97,7 +97,7 @@ module.exports = { COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()), }), new FlowWebpackPlugin({ - reportingSeverity: 'warning' + reportingSeverity: 'warning', }), new HtmlWebpackPlugin({ chunks: ['index'], From 1ea6d0be4cc87592fe929ddc5e5ee216c859132f Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sun, 23 Sep 2018 08:28:12 +0200 Subject: [PATCH 08/28] eslint ./src/utils --- .eslintrc | 1 + src/utils/ethUtils.js | 19 +++++++++---------- src/utils/promiseUtils.js | 13 +++---------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/.eslintrc b/.eslintrc index 7c00e41a..9f709b13 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,6 +12,7 @@ "jest": true }, "rules": { + "import/prefer-default-export": 0, "no-use-before-define": 0, "no-plusplus": 0, "class-methods-use-this": 0, diff --git a/src/utils/ethUtils.js b/src/utils/ethUtils.js index 82d5744a..2d4b553e 100644 --- a/src/utils/ethUtils.js +++ b/src/utils/ethUtils.js @@ -5,14 +5,11 @@ import EthereumjsUtil from 'ethereumjs-util'; export const decimalToHex = (dec: number): string => new BigNumber(dec).toString(16); -export const padLeftEven = (hex: string): string => { - hex = hex.length % 2 != 0 ? `0${hex}` : hex; - return hex; -}; +export const padLeftEven = (hex: string): string => (hex.length % 2 !== 0 ? `0${hex}` : hex); -export const sanitizeHex = (hex: number | string): ?string => { - if (typeof hex !== 'string') return null; - hex = hex.substring(0, 2) === '0x' ? hex.substring(2) : hex; +export const sanitizeHex = ($hex: number | string): ?string => { + if (typeof $hex !== 'string') return null; + const hex = $hex.substring(0, 2) === '0x' ? $hex.substring(2) : $hex; if (hex === '') return ''; return `0x${padLeftEven(hex)}`; }; @@ -35,10 +32,12 @@ export const validateAddress = (address: string): ?string => { const hasUpperCase = new RegExp('^(.*[A-Z].*)$'); if (address.length < 1) { return 'Address is not set'; - } else if (!EthereumjsUtil.isValidAddress(address)) { + } + if (!EthereumjsUtil.isValidAddress(address)) { return 'Address is not valid'; - } else if (address.match(hasUpperCase) && !EthereumjsUtil.isValidChecksumAddress(address)) { + } + if (address.match(hasUpperCase) && !EthereumjsUtil.isValidChecksumAddress(address)) { return 'Address is not a valid checksum'; } return null; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/promiseUtils.js b/src/utils/promiseUtils.js index 48be95a1..6567d30c 100644 --- a/src/utils/promiseUtils.js +++ b/src/utils/promiseUtils.js @@ -1,12 +1,5 @@ /* @flow */ - -// import root from 'window-or-global'; -// import Promise from 'es6-promise'; - -export async function resolveAfter(msec: number, value?: any): Promise { - await new Promise((resolve) => { - //root.setTimeout(resolve, msec, value); - window.setTimeout(resolve, msec, value); - }); -} \ No newline at end of file +export const resolveAfter = (msec: number, value?: any): Promise => new Promise((resolve) => { + window.setTimeout(resolve, msec, value); +}); From de7e8914cbae718a5dc07205000a54d1253cc347 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sun, 23 Sep 2018 08:31:33 +0200 Subject: [PATCH 09/28] eslint ./src/services --- src/services/CoinmarketcapService.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/CoinmarketcapService.js b/src/services/CoinmarketcapService.js index af6e914a..d6c0e9ea 100644 --- a/src/services/CoinmarketcapService.js +++ b/src/services/CoinmarketcapService.js @@ -13,7 +13,6 @@ import type { AsyncAction, GetState, } from 'flowtype'; -import type { Config, FiatValueTicker } from 'reducers/LocalStorageReducer'; export const RATE_UPDATE: 'rate__update' = 'rate__update'; @@ -24,11 +23,11 @@ export type FiatRateAction = { } const loadRateAction = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const config: ?Config = getState().localStorage.config; + const { config } = getState().localStorage; if (!config) return; try { - config.fiatValueTickers.forEach(async (ticker: FiatValueTicker) => { + config.fiatValueTickers.forEach(async (ticker) => { // const rate: ?Array = await JSONRequest(`${ticker.url}?convert=USD`, 'json'); const rate: ?Array = await httpRequest(`${ticker.url}?convert=USD`, 'json'); if (rate) { From d684eec819f06de95632c4a3c3c1e0414f8bd2cf Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sun, 23 Sep 2018 08:36:01 +0200 Subject: [PATCH 10/28] eslint ./src/reducers --- src/reducers/AccountsReducer.js | 6 +++--- src/reducers/FiatRateReducer.js | 2 -- src/reducers/ModalReducer.js | 1 - src/reducers/PendingTxReducer.js | 4 +++- src/reducers/Web3Reducer.js | 1 - src/reducers/index.js | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/reducers/AccountsReducer.js b/src/reducers/AccountsReducer.js index df05ff00..3e605191 100644 --- a/src/reducers/AccountsReducer.js +++ b/src/reducers/AccountsReducer.js @@ -45,7 +45,7 @@ const createAccount = (state: State, account: Account): State => { if (exist) { return state; } - const newState: State = [ ...state ]; + const newState: State = [...state]; newState.push(account); return newState; }; @@ -66,7 +66,7 @@ const updateAccount = (state: State, account: Account): State => { const newState: State = [...state]; newState[index] = account; return newState; -} +}; const setBalance = (state: State, action: AccountSetBalanceAction): State => { // const index: number = state.findIndex(account => account.address === action.address && account.network === action.network && account.deviceState === action.deviceState); @@ -100,7 +100,7 @@ export default (state: State = initialState, action: Action): State => { //case CONNECT.FORGET_SINGLE : // return forgetAccounts(state, action); - case ACCOUNT.UPDATE : + case ACCOUNT.UPDATE: return updateAccount(state, action.payload); case ACCOUNT.SET_BALANCE: diff --git a/src/reducers/FiatRateReducer.js b/src/reducers/FiatRateReducer.js index aee1ff80..af190d11 100644 --- a/src/reducers/FiatRateReducer.js +++ b/src/reducers/FiatRateReducer.js @@ -1,6 +1,5 @@ /* @flow */ - import { RATE_UPDATE } from 'services/CoinmarketcapService'; import type { Action } from 'flowtype'; @@ -27,7 +26,6 @@ const update = (state: Array, action: FiatRateAction): Array => { return newState; }; - export default (state: Array = initialState, action: Action): Array => { switch (action.type) { case RATE_UPDATE: diff --git a/src/reducers/ModalReducer.js b/src/reducers/ModalReducer.js index bd86b456..49b8bc07 100644 --- a/src/reducers/ModalReducer.js +++ b/src/reducers/ModalReducer.js @@ -94,7 +94,6 @@ export default function modal(state: State = initialState, action: Action): Stat case UI.CLOSE_UI_WINDOW: case MODAL.CLOSE: - case CONNECT.FORGET: case CONNECT.FORGET_SINGLE: case CONNECT.REMEMBER: diff --git a/src/reducers/PendingTxReducer.js b/src/reducers/PendingTxReducer.js index b8c80f57..9f4ab635 100644 --- a/src/reducers/PendingTxReducer.js +++ b/src/reducers/PendingTxReducer.js @@ -39,11 +39,13 @@ const add = (state: State, action: SendTxAction): State => { return newState; }; -const add_NEW = (state: State, payload: any): State => { +/* +const addFromBloockbokNotifiaction = (state: State, payload: any): State => { const newState = [...state]; newState.push(payload); return newState; }; +*/ const remove = (state: State, id: string): State => state.filter(tx => tx.id !== id); diff --git a/src/reducers/Web3Reducer.js b/src/reducers/Web3Reducer.js index 9c25d9c8..cc903fdf 100644 --- a/src/reducers/Web3Reducer.js +++ b/src/reducers/Web3Reducer.js @@ -4,7 +4,6 @@ import Web3 from 'web3'; import type { Contract } from 'web3'; -import * as STORAGE from 'actions/constants/localStorage'; import * as WEB3 from 'actions/constants/web3'; import type { Action } from 'flowtype'; diff --git a/src/reducers/index.js b/src/reducers/index.js index ed6f1aa6..a4955da5 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -40,7 +40,7 @@ const reducers = { fiat, wallet, devices, - blockchain + blockchain, }; export type Reducers = typeof reducers; From ef235e6ab8123a3a62948810b06c8506d36a2d03 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sun, 23 Sep 2018 08:49:43 +0200 Subject: [PATCH 11/28] eslint ./src/actions --- src/actions/AccountsActions.js | 4 ++-- src/actions/ReceiveActions.js | 6 +++--- src/actions/TokenActions.js | 11 +++++++---- src/actions/TxActions.js | 34 ++++++++++++++-------------------- src/actions/WalletActions.js | 4 ---- 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/actions/AccountsActions.js b/src/actions/AccountsActions.js index 8c14e594..27d4c46b 100644 --- a/src/actions/AccountsActions.js +++ b/src/actions/AccountsActions.js @@ -1,7 +1,7 @@ /* @flow */ import * as ACCOUNT from 'actions/constants/account'; -import type { Action, TrezorDevice } from 'flowtype'; +import type { Action } from 'flowtype'; import type { Account, State } from 'reducers/AccountsReducer'; export type AccountFromStorageAction = { @@ -60,5 +60,5 @@ export const setNonce = (address: string, network: string, deviceState: string, export const update = (account: Account): Action => ({ type: ACCOUNT.UPDATE, - payload: account + payload: account, }); diff --git a/src/actions/ReceiveActions.js b/src/actions/ReceiveActions.js index 51b7f548..6f193e48 100644 --- a/src/actions/ReceiveActions.js +++ b/src/actions/ReceiveActions.js @@ -48,7 +48,7 @@ export const showUnverifiedAddress = (): Action => ({ }); //export const showAddress = (address_n: string): AsyncAction => { -export const showAddress = (address_n: Array): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const showAddress = (path: Array): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const selected = getState().wallet.selectedDevice; if (!selected) return; @@ -66,7 +66,7 @@ export const showAddress = (address_n: Array): AsyncAction => async (dis instance: selected.instance, state: selected.state, }, - path: address_n, + path, useEmptyPassphrase: !selected.instance, }); @@ -90,7 +90,7 @@ export const showAddress = (address_n: Array): AsyncAction => async (dis { label: 'Try again', callback: () => { - dispatch(showAddress(address_n)); + dispatch(showAddress(path)); }, }, ], diff --git a/src/actions/TokenActions.js b/src/actions/TokenActions.js index 68860968..eea041e8 100644 --- a/src/actions/TokenActions.js +++ b/src/actions/TokenActions.js @@ -27,7 +27,8 @@ export type TokenAction = { // action from component -export const load = (input: string, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const load = ($input: string, network: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + let input = $input; if (input.length < 1) input = '0x'; const tokens = getState().localStorage.tokens[network]; @@ -43,10 +44,12 @@ export const load = (input: string, network: string): AsyncAction => async (disp return result.slice(0, 100); } - const info = await dispatch( BlockchainActions.getTokenInfo(input, network) ); + const info = await dispatch(BlockchainActions.getTokenInfo(input, network)); if (info) { return [info]; } + + return null; }; export const setBalance = (tokenAddress: string, ethAddress: string, balance: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { @@ -63,7 +66,7 @@ export const setBalance = (tokenAddress: string, ethAddress: string, balance: st }); }; -export const add = (token: NetworkToken, account: Account): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const add = (token: NetworkToken, account: Account): AsyncAction => async (dispatch: Dispatch): Promise => { const tkn: Token = { loaded: false, deviceState: account.deviceState, @@ -81,7 +84,7 @@ export const add = (token: NetworkToken, account: Account): AsyncAction => async payload: tkn, }); - const tokenBalance = await dispatch( BlockchainActions.getTokenBalance(tkn) ); + const tokenBalance = await dispatch(BlockchainActions.getTokenBalance(tkn)); dispatch(setBalance(token.address, account.address, tokenBalance)); }; diff --git a/src/actions/TxActions.js b/src/actions/TxActions.js index 8bdaed0e..67c93c9f 100644 --- a/src/actions/TxActions.js +++ b/src/actions/TxActions.js @@ -3,19 +3,15 @@ import EthereumjsTx from 'ethereumjs-tx'; import EthereumjsUnits from 'ethereumjs-units'; import BigNumber from 'bignumber.js'; -import { toHex } from 'web3-utils'; -import { initWeb3 } from './Web3Actions'; +import { toHex } from 'web3-utils'; // eslint-disable-line import/no-extraneous-dependencies +import { initWeb3 } from 'actions/Web3Actions'; import type { Dispatch, - GetState, PromiseAction, } from 'flowtype'; -import type { - EthereumTransaction -} from 'trezor-connect'; - +import type { EthereumTransaction } from 'trezor-connect'; import type { Token } from 'reducers/TokensReducer'; type EthereumTxRequest = { @@ -30,19 +26,18 @@ type EthereumTxRequest = { nonce: number; } - -export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const instance = await dispatch( initWeb3(tx.network) ); - const token = tx.token; +export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction => async (dispatch: Dispatch): Promise => { + const instance = await dispatch(initWeb3(tx.network)); + const { token } = tx; let data: string = `0x${tx.data}`; // TODO: check if already prefixed - let value: string = toHex( EthereumjsUnits.convert(tx.amount, 'ether', 'wei') ); - let to: string = tx.to; + let value: string = toHex(EthereumjsUnits.convert(tx.amount, 'ether', 'wei')); + let to: string = tx.to; // eslint-disable-line prefer-destructuring if (token) { // smart contract transaction const contract = instance.erc20.clone(); contract.options.address = token.address; - const tokenAmount: string = new BigNumber(tx.amount).times(Math.pow(10, token.decimals)).toString(10); + const tokenAmount: string = new BigNumber(tx.amount).times(10 ** token.decimals).toString(10); data = instance.erc20.methods.transfer(to, tokenAmount).encodeABI(); value = '0x00'; to = token.address; @@ -55,15 +50,14 @@ export const prepareEthereumTx = (tx: EthereumTxRequest): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const serializeEthereumTx = (tx: EthereumTransaction): PromiseAction => async (): Promise => { const ethTx = new EthereumjsTx(tx); - return `0x${ ethTx.serialize().toString('hex') }`; - // return toHex( ethTx.serialize() ); -} \ No newline at end of file + return `0x${ethTx.serialize().toString('hex')}`; +}; \ No newline at end of file diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index 32830e26..9126c541 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -6,10 +6,6 @@ import * as WALLET from 'actions/constants/wallet'; import * as stateUtils from 'reducers/utils'; import type { - Account, - Coin, - Discovery, - Token, Device, TrezorDevice, RouterLocationState, From b36b9667e48691240b1ee2f173b0535cc452daf2 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sun, 23 Sep 2018 09:13:09 +0200 Subject: [PATCH 12/28] eslint ./src/actions 2 --- src/actions/DiscoveryActions.js | 39 +++++++++++++-------------------- src/actions/ModalActions.js | 2 +- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/actions/DiscoveryActions.js b/src/actions/DiscoveryActions.js index 0daea108..cda570ed 100644 --- a/src/actions/DiscoveryActions.js +++ b/src/actions/DiscoveryActions.js @@ -9,10 +9,7 @@ import type { ThunkAction, AsyncAction, PromiseAction, Action, GetState, Dispatch, TrezorDevice, } from 'flowtype'; import type { Discovery, State } from 'reducers/DiscoveryReducer'; -import * as AccountsActions from './AccountsActions'; import * as BlockchainActions from './BlockchainActions'; -import { setBalance as setTokenBalance } from './TokenActions'; - export type DiscoveryStartAction = { type: typeof DISCOVERY.START, @@ -65,7 +62,7 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b return; } - const discovery: State = getState().discovery; + const { discovery } = getState(); const discoveryProcess: ?Discovery = discovery.find(d => d.deviceState === device.state && d.network === network); if (!selected.connected && (!discoveryProcess || !discoveryProcess.completed)) { @@ -88,7 +85,7 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b } if (!discoveryProcess) { - dispatch(begin(device, network)) + dispatch(begin(device, network)); } else if (discoveryProcess.completed && !ignoreCompleted) { dispatch({ type: DISCOVERY.COMPLETE, @@ -172,9 +169,8 @@ const begin = (device: TrezorDevice, network: string): AsyncAction => async (dis dispatch(start(device, network)); }; -const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { +const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise => { const { completed } = discoveryProcess; - discoveryProcess.completed = false; const derivedKey = discoveryProcess.hdKey.derive(`m/${discoveryProcess.accountIndex}`); const path = discoveryProcess.basePath.concat(discoveryProcess.accountIndex); @@ -183,15 +179,13 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy const { network } = discoveryProcess; // TODO: check if address was created before - try { - const account = await dispatch( BlockchainActions.discoverAccount(device, ethAddress, network) ); + const account = await dispatch(BlockchainActions.discoverAccount(device, ethAddress, network)); if (discoveryProcess.interrupted) return; // const accountIsEmpty = account.transactions <= 0 && account.nonce <= 0 && account.balance === '0'; const accountIsEmpty = account.nonce <= 0 && account.balance === '0'; if (!accountIsEmpty || (accountIsEmpty && completed) || (accountIsEmpty && discoveryProcess.accountIndex === 0)) { - dispatch({ type: ACCOUNT.CREATE, payload: { @@ -205,22 +199,20 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy balance: account.balance, nonce: account.nonce, block: account.block, - transactions: account.transactions - } + transactions: account.transactions, + }, }); } if (accountIsEmpty) { - dispatch( finish(device, discoveryProcess) ); - } else { - if (!completed) { dispatch( discoverAccount(device, discoveryProcess) ); } + dispatch(finish(device, discoveryProcess)); + } else if (!completed) { + dispatch(discoverAccount(device, discoveryProcess)); } - } catch (error) { - dispatch({ type: DISCOVERY.STOP, - device + device, }); dispatch({ @@ -243,7 +235,7 @@ const discoverAccount = (device: TrezorDevice, discoveryProcess: Discovery): Asy } }; -const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { +const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction => async (dispatch: Dispatch): Promise => { await TrezorConnect.getFeatures({ device: { path: device.path, @@ -254,7 +246,7 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction useEmptyPassphrase: !device.instance, }); - await dispatch( BlockchainActions.subscribe(discoveryProcess.network) ); + await dispatch(BlockchainActions.subscribe(discoveryProcess.network)); if (discoveryProcess.interrupted) return; @@ -263,13 +255,12 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction device, network: discoveryProcess.network, }); +}; -} - -export const reconnect = (network: string): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { +export const reconnect = (network: string): PromiseAction => async (dispatch: Dispatch): Promise => { await dispatch(BlockchainActions.subscribe(network)); dispatch(restore()); -} +}; export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const selected = getState().wallet.selectedDevice; diff --git a/src/actions/ModalActions.js b/src/actions/ModalActions.js index cc55e4af..25bebdfc 100644 --- a/src/actions/ModalActions.js +++ b/src/actions/ModalActions.js @@ -25,7 +25,7 @@ export const onPinSubmit = (value: string): Action => { }; export const onPassphraseSubmit = (passphrase: string): AsyncAction => async (dispatch: Dispatch): Promise => { - const resp = await TrezorConnect.uiResponse({ + await TrezorConnect.uiResponse({ type: UI.RECEIVE_PASSPHRASE, payload: { value: passphrase, From 51253665be3674a7d47b8c8addf2fb744186506a Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 15:39:56 +0200 Subject: [PATCH 13/28] change blockchain/web3 "estimateGasLimit" to return string --- src/actions/BlockchainActions.js | 9 ++++++--- src/actions/Web3Actions.js | 18 +++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/actions/BlockchainActions.js b/src/actions/BlockchainActions.js index 5414df9d..2ba9c50a 100644 --- a/src/actions/BlockchainActions.js +++ b/src/actions/BlockchainActions.js @@ -83,9 +83,12 @@ export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAc } } -export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { - return await dispatch( Web3Actions.estimateGasLimit(network, { to: '', data, value, gasPrice }) ); -} +export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction => async (dispatch: Dispatch): Promise => dispatch(Web3Actions.estimateGasLimit(network, { + to: '', + data, + value, + gasPrice, +})); export const onBlockMined = (coinInfo: any): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { // incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type diff --git a/src/actions/Web3Actions.js b/src/actions/Web3Actions.js index b4611bd3..5a6ebab7 100644 --- a/src/actions/Web3Actions.js +++ b/src/actions/Web3Actions.js @@ -279,16 +279,20 @@ export const updateGasPrice = (network: string): PromiseAction => async (d } -export const estimateGasLimit = (network: string, options: EstimateGasOptions): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const instance = await dispatch( initWeb3(network) ); +export const estimateGasLimit = (network: string, $options: EstimateGasOptions): PromiseAction => async (dispatch: Dispatch): Promise => { + const instance = await dispatch(initWeb3(network)); // TODO: allow data starting with 0x ... - options.to = '0x0000000000000000000000000000000000000000'; - options.data = `0x${options.data.length % 2 === 0 ? options.data : `0${options.data}`}`; - options.value = instance.web3.utils.toHex( EthereumjsUnits.convert(options.value || '0', 'ether', 'wei') ); - options.gasPrice = instance.web3.utils.toHex( EthereumjsUnits.convert(options.gasPrice, 'gwei', 'wei') ); + const data = `0x${$options.data.length % 2 === 0 ? $options.data : `0${$options.data}`}`; + const options = { + ...$options, + to: '0x0000000000000000000000000000000000000000', + data, + value: instance.web3.utils.toHex(EthereumjsUnits.convert($options.value || '0', 'ether', 'wei')), + gasPrice: instance.web3.utils.toHex(EthereumjsUnits.convert($options.gasPrice, 'gwei', 'wei')), + }; const limit = await instance.web3.eth.estimateGas(options); - return limit; + return limit.toString(); }; export const disconnect = (coinInfo: any): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { From a9e811ab5ba232b0e6adfbfeba13f2c19d4361da Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 15:40:32 +0200 Subject: [PATCH 14/28] add utility to observe reducers fields change --- src/reducers/utils/index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/reducers/utils/index.js b/src/reducers/utils/index.js index b5c0056c..9b5ba7f5 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -115,3 +115,22 @@ export const getWeb3 = (state: State): ?Web3Instance => { if (!locationState.network) return null; return state.web3.find(w3 => w3.network === locationState.network); }; + +export const observeChanges = (prev: ?(Object | Array), current: ?(Object | Array), fields?: Array): boolean => { + if (prev !== current) { + // 1. one of the objects is null/undefined + if (!prev || !current) return true; + // 2. object are Arrays and they have different length + if (Array.isArray(prev) && Array.isArray(current)) return prev.length !== current.length; + // 3. no nested field to check + if (!Array.isArray(fields)) return true; + // 4. validate nested field + if (prev instanceof Object && current instanceof Object) { + for (let i = 0; i < fields.length; i++) { + const key = fields[i]; + if (prev[key] !== current[key]) return true; + } + } + } + return false; +}; From 88d2a65340bbc0bc63ad777fbee988ef2d5562d1 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 18:46:53 +0200 Subject: [PATCH 15/28] BlockchainAction "estimateGasLimit" with proxied responses --- src/actions/BlockchainActions.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/actions/BlockchainActions.js b/src/actions/BlockchainActions.js index 2ba9c50a..ecec58e0 100644 --- a/src/actions/BlockchainActions.js +++ b/src/actions/BlockchainActions.js @@ -83,12 +83,31 @@ export const getGasPrice = (network: string, defaultGasPrice: number): PromiseAc } } -export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction => async (dispatch: Dispatch): Promise => dispatch(Web3Actions.estimateGasLimit(network, { - to: '', - data, - value, - gasPrice, -})); +const estimateProxy: Array> = []; +export const estimateGasLimit = (network: string, data: string, value: string, gasPrice: string): PromiseAction => async (dispatch: Dispatch): Promise => { + // Since this method could be called multiple times in short period of time + // check for pending calls in proxy and if there more than two (first is current running and the second is waiting for result of first) + // TODO: should reject second call immediately? + if (estimateProxy.length > 0) { + // wait for proxy result (but do not process it) + await estimateProxy[0]; + } + + const call = dispatch(Web3Actions.estimateGasLimit(network, { + to: '', + data, + value, + gasPrice, + })); + // add current call to proxy + estimateProxy.push(call); + // wait for result + const result = await call; + // remove current call from proxy + estimateProxy.splice(0, 1); + // return result + return result; +}; export const onBlockMined = (coinInfo: any): PromiseAction => async (dispatch: Dispatch, getState: GetState): Promise => { // incoming "coinInfo" from TrezorConnect is CoinInfo | EthereumNetwork type From 1846d936e212765b1274f8d49ab0b830c9f8a32e Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 18:47:31 +0200 Subject: [PATCH 16/28] updated SessionStorage actions --- src/actions/SessionStorageActions.js | 43 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/actions/SessionStorageActions.js b/src/actions/SessionStorageActions.js index 9457aa14..33c953c8 100644 --- a/src/actions/SessionStorageActions.js +++ b/src/actions/SessionStorageActions.js @@ -4,47 +4,56 @@ import type { State as SendFormState } from 'reducers/SendFormReducer'; import type { ThunkAction, + PayloadAction, GetState, Dispatch, } from 'flowtype'; -const PREFIX: string = 'trezor:draft-tx:'; +const TX_PREFIX: string = 'trezor:draft-tx:'; -export const save = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { +export const saveDraftTransaction = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { if (typeof window.localStorage === 'undefined') return; - const location = getState().router.location.pathname; const state = getState().sendForm; - if (!state.untouched) { - try { - window.sessionStorage.setItem(`${PREFIX}${location}`, JSON.stringify(state)); - } catch (error) { - console.error(`Saving sessionStorage error: ${error}`); - } + if (state.untouched) return; + + const location = getState().router.location.pathname; + try { + // save state as it is + // "loadDraftTransaction" will do the validation + window.sessionStorage.setItem(`${TX_PREFIX}${location}`, JSON.stringify(state)); + } catch (error) { + console.error(`Saving sessionStorage error: ${error}`); } }; -export const load = (location: string): ?SendFormState => { - if (typeof window.localStorage === 'undefined') return; +export const loadDraftTransaction = (): PayloadAction => (dispatch: Dispatch, getState: GetState): ?SendFormState => { + if (typeof window.localStorage === 'undefined') return null; try { - const value: string = window.sessionStorage.getItem(`${PREFIX}${location}`); + const location = getState().router.location.pathname; + const value: string = window.sessionStorage.getItem(`${TX_PREFIX}${location}`); const state: ?SendFormState = JSON.parse(value); - if (state && state.address === '' && (state.amount === '' || state.amount === '0')) { - window.sessionStorage.removeItem(`${PREFIX}${location}`); - return; + if (state) { + // decide if draft is valid and should be returned + // ignore this draft if has any error + if (Object.keys(state.errors).length > 0) { + window.sessionStorage.removeItem(`${TX_PREFIX}${location}`); + return null; + } + return state; } - return state; } catch (error) { console.error(`Loading sessionStorage error: ${error}`); } + return null; }; export const clear = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { if (typeof window.localStorage === 'undefined') return; const location = getState().router.location.pathname; try { - window.sessionStorage.removeItem(`${PREFIX}${location}`); + window.sessionStorage.removeItem(`${TX_PREFIX}${location}`); } catch (error) { console.error(`Clearing sessionStorage error: ${error}`); } From 52cf4f1e8e575468536c33e7959e89f5879a96c6 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 18:48:28 +0200 Subject: [PATCH 17/28] clearing sendForm and sessionStorage references from SelectedAccountActions --- src/actions/SelectedAccountActions.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/actions/SelectedAccountActions.js b/src/actions/SelectedAccountActions.js index dd54a365..4e01c1a5 100644 --- a/src/actions/SelectedAccountActions.js +++ b/src/actions/SelectedAccountActions.js @@ -3,13 +3,9 @@ import { LOCATION_CHANGE } from 'react-router-redux'; import * as ACCOUNT from 'actions/constants/account'; -import * as SEND from 'actions/constants/send'; import * as NOTIFICATION from 'actions/constants/notification'; import * as PENDING from 'actions/constants/pendingTx'; -import * as SendFormActions from 'actions/SendFormActions'; -import * as SessionStorageActions from 'actions/SessionStorageActions'; - import * as stateUtils from 'reducers/utils'; import type { @@ -36,17 +32,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct const state: State = getState(); const { location } = state.router; - // reset form to default - if (action.type === SEND.TX_COMPLETE) { - // dispatch( SendFormActions.init() ); - // linear action - // SessionStorageActions.clear(location.pathname); - } - - if (prevState.sendForm !== state.sendForm) { - dispatch(SessionStorageActions.save()); - } - // handle devices state change (from trezor-connect events or location change) if (locationChange || prevState.accounts !== state.accounts @@ -85,11 +70,6 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct payload, }); - // initialize SendFormReducer - if (location.state.send && getState().sendForm.currency === '') { - dispatch(SendFormActions.init()); - } - if (location.state.send) { const rejectedTxs = pending.filter(tx => tx.rejected); rejectedTxs.forEach((tx) => { From 6cf3cb4bdb33a7a747cfdf8b31bac64c1dafa1da Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 18:49:05 +0200 Subject: [PATCH 18/28] refactoring SendFromActions --- src/actions/SendFormActions.js | 825 +++++++---------------- src/actions/SendFormValidationActions.js | 413 ++++++++++++ src/actions/constants/send.js | 18 +- src/reducers/SendFormReducer.js | 85 +-- 4 files changed, 661 insertions(+), 680 deletions(-) create mode 100644 src/actions/SendFormValidationActions.js diff --git a/src/actions/SendFormActions.js b/src/actions/SendFormActions.js index eab6a786..1302d611 100644 --- a/src/actions/SendFormActions.js +++ b/src/actions/SendFormActions.js @@ -1,33 +1,27 @@ /* @flow */ - -import EthereumjsUtil from 'ethereumjs-util'; -import EthereumjsUnits from 'ethereumjs-units'; -import EthereumjsTx from 'ethereumjs-tx'; import TrezorConnect from 'trezor-connect'; import BigNumber from 'bignumber.js'; import * as NOTIFICATION from 'actions/constants/notification'; import * as SEND from 'actions/constants/send'; +import * as WEB3 from 'actions/constants/web3'; +import * as ValidationActions from 'actions/SendFormValidationActions'; import { initialState } from 'reducers/SendFormReducer'; import { findToken } from 'reducers/TokensReducer'; -import { findDevice, getPendingAmount, getPendingNonce } from 'reducers/utils'; -import * as stateUtils from 'reducers/utils'; -import { validateAddress } from 'utils/ethUtils'; +import * as reducerUtils from 'reducers/utils'; import type { Dispatch, GetState, + State as ReducersState, Action, ThunkAction, AsyncAction, TrezorDevice, } from 'flowtype'; -import type { Coin } from 'reducers/LocalStorageReducer'; -import type { Token } from 'reducers/TokensReducer'; import type { State, FeeLevel } from 'reducers/SendFormReducer'; import type { Account } from 'reducers/AccountsReducer'; -import type { Props } from 'views/Wallet/views/AccountSend/Container'; import * as SessionStorageActions from './SessionStorageActions'; import { prepareEthereumTx, serializeEthereumTx } from './TxActions'; import * as BlockchainActions from './BlockchainActions'; @@ -44,188 +38,73 @@ export type SendTxAction = { txData: any, }; -export type SendFormAction = SendTxAction | { - type: typeof SEND.INIT, - state: State -} | { - type: typeof SEND.DISPOSE -} | { - type: typeof SEND.TOGGLE_ADVANCED -} | { - type: typeof SEND.VALIDATION, - errors: {[k: string]: string}, - warnings: {[k: string]: string}, - infos: {[k: string]: string} -} | { - type: typeof SEND.ADDRESS_VALIDATION, - state: State -} | { - type: typeof SEND.ADDRESS_CHANGE, - state: State -} | { - type: typeof SEND.AMOUNT_CHANGE, - state: State -} | { - type: typeof SEND.CURRENCY_CHANGE, - state: State -} | { - type: typeof SEND.SET_MAX, - state: State -} | { - type: typeof SEND.FEE_LEVEL_CHANGE, - state: State -} | { - type: typeof SEND.UPDATE_FEE_LEVELS, - state: State -} | { - type: typeof SEND.FEE_LEVEL_CHANGE, - state: State -} | { - type: typeof SEND.GAS_PRICE_CHANGE, - state: State -} | { - type: typeof SEND.GAS_LIMIT_CHANGE, - state: State +export type SendFormAction = { + type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE, + state: State, } | { - type: typeof SEND.NONCE_CHANGE, - state: State -} | { - type: typeof SEND.DATA_CHANGE, - state: State -} | { - type: typeof SEND.SEND, -} | { - type: typeof SEND.TX_ERROR, -} | { - type: typeof SEND.FROM_SESSION_STORAGE, - address: string, - amount: string, - setMax: boolean, - selectedCurrency: string, - selectedFeeLevel: any, - advanced: boolean, - gasLimit: string, - gasPrice: string, - data: string, - nonce: string, - touched: any, -} - -//const numberRegExp = new RegExp('^([0-9]{0,10}\\.)?[0-9]{1,18}$'); -const numberRegExp: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$'); - -export const calculateFee = (gasPrice: string, gasLimit: string): string => { - try { - return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); - } catch (error) { - return '0'; + type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR, +} | SendTxAction; + +/* +* Called from WalletService on EACH action +*/ +export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const currentState = getState(); + // do not proceed if it's not "send" url + if (!currentState.router.location.state.send) return; + + // if action type is SEND.VALIDATION which is called as result of this process + // save data to session storage + if (action.type === SEND.VALIDATION) { + dispatch(SessionStorageActions.saveDraftTransaction()); + return; } -}; -export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => { - try { - return new BigNumber(amount).plus(calculateFee(gasPrice, gasLimit)).toString(10); - } catch (error) { - return '0'; + // if send form was not initialized + if (currentState.sendForm.currency === '') { + dispatch(init()); + return; } -}; -export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimit: string): string => { - try { - // TODO - minus pendings - const fee = calculateFee(gasPrice, gasLimit); - const max = balance.minus(fee); - if (max.lessThan(0)) return '0'; - return max.toString(10); - } catch (error) { - return '0'; + // handle gasPrice update from backend + // recalculate fee levels if needed + if (action.type === WEB3.GAS_PRICE_UPDATED) { + dispatch(ValidationActions.onGasPriceUpdated(action.network, action.gasPrice)); + return; } -}; - -export const calculate = (prevProps: Props, props: Props) => { - const { - account, - tokens, - pending, - } = props.selectedAccount; - if (!account) return; - - const state = props.sendForm; - const isToken: boolean = state.currency !== state.networkSymbol; - - // account balance - // token balance - // gasLimit, gasPrice changed - - // const shouldRecalculateAmount = - // (prevProps.selectedAccount.account !== account) - // || (prevProps.) - - if (state.setMax) { - const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, isToken); - - if (isToken) { - const token: ?Token = findToken(tokens, account.address, state.currency, account.deviceState); - if (token) { - state.amount = new BigNumber(token.balance).minus(pendingAmount).toString(10); - } - } else { - const b = new BigNumber(account.balance).minus(pendingAmount); - state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit); - } + let shouldUpdate: boolean = false; + // check if "selectedAccount" reducer changed + const selectedAccountChanged = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, ['account', 'tokens', 'pending']); + if (selectedAccountChanged) { + // double check + // there are only few fields that we are interested in + // check them to avoid unnecessary calculation and validation + const accountChanged = reducerUtils.observeChanges(prevState.selectedAccount.account, currentState.selectedAccount.account, ['balance', 'nonce']); + const tokensChanged = reducerUtils.observeChanges(prevState.selectedAccount.tokens, currentState.selectedAccount.tokens); + const pendingChanged = reducerUtils.observeChanges(prevState.selectedAccount.pending, currentState.selectedAccount.pending); + shouldUpdate = accountChanged || tokensChanged || pendingChanged; } - // amount changed - // fee changed - state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); - - if (state.selectedFeeLevel.value === 'Custom') { - state.selectedFeeLevel.label = `${calculateFee(state.gasPrice, state.gasLimit)} ${state.networkSymbol}`; - state.selectedFeeLevel.gasPrice = state.gasPrice; + // check if "sendForm" reducer changed + if (!shouldUpdate) { + shouldUpdate = reducerUtils.observeChanges(prevState.sendForm, currentState.sendForm); } -}; - -export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLimit: string, selected?: FeeLevel): Array => { - const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice; - const quarter: BigNumber = price.dividedBy(4); - const high: string = price.plus(quarter.times(2)).toString(10); - const low: string = price.minus(quarter.times(2)).toString(10); - - const customLevel: FeeLevel = selected && selected.value === 'Custom' ? { - value: 'Custom', - gasPrice: selected.gasPrice, - // label: `${ calculateFee(gasPrice, gasLimit) } ${ symbol }` - label: `${calculateFee(selected.gasPrice, gasLimit)} ${symbol}`, - } : { - value: 'Custom', - gasPrice: low, - label: '', - }; - - return [ - { - value: 'High', - gasPrice: high, - label: `${calculateFee(high, gasLimit)} ${symbol}`, - }, - { - value: 'Normal', - gasPrice: gasPrice.toString(), - label: `${calculateFee(price.toString(10), gasLimit)} ${symbol}`, - }, - { - value: 'Low', - gasPrice: low, - label: `${calculateFee(low, gasLimit)} ${symbol}`, - }, - customLevel, - ]; + if (shouldUpdate) { + const validated = dispatch(ValidationActions.validation()); + dispatch({ + type: SEND.VALIDATION, + state: validated, + }); + } }; - -// initialize component +/* +* Called from "observe" action +* Initialize "sendForm" reducer data +* Get data either from session storage or "selectedAccount" reducer +*/ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { account, @@ -234,8 +113,9 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS if (!account || !network) return; - const stateFromStorage = SessionStorageActions.load(getState().router.location.pathname); + const stateFromStorage = dispatch(SessionStorageActions.loadDraftTransaction()); if (stateFromStorage) { + // TODO: consider if current gasPrice should be set here as "recommendedGasPrice" dispatch({ type: SEND.INIT, state: stateFromStorage, @@ -243,269 +123,70 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS return; } - // TODO: check if there are some unfinished tx in localStorage - - - // const gasPrice: BigNumber = new BigNumber(EthereumjsUnits.convert(web3.gasPrice, 'wei', 'gwei')) || new BigNumber(network.defaultGasPrice); const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.network, network.defaultGasPrice)); - // const gasPrice: BigNumber = new BigNumber(network.defaultGasPrice); - const gasLimit: string = network.defaultGasLimit.toString(); - const feeLevels: Array = getFeeLevels(network.symbol, gasPrice, gasLimit); - - // TODO: get nonce - // TODO: LOAD DATA FROM SESSION STORAGE - - const state: State = { - ...initialState, - networkName: network.network, - networkSymbol: network.symbol, - currency: network.symbol, - feeLevels, - selectedFeeLevel: feeLevels.find(f => f.value === 'Normal'), - recommendedGasPrice: gasPrice.toString(), - gasLimit, - gasPrice: gasPrice.toString(), - }; + const gasLimit = network.defaultGasLimit.toString(); + const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel); dispatch({ type: SEND.INIT, - state, - }); -}; - -export const toggleAdvanced = (/* address: string */): Action => ({ - type: SEND.TOGGLE_ADVANCED, -}); - - -const addressValidation = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const { - account, - network, - } = getState().selectedAccount; - if (!account || !network) return; - - const state: State = getState().sendForm; - const infos = { ...state.infos }; - const warnings = { ...state.warnings }; - - - if (state.untouched || !state.touched.address) return; - - const savedAccounts = getState().accounts.filter(a => a.address.toLowerCase() === state.address.toLowerCase()); - if (savedAccounts.length > 0) { - // check if found account belongs to this network - // corner-case: when same derivation path is used on different networks - const currentNetworkAccount = savedAccounts.find(a => a.network === network.network); - if (currentNetworkAccount) { - const device: ?TrezorDevice = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState); - if (device) { - infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`; - } - } else { - const otherNetworkAccount = savedAccounts[0]; - const device: ?TrezorDevice = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState); - const { coins } = getState().localStorage.config; - const otherNetwork: ?Coin = coins.find(c => c.network === otherNetworkAccount.network); - if (device && otherNetwork) { - warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`; - } - } - } else { - delete warnings.address; - delete infos.address; - } - - dispatch({ - type: SEND.ADDRESS_VALIDATION, state: { - ...state, - infos, - warnings, + ...initialState, + networkName: network.network, + networkSymbol: network.symbol, + currency: network.symbol, + feeLevels, + selectedFeeLevel, + recommendedGasPrice: gasPrice.toString(), + gasLimit, + gasPrice: gasPrice.toString(), }, }); }; +/* +* Called from UI from "advanced" button +*/ +export const toggleAdvanced = (): Action => ({ + type: SEND.TOGGLE_ADVANCED, +}); -export const validation = (props: Props): void => { - const { - account, - network, - tokens, - pending, - } = props.selectedAccount; - if (!account || !network) return; - - - const state: State = props.sendForm; - - const errors: {[k: string]: string} = {}; - const warnings: {[k: string]: string} = {}; - const infos: {[k: string]: string} = {}; - - if (state.untouched) return; - // valid address - if (state.touched.address) { - const addressError = validateAddress(state.address); - if (addressError) { - errors.address = addressError; - } - - // address warning or info may be set in addressValidation ThunkAction - // do not override them - if (state.warnings.address) { - warnings.address = state.warnings.address; - } - - if (state.infos.address) { - infos.address = state.infos.address; - } - } - - // valid amount - // https://stackoverflow.com/a/42701461 - //const regexp = new RegExp('^(?:[0-9]{0,10}\\.)?[0-9]{1,18}$'); - if (state.touched.amount) { - if (state.amount.length < 1) { - errors.amount = 'Amount is not set'; - } else if (state.amount.length > 0 && !state.amount.match(numberRegExp)) { - errors.amount = 'Amount is not a number'; - } else { - let decimalRegExp: RegExp; - const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, state.currency !== state.networkSymbol); - - if (state.currency !== state.networkSymbol) { - const token = findToken(tokens, account.address, state.currency, account.deviceState); - if (token) { - if (parseInt(token.decimals) > 0) { - //decimalRegExp = new RegExp('^(0|0\\.([0-9]{0,' + token.decimals + '})?|[1-9]+\\.?([0-9]{0,' + token.decimals + '})?|\\.[0-9]{1,' + token.decimals + '})$'); - decimalRegExp = new RegExp(`^(0|0\\.([0-9]{0,${token.decimals}})?|[1-9][0-9]*\\.?([0-9]{0,${token.decimals}})?|\\.[0-9]{1,${token.decimals}})$`); - } else { - // decimalRegExp = new RegExp('^(0|0\\.?|[1-9]+\\.?)$'); - decimalRegExp = new RegExp('^[0-9]+$'); - } - - if (!state.amount.match(decimalRegExp)) { - errors.amount = `Maximum ${token.decimals} decimals allowed`; - } else if (new BigNumber(state.total).greaterThan(account.balance)) { - errors.amount = `Not enough ${state.networkSymbol} to cover transaction fee`; - } else if (new BigNumber(state.amount).greaterThan(new BigNumber(token.balance).minus(pendingAmount))) { - errors.amount = 'Not enough funds'; - } else if (new BigNumber(state.amount).lessThanOrEqualTo('0')) { - errors.amount = 'Amount is too low'; - } - } - } else { - decimalRegExp = new RegExp('^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$'); - if (!state.amount.match(decimalRegExp)) { - errors.amount = 'Maximum 18 decimals allowed'; - } else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) { - errors.amount = 'Not enough funds'; - } - } - } - } - - // valid gas limit - if (state.touched.gasLimit) { - if (state.gasLimit.length < 1) { - errors.gasLimit = 'Gas limit is not set'; - } else if (state.gasLimit.length > 0 && !state.gasLimit.match(numberRegExp)) { - errors.gasLimit = 'Gas limit is not a number'; - } else { - const gl: BigNumber = new BigNumber(state.gasLimit); - if (gl.lessThan(1)) { - errors.gasLimit = 'Gas limit is too low'; - } else if (gl.lessThan(state.currency !== state.networkSymbol ? network.defaultGasLimitTokens : network.defaultGasLimit)) { - warnings.gasLimit = 'Gas limit is below recommended'; - } - } - } - - // valid gas price - if (state.touched.gasPrice) { - if (state.gasPrice.length < 1) { - errors.gasPrice = 'Gas price is not set'; - } else if (state.gasPrice.length > 0 && !state.gasPrice.match(numberRegExp)) { - errors.gasPrice = 'Gas price is not a number'; - } else { - const gp: BigNumber = new BigNumber(state.gasPrice); - if (gp.greaterThan(1000)) { - warnings.gasPrice = 'Gas price is too high'; - } else if (gp.lessThanOrEqualTo('0')) { - errors.gasPrice = 'Gas price is too low'; - } - } - } - - // valid nonce - if (state.touched.nonce) { - const re = new RegExp('^[0-9]+$'); - if (state.nonce.length < 1) { - errors.nonce = 'Nonce is not set'; - } else if (!state.nonce.match(re)) { - errors.nonce = 'Nonce is not a valid number'; - } else { - const n: BigNumber = new BigNumber(state.nonce); - if (n.lessThan(account.nonce)) { - warnings.nonce = 'Nonce is lower than recommended'; - } else if (n.greaterThan(account.nonce)) { - warnings.nonce = 'Nonce is greater than recommended'; - } - } - } - - // valid data - if (state.touched.data && state.data.length > 0) { - const re = /^[0-9A-Fa-f]+$/g; - if (!re.test(state.data)) { - errors.data = 'Data is not valid hexadecimal'; - } - } - - // valid nonce? - - state.errors = errors; - state.warnings = warnings; - state.infos = infos; -}; - - +/* +* Called from UI on "address" field change +*/ export const onAddressChange = (address: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const state: State = getState().sendForm; - const touched = { ...state.touched }; - touched.address = true; - dispatch({ - type: SEND.ADDRESS_CHANGE, + type: SEND.CHANGE, state: { ...state, untouched: false, - touched, + touched: { ...state.touched, address: true }, address, }, }); - - dispatch(addressValidation()); }; +/* +* Called from UI on "amount" field change +*/ export const onAmountChange = (amount: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const state = getState().sendForm; - const touched = { ...state.touched }; - touched.amount = true; - dispatch({ - type: SEND.AMOUNT_CHANGE, + type: SEND.CHANGE, state: { ...state, untouched: false, - touched, + touched: { ...state.touched, amount: true }, setMax: false, amount, }, }); }; +/* +* Called from UI on "currency" selection change +*/ export const onCurrencyChange = (currency: { value: string, label: string }): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const { account, @@ -513,85 +194,84 @@ export const onCurrencyChange = (currency: { value: string, label: string }): Th } = getState().selectedAccount; if (!account || !network) return; - const currentState: State = getState().sendForm; - const isToken: boolean = currency.value !== currentState.networkSymbol; - const gasLimit: string = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); - - const feeLevels: Array = getFeeLevels(network.symbol, currentState.recommendedGasPrice, gasLimit, currentState.selectedFeeLevel); - const selectedFeeLevel: ?FeeLevel = feeLevels.find(f => f.value === currentState.selectedFeeLevel.value); - if (!selectedFeeLevel) return; - - const state: State = { - ...currentState, - currency: currency.value, - // amount, - // total, - feeLevels, - selectedFeeLevel, - gasLimit, - }; + const state = getState().sendForm; + + const isToken = currency.value !== state.networkSymbol; + const gasLimit = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); + + const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); dispatch({ - type: SEND.CURRENCY_CHANGE, - state, + type: SEND.CHANGE, + state: { + ...state, + currency: currency.value, + feeLevels, + selectedFeeLevel, + gasLimit, + }, }); }; +/* +* Called from UI from "set max" button +*/ export const onSetMax = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const state = getState().sendForm; - const touched = { ...state.touched }; - touched.amount = true; - dispatch({ - type: SEND.SET_MAX, + type: SEND.CHANGE, state: { ...state, untouched: false, - touched, + touched: { ...state.touched, amount: true }, setMax: !state.setMax, }, }); }; +/* +* Called from UI on "fee" selection change +*/ export const onFeeLevelChange = (feeLevel: FeeLevel): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const { - network, - } = getState().selectedAccount; - if (!network) return; - - const currentState: State = getState().sendForm; - const isToken: boolean = currentState.currency !== currentState.networkSymbol; + const state = getState().sendForm; - const state: State = { - ...currentState, - untouched: false, - selectedFeeLevel: feeLevel, - }; - - if (feeLevel.value === 'Custom') { - state.advanced = true; - feeLevel.gasPrice = state.gasPrice; - feeLevel.label = `${calculateFee(state.gasPrice, state.gasLimit)} ${state.networkSymbol}`; - } else { - const customLevel: ?FeeLevel = state.feeLevels.find(f => f.value === 'Custom'); - if (customLevel) customLevel.label = ''; - state.gasPrice = feeLevel.gasPrice; + const isCustom = feeLevel.value === 'Custom'; + let newGasLimit = state.gasLimit; + let newGasPrice = state.gasPrice; + const advanced = isCustom ? true : state.advanced; + + if (!isCustom) { + // if selected fee is not custom + // update gasLimit to default and gasPrice to selected value + const { network } = getState().selectedAccount; + if (!network) return; + const isToken = state.currency !== state.networkSymbol; if (isToken) { - state.gasLimit = network.defaultGasLimitTokens.toString(); + newGasLimit = network.defaultGasLimitTokens.toString(); } else { - state.gasLimit = state.data.length > 0 ? state.gasLimit : network.defaultGasLimit.toString(); + // corner case: gas limit was changed by user OR by "estimateGasPrice" action + // leave gasLimit as it is + newGasLimit = state.touched.gasLimit ? state.gasLimit : network.defaultGasLimit.toString(); } + newGasPrice = feeLevel.gasPrice; } dispatch({ - type: SEND.FEE_LEVEL_CHANGE, - state, + type: SEND.CHANGE, + state: { + ...state, + advanced, + selectedFeeLevel: feeLevel, + gasLimit: newGasLimit, + gasPrice: newGasPrice, + }, }); }; -// Manually triggered from user -// Update gasPrice to recommended value - +/* +* Called from UI from "update recommended fees" button +*/ export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { const { account, @@ -599,118 +279,122 @@ export const updateFeeLevels = (): ThunkAction => (dispatch: Dispatch, getState: } = getState().selectedAccount; if (!account || !network) return; - const currentState: State = getState().sendForm; - const isToken: boolean = currentState.currency !== currentState.networkSymbol; - let gasLimit: string = isToken ? network.defaultGasLimitTokens.toString() : network.defaultGasLimit.toString(); - - // override custom settings - if (currentState.selectedFeeLevel.value === 'Custom') { - // update only gasPrice - currentState.selectedFeeLevel.gasPrice = currentState.recommendedGasPrice; - // leave gas limit as it was - ({ gasLimit } = currentState); - } - - const feeLevels: Array = getFeeLevels(network.symbol, currentState.recommendedGasPrice, gasLimit, currentState.selectedFeeLevel); - const selectedFeeLevel: ?FeeLevel = feeLevels.find(f => f.value === currentState.selectedFeeLevel.value); - if (!selectedFeeLevel) return; - - const state: State = { - ...currentState, - feeLevels, - selectedFeeLevel, - gasPrice: selectedFeeLevel.gasPrice, - gasPriceNeedsUpdate: false, - }; + const state: State = getState().sendForm; + const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, state.gasLimit, state.selectedFeeLevel); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); dispatch({ - type: SEND.UPDATE_FEE_LEVELS, - state, + type: SEND.CHANGE, + state: { + ...state, + feeLevels, + selectedFeeLevel, + gasPrice: selectedFeeLevel.gasPrice, + gasPriceNeedsUpdate: false, + }, }); }; +/* +* Called from UI on "gas price" field change +*/ export const onGasPriceChange = (gasPrice: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const currentState: State = getState().sendForm; - const isToken: boolean = currentState.currency !== currentState.networkSymbol; - - const touched = { ...currentState.touched }; - touched.gasPrice = true; - - const state: State = { - ...currentState, - untouched: false, - touched, - gasPrice, - }; - - if (currentState.selectedFeeLevel.value !== 'Custom') { - const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); - if (!customLevel) return; - state.selectedFeeLevel = customLevel; - } + const state: State = getState().sendForm; + // switch to custom fee level + let newSelectedFeeLevel = state.selectedFeeLevel; + if (state.selectedFeeLevel.value !== 'Custom') newSelectedFeeLevel = state.feeLevels.find(f => f.value === 'Custom'); dispatch({ - type: SEND.GAS_PRICE_CHANGE, - state, + type: SEND.CHANGE, + state: { + ...state, + untouched: false, + touched: { ...state.touched, gasPrice: true }, + gasPrice, + selectedFeeLevel: newSelectedFeeLevel, + }, }); }; -export const onGasLimitChange = (gasLimit: string/* , shouldUpdateFeeLevels: boolean = false */): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { - const currentState: State = getState().sendForm; - - const touched = { ...currentState.touched }; - touched.gasLimit = true; - - const state: State = { - ...currentState, - calculatingGasLimit: false, - untouched: false, - touched, - gasLimit, - }; - - if (currentState.selectedFeeLevel.value !== 'Custom') { - const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); - if (!customLevel) return; - state.selectedFeeLevel = customLevel; - } +/* +* Called from UI on "data" field change +* OR from "estimateGasPrice" action +*/ +export const onGasLimitChange = (gasLimit: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const { network } = getState().selectedAccount; + if (!network) return; + const state: State = getState().sendForm; + // recalculate feeLevels with recommended gasPrice + const feeLevels = ValidationActions.getFeeLevels(network.symbol, state.recommendedGasPrice, gasLimit, state.selectedFeeLevel); + const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); dispatch({ - type: SEND.GAS_LIMIT_CHANGE, - state, + type: SEND.CHANGE, + state: { + ...state, + calculatingGasLimit: false, + untouched: false, + touched: { ...state.touched, gasLimit: true }, + gasLimit, + feeLevels, + selectedFeeLevel, + }, }); }; -export const onNonceChange = (nonce: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const currentState: State = getState().sendForm; - const touched = { ...currentState.touched }; - touched.nonce = true; - - const state: State = { - ...currentState, - untouched: false, - touched, - nonce, - }; +/* +* Called from UI on "nonce" field change +*/ +export const onNonceChange = (nonce: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state: State = getState().sendForm; + dispatch({ + type: SEND.CHANGE, + state: { + ...state, + untouched: false, + touched: { ...state.touched, nonce: true }, + nonce, + }, + }); +}; +/* +* Called from UI on "data" field change +*/ +export const onDataChange = (data: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const state: State = getState().sendForm; dispatch({ - type: SEND.NONCE_CHANGE, - state, + type: SEND.CHANGE, + state: { + ...state, + calculatingGasLimit: true, + untouched: false, + touched: { ...state.touched, data: true }, + data, + }, }); + + dispatch(estimateGasPrice()); }; +/* +* Internal method +* Called from "onDataChange" action +* try to asynchronously download data from backend +*/ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const { - network, - } = getState().selectedAccount; - if (!network) return; - const state: State = getState().sendForm; - const requestedData = state.data; + const { network } = getState().selectedAccount; + if (!network) { + // stop "calculatingGasLimit" process + dispatch(onGasLimitChange(state.gasLimit)); + return; + } - const re = /^[0-9A-Fa-f]+$/g; + const requestedData = state.data; + const re = /^[0-9A-Fa-f]+$/g; // TODO: allow "0x" prefix if (!re.test(requestedData)) { - // to stop calculating + // stop "calculatingGasLimit" process dispatch(onGasLimitChange(requestedData.length > 0 ? state.gasLimit : network.defaultGasLimit.toString())); return; } @@ -721,40 +405,18 @@ const estimateGasPrice = (): AsyncAction => async (dispatch: Dispatch, getState: return; } - const gasLimit: number = await dispatch(BlockchainActions.estimateGasLimit(network.network, state.data, state.amount, state.gasPrice)); + const gasLimit = await dispatch(BlockchainActions.estimateGasLimit(network.network, state.data, state.amount, state.gasPrice)); + // double check "data" field + // possible race condition when data changed before backend respond if (getState().sendForm.data === requestedData) { - dispatch(onGasLimitChange(gasLimit.toString())); + dispatch(onGasLimitChange(gasLimit)); } }; -export const onDataChange = (data: string): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const currentState: State = getState().sendForm; - const touched = { ...currentState.touched }; - touched.data = true; - - const state: State = { - ...currentState, - calculatingGasLimit: true, - untouched: false, - touched, - data, - }; - - if (currentState.selectedFeeLevel.value !== 'Custom') { - const customLevel = currentState.feeLevels.find(f => f.value === 'Custom'); - if (!customLevel) return; - state.selectedFeeLevel = customLevel; - } - - dispatch({ - type: SEND.DATA_CHANGE, - state, - }); - - dispatch(estimateGasPrice()); -}; - +/* +* Called from UI from "send" button +*/ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const { account, @@ -767,8 +429,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge const currentState: State = getState().sendForm; const isToken: boolean = currentState.currency !== currentState.networkSymbol; - const address_n = account.addressPath; - const pendingNonce: number = stateUtils.getPendingNonce(pending); + const pendingNonce: number = reducerUtils.getPendingNonce(pending); const nonce = pendingNonce > 0 && pendingNonce >= account.nonce ? pendingNonce : account.nonce; const txData = await dispatch(prepareEthereumTx({ @@ -793,7 +454,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge state: selected.state, }, useEmptyPassphrase: !selected.instance, - path: address_n, + path: account.addressPath, transaction: txData, }); @@ -826,7 +487,7 @@ export const onSend = (): AsyncAction => async (dispatch: Dispatch, getState: Ge throw new Error(push.payload.error); } - const txid = push.payload.txid; + const { txid } = push.payload; dispatch({ type: SEND.TX_COMPLETE, diff --git a/src/actions/SendFormValidationActions.js b/src/actions/SendFormValidationActions.js new file mode 100644 index 00000000..6f7b09f0 --- /dev/null +++ b/src/actions/SendFormValidationActions.js @@ -0,0 +1,413 @@ +/* @flow */ + +import BigNumber from 'bignumber.js'; +import EthereumjsUtil from 'ethereumjs-util'; +import EthereumjsUnits from 'ethereumjs-units'; +import { findToken } from 'reducers/TokensReducer'; +import { findDevice, getPendingAmount } from 'reducers/utils'; +import * as SEND from 'actions/constants/send'; + +import type { + Dispatch, + GetState, + PayloadAction, +} from 'flowtype'; +import type { State, FeeLevel } from 'reducers/SendFormReducer'; + +// general regular expressions +const NUMBER_RE: RegExp = new RegExp('^(0|0\\.([0-9]+)?|[1-9][0-9]*\\.?([0-9]+)?|\\.[0-9]+)$'); +const UPPERCASE_RE = new RegExp('^(.*[A-Z].*)$'); +const ABS_RE = new RegExp('^[0-9]+$'); +const ETH_18_RE = new RegExp('^(0|0\\.([0-9]{0,18})?|[1-9][0-9]*\\.?([0-9]{0,18})?|\\.[0-9]{0,18})$'); +const HEX_RE = new RegExp('^[0-9A-Fa-f]+$'); +const dynamicRegexp = (decimals: number): RegExp => { + if (decimals > 0) { + return new RegExp(`^(0|0\\.([0-9]{0,${decimals}})?|[1-9][0-9]*\\.?([0-9]{0,${decimals}})?|\\.[0-9]{1,${decimals}})$`); + } + return ABS_RE; +}; + +/* +* Called from SendFormActions.observe +* Reaction for WEB3.GAS_PRICE_UPDATED action +*/ +export const onGasPriceUpdated = (network: string, gasPrice: string): PayloadAction => (dispatch: Dispatch, getState: GetState): void => { + // testing random data + // function getRandomInt(min, max) { + // return Math.floor(Math.random() * (max - min + 1)) + min; + // } + // const newPrice = getRandomInt(10, 50).toString(); + + const state = getState().sendForm; + if (network === state.networkSymbol) return; + + // check if new price is different then currently recommended + const newPrice: string = EthereumjsUnits.convert(gasPrice, 'wei', 'gwei'); + + if (newPrice !== state.recommendedGasPrice) { + if (!state.untouched) { + // if there is a transaction draft let the user know + // and let him update manually + dispatch({ + type: SEND.CHANGE, + state: { + ...state, + gasPriceNeedsUpdate: true, + recommendedGasPrice: newPrice, + }, + }); + } else { + // automatically update feeLevels and gasPrice + const feeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit); + const selectedFeeLevel = getSelectedFeeLevel(feeLevels, state.selectedFeeLevel); + dispatch({ + type: SEND.CHANGE, + state: { + ...state, + gasPriceNeedsUpdate: false, + recommendedGasPrice: newPrice, + gasPrice: selectedFeeLevel.gasPrice, + feeLevels, + selectedFeeLevel, + }, + }); + } + } +}; + +/* +* Recalculate amount, total and fees +*/ +export const validation = (): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { + // clone deep nested object + // to avoid overrides across state history + let state: State = JSON.parse(JSON.stringify(getState().sendForm)); + // reset errors + state.errors = {}; + state.warnings = {}; + state.infos = {}; + state = dispatch(recalculate(state)); + state = dispatch(updateCustomFeeLabel(state)); + state = dispatch(addressValidation(state)); + state = dispatch(addressLabel(state)); + state = dispatch(amountValidation(state)); + state = dispatch(gasLimitValidation(state)); + state = dispatch(gasPriceValidation(state)); + state = dispatch(nonceValidation(state)); + state = dispatch(dataValidation(state)); + return state; +}; + +export const recalculate = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { + const { + account, + tokens, + pending, + } = getState().selectedAccount; + if (!account) return $state; + + const state = { ...$state }; + const isToken = state.currency !== state.networkSymbol; + + if (state.setMax) { + const pendingAmount = getPendingAmount(pending, state.currency, isToken); + if (isToken) { + const token = findToken(tokens, account.address, state.currency, account.deviceState); + if (token) { + state.amount = new BigNumber(token.balance).minus(pendingAmount).toString(10); + } + } else { + const b = new BigNumber(account.balance).minus(pendingAmount); + state.amount = calculateMaxAmount(b, state.gasPrice, state.gasLimit); + } + } + + state.total = calculateTotal(isToken ? '0' : state.amount, state.gasPrice, state.gasLimit); + return state; +}; + +export const updateCustomFeeLabel = ($state: State): PayloadAction => (): State => { + const state = { ...$state }; + if ($state.selectedFeeLevel.value === 'Custom') { + state.selectedFeeLevel = { + ...state.selectedFeeLevel, + gasPrice: state.gasPrice, + label: `${calculateFee(state.gasPrice, state.gasLimit)} ${state.networkSymbol}`, + }; + } + return state; +}; + +/* +* Address value validation +*/ +export const addressValidation = ($state: State): PayloadAction => (): State => { + const state = { ...$state }; + if (!state.touched.address) return state; + + const { address } = state; + + if (address.length < 1) { + state.errors.address = 'Address is not set'; + } else if (!EthereumjsUtil.isValidAddress(address)) { + state.errors.address = 'Address is not valid'; + } else if (address.match(UPPERCASE_RE) && !EthereumjsUtil.isValidChecksumAddress(address)) { + state.errors.address = 'Address is not a valid checksum'; + } + return state; +}; + +/* +* Address label assignation +*/ +export const addressLabel = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { + const state = { ...$state }; + if (!state.touched.address || state.errors.address) return state; + + const { + account, + network, + } = getState().selectedAccount; + if (!account || !network) return state; + const { address } = state; + + const savedAccounts = getState().accounts.filter(a => a.address.toLowerCase() === address.toLowerCase()); + if (savedAccounts.length > 0) { + // check if found account belongs to this network + const currentNetworkAccount = savedAccounts.find(a => a.network === network.network); + if (currentNetworkAccount) { + const device = findDevice(getState().devices, currentNetworkAccount.deviceID, currentNetworkAccount.deviceState); + if (device) { + state.infos.address = `${device.instanceLabel} Account #${(currentNetworkAccount.index + 1)}`; + } + } else { + // corner-case: the same derivation path is used on different networks + const otherNetworkAccount = savedAccounts[0]; + const device = findDevice(getState().devices, otherNetworkAccount.deviceID, otherNetworkAccount.deviceState); + const { coins } = getState().localStorage.config; + const otherNetwork = coins.find(c => c.network === otherNetworkAccount.network); + if (device && otherNetwork) { + state.warnings.address = `Looks like it's ${device.instanceLabel} Account #${(otherNetworkAccount.index + 1)} address of ${otherNetwork.name} network`; + } + } + } + + return state; +}; + +/* +* Amount value validation +*/ +export const amountValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { + const state = { ...$state }; + if (!state.touched.amount) return state; + + const { + account, + tokens, + pending, + } = getState().selectedAccount; + if (!account) return state; + + const { amount } = state; + if (amount.length < 1) { + state.errors.amount = 'Amount is not set'; + } else if (amount.length > 0 && !amount.match(NUMBER_RE)) { + state.errors.amount = 'Amount is not a number'; + } else { + const isToken: boolean = state.currency !== state.networkSymbol; + const pendingAmount: BigNumber = getPendingAmount(pending, state.currency, isToken); + + if (isToken) { + const token = findToken(tokens, account.address, state.currency, account.deviceState); + if (!token) return state; + const decimalRegExp = dynamicRegexp(parseInt(token.decimals, 0)); + + if (!state.amount.match(decimalRegExp)) { + state.errors.amount = `Maximum ${token.decimals} decimals allowed`; + } else if (new BigNumber(state.total).greaterThan(account.balance)) { + state.errors.amount = `Not enough ${state.networkSymbol} to cover transaction fee`; + } else if (new BigNumber(state.amount).greaterThan(new BigNumber(token.balance).minus(pendingAmount))) { + state.errors.amount = 'Not enough funds'; + } else if (new BigNumber(state.amount).lessThanOrEqualTo('0')) { + state.errors.amount = 'Amount is too low'; + } + } else if (!state.amount.match(ETH_18_RE)) { + state.errors.amount = 'Maximum 18 decimals allowed'; + } else if (new BigNumber(state.total).greaterThan(new BigNumber(account.balance).minus(pendingAmount))) { + state.errors.amount = 'Not enough funds'; + } + } + return state; +}; + +/* +* Gas limit value validation +*/ +export const gasLimitValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { + const state = { ...$state }; + if (!state.touched.gasLimit) return state; + + const { + network, + } = getState().selectedAccount; + if (!network) return state; + + const { gasLimit } = state; + if (gasLimit.length < 1) { + state.errors.gasLimit = 'Gas limit is not set'; + } else if (gasLimit.length > 0 && !gasLimit.match(NUMBER_RE)) { + state.errors.gasLimit = 'Gas limit is not a number'; + } else { + const gl: BigNumber = new BigNumber(gasLimit); + if (gl.lessThan(1)) { + state.errors.gasLimit = 'Gas limit is too low'; + } else if (gl.lessThan(state.currency !== state.networkSymbol ? network.defaultGasLimitTokens : network.defaultGasLimit)) { + state.warnings.gasLimit = 'Gas limit is below recommended'; + } + } + return state; +}; + +/* +* Gas price value validation +*/ +export const gasPriceValidation = ($state: State): PayloadAction => (): State => { + const state = { ...$state }; + if (!state.touched.gasPrice) return state; + + const { gasPrice } = state; + if (gasPrice.length < 1) { + state.errors.gasPrice = 'Gas price is not set'; + } else if (gasPrice.length > 0 && !gasPrice.match(NUMBER_RE)) { + state.errors.gasPrice = 'Gas price is not a number'; + } else { + const gp: BigNumber = new BigNumber(gasPrice); + if (gp.greaterThan(1000)) { + state.warnings.gasPrice = 'Gas price is too high'; + } else if (gp.lessThanOrEqualTo('0')) { + state.errors.gasPrice = 'Gas price is too low'; + } + } + return state; +}; + +/* +* Nonce value validation +*/ +export const nonceValidation = ($state: State): PayloadAction => (dispatch: Dispatch, getState: GetState): State => { + const state = { ...$state }; + if (!state.touched.nonce) return state; + + const { + account, + } = getState().selectedAccount; + if (!account) return state; + + const { nonce } = state; + if (nonce.length < 1) { + state.errors.nonce = 'Nonce is not set'; + } else if (!nonce.match(ABS_RE)) { + state.errors.nonce = 'Nonce is not a valid number'; + } else { + const n: BigNumber = new BigNumber(nonce); + if (n.lessThan(account.nonce)) { + state.warnings.nonce = 'Nonce is lower than recommended'; + } else if (n.greaterThan(account.nonce)) { + state.warnings.nonce = 'Nonce is greater than recommended'; + } + } + return state; +}; + +/* +* Gas price value validation +*/ +export const dataValidation = ($state: State): PayloadAction => (): State => { + const state = { ...$state }; + if (!state.touched.data || state.data.length === 0) return state; + if (!HEX_RE.test(state.data)) { + state.errors.data = 'Data is not valid hexadecimal'; + } + return state; +}; + +/* +* UTILITIES +*/ + +export const calculateFee = (gasPrice: string, gasLimit: string): string => { + try { + return EthereumjsUnits.convert(new BigNumber(gasPrice).times(gasLimit), 'gwei', 'ether'); + } catch (error) { + return '0'; + } +}; + +export const calculateTotal = (amount: string, gasPrice: string, gasLimit: string): string => { + try { + return new BigNumber(amount).plus(calculateFee(gasPrice, gasLimit)).toString(10); + } catch (error) { + return '0'; + } +}; + +export const calculateMaxAmount = (balance: BigNumber, gasPrice: string, gasLimit: string): string => { + try { + // TODO - minus pendings + const fee = calculateFee(gasPrice, gasLimit); + const max = balance.minus(fee); + if (max.lessThan(0)) return '0'; + return max.toString(10); + } catch (error) { + return '0'; + } +}; + +export const getFeeLevels = (symbol: string, gasPrice: BigNumber | string, gasLimit: string, selected?: FeeLevel): Array => { + const price: BigNumber = typeof gasPrice === 'string' ? new BigNumber(gasPrice) : gasPrice; + const quarter: BigNumber = price.dividedBy(4); + const high: string = price.plus(quarter.times(2)).toString(10); + const low: string = price.minus(quarter.times(2)).toString(10); + + const customLevel: FeeLevel = selected && selected.value === 'Custom' ? { + value: 'Custom', + gasPrice: selected.gasPrice, + // label: `${ calculateFee(gasPrice, gasLimit) } ${ symbol }` + label: `${calculateFee(selected.gasPrice, gasLimit)} ${symbol}`, + } : { + value: 'Custom', + gasPrice: low, + label: '', + }; + + return [ + { + value: 'High', + gasPrice: high, + label: `${calculateFee(high, gasLimit)} ${symbol}`, + }, + { + value: 'Normal', + gasPrice: gasPrice.toString(), + label: `${calculateFee(price.toString(10), gasLimit)} ${symbol}`, + }, + { + value: 'Low', + gasPrice: low, + label: `${calculateFee(low, gasLimit)} ${symbol}`, + }, + customLevel, + ]; +}; + +export const getSelectedFeeLevel = (feeLevels: Array, selected: FeeLevel): FeeLevel => { + const { value } = selected; + let selectedFeeLevel: ?FeeLevel; + selectedFeeLevel = feeLevels.find(f => f.value === value); + if (!selectedFeeLevel) { + // fallback to default + selectedFeeLevel = feeLevels.find(f => f.value === 'Normal'); + } + return selectedFeeLevel || selected; +}; \ No newline at end of file diff --git a/src/actions/constants/send.js b/src/actions/constants/send.js index 08528692..a5d12906 100644 --- a/src/actions/constants/send.js +++ b/src/actions/constants/send.js @@ -1,23 +1,9 @@ /* @flow */ - export const INIT: 'send__init' = 'send__init'; -export const DISPOSE: 'send__dispose' = 'send__dispose'; +export const CHANGE: 'send__change' = 'send__change'; export const VALIDATION: 'send__validation' = 'send__validation'; -export const ADDRESS_VALIDATION: 'send__address_validation' = 'send__address_validation'; -export const ADDRESS_CHANGE: 'send__address_change' = 'send__address_change'; -export const AMOUNT_CHANGE: 'send__amount_change' = 'send__amount_change'; -export const SET_MAX: 'send__set_max' = 'send__set_max'; -export const CURRENCY_CHANGE: 'send__currency_change' = 'send__currency_change'; -export const FEE_LEVEL_CHANGE: 'send__fee_level_change' = 'send__fee_level_change'; -export const GAS_PRICE_CHANGE: 'send__gas_price_change' = 'send__gas_price_change'; -export const GAS_LIMIT_CHANGE: 'send__gas_limit_change' = 'send__gas_limit_change'; -export const NONCE_CHANGE: 'send__nonce_change' = 'send__nonce_change'; -export const UPDATE_FEE_LEVELS: 'send__update_fee_levels' = 'send__update_fee_levels'; -export const DATA_CHANGE: 'send__data_change' = 'send__data_change'; -export const SEND: 'send__submit' = 'send__submit'; +export const TX_SENDING: 'send__tx_sending' = 'send__tx_sending'; export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete'; export const TX_ERROR: 'send__tx_error' = 'send__tx_error'; export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced'; - -export const FROM_SESSION_STORAGE: 'send__from_session_storage' = 'send__from_session_storage'; \ No newline at end of file diff --git a/src/reducers/SendFormReducer.js b/src/reducers/SendFormReducer.js index df8902e3..b370be6e 100644 --- a/src/reducers/SendFormReducer.js +++ b/src/reducers/SendFormReducer.js @@ -1,17 +1,9 @@ /* @flow */ - -import EthereumjsUnits from 'ethereumjs-units'; import * as SEND from 'actions/constants/send'; -import * as WEB3 from 'actions/constants/web3'; import * as ACCOUNT from 'actions/constants/account'; -import { getFeeLevels } from 'actions/SendFormActions'; - import type { Action } from 'flowtype'; -import type { - Web3UpdateGasPriceAction, -} from 'actions/Web3Actions'; export type FeeLevel = { label: string; @@ -82,46 +74,16 @@ export const initialState: State = { infos: {}, }; - -const onGasPriceUpdated = (state: State, action: Web3UpdateGasPriceAction): State => { - // function getRandomInt(min, max) { - // return Math.floor(Math.random() * (max - min + 1)) + min; - // } - // const newPrice = getRandomInt(10, 50).toString(); - const newPrice: string = EthereumjsUnits.convert(action.gasPrice, 'wei', 'gwei'); - if (action.network === state.networkName && newPrice !== state.recommendedGasPrice) { - const newState: State = { ...state }; - if (!state.untouched) { - newState.gasPriceNeedsUpdate = true; - newState.recommendedGasPrice = newPrice; - } else { - const newFeeLevels = getFeeLevels(state.networkSymbol, newPrice, state.gasLimit); - const selectedFeeLevel: ?FeeLevel = newFeeLevels.find(f => f.value === 'Normal'); - if (!selectedFeeLevel) return state; - newState.recommendedGasPrice = newPrice; - newState.feeLevels = newFeeLevels; - newState.selectedFeeLevel = selectedFeeLevel; - newState.gasPrice = selectedFeeLevel.gasPrice; - } - return newState; - } - return state; -}; - - export default (state: State = initialState, action: Action): State => { switch (action.type) { case SEND.INIT: + case SEND.CHANGE: + case SEND.VALIDATION: return action.state; case ACCOUNT.DISPOSE: return initialState; - // this will be called right after Web3 instance initialization before any view is shown - // and async during app live time - case WEB3.GAS_PRICE_UPDATED: - return onGasPriceUpdated(state, action); - case SEND.TOGGLE_ADVANCED: return { @@ -129,22 +91,7 @@ export default (state: State = initialState, action: Action): State => { advanced: !state.advanced, }; - - // user actions - case SEND.ADDRESS_CHANGE: - case SEND.ADDRESS_VALIDATION: - case SEND.AMOUNT_CHANGE: - case SEND.SET_MAX: - case SEND.CURRENCY_CHANGE: - case SEND.FEE_LEVEL_CHANGE: - case SEND.UPDATE_FEE_LEVELS: - case SEND.GAS_PRICE_CHANGE: - case SEND.GAS_LIMIT_CHANGE: - case SEND.NONCE_CHANGE: - case SEND.DATA_CHANGE: - return action.state; - - case SEND.SEND: + case SEND.TX_SENDING: return { ...state, sending: true, @@ -156,32 +103,6 @@ export default (state: State = initialState, action: Action): State => { sending: false, }; - case SEND.VALIDATION: - return { - ...state, - errors: action.errors, - warnings: action.warnings, - infos: action.infos, - }; - - case SEND.FROM_SESSION_STORAGE: - return { - ...state, - - address: action.address, - amount: action.amount, - setMax: action.setMax, - selectedCurrency: action.selectedCurrency, - selectedFeeLevel: action.selectedFeeLevel, - advanced: action.advanced, - gasLimit: action.gasLimit, - gasPrice: action.gasPrice, - data: action.data, - nonce: action.nonce, - untouched: false, - touched: action.touched, - }; - default: return state; } From 9f98e7dd7e20a1d69562091c9062b1040e87d8a3 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 18:49:25 +0200 Subject: [PATCH 19/28] observe from WalletService --- src/services/WalletService.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/WalletService.js b/src/services/WalletService.js index ab9f6b95..8856b0c9 100644 --- a/src/services/WalletService.js +++ b/src/services/WalletService.js @@ -10,6 +10,7 @@ import * as NotificationActions from 'actions/NotificationActions'; import * as LocalStorageActions from 'actions/LocalStorageActions'; import * as TrezorConnectActions from 'actions/TrezorConnectActions'; import * as SelectedAccountActions from 'actions/SelectedAccountActions'; +import * as SendFormActionActions from 'actions/SendFormActions'; import type { Middleware, @@ -91,6 +92,8 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state)); } + // observe send form props changes + api.dispatch(SendFormActionActions.observe(prevState, action)); // update common values in WallerReducer api.dispatch(WalletActions.updateSelectedValues(prevState, action)); From 91f731c34e13db21a3e70ce3c7bd53885f7fe095 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Sat, 22 Sep 2018 18:50:15 +0200 Subject: [PATCH 20/28] making sendform component a stateless + spliting advanced to a separate component --- .../AccountSend/components/Advanced/index.js | 221 ++++++ src/views/Wallet/views/AccountSend/index.js | 663 ++++++------------ 2 files changed, 450 insertions(+), 434 deletions(-) create mode 100644 src/views/Wallet/views/AccountSend/components/Advanced/index.js diff --git a/src/views/Wallet/views/AccountSend/components/Advanced/index.js b/src/views/Wallet/views/AccountSend/components/Advanced/index.js new file mode 100644 index 00000000..dd9d44e6 --- /dev/null +++ b/src/views/Wallet/views/AccountSend/components/Advanced/index.js @@ -0,0 +1,221 @@ +/* @flow */ + +import React from 'react'; +import styled from 'styled-components'; +import colors from 'config/colors'; + +import Input from 'components/inputs/Input'; +import Textarea from 'components/Textarea'; +import Tooltip from 'components/Tooltip'; +import Icon from 'components/Icon'; +import Link from 'components/Link'; +import ICONS from 'config/icons'; + +import type { Props } from '../../Container'; + +// duplicates from ../../Container +const InputRow = styled.div` + margin-bottom: 20px; +`; +// duplicates end + +const InputLabelWrapper = styled.div` + display: flex; + align-items: center; +`; + +const GreenSpan = styled.span` + color: ${colors.GREEN_PRIMARY}; +`; + +const AdvancedSettingsWrapper = styled.div` + padding: 20px 0; + display: flex; + flex-direction: column; + justify-content: space-between; + + border-top: 1px solid ${colors.DIVIDER}; +`; + +const GasInputRow = styled(InputRow)` + width: 100%; + display: flex; +`; + +const GasInput = styled(Input)` + &:first-child { + padding-right: 20px; + } +`; + +const StyledTextarea = styled(Textarea)` + margin-bottom: 20px; + height: 80px; +`; + +const AdvancedSettingsSendButtonWrapper = styled.div` + width: 100%; + display: flex; + justify-content: flex-end; +`; + +const getGasLimitInputState = (gasLimitErrors: string, gasLimitWarnings: string): string => { + let state = ''; + if (gasLimitWarnings && !gasLimitErrors) { + state = 'warning'; + } + if (gasLimitErrors) { + state = 'error'; + } + return state; +}; + +const getGasPriceInputState = (gasPriceErrors: string, gasPriceWarnings: string): string => { + let state = ''; + if (gasPriceWarnings && !gasPriceErrors) { + state = 'warning'; + } + if (gasPriceErrors) { + state = 'error'; + } + return state; +}; + +// stateless component +const AdvancedForm = (props: Props) => { + const { + network, + } = props.selectedAccount; + if (!network) return null; + const { + networkSymbol, + currency, + recommendedGasPrice, + errors, + warnings, + infos, + data, + gasLimit, + gasPrice, + } = props.sendForm; + const { + onGasLimitChange, + onGasPriceChange, + onDataChange, + } = props.sendFormActions; + + let gasLimitTooltipCurrency: string; + let gasLimitTooltipValue: string; + if (networkSymbol !== currency) { + gasLimitTooltipCurrency = 'tokens'; + gasLimitTooltipValue = network.defaultGasLimitTokens.toString(10); + } else { + gasLimitTooltipCurrency = networkSymbol; + gasLimitTooltipValue = network.defaultGasLimit.toString(10); + } + + return ( + + + + Gas limit + + Gas limit is the amount of gas to send with your transaction.
+ TX fee = gas price * gas limit & is paid to miners for including your TX in a block.
+ Increasing this number will not get your TX mined faster.
+ Default value for sending {gasLimitTooltipCurrency} is {gasLimitTooltipValue} + + )} + placement="top" + > + +
+ + )} + bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit} + value={gasLimit} + isDisabled={networkSymbol === currency && data.length > 0} + onChange={event => onGasLimitChange(event.target.value)} + /> + + + Gas price + + Gas Price is the amount you pay per unit of gas.
+ TX fee = gas price * gas limit & is paid to miners for including your TX in a block.
+ Higher the gas price = faster transaction, but more expensive. Recommended is {recommendedGasPrice} GWEI.
+ Read more + + )} + placement="top" + > + +
+ + )} + bottomText={errors.gasPrice || warnings.gasPrice || infos.gasPrice} + value={gasPrice} + onChange={event => onGasPriceChange(event.target.value)} + /> +
+ + + Data + + Data is usually used when you send transactions to contracts. + + )} + placement="top" + > + + + + )} + bottomText={errors.data || warnings.data || infos.data} + disabled={networkSymbol !== currency} + value={networkSymbol !== currency ? '' : data} + onChange={event => onDataChange(event.target.value)} + /> + + + { props.children } + +
+ ); +}; + +export default AdvancedForm; \ No newline at end of file diff --git a/src/views/Wallet/views/AccountSend/index.js b/src/views/Wallet/views/AccountSend/index.js index c2318e72..64863ab3 100644 --- a/src/views/Wallet/views/AccountSend/index.js +++ b/src/views/Wallet/views/AccountSend/index.js @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component } from 'react'; +import React from 'react'; import styled, { css } from 'styled-components'; import { Select } from 'components/Select'; import Button from 'components/Button'; @@ -12,11 +12,9 @@ import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables'; import colors from 'config/colors'; import P from 'components/Paragraph'; import { H2 } from 'components/Heading'; -import Textarea from 'components/Textarea'; -import Tooltip from 'components/Tooltip'; -import { calculate, validation } from 'actions/SendFormActions'; import SelectedAccount from 'views/Wallet/components/SelectedAccount'; import type { Token } from 'flowtype'; +import AdvancedForm from './components/Advanced'; import PendingTransactions from './components/PendingTransactions'; import type { Props } from './Container'; @@ -25,11 +23,6 @@ import type { Props } from './Container'; // and put it inside config/variables.js const SmallScreenWidth = '850px'; -type State = { - isAdvancedSettingsHidden: boolean, - shouldAnimateAdvancedSettingsToggle: boolean, -}; - const Wrapper = styled.section` padding: 0 48px; `; @@ -143,453 +136,255 @@ const SendButton = styled(Button)` } `; -const AdvancedSettingsWrapper = styled.div` - padding: 20px 0; - display: flex; - flex-direction: column; - justify-content: space-between; - - border-top: 1px solid ${colors.DIVIDER}; -`; - -const GasInputRow = styled(InputRow)` - width: 100%; - display: flex; -`; - -const GasInput = styled(Input)` - &:first-child { - padding-right: 20px; - } -`; - -const AdvancedSettingsSendButtonWrapper = styled.div` - width: 100%; - display: flex; - justify-content: flex-end; -`; - -const StyledTextarea = styled(Textarea)` - margin-bottom: 20px; - height: 80px; -`; - const AdvancedSettingsIcon = styled(Icon)` margin-left: 10px; `; -const GreenSpan = styled.span` - color: ${colors.GREEN_PRIMARY}; -`; - -const InputLabelWrapper = styled.div` - display: flex; - align-items: center; -`; - -class AccountSend extends Component { - constructor(props: Props) { - super(props); - this.state = { - isAdvancedSettingsHidden: true, - shouldAnimateAdvancedSettingsToggle: false, - }; - } - - componentWillReceiveProps(newProps: Props) { - calculate(this.props, newProps); - validation(newProps); +// render helpers +const getAddressInputState = (address: string, addressErrors: string, addressWarnings: string): string => { + let state = ''; + if (address && !addressErrors) { + state = 'success'; } - - getAddressInputState(address: string, addressErrors: string, addressWarnings: string) { - let state = ''; - if (address && !addressErrors) { - state = 'success'; - } - if (addressWarnings && !addressErrors) { - state = 'warning'; - } - if (addressErrors) { - state = 'error'; - } - return state; + if (addressWarnings && !addressErrors) { + state = 'warning'; } - - getAmountInputState(amountErrors: string, amountWarnings: string) { - let state = ''; - if (amountWarnings && !amountErrors) { - state = 'warning'; - } - if (amountErrors) { - state = 'error'; - } - return state; + if (addressErrors) { + state = 'error'; } + return state; +}; - getGasLimitInputState(gasLimitErrors: string, gasLimitWarnings: string) { - let state = ''; - if (gasLimitWarnings && !gasLimitErrors) { - state = 'warning'; - } - if (gasLimitErrors) { - state = 'error'; - } - return state; +const getAmountInputState = (amountErrors: string, amountWarnings: string): string => { + let state = ''; + if (amountWarnings && !amountErrors) { + state = 'warning'; } - - getGasPriceInputState(gasPriceErrors: string, gasPriceWarnings: string) { - let state = ''; - if (gasPriceWarnings && !gasPriceErrors) { - state = 'warning'; - } - if (gasPriceErrors) { - state = 'error'; - } - return state; + if (amountErrors) { + state = 'error'; } + return state; +}; - getTokensSelectData(tokens: Array, accountNetwork: any) { - const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol })); - tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol }); +const getTokensSelectData = (tokens: Array, accountNetwork: any): Array<{ value: string, label: string }> => { + const tokensSelectData: Array<{ value: string, label: string }> = tokens.map(t => ({ value: t.symbol, label: t.symbol })); + tokensSelectData.unshift({ value: accountNetwork.symbol, label: accountNetwork.symbol }); - return tokensSelectData; - } + return tokensSelectData; +}; - handleToggleAdvancedSettingsButton() { - this.toggleAdvancedSettings(); +// stateless component +const AccountSend = (props: Props) => { + const device = props.wallet.selectedDevice; + const { + account, + network, + discovery, + tokens, + } = props.selectedAccount; + const { + address, + amount, + setMax, + networkSymbol, + currency, + feeLevels, + selectedFeeLevel, + gasPriceNeedsUpdate, + total, + errors, + warnings, + infos, + data, + sending, + advanced, + } = props.sendForm; + + const { + toggleAdvanced, + onAddressChange, + onAmountChange, + onSetMax, + onCurrencyChange, + onFeeLevelChange, + updateFeeLevels, + onSend, + } = props.sendFormActions; + + if (!device || !account || !discovery || !network) return null; + + let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending; + let sendButtonText: string = 'Send'; + if (networkSymbol !== currency && amount.length > 0 && !errors.amount) { + sendButtonText += ` ${amount} ${currency.toUpperCase()}`; + } else if (networkSymbol === currency && total !== '0') { + sendButtonText += ` ${total} ${network.symbol}`; } - toggleAdvancedSettings() { - this.setState(previousState => ({ - isAdvancedSettingsHidden: !previousState.isAdvancedSettingsHidden, - shouldAnimateAdvancedSettingsToggle: true, - })); + if (!device.connected) { + sendButtonText = 'Device is not connected'; + isSendButtonDisabled = true; + } else if (!device.available) { + sendButtonText = 'Device is unavailable'; + isSendButtonDisabled = true; + } else if (!discovery.completed) { + sendButtonText = 'Loading accounts'; + isSendButtonDisabled = true; } - render() { - const device = this.props.wallet.selectedDevice; - const { - account, - network, - discovery, - tokens, - } = this.props.selectedAccount; - const { - address, - amount, - setMax, - networkSymbol, - currency, - feeLevels, - selectedFeeLevel, - recommendedGasPrice, - gasPriceNeedsUpdate, - total, - errors, - warnings, - infos, - data, - sending, - gasLimit, - gasPrice, - } = this.props.sendForm; - - const { - onAddressChange, - onAmountChange, - onSetMax, - onCurrencyChange, - onFeeLevelChange, - updateFeeLevels, - onSend, - onGasLimitChange, - onGasPriceChange, - onDataChange, - } = this.props.sendFormActions; - - if (!device || !account || !discovery || !network) return null; - - let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending; - let sendButtonText: string = 'Send'; - if (networkSymbol !== currency && amount.length > 0 && !errors.amount) { - sendButtonText += ` ${amount} ${currency.toUpperCase()}`; - } else if (networkSymbol === currency && total !== '0') { - sendButtonText += ` ${total} ${network.symbol}`; - } - - if (!device.connected) { - sendButtonText = 'Device is not connected'; - isSendButtonDisabled = true; - } else if (!device.available) { - sendButtonText = 'Device is unavailable'; - isSendButtonDisabled = true; - } else if (!discovery.completed) { - sendButtonText = 'Loading accounts'; - isSendButtonDisabled = true; - } - - const tokensSelectData = this.getTokensSelectData(tokens, network); - - let gasLimitTooltipCurrency: string; - let gasLimitTooltipValue: string; - if (networkSymbol !== currency) { - gasLimitTooltipCurrency = 'tokens'; - gasLimitTooltipValue = network.defaultGasLimitTokens.toString(10); - } else { - gasLimitTooltipCurrency = networkSymbol; - gasLimitTooltipValue = network.defaultGasLimit.toString(10); - } - - - return ( - - - Send Ethereum or tokens - - onAddressChange(event.target.value)} - /> - - - - onAmountChange(event.target.value)} - bottomText={errors.amount || warnings.amount || infos.amount} - sideAddons={[ - ( - onSetMax()} - isActive={setMax} - > - {!setMax && ( - - )} - {setMax && ( - - )} - Set max - - ), - ( - - ), - ]} - /> - - - - - Fee - {gasPriceNeedsUpdate && ( - - - Recommended fees updated. Click here to use them - - )} - - onAddressChange(event.target.value)} + /> + + + + onAmountChange(event.target.value)} + bottomText={errors.amount || warnings.amount || infos.amount} + sideAddons={[ + ( + onSetMax()} + isActive={setMax} + > + {!setMax && ( + )} - bottomText={errors.gasLimit || warnings.gasLimit || infos.gasLimit} - value={gasLimit} - isDisabled={networkSymbol === currency && data.length > 0} - onChange={event => onGasLimitChange(event.target.value)} - /> - - - Gas price - - Gas Price is the amount you pay per unit of gas.
- TX fee = gas price * gas limit & is paid to miners for including your TX in a block.
- Higher the gas price = faster transaction, but more expensive. Recommended is {recommendedGasPrice} GWEI.
- Read more - - )} - placement="top" - > - -
- + {setMax && ( + )} - bottomText={errors.gasPrice || warnings.gasPrice || infos.gasPrice} - value={gasPrice} - onChange={event => onGasPriceChange(event.target.value)} + Set max +
+ ), + ( + - - - - Data - - Data is usually used when you send transactions to contracts. - - )} - placement="top" - > - - - - )} - disabled={networkSymbol !== currency} - value={networkSymbol !== currency ? '' : data} - onChange={event => onDataChange(event.target.value)} - /> - - - onSend()} - > - {sendButtonText} - - - - )} - - {this.props.selectedAccount.pending.length > 0 && ( - +
+ + + + Fee + {gasPriceNeedsUpdate && ( + + + Recommended fees updated. Click here to use them + + )} + +