From 1ea41478b131d5b2f3788937988c12d6e9ac70fc Mon Sep 17 00:00:00 2001 From: Empowerful <45366397+Empowerful@users.noreply.github.com> Date: Wed, 25 Sep 2019 18:07:39 -0400 Subject: [PATCH] Protect the seed by refactoring the application as two independent processes running on a microkernel. --- .eslintrc | 2 + config/env/production.js | 2 +- config/paths.js | 1 - package.json | 51 +- .../src/SecurityModule/core.js | 36 ++ .../src/SecurityModule/core.spec.js | 94 ++++ .../src/SecurityModule/index.js | 53 ++ .../src/network/api/http.js | 5 +- .../src/network/api/index.js | 4 +- .../src/network/walletApi.js | 13 +- .../src/redux/kvStore/eth/sagaRegister.js | 4 +- .../src/redux/kvStore/eth/sagas.js | 27 +- .../src/redux/kvStore/root/sagas.js | 44 +- .../src/redux/kvStore/sagaRegister.js | 22 +- .../src/redux/kvStore/sagas.js | 24 +- .../src/redux/kvStore/xlm/sagaRegister.js | 4 +- .../src/redux/kvStore/xlm/sagas.js | 38 +- .../src/redux/kvStore/xlm/sagas.spec.js | 93 ---- .../src/redux/payment/bch/sagas.js | 11 +- .../src/redux/payment/btc/sagas.js | 8 +- .../src/redux/payment/btc/sagas.spec.js | 9 +- .../src/redux/payment/eth/sagas.js | 53 +- .../src/redux/payment/sagas.js | 10 +- .../src/redux/payment/xlm/sagas.js | 37 +- .../src/redux/payment/xlm/sagas.spec.js | 3 - .../src/redux/rootSaga.js | 12 +- .../blockchain-wallet-v4/src/redux/sagas.js | 14 +- .../src/redux/settings/sagaRegister.js | 4 +- .../__snapshots__/walletReducers.spec.js.snap | 85 +++ .../src/redux/wallet/actionTypes.js | 2 + .../src/redux/wallet/actions.js | 8 + .../src/redux/wallet/reducers.js | 5 + .../src/redux/wallet/sagaRegister.js | 7 +- .../src/redux/wallet/sagas.js | 24 +- .../src/redux/wallet/walletReducers.spec.js | 8 + .../src/redux/walletSync/middleware.js | 9 +- .../blockchain-wallet-v4/src/remote/index.js | 2 + .../blockchain-wallet-v4/src/signer/bch.js | 5 +- .../blockchain-wallet-v4/src/signer/bsv.js | 5 +- .../blockchain-wallet-v4/src/signer/btc.js | 5 +- .../blockchain-wallet-v4/src/signer/eth.js | 47 +- .../blockchain-wallet-v4/src/signer/wifs.js | 11 +- .../blockchain-wallet-v4/src/signer/xlm.js | 4 +- .../src/types/HDWallet.js | 16 +- .../src/types/HDWallet.spec.js | 43 +- .../src/types/KVStoreEntry.js | 20 +- .../src/types/Serializer.js | 17 +- .../blockchain-wallet-v4/src/types/Wallet.js | 42 +- .../src/types/Wallet.spec.js | 24 +- .../blockchain-wallet-v4/src/types/Wrapper.js | 11 + .../src/types/Wrapper.spec.js | 4 + .../types/__snapshots__/Wallet.spec.js.snap | 87 +++ .../types/__snapshots__/Wrapper.spec.js.snap | 83 +++ .../blockchain-wallet-v4/src/utils/eth.js | 43 +- .../src/utils/functional.js | 13 +- .../blockchain-wallet-v4/src/utils/xlm.js | 15 +- packages/main-process/babel.config.js | 32 +- packages/main-process/package.json | 15 +- packages/main-process/src/IPC/Middleware.js | 54 ++ packages/main-process/src/IPC/index.js | 28 + .../__snapshots__/index.spec.js.snap | 1 + .../main-process/src/data/analytics/sagas.js | 13 +- .../main-process/src/data/auth/actionTypes.js | 1 + .../main-process/src/data/auth/actions.js | 6 + .../src/data/auth/sagaRegister.js | 5 +- packages/main-process/src/data/auth/sagas.js | 19 +- .../main-process/src/data/auth/sagas.spec.js | 47 +- .../importBtcAddress/sagaRegister.js | 4 +- .../src/data/components/sagaRegister.js | 46 +- .../main-process/src/data/components/sagas.js | 4 +- .../src/data/modules/sagaRegister.js | 16 +- .../main-process/src/data/modules/sagas.js | 16 +- .../src/data/modules/settings/sagaRegister.js | 5 +- .../src/data/modules/settings/sagas.js | 53 +- .../src/data/modules/settings/sagas.spec.js | 22 - .../src/data/preferences/sagaRegister.js | 4 +- .../src/data/preferences/sagas.js | 4 +- packages/main-process/src/data/rootSaga.js | 77 +-- packages/main-process/src/index.dev.js | 17 +- packages/main-process/src/index.html | 27 - packages/main-process/src/index.prod.js | 12 +- .../Wallet/Header/SecurityCenter/index.js | 12 +- .../src/layouts/Wallet/Header/index.js | 3 +- .../main-process/src/modals/Wallet/index.js | 3 +- packages/main-process/src/modals/index.js | 3 +- .../src/scenes/Settings/General/index.js | 20 - packages/main-process/src/scenes/app.js | 39 +- packages/main-process/src/store/index.js | 213 ++++---- packages/main-process/src/store/index.spec.js | 172 ------ packages/main-process/src/template.html | 12 + packages/main-process/webpack.config.ci.js | 80 +++ packages/main-process/webpack.config.dev.js | 86 +++ packages/main-process/webpack.debug.js | 78 +++ packages/root-process/babel.config.js | 8 + packages/root-process/package.json | 20 + .../images/favicon/android-chrome-192x192.png | Bin 0 -> 4868 bytes .../images/favicon/android-chrome-512x512.png | Bin 0 -> 13373 bytes .../images/favicon/apple-touch-icon.png | Bin 0 -> 2421 bytes .../assets/images/favicon/browserconfig.xml | 9 + .../assets/images/favicon/favicon-16x16.png | Bin 0 -> 798 bytes .../assets/images/favicon/favicon-32x32.png | Bin 0 -> 1079 bytes .../src/assets/images/favicon/favicon.ico | Bin 0 -> 15086 bytes .../assets/images/favicon/mstile-144x144.png | Bin 0 -> 2488 bytes .../assets/images/favicon/mstile-150x150.png | Bin 0 -> 2502 bytes .../assets/images/favicon/mstile-310x150.png | Bin 0 -> 2678 bytes .../assets/images/favicon/mstile-310x310.png | Bin 0 -> 8106 bytes .../assets/images/favicon/mstile-70x70.png | Bin 0 -> 1882 bytes .../images/favicon/safari-pinned-tab.svg | 18 + .../assets/images/favicon/site.webmanifest | 26 + packages/root-process/src/favicons.js | 14 + packages/root-process/src/index.js | 203 +++++++ packages/root-process/src/template.html | 62 +++ packages/root-process/webpack.config.ci.js | 49 ++ packages/root-process/webpack.config.dev.js | 48 ++ packages/root-process/webpack.debug.js | 48 ++ packages/security-process/babel.config.js | 32 +- packages/security-process/package.json | 8 +- .../security-process/src/IPC/Middleware.js | 71 +++ packages/security-process/src/IPC/index.js | 28 + .../security-process/src/data/actionTypes.js | 6 - packages/security-process/src/data/actions.js | 2 - .../src/data/auth/actionTypes.js | 1 + .../security-process/src/data/auth/actions.js | 6 + .../src/data/auth/sagaRegister.js | 5 +- .../security-process/src/data/auth/sagas.js | 55 +- .../src/data/auth/sagas.spec.js | 268 +--------- .../src/data/components/actions.js | 64 +-- .../src/data/components/sagaRegister.js | 58 -- .../src/data/goals/sagas.spec.js | 35 -- packages/security-process/src/data/model.js | 4 +- .../src/data/modules/actionTypes.js | 15 +- .../src/data/modules/actions.js | 14 +- .../src/data/modules/sagaRegister.js | 16 +- .../src/data/modules/sagas.js | 16 +- .../src/data/modules/selectors.js | 4 +- .../src/data/modules/settings/actions.js | 5 - .../src/data/modules/settings/sagaRegister.js | 8 +- .../src/data/modules/settings/sagas.js | 83 +-- .../src/data/modules/settings/sagas.spec.js | 149 +++--- .../src/data/preferences/sagaRegister.js | 4 +- .../src/data/preferences/sagas.js | 3 +- .../security-process/src/data/rootReducer.js | 8 - .../security-process/src/data/rootSaga.js | 38 +- packages/security-process/src/data/sagas.js | 13 +- .../security-process/src/data/selectors.js | 4 - packages/security-process/src/index.dev.js | 22 +- packages/security-process/src/index.html | 27 - packages/security-process/src/index.prod.js | 16 +- .../src/layouts/Security/Header/index.js | 63 +++ .../src/layouts/Security/index.js | 104 ++++ .../src/modals/Settings/index.js | 2 - .../src/modals/Wallet/UpgradeWallet/index.js | 0 .../modals/Wallet/UpgradeWallet/template.js | 0 .../src/modals/Wallet/index.js | 3 +- packages/security-process/src/modals/index.js | 132 +---- .../security-process/src/scenes/Home/index.js | 69 +-- .../AdvancedSecurity/PairingCode/index.js | 89 ++++ .../AdvancedSecurity/WalletId/index.js | 49 ++ .../SecurityCenter/AdvancedSecurity/index.js | 19 + .../RecordBackupPhrase/FirstStep/index.js | 2 + packages/security-process/src/scenes/app.js | 165 ++---- .../src/services/LocalesService/index.js | 9 +- packages/security-process/src/store/index.js | 213 ++++---- packages/security-process/src/template.html | 12 + .../security-process/webpack.config.ci.js | 80 +++ .../security-process/webpack.config.dev.js | 86 +++ packages/security-process/webpack.debug.js | 78 +++ packages/web-microkernel/babel.config.js | 12 + packages/web-microkernel/package.json | 22 + packages/web-microkernel/src/Core.js | 497 ++++++++++++++++++ packages/web-microkernel/src/Core.spec.js | 291 ++++++++++ packages/web-microkernel/src/index.js | 63 +++ packages/web-microkernel/src/mocks.js | 37 ++ packages/web-microkernel/wallaby.js | 13 + packages/web-microkernel/yarn.lock | 8 + webpack.config.ci.js | 138 ++--- webpack.config.dev.js | 405 ++++++-------- webpack.debug.js | 378 ++++++------- yarn.lock | 339 +++++++++++- 179 files changed, 4686 insertions(+), 2857 deletions(-) create mode 100644 packages/blockchain-wallet-v4/src/SecurityModule/core.js create mode 100644 packages/blockchain-wallet-v4/src/SecurityModule/core.spec.js create mode 100644 packages/blockchain-wallet-v4/src/SecurityModule/index.js delete mode 100755 packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.spec.js create mode 100644 packages/blockchain-wallet-v4/src/redux/wallet/__snapshots__/walletReducers.spec.js.snap create mode 100644 packages/blockchain-wallet-v4/src/types/__snapshots__/Wallet.spec.js.snap create mode 100644 packages/blockchain-wallet-v4/src/types/__snapshots__/Wrapper.spec.js.snap create mode 100644 packages/main-process/src/IPC/Middleware.js create mode 100644 packages/main-process/src/IPC/index.js delete mode 100644 packages/main-process/src/index.html delete mode 100644 packages/main-process/src/store/index.spec.js create mode 100644 packages/main-process/src/template.html create mode 100644 packages/main-process/webpack.config.ci.js create mode 100644 packages/main-process/webpack.config.dev.js create mode 100644 packages/main-process/webpack.debug.js create mode 100644 packages/root-process/babel.config.js create mode 100644 packages/root-process/package.json create mode 100644 packages/root-process/src/assets/images/favicon/android-chrome-192x192.png create mode 100644 packages/root-process/src/assets/images/favicon/android-chrome-512x512.png create mode 100644 packages/root-process/src/assets/images/favicon/apple-touch-icon.png create mode 100644 packages/root-process/src/assets/images/favicon/browserconfig.xml create mode 100644 packages/root-process/src/assets/images/favicon/favicon-16x16.png create mode 100644 packages/root-process/src/assets/images/favicon/favicon-32x32.png create mode 100644 packages/root-process/src/assets/images/favicon/favicon.ico create mode 100644 packages/root-process/src/assets/images/favicon/mstile-144x144.png create mode 100644 packages/root-process/src/assets/images/favicon/mstile-150x150.png create mode 100644 packages/root-process/src/assets/images/favicon/mstile-310x150.png create mode 100644 packages/root-process/src/assets/images/favicon/mstile-310x310.png create mode 100644 packages/root-process/src/assets/images/favicon/mstile-70x70.png create mode 100644 packages/root-process/src/assets/images/favicon/safari-pinned-tab.svg create mode 100644 packages/root-process/src/assets/images/favicon/site.webmanifest create mode 100644 packages/root-process/src/favicons.js create mode 100644 packages/root-process/src/index.js create mode 100644 packages/root-process/src/template.html create mode 100644 packages/root-process/webpack.config.ci.js create mode 100644 packages/root-process/webpack.config.dev.js create mode 100644 packages/root-process/webpack.debug.js create mode 100644 packages/security-process/src/IPC/Middleware.js create mode 100644 packages/security-process/src/IPC/index.js delete mode 100644 packages/security-process/src/index.html create mode 100644 packages/security-process/src/layouts/Security/Header/index.js create mode 100644 packages/security-process/src/layouts/Security/index.js rename packages/{main-process => security-process}/src/modals/Wallet/UpgradeWallet/index.js (100%) rename packages/{main-process => security-process}/src/modals/Wallet/UpgradeWallet/template.js (100%) create mode 100644 packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/PairingCode/index.js create mode 100644 packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/WalletId/index.js create mode 100644 packages/security-process/src/template.html create mode 100644 packages/security-process/webpack.config.ci.js create mode 100644 packages/security-process/webpack.config.dev.js create mode 100644 packages/security-process/webpack.debug.js create mode 100644 packages/web-microkernel/babel.config.js create mode 100644 packages/web-microkernel/package.json create mode 100644 packages/web-microkernel/src/Core.js create mode 100644 packages/web-microkernel/src/Core.spec.js create mode 100644 packages/web-microkernel/src/index.js create mode 100644 packages/web-microkernel/src/mocks.js create mode 100644 packages/web-microkernel/wallaby.js create mode 100644 packages/web-microkernel/yarn.lock diff --git a/.eslintrc b/.eslintrc index 26e0bdeba83..33a125ba3b5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,6 +24,8 @@ }, "rules": { "camelcase": 0, + "no-undef": "error", + "no-unused-vars": "warn", "generator-star-spacing": ["error", {"before": true, "after": true}], "jest/no-disabled-tests": 1, "jest/no-focused-tests": 2, diff --git a/config/env/production.js b/config/env/production.js index 9355f37df11..0e9e313c1f5 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -10,6 +10,6 @@ module.exports = { ROOT_URL: 'https://blockchain.info', THE_PIT_URL: 'https://pit.blockchain.com', WEB_SOCKET_URL: 'wss://ws.blockchain.info', - WALLET_HELPER_DOMAIN: 'https://wallet-helper.blockchain.info', + WALLET_HELPER_DOMAIN: 'https://wallet-helper.blockchain.com', VERIFF_URL: 'https://magic.veriff.me' } diff --git a/config/paths.js b/config/paths.js index 0ab3e0a8c74..3f748362742 100644 --- a/config/paths.js +++ b/config/paths.js @@ -6,7 +6,6 @@ const resolveRoot = relativePath => path.resolve(appDirectory, relativePath) module.exports = { appBuild: resolveRoot('lib'), ciBuild: resolveRoot('dist'), - src: resolveRoot('packages/blockchain-wallet-v4-frontend/src'), pkgJson: resolveRoot('package.json'), envConfig: resolveRoot('config/env'), sslConfig: resolveRoot('config/ssl') diff --git a/package.json b/package.json index 8adbba9617d..ec158fcf515 100644 --- a/package.json +++ b/package.json @@ -57,24 +57,29 @@ ] }, "scripts": { - "analyze": "yarn workspace blockchain-wallet-v4-frontend analyze", - "build": "yarn workspace blockchain-wallet-v4-frontend build:dev", - "build:prod": "yarn workspace blockchain-wallet-v4-frontend build:prod", - "ci:compile": "yarn workspace blockchain-wallet-v4-frontend ci:compile", + "analyze": "cross-env-shell ANALYZE=true NODE_ENV=production webpack-cli --config webpack.config.ci.js", + "build:dev": "cross-env-shell NODE_ENV=development webpack-cli --config webpack.config.dev.js --progress --colors", + "build:prod": "cross-env-shell NODE_ENV=production webpack-cli --config webpack.config.dev.js --progress --colors", + "build:staging": "cross-env-shell NODE_ENV=staging webpack-cli --config webpack.config.dev.js --progress --colors", + "build:testnet": "cross-env-shell NODE_ENV=testnet webpack-cli --config webpack.config.dev.js --progress --colors", + "ci:compile": "cross-env-shell NODE_ENV=production webpack-cli --config webpack.config.ci.js --display-error-details", "ci:coverage:components": "yarn workspace blockchain-info-components ci:coverage:components", "ci:coverage:core": "yarn workspace blockchain-wallet-v4 ci:coverage:core", - "ci:coverage:frontend": "yarn workspace blockchain-wallet-v4-frontend ci:coverage:frontend", + "ci:coverage:main-process": "yarn workspace main-process ci:coverage:frontend", + "ci:coverage:security-process": "yarn workspace security-process ci:coverage:frontend", "ci:coverage:report": "istanbul report --root ./coverage --dir ./coverage/ lcov && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", "ci:lint": "prettier './packages/*/src/**/*.js' --loglevel error --write && eslint './packages/*/src/**/*.js' --fix && stylelint './packages/*/src/**/*.js'", "ci:test:build": "yarn wsrun test:build --serial", "ci:test:core:components": "yarn wsrun ci:test --serial --exclude-missing", - "ci:test:frontend": "yarn workspace blockchain-wallet-v4-frontend ci:test:frontend", + "ci:test:main-process": "yarn workspace main-process ci:test:frontend", + "ci:test:security-process": "yarn workspace security-process ci:test:frontend", "clean": "cross-env yarn wsrun clean && rimraf build && rimraf coverage && rimraf dist && rimraf *.log && rimraf node_modules", "coverage": "cross-env rimraf coverage && yarn wsrun coverage --fast-exit && istanbul report --root ./coverage --dir ./coverage/ text-summary html", "coverage:components": "yarn workspace blockchain-info-components coverage", "coverage:core": "yarn workspace blockchain-wallet-v4 coverage", - "coverage:frontend": "yarn workspace blockchain-wallet-v4-frontend coverage", - "debug:prod": "yarn workspace blockchain-wallet-v4-frontend debug:prod", + "coverage:main-process": "yarn workspace main-process coverage", + "coverage:security-process": "yarn workspace security-process coverage", + "debug:prod": "cross-env-shell NODE_ENV=production webpack-dev-server --config webpack.debug.js --progress --colors", "fix": "cross-env yarn prettier && yarn lint:fix && yarn test:components:update && yarn test:frontend:update", "link:resolved:paths": "yarn wsrun link:resolved:paths --exclude-missing", "lint": "eslint --cache './packages/*/src/**/*.js'", @@ -82,17 +87,19 @@ "lint:core": "eslint './packages/blockchain-wallet-v4/src/**/*.js'", "lint:css": "stylelint './packages/*/src/**/*.js'", "lint:fix": "eslint './packages/*/src/**/*.js' --fix", - "lint:frontend": "eslint './packages/blockchain-wallet-v4-frontend/src/**/*.js'", - "manage:translations": "yarn workspace blockchain-wallet-v4-frontend manage:translations", + "lint:main-process": "eslint './packages/main-process/src/**/*.js'", + "lint:security-process": "eslint './packages/security-process/src/**/*.js'", + "manage:translations": "yarn build:prod && node ./translationRunner.js", "prettier": "prettier './packages/*/src/**/*.js' --loglevel error --write", "prettier:components": "prettier './packages/blockchain-info-components/src/**/*.js' --list-different --loglevel error --write", "prettier:core": "prettier './packages/blockchain-wallet-v4/src/**/*.js' --list-different --loglevel error --write", - "prettier:frontend": "prettier './packages/blockchain-wallet-v4-frontend/src/**/*.js' --list-different --loglevel error --write", - "start": "yarn workspace blockchain-wallet-v4-frontend start:dev", - "start:dev": "yarn workspace blockchain-wallet-v4-frontend start:dev", - "start:prod": "yarn workspace blockchain-wallet-v4-frontend start:prod", - "start:staging": "yarn workspace blockchain-wallet-v4-frontend start:staging", - "start:testnet": "yarn workspace blockchain-wallet-v4-frontend start:testnet", + "prettier:main-process": "prettier './packages/main-process/src/**/*.js' --list-different --loglevel error --write", + "prettier:security-process": "prettier './packages/security-process/src/**/*.js' --list-different --loglevel error --write", + "start": "yarn start:dev", + "start:dev": "cross-env-shell NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", + "start:prod": "cross-env-shell DISABLE_SSL=true NODE_ENV=production webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", + "start:staging": "cross-env-shell NODE_ENV=staging webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", + "start:testnet": "cross-env-shell NODE_ENV=testnet webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", "storybook:build": "yarn workspace blockchain-info-components storybook:build", "storybook:serve": "yarn workspace blockchain-info-components storybook:serve", "storybook:deploy": "yarn workspace blockchain-info-components storybook:build && yarn workspace blockchain-info-components storybook:deploy", @@ -104,10 +111,14 @@ "test:core": "yarn workspace blockchain-wallet-v4 test", "test:core:debug": "yarn workspace blockchain-wallet-v4 test:debug", "test:core:watch": "yarn workspace blockchain-wallet-v4 test:watch", - "test:frontend": "yarn workspace blockchain-wallet-v4-frontend test", - "test:frontend:debug": "yarn workspace blockchain-wallet-v4-frontend test:debug", - "test:frontend:update": "yarn workspace blockchain-wallet-v4-frontend test:update", - "test:frontend:watch": "yarn workspace blockchain-wallet-v4-frontend test:watch", + "test:main-process": "yarn workspace main-process test", + "test:main-process:debug": "yarn workspace main-process test:debug", + "test:main-process:update": "yarn workspace main-process test:update", + "test:main-process:watch": "yarn workspace main-process test:watch", + "test:security-process": "yarn workspace security-process test", + "test:security-process:debug": "yarn workspace security-process test:debug", + "test:security-process:update": "yarn workspace security-process test:update", + "test:security-process:watch": "yarn workspace security-process test:watch", "release": "release-it" }, "dependencies": { diff --git a/packages/blockchain-wallet-v4/src/SecurityModule/core.js b/packages/blockchain-wallet-v4/src/SecurityModule/core.js new file mode 100644 index 00000000000..25d1e714dda --- /dev/null +++ b/packages/blockchain-wallet-v4/src/SecurityModule/core.js @@ -0,0 +1,36 @@ +export default ({ BIP39, Bitcoin, crypto, ed25519, EthHd }) => { + const credentialsEntropy = ({ guid, password, sharedKey }) => + crypto.sha256(Buffer.from(guid + sharedKey + password)) + + const entropyToSeed = entropy => + BIP39.mnemonicToSeed(BIP39.entropyToMnemonic(entropy)) + + const deriveBIP32KeyFromSeedHex = ({ entropy, network }, path) => { + const seed = entropyToSeed(entropy) + + return Bitcoin.HDNode.fromSeedBuffer(seed, network) + .derivePath(path) + .toBase58() + } + + // Derivation error using seedHex directly instead of seed derived from + // mnemonic derived from seedHex + const deriveLegacyEthereumKey = ({ entropy }) => + EthHd.fromMasterSeed(entropy) + .derivePath(`m/44'/60'/0'/0/0`) + .getWallet() + .getPrivateKey() + + const deriveSLIP10ed25519Key = async ({ entropy }, path) => { + const seed = entropyToSeed(entropy) + return ed25519.derivePath(path, seed.toString(`hex`)) + } + + return { + credentialsEntropy, + deriveBIP32KeyFromSeedHex, + deriveLegacyEthereumKey, + deriveSLIP10ed25519Key, + entropyToSeed + } +} diff --git a/packages/blockchain-wallet-v4/src/SecurityModule/core.spec.js b/packages/blockchain-wallet-v4/src/SecurityModule/core.spec.js new file mode 100644 index 00000000000..7c89315bc4f --- /dev/null +++ b/packages/blockchain-wallet-v4/src/SecurityModule/core.spec.js @@ -0,0 +1,94 @@ +import BIP39 from 'bip39' +import Bitcoin from 'bitcoinjs-lib' +import * as ed25519 from 'ed25519-hd-key' +import EthHd from 'ethereumjs-wallet/hdkey' +import * as StellarSdk from 'stellar-sdk' + +import Core from './core' +import * as crypto from '../walletCrypto' +import { taskToPromise } from '../utils/functional' + +const core = Core({ BIP39, Bitcoin, crypto, ed25519, EthHd, taskToPromise }) + +it(`generates entropy from the user's credentials`, () => { + expect( + core + .credentialsEntropy({ + guid: `50dae286-e42e-4d67-8419-d5dcc563746c`, + password: `password`, + sharedKey: `b91c904b-53ab-44b1-bf79-5b60c018da15` + }) + .toString(`base64`) + ).toEqual(`jqdTiIA0jYETn9EjAGljE5697lc8kSkxod79srxfLug=`) +}) + +it(`entropyToSeed`, () => { + expect( + core.entropyToSeed(`713a3ae074e60e56c6bd0557c4984af1`).toString(`base64`) + ).toEqual( + `5KWmMucJQ65/B2Wd8TMhYJN/rYJYchakxkMVoPs5SX7koB923atMumgUeXfzoUe2rVhMQYCOgjigf2zEtYLxhg==` + ) +}) + +it(`derives a BIP32 key from seedHex`, async () => { + expect( + await core.deriveBIP32KeyFromSeedHex( + { + network: Bitcoin.networks.bitcoin, + entropy: `713a3ae074e60e56c6bd0557c4984af1` + }, + `m/0` + ) + ).toEqual( + `xprv9vJpjafE9tbBCPBrcv5hBq1tUP4s4d3kZRHewAkGwzjvPZ3Jm8nt9eYwoLUcjnBKdB46WZmzuoEqWLJNB2GwyfShQ1y3Pn7AoVsGYXgzabG` + ) +}) + +// Derivation error using seedHex directly instead of seed derived from +// mnemonic derived from seedHex +it(`derives a legacy Ethereum key from seedHex`, async () => { + expect( + (await core.deriveLegacyEthereumKey({ + entropy: `e39c77ed95097f9006c34e1a843aa151` + })).toString(`hex`) + ).toEqual(`bb9c3e500b9c41ce9836619fb840436c2d98695d6dc43fb73e6e02df7ee7fc5c`) +}) + +describe(`derives a SLIP-10 ed25519 key from the seed`, () => { + const testVectors = [ + { + seedHex: '713a3ae074e60e56c6bd0557c4984af1', + publicKey: 'GDRXE2BQUC3AZNPVFSCEZ76NJ3WWL25FYFK6RGZGIEKWE4SOOHSUJUJ6', + secret: 'SBGWSG6BTNCKCOB3DIFBGCVMUPQFYPA2G4O34RMTB343OYPXU5DJDVMN' + }, + { + seedHex: 'b781c27351c7024355cf7f0b0efdc7f85e046cf9', + publicKey: 'GAVXVW5MCK7Q66RIBWZZKZEDQTRXWCZUP4DIIFXCCENGW2P6W4OA34RH', + secret: 'SAKS7I2PNDBE5SJSUSU2XLJ7K5XJ3V3K4UDFAHMSBQYPOKE247VHAGDB' + }, + { + seedHex: + '150df9e3ab10f3f8f1428d723a6539662e181ec8781355396cec5fc2ce08d760', + publicKey: 'GC3MMSXBWHL6CPOAVERSJITX7BH76YU252WGLUOM5CJX3E7UCYZBTPJQ', + secret: 'SAEWIVK3VLNEJ3WEJRZXQGDAS5NVG2BYSYDFRSH4GKVTS5RXNVED5AX7' + }, + { + seedHex: '00000000000000000000000000000000', + publicKey: 'GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX', + secret: 'SBUV3MRWKNS6AYKZ6E6MOUVF2OYMON3MIUASWL3JLY5E3ISDJFELYBRZ' + } + ] + + testVectors.forEach(({ publicKey, secret, seedHex }, index) => { + it(`test vector ${index}`, async () => { + const { key } = await core.deriveSLIP10ed25519Key( + { entropy: Buffer.from(seedHex, `hex`) }, + `m/44'/148'/0'` + ) + + const keypair = StellarSdk.Keypair.fromRawEd25519Seed(key) + expect(keypair.publicKey()).toEqual(publicKey) + expect(keypair.secret()).toEqual(secret) + }) + }) +}) diff --git a/packages/blockchain-wallet-v4/src/SecurityModule/index.js b/packages/blockchain-wallet-v4/src/SecurityModule/index.js new file mode 100644 index 00000000000..1007df6fab1 --- /dev/null +++ b/packages/blockchain-wallet-v4/src/SecurityModule/index.js @@ -0,0 +1,53 @@ +// Functions that require sensitive information to perform (e.g., password, +// seed, and sharedKey). Think of this module as similar to a Hardware Security +// Module. + +import BIP39 from 'bip39' +import Bitcoin from 'bitcoinjs-lib' +import * as ed25519 from 'ed25519-hd-key' + +import * as selectors from '../redux/wallet/selectors' +import Core from './core' +import * as types from '../types' +import { taskToPromise } from '../utils/functional' +import * as crypto from '../walletCrypto' + +const core = Core({ BIP39, Bitcoin, crypto, ed25519 }) + +export default ({ store }) => { + const getSeedHex = ({ secondPassword }) => { + const state = store.getState() + const wallet = selectors.getWallet(state) + return taskToPromise(types.Wallet.getSeedHex(secondPassword, wallet)) + } + + const credentialsEntropy = ({ guid, sharedKey }) => { + const state = store.getState() + const password = selectors.getMainPassword(state) + return core.credentialsEntropy({ guid, password, sharedKey }) + } + + const deriveBIP32Key = async ({ network, secondPassword }, path) => { + const entropy = await getSeedHex({ secondPassword }) + return core.deriveBIP32KeyFromSeedHex({ entropy, network }, path) + } + + // Derivation error using seedHex directly instead of seed derived from + // mnemonic derived from seedHex + const deriveLegacyEthereumKey = async ({ secondPassword }) => { + const entropy = await getSeedHex({ secondPassword }) + return core.deriveLegacyEthereumKey({ entropy }) + } + + const deriveSLIP10ed25519Key = async ({ secondPassword }, path) => { + const entropy = await getSeedHex({ secondPassword }) + return core.deriveSLIP10ed25519Key({ entropy }, path) + } + + return { + credentialsEntropy, + deriveBIP32Key, + deriveLegacyEthereumKey, + deriveSLIP10ed25519Key + } +} diff --git a/packages/blockchain-wallet-v4/src/network/api/http.js b/packages/blockchain-wallet-v4/src/network/api/http.js index c9b5772c876..1e057fae36e 100755 --- a/packages/blockchain-wallet-v4/src/network/api/http.js +++ b/packages/blockchain-wallet-v4/src/network/api/http.js @@ -2,10 +2,12 @@ import axios from 'axios' import queryString from 'query-string' import { prop, path, pathOr, merge } from 'ramda' +import * as kernel from 'web-microkernel/src' + axios.defaults.withCredentials = false axios.defaults.timeout = Infinity -export default ({ apiKey }) => { +export default ({ apiKey, imports }) => { const encodeData = (data, contentType) => { const defaultData = { api_code: apiKey, @@ -39,6 +41,7 @@ export default ({ apiKey }) => { ...options }) => axios({ + adapter: kernel.sanitizeFunction(imports.axios), url: `${url}${endPoint}`, method, data: encodeData(data, contentType), diff --git a/packages/blockchain-wallet-v4/src/network/api/index.js b/packages/blockchain-wallet-v4/src/network/api/index.js index 4abb22066f1..ce133c39966 100755 --- a/packages/blockchain-wallet-v4/src/network/api/index.js +++ b/packages/blockchain-wallet-v4/src/network/api/index.js @@ -16,17 +16,15 @@ import sfox from './sfox' import trades from './trades' import wallet from './wallet' import xlm from './xlm' -import httpService from './http' import apiAuthorize from './apiAuthorize' export default ({ + http, options, - apiKey, getAuthCredentials, reauthenticate, networks } = {}) => { - const http = httpService({ apiKey }) const authorizedHttp = apiAuthorize(http, getAuthCredentials, reauthenticate) const apiUrl = options.domains.api const coinifyUrl = options.domains.coinify diff --git a/packages/blockchain-wallet-v4/src/network/walletApi.js b/packages/blockchain-wallet-v4/src/network/walletApi.js index 731af279320..3c372744b9f 100755 --- a/packages/blockchain-wallet-v4/src/network/walletApi.js +++ b/packages/blockchain-wallet-v4/src/network/walletApi.js @@ -20,18 +20,9 @@ import { futurizeP } from 'futurize' import createApi from './api' import * as Coin from '../coinSelection/coin.js' -const createWalletApi = ( - { options, apiKey, getAuthCredentials, reauthenticate, networks } = {}, - returnType -) => { +const createWalletApi = (options = {}, returnType) => { // //////////////////////////////////////////////////////////////// - const ApiPromise = createApi({ - options, - apiKey, - getAuthCredentials, - reauthenticate, - networks - }) + const ApiPromise = createApi(options) const eitherToTask = e => e.fold(Task.rejected, Task.of) const taskToPromise = t => new Promise((resolve, reject) => t.fork(reject, resolve)) diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagaRegister.js b/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagaRegister.js index 5ec889929df..a80a9eb12e3 100755 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagaRegister.js +++ b/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, networks }) => { - const kvStoreEthSagas = sagas({ api, networks }) +export default (...args) => { + const kvStoreEthSagas = sagas(...args) return function * coreKvStoreEthSaga () { yield takeLatest(AT.FETCH_METADATA_ETH, kvStoreEthSagas.fetchMetadataEth) diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagas.js b/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagas.js index 9b61cc7768d..0c4c53599dd 100755 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/kvStore/eth/sagas.js @@ -1,5 +1,6 @@ import { assoc, + curry, includes, filter, forEach, @@ -20,28 +21,24 @@ import { KVStoreEntry } from '../../../types' import { getMetadataXpriv } from '../root/selectors' import { derivationMap, ETH } from '../config' import * as eth from '../../../utils/eth' -import { getMnemonic } from '../../wallet/selectors' import { getErc20CoinList, getSupportedCoins } from '../../walletOptions/selectors' import { callTask } from '../../../utils/functional' -export default ({ api, networks } = {}) => { - const deriveAccount = function * (password) { - try { - const obtainMnemonic = state => getMnemonic(state, password) - const mnemonicT = yield select(obtainMnemonic) - const mnemonic = yield callTask(mnemonicT) - const defaultIndex = 0 - const addr = eth.deriveAddress(mnemonic, defaultIndex) +export default ({ api, networks, securityModule } = {}) => { + const deriveAccount = function * (secondPassword) { + const defaultIndex = 0 - return { defaultIndex, addr } - } catch (e) { - throw new Error( - '[NOT IMPLEMENTED] MISSING_SECOND_PASSWORD in core.createEth saga' - ) - } + const addr = yield call( + eth.deriveAddress, + securityModule, + secondPassword, + defaultIndex + ) + + return { defaultIndex, addr } } const buildErc20Entry = (token, coinModels) => ({ label: `My ${coinModels[token].displayName} Wallet`, diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/root/sagas.js b/packages/blockchain-wallet-v4/src/redux/kvStore/root/sagas.js index 8d82d26239d..9ffdd1c7c5f 100755 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/root/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/kvStore/root/sagas.js @@ -1,18 +1,13 @@ import { call, put, select } from 'redux-saga/effects' import { prop, compose, isNil } from 'ramda' import * as A from './actions' -import BIP39 from 'bip39' import { KVStoreEntry } from '../../../types' -import { - getMnemonic, - getGuid, - getMainPassword, - getSharedKey -} from '../../wallet/selectors' +import { getGuid, getSharedKey } from '../../wallet/selectors' + const taskToPromise = t => new Promise((resolve, reject) => t.fork(reject, resolve)) -export default ({ api, networks }) => { +export default ({ api, securityModule = {}, networks }) => { const callTask = function * (task) { return yield call( compose( @@ -23,16 +18,15 @@ export default ({ api, networks }) => { } const createRoot = function * ({ password }) { try { - const obtainMnemonic = state => getMnemonic(state, password) - const mnemonicT = yield select(obtainMnemonic) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) - const seedHex = BIP39.mnemonicToEntropy(mnemonic) - const getMetadataNode = compose( - KVStoreEntry.deriveMetadataNode, - KVStoreEntry.getMasterHDNode(networks.btc) + const metadata = yield call( + securityModule.deriveBIP32Key, + { + network: networks.btc, + secondPassword: password + }, + `m/${KVStoreEntry.metadataPurpose}'` ) - const metadataNode = getMetadataNode(seedHex) - const metadata = metadataNode.toBase58() + yield put(A.updateMetadataRoot({ metadata })) } catch (e) { throw new Error('create root Metadata :: Error decrypting mnemonic') @@ -43,14 +37,18 @@ export default ({ api, networks }) => { try { const guid = yield select(getGuid) const sharedKey = yield select(getSharedKey) - const mainPassword = yield select(getMainPassword) yield put(A.fetchMetadataRootLoading()) - const kv = KVStoreEntry.fromCredentials( + + const entropy = yield call(securityModule.credentialsEntropy, { guid, - sharedKey, - mainPassword, - networks.btc - ) + sharedKey + }) + + const kv = yield call(KVStoreEntry.fromEntropy, { + entropy, + network: networks.btc + }) + const newkv = yield callTask(api.fetchKVStore(kv)) yield put(A.fetchMetadataRootSuccess(newkv)) if (isNil(prop('metadata', newkv.value))) { diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/sagaRegister.js b/packages/blockchain-wallet-v4/src/redux/kvStore/sagaRegister.js index 936519aa3fc..bb048e4c70b 100755 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/sagaRegister.js +++ b/packages/blockchain-wallet-v4/src/redux/kvStore/sagaRegister.js @@ -11,16 +11,16 @@ import contacts from './contacts/sagaRegister' import lockbox from './lockbox/sagaRegister' import userCredentials from './userCredentials/sagaRegister' -export default ({ api, networks }) => +export default (...args) => function * coreKvStoreSaga () { - yield fork(whatsNew({ api, networks })) - yield fork(eth({ api, networks })) - yield fork(bch({ api, networks })) - yield fork(btc({ api, networks })) - yield fork(xlm({ api, networks })) - yield fork(shapeShift({ api, networks })) - yield fork(buySell({ api, networks })) - yield fork(contacts({ api, networks })) - yield fork(lockbox({ api, networks })) - yield fork(userCredentials({ api, networks })) + yield fork(whatsNew(...args)) + yield fork(eth(...args)) + yield fork(bch(...args)) + yield fork(btc(...args)) + yield fork(xlm(...args)) + yield fork(shapeShift(...args)) + yield fork(buySell(...args)) + yield fork(contacts(...args)) + yield fork(lockbox(...args)) + yield fork(userCredentials(...args)) } diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/sagas.js b/packages/blockchain-wallet-v4/src/redux/kvStore/sagas.js index 5114ebff65e..832cdf3f55b 100755 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/kvStore/sagas.js @@ -10,16 +10,16 @@ import userCredentials from './userCredentials/sagas' import shapeShift from './shapeShift/sagas' import xlm from './xlm/sagas' -export default ({ api, networks }) => ({ - bch: bch({ api, networks }), - btc: btc({ api, networks }), - eth: eth({ api, networks }), - root: root({ api, networks }), - lockbox: lockbox({ api, networks }), - buySell: buySell({ api, networks }), - whatsNew: whatsNew({ api, networks }), - contacts: contacts({ api, networks }), - shapeShift: shapeShift({ api, networks }), - userCredentials: userCredentials({ api, networks }), - xlm: xlm({ api, networks }) +export default (...args) => ({ + bch: bch(...args), + btc: btc(...args), + eth: eth(...args), + root: root(...args), + lockbox: lockbox(...args), + buySell: buySell(...args), + whatsNew: whatsNew(...args), + contacts: contacts(...args), + shapeShift: shapeShift(...args), + userCredentials: userCredentials(...args), + xlm: xlm(...args) }) diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagaRegister.js b/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagaRegister.js index 194aa8e2694..d23831ae065 100755 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagaRegister.js +++ b/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, networks }) => { - const kvStoreXlmSagas = sagas({ api, networks }) +export default (...args) => { + const kvStoreXlmSagas = sagas(...args) return function * coreKvStoreXlmSaga () { yield takeLatest(AT.FETCH_METADATA_XLM, kvStoreXlmSagas.fetchMetadataXlm) diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.js b/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.js index 836dce856c4..be78e9c25e8 100755 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.js @@ -5,34 +5,26 @@ import * as A from './actions' import { KVStoreEntry } from '../../../types' import { getMetadataXpriv } from '../root/selectors' import { derivationMap, XLM } from '../config' -import { getMnemonic } from '../../wallet/selectors' import { getKeyPair } from '../../../utils/xlm' import { callTask } from '../../../utils/functional' -export default ({ api, networks } = {}) => { +export default ({ api, networks, securityModule } = {}) => { const createXlm = function * ({ kv, password }) { - try { - const mnemonicT = yield select(getMnemonic, password) - const mnemonic = yield callTask(mnemonicT) - const keypair = getKeyPair(mnemonic) - const xlm = { - default_account_idx: 0, - accounts: [ - { - publicKey: keypair.publicKey(), - label: 'My Stellar Wallet', - archived: false - } - ], - tx_notes: {} - } - const newkv = set(KVStoreEntry.value, xlm, kv) - yield put(A.createMetadataXlm(newkv)) - } catch (e) { - throw new Error( - '[NOT IMPLEMENTED] MISSING_SECOND_PASSWORD in core.createXlm saga' - ) + const keypair = yield call(getKeyPair, securityModule, password) + + const xlm = { + default_account_idx: 0, + accounts: [ + { + publicKey: keypair.publicKey(), + label: 'My Stellar Wallet', + archived: false + } + ], + tx_notes: {} } + const newkv = set(KVStoreEntry.value, xlm, kv) + yield put(A.createMetadataXlm(newkv)) } const fetchMetadataXlm = function * (secondPasswordSagaEnhancer) { diff --git a/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.spec.js b/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.spec.js deleted file mode 100755 index e4ad69da46b..00000000000 --- a/packages/blockchain-wallet-v4/src/redux/kvStore/xlm/sagas.spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import { select } from 'redux-saga/effects' -import { expectSaga } from 'redux-saga-test-plan' -import Task from 'data.task' -import BIP39 from 'bip39' -import * as ed25519 from 'ed25519-hd-key' -import { set } from 'ramda-lens' - -import { getMnemonic } from '../../wallet/selectors' -import { KVStoreEntry } from '../../../types' -import { derivationMap, XLM } from '../config' -import * as A from './actions' -import sagas from './sagas' - -jest.spyOn(BIP39, 'mnemonicToSeed') -jest.spyOn(ed25519, 'derivePath') - -const TEST_DATA = [ - { - mnemonic: - 'illness spike retreat truth genius clock brain pass fit cave bargain toe', - seedHex: - 'e4a5a632e70943ae7f07659df1332160937fad82587216a4c64315a0fb39497ee4a01f76ddab4cba68147977f3a147b6ad584c41808e8238a07f6cc4b582f186', - publicKey: 'GDRXE2BQUC3AZNPVFSCEZ76NJ3WWL25FYFK6RGZGIEKWE4SOOHSUJUJ6', - secret: 'SBGWSG6BTNCKCOB3DIFBGCVMUPQFYPA2G4O34RMTB343OYPXU5DJDVMN' - }, - { - mnemonic: - 'resource asthma orphan phone ice canvas fire useful arch jewel impose vague theory cushion top', - seedHex: - '7b36d4e725b48695c3ffd2b4b317d5552cb157c1a26c46d36a05317f0d3053eb8b3b6496ba39ebd9312d10e3f9937b47a6790541e7c577da027a564862e92811', - publicKey: 'GAVXVW5MCK7Q66RIBWZZKZEDQTRXWCZUP4DIIFXCCENGW2P6W4OA34RH', - secret: 'SAKS7I2PNDBE5SJSUSU2XLJ7K5XJ3V3K4UDFAHMSBQYPOKE247VHAGDB' - }, - { - mnemonic: - 'bench hurt jump file august wise shallow faculty impulse spring exact slush thunder author capable act festival slice deposit sauce coconut afford frown better', - seedHex: - '937ae91f6ab6f12461d9936dfc1375ea5312d097f3f1eb6fed6a82fbe38c85824da8704389831482db0433e5f6c6c9700ff1946aa75ad8cc2654d6e40f567866', - publicKey: 'GC3MMSXBWHL6CPOAVERSJITX7BH76YU252WGLUOM5CJX3E7UCYZBTPJQ', - secret: 'SAEWIVK3VLNEJ3WEJRZXQGDAS5NVG2BYSYDFRSH4GKVTS5RXNVED5AX7' - }, - { - mnemonic: - 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', - seedHex: - '5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4', - publicKey: 'GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX', - secret: 'SBUV3MRWKNS6AYKZ6E6MOUVF2OYMON3MIUASWL3JLY5E3ISDJFELYBRZ' - } -] - -const api = {} -const { createXlm } = sagas({ api }) - -const password = 'pAssword1<>!' -const kv = KVStoreEntry.createEmpty(derivationMap[XLM]) - -describe('Create XLM', () => { - beforeEach(() => { - BIP39.mnemonicToSeed.mockClear() - ed25519.derivePath.mockClear() - }) - TEST_DATA.forEach((testData, index) => { - it(`Test data ${index}: should select mnemonic`, () => { - const xlm = { - default_account_idx: 0, - accounts: [ - { - publicKey: testData.publicKey, - label: 'My Stellar Wallet', - archived: false - } - ], - tx_notes: {} - } - const newkv = set(KVStoreEntry.value, xlm, kv) - - return expectSaga(createXlm, { kv, password }) - .provide([[select(getMnemonic, password), Task.of(testData.mnemonic)]]) - .put(A.createMetadataXlm(newkv)) - .run() - .then(() => { - expect(BIP39.mnemonicToSeed).toHaveBeenCalledTimes(1) - expect(BIP39.mnemonicToSeed).toHaveBeenCalledWith(testData.mnemonic) - expect(ed25519.derivePath).toHaveBeenCalledTimes(1) - expect(ed25519.derivePath).toHaveBeenCalledWith( - "m/44'/148'/0'", - testData.seedHex - ) - }) - }) - }) -}) diff --git a/packages/blockchain-wallet-v4/src/redux/payment/bch/sagas.js b/packages/blockchain-wallet-v4/src/redux/payment/bch/sagas.js index da154051f5c..dd79fc3da88 100755 --- a/packages/blockchain-wallet-v4/src/redux/payment/bch/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/payment/bch/sagas.js @@ -46,7 +46,7 @@ const fallbackFees = { priority: 4, regular: 4 } .chain().fee(myFee).amount(myAmount).done() */ -export default ({ api }) => { +export default ({ api, securityModule }) => { // /////////////////////////////////////////////////////////////////////////// const settingsSagas = settingsSagaFactory({ api }) const pushBchTx = futurizeP(Task)(api.pushBchTx) @@ -249,7 +249,14 @@ export default ({ api }) => { case ADDRESS_TYPES.ACCOUNT: return yield call(() => taskToPromise( - bch.signHDWallet(network, password, wrapper, selection, coinDust) + bch.signHDWallet( + securityModule, + network, + password, + wrapper, + selection, + coinDust + ) ) ) case ADDRESS_TYPES.LEGACY: diff --git a/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.js b/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.js index 02d91f0797a..9c0ed9126ab 100755 --- a/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.js @@ -44,7 +44,7 @@ export const taskToPromise = t => .chain().fee(myFee).amount(myAmount).done() */ -export default ({ api }) => { +export default ({ api, securityModule }) => { const settingsSagas = settingsSagaFactory({ api }) const __pushBtcTx = futurizeP(Task)(api.pushBtcTx) const __getWalletUnspent = (network, fromData) => @@ -218,6 +218,7 @@ export default ({ api }) => { } const __calculateSignature = function * ( + securityModule, network, password, transport, @@ -233,7 +234,9 @@ export default ({ api }) => { switch (fromType) { case ADDRESS_TYPES.ACCOUNT: return yield call(() => - taskToPromise(btc.signHDWallet(network, password, wrapper, selection)) + taskToPromise( + btc.signHDWallet(securityModule, network, password, wrapper, selection) + ) ) case ADDRESS_TYPES.LEGACY: return yield call(() => @@ -326,6 +329,7 @@ export default ({ api }) => { * sign (password, transport, scrambleKey) { let signed = yield call( __calculateSignature, + securityModule, network, password, transport, diff --git a/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.spec.js b/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.spec.js index 22c420b0c2a..2912df2e056 100755 --- a/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.spec.js +++ b/packages/blockchain-wallet-v4/src/redux/payment/btc/sagas.spec.js @@ -67,6 +67,8 @@ Coin.fromJS.mockImplementation(() => true) let api = { getBtcFees: () => feeResult } describe('createPayment', () => { + const securityModule = {} + let { create, __calculateTo, @@ -79,7 +81,7 @@ describe('createPayment', () => { __calculateSignature, __calculateSweepSelection, __getWalletUnspent - } = createPaymentFactory({ api }) + } = createPaymentFactory({ api, securityModule }) let payment = create({ network, payment: p }) describe('*init', () => { @@ -159,6 +161,7 @@ describe('createPayment', () => { expect(gen.next(PASSWORD_VALUE).value).toEqual( call( __calculateSignature, + securityModule, network, PASSWORD_VALUE, TRANSPORT_VALUE, @@ -187,6 +190,7 @@ describe('createPayment', () => { it('should follow the ADDRESS_TYPES.ACCOUNT case', () => { let WRAPPER_VALUE = {} let result = __calculateSignature( + securityModule, network, PASSWORD_VALUE, TRANSPORT_VALUE, @@ -201,6 +205,7 @@ describe('createPayment', () => { it('should follow the ADDRESS_TYPES.LEGACY case', () => { let WRAPPER_VALUE = {} let result = __calculateSignature( + securityModule, network, PASSWORD_VALUE, TRANSPORT_VALUE, @@ -214,6 +219,7 @@ describe('createPayment', () => { }) it('should follow the ADDRESS_TYPES.EXTERNAL case', () => { let result = __calculateSignature( + securityModule, network, PASSWORD_VALUE, TRANSPORT_VALUE, @@ -226,6 +232,7 @@ describe('createPayment', () => { }) it('should follow the ADDRESS_TYPES.WATCH_ONLY case', () => { let result = __calculateSignature( + securityModule, network, PASSWORD_VALUE, TRANSPORT_VALUE, diff --git a/packages/blockchain-wallet-v4/src/redux/payment/eth/sagas.js b/packages/blockchain-wallet-v4/src/redux/payment/eth/sagas.js index 319575e85e5..b2edb6373ab 100755 --- a/packages/blockchain-wallet-v4/src/redux/payment/eth/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/payment/eth/sagas.js @@ -37,7 +37,7 @@ const taskToPromise = t => .chain().amount(myAmount).done() */ -export default ({ api }) => { +export default ({ api, securityModule }) => { const settingsSagas = settingsSagaFactory({ api }) const selectIndex = function * (from) { const appState = yield select(identity) @@ -60,8 +60,8 @@ export default ({ api }) => { } const calculateSignature = function * ( + securityModule, network, - password, transport, scrambleKey, p @@ -69,9 +69,6 @@ export default ({ api }) => { switch (p.raw.fromType) { case ADDRESS_TYPES.ACCOUNT: { let sign - const appState = yield select(identity) - const mnemonicT = S.wallet.getMnemonic(appState, password) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) if (p.isErc20) { const contractAddress = (yield select( S.kvStore.eth.getErc20ContractAddr, @@ -79,10 +76,10 @@ export default ({ api }) => { )).getOrFail('missing_contract_addr') sign = data => taskToPromise( - eth.signErc20(network, mnemonic, data, contractAddress) + eth.signErc20(network, securityModule, data, contractAddress) ) } else { - sign = data => taskToPromise(eth.sign(network, mnemonic, data)) + sign = data => taskToPromise(eth.sign(network, securityModule, data)) } return yield call(sign, p.raw) } @@ -276,34 +273,24 @@ export default ({ api }) => { return makePayment(mergeRight(p, { raw })) }, - * sign (password, transport, scrambleKey) { - try { - const signed = yield call( - calculateSignature, - network, - password, - transport, - scrambleKey, - p - ) - return makePayment(mergeRight(p, { signed })) - } catch (e) { - throw new Error('missing_mnemonic') - } + * sign (transport, scrambleKey) { + const signed = yield call( + calculateSignature, + network, + transport, + scrambleKey, + p + ) + return makePayment(mergeRight(p, { signed })) }, - * signLegacy (password) { - try { - const appState = yield select(identity) - const seedHexT = S.wallet.getSeedHex(appState, password) - const seedHex = yield call(() => taskToPromise(seedHexT)) - const signLegacy = data => - taskToPromise(eth.signLegacy(network, seedHex, data)) - const signed = yield call(signLegacy, p.raw) - return makePayment(mergeRight(p, { signed })) - } catch (e) { - throw new Error('missing_seed_hex') - } + * signLegacy (secondPassword) { + const signLegacy = data => + taskToPromise( + eth.signLegacy(network, securityModule, secondPassword, data) + ) + const signed = yield call(signLegacy, p.raw) + return makePayment(mergeRight(p, { signed })) }, * publish () { diff --git a/packages/blockchain-wallet-v4/src/redux/payment/sagas.js b/packages/blockchain-wallet-v4/src/redux/payment/sagas.js index cc9c21be76b..968f95d3d56 100755 --- a/packages/blockchain-wallet-v4/src/redux/payment/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/payment/sagas.js @@ -3,9 +3,9 @@ import bch from './bch/sagas' import eth from './eth/sagas' import xlm from './xlm/sagas' -export default ({ api }) => ({ - btc: btc({ api }), - bch: bch({ api }), - eth: eth({ api }), - xlm: xlm({ api }) +export default (...args) => ({ + btc: btc(...args), + bch: bch(...args), + eth: eth(...args), + xlm: xlm(...args) }) diff --git a/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.js b/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.js index 5278b94d566..789866fb4bd 100755 --- a/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.js @@ -54,7 +54,7 @@ export const NO_TX_ERROR = 'No transaction' export const NO_SIGNED_ERROR = 'No signed tx' export const WRONG_MEMO_FORMAT = 'Bad memo' -export default ({ api }) => { +export default ({ api, securityModule }) => { const settingsSagas = settingsSagaFactory({ api }) // /////////////////////////////////////////////////////////////////////////// const calculateTo = destination => { @@ -66,7 +66,7 @@ export default ({ api }) => { } const calculateSignature = function * ( - password, + secondPassword, transaction, transport, scrambleKey, @@ -75,9 +75,12 @@ export default ({ api }) => { switch (fromType) { case ADDRESS_TYPES.ACCOUNT: if (!transaction) throw new Error(NO_TX_ERROR) - const mnemonicT = yield select(flip(S.wallet.getMnemonic)(password)) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) - return xlmSigner.sign({ transaction }, mnemonic) + + return yield call( + xlmSigner.sign, + { secondPassword, securityModule }, + transaction + ) case ADDRESS_TYPES.LOCKBOX: return yield call( xlmSigner.signWithLockbox, @@ -258,20 +261,16 @@ export default ({ api }) => { }, * sign (password, transport, scrambleKey) { - try { - const transaction = prop('transaction', p) - const signed = yield call( - calculateSignature, - password, - transaction, - transport, - scrambleKey, - path(['from', 'type'], p) - ) - return makePayment(merge(p, { signed })) - } catch (e) { - throw new Error('missing_mnemonic') - } + const transaction = prop('transaction', p) + const signed = yield call( + calculateSignature, + password, + transaction, + transport, + scrambleKey, + path(['from', 'type'], p) + ) + return makePayment(merge(p, { signed })) }, * publish () { diff --git a/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.spec.js b/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.spec.js index 72ffad22e72..aa8a466fd30 100755 --- a/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.spec.js +++ b/packages/blockchain-wallet-v4/src/redux/payment/xlm/sagas.spec.js @@ -99,7 +99,6 @@ S.data.xlm.getAccount.mockImplementation(id => () => { if (id === OTHER_ACCOUNT_ID) return Remote.of(STUB_OTHER_ACCOUNT) return null }) -S.wallet.getMnemonic.mockReturnValue(() => Task.of(STUB_MNEMONIC)) xlmSigner.sign.mockReturnValue(STUB_SIGNED_TX) @@ -425,8 +424,6 @@ describe.skip('payment', () => { payment = await expectSaga(payment.sign, STUB_PASSWORD) .run() .then(prop('returnValue')) - expect(S.wallet.getMnemonic).toHaveBeenCalledTimes(1) - expect(S.wallet.getMnemonic.mock.calls[0][1]).toBe(STUB_PASSWORD) expect(xlmSigner.sign).toHaveBeenCalledTimes(1) expect(xlmSigner.sign).toHaveBeenCalledWith( { transaction: STUB_TX }, diff --git a/packages/blockchain-wallet-v4/src/redux/rootSaga.js b/packages/blockchain-wallet-v4/src/redux/rootSaga.js index 27f860e56c0..1dab0b1374c 100755 --- a/packages/blockchain-wallet-v4/src/redux/rootSaga.js +++ b/packages/blockchain-wallet-v4/src/redux/rootSaga.js @@ -5,13 +5,13 @@ import walletOptions from './walletOptions/sagaRegister' import settings from './settings/sagaRegister' import wallet from './wallet/sagaRegister' -export default ({ api, networks, options }) => +export default (...args) => function * coreSaga () { yield all([ - fork(data({ api, options, networks })), - fork(kvStore({ api, networks })), - fork(walletOptions({ api, options })), - fork(settings({ api })), - fork(wallet({ api, networks })) + fork(data(...args)), + fork(kvStore(...args)), + fork(walletOptions(...args)), + fork(settings(...args)), + fork(wallet(...args)) ]) } diff --git a/packages/blockchain-wallet-v4/src/redux/sagas.js b/packages/blockchain-wallet-v4/src/redux/sagas.js index 23a574f8cc0..63a49a60e81 100755 --- a/packages/blockchain-wallet-v4/src/redux/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/sagas.js @@ -5,11 +5,11 @@ import walletOptions from './walletOptions/sagas' import kvStore from './kvStore/sagas' import payment from './payment/sagas' -export default ({ api, networks, options }) => ({ - data: data({ api, options, networks }), - settings: settings({ api }), - wallet: wallet({ api, networks }), - walletOptions: walletOptions({ api }), - kvStore: kvStore({ api, networks }), - payment: payment({ api, options }) +export default (...args) => ({ + data: data(...args), + settings: settings(...args), + wallet: wallet(...args), + walletOptions: walletOptions(...args), + kvStore: kvStore(...args), + payment: payment(...args) }) diff --git a/packages/blockchain-wallet-v4/src/redux/settings/sagaRegister.js b/packages/blockchain-wallet-v4/src/redux/settings/sagaRegister.js index 2bdb0b3fe4d..b06fa28a531 100755 --- a/packages/blockchain-wallet-v4/src/redux/settings/sagaRegister.js +++ b/packages/blockchain-wallet-v4/src/redux/settings/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api }) => { - const settingsSagas = sagas({ api }) +export default (...args) => { + const settingsSagas = sagas(...args) return function * coreSettingsSaga () { yield takeLatest(AT.FETCH_SETTINGS, settingsSagas.fetchSettings) diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/__snapshots__/walletReducers.spec.js.snap b/packages/blockchain-wallet-v4/src/redux/wallet/__snapshots__/walletReducers.spec.js.snap new file mode 100644 index 00000000000..381885be8ec --- /dev/null +++ b/packages/blockchain-wallet-v4/src/redux/wallet/__snapshots__/walletReducers.spec.js.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`reducers wallet should handle MERGE_WRAPPER 1`] = ` +Immutable.Map { + "sync_pubkeys": false, + "payload_checksum": "payload_checksum", + "storage_token": "storage_token", + "version": 3, + "language": "en", + "wallet": Immutable.Map { + "addresses": Immutable.Map { + "19XmKRY66VnUn5irHAafyoTfiwFuGLUxKF": Immutable.Map { + "addr": "19XmKRY66VnUn5irHAafyoTfiwFuGLUxKF", + "priv": "38W3vsxt246Lhawvz81y2HQVnsJrBUX87jSTUUnixPsE", + "tag": 0, + "label": "", + "created_time": 1492721419269, + "created_device_name": "javascript_web", + "created_device_version": "3.0", + }, + "14mQxLtEagsS8gYsdWJbzthFFuPDqDgtxQ": Immutable.Map { + "addr": "14mQxLtEagsS8gYsdWJbzthFFuPDqDgtxQ", + "priv": "BpD2ZuJjZ8PJPpDX6ZmsKFsXHkL7XV3dt385zghMfF6C", + "tag": 0, + "label": "labeled_imported", + "created_time": 1492721432222, + "created_device_name": "javascript_web", + "created_device_version": "3.0", + }, + "1JD73aGSdeqKUjJ4ntP4eCyUiuZ3ogJE1u": Immutable.Map { + "addr": "1JD73aGSdeqKUjJ4ntP4eCyUiuZ3ogJE1u", + "priv": null, + "tag": 0, + "label": "", + "created_time": 1492721461228, + "created_device_name": "javascript_web", + "created_device_version": "3.0", + }, + }, + "tx_notes": Immutable.Map {}, + "guid": "new guid", + "metadataHDNode": "xprv9tygGQP8be7uzNm5Czuy41juTK9pUKnWyZtDxgbmSEcCYa9VdvvtSknEyiKitqqm2TMv14NjXPQ68XLwSdH6Scc5GwXoZ31yRZZysxhVGU7", + "tx_names": Immutable.List [], + "double_encryption": false, + "address_book": Immutable.Map {}, + "hd_wallets": Immutable.List [ + Immutable.Map { + "seedHex": "6a4d9524d413fdf69ca1b5664d1d6db0", + "passphrase": "", + "mnemonic_verified": false, + "default_account_idx": 0, + "accounts": Immutable.List [ + Immutable.Map { + "label": "My Bitcoin Wallet", + "archived": false, + "xpriv": "xprv9yL1ousLjQQzGNBAYykaT8J3U626NV6zbLYkRv8rvUDpY4f1RnrvAXQneGXC9UNuNvGXX4j6oHBK5KiV2hKevRxY5ntis212oxjEL11ysuG", + "xpub": "xpub6CKNDRQEZmyHUrFdf1HapGEn27ramwpqxZUMEJYUUokoQrz9yLBAiKjGVWDuiCT39udj1r3whqQN89Tar5KrojH8oqSy7ytzJKW8gwmhwD3", + "address_labels": Immutable.Map { + "0": Immutable.Map { + "index": 0, + "label": "labeled_address", + }, + }, + "cache": Immutable.Map { + "receiveAccount": "xpub6F41z8MqNcJMvKQgAd5QE2QYo32cocYigWp1D8726ykMmaMqvtqLkvuL1NqGuUJvU3aWyJaV2J4V6sD7Pv59J3tYGZdYRSx8gU7EG8ZuPSY", + "changeAccount": "xpub6F41z8MqNcJMwmeUExdCv7UXvYBEgQB29SWq9jyxuZ7WefmSTWcwXB6NRAJkGCkB3L1Eu4ttzWnPVKZ6REissrQ4i6p8gTi9j5YwDLxmZ8p", + }, + "index": 0, + }, + ], + }, + ], + "sharedKey": "8a260b2b-5257-4357-ac56-7a7efca323ea", + "options": Immutable.Map { + "pbkdf2_iterations": 5000, + "fee_per_kb": 10000, + "html5_notifications": false, + "logout_time": 600000, + }, + }, + "war_checksum": "war_checksum", + "password": "password", + "pbkdf2_iterations": 5000, +} +`; diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/actionTypes.js b/packages/blockchain-wallet-v4/src/redux/wallet/actionTypes.js index 1e71c3b27bd..de157ae6deb 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/actionTypes.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/actionTypes.js @@ -1,4 +1,6 @@ // setters +export const MERGE_WRAPPER = '@CORE.MERGE_WRAPPER' +export const NEW_HDACCOUNT = '@CORE.NEW_HDACCOUNT' export const SET_WRAPPER = '@CORE.SET_WRAPPER' export const REFRESH_WRAPPER = '@CORE.REFRESH_WRAPPER' export const SET_MAIN_PASSWORD = '@CORE.SET_MAIN_PASSWORD' diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/actions.js b/packages/blockchain-wallet-v4/src/redux/wallet/actions.js index 624d77e3563..24afa32a1fa 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/actions.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/actions.js @@ -1,6 +1,14 @@ import * as T from './actionTypes' // setters + +export const mergeWrapper = payload => ({ type: T.MERGE_WRAPPER, payload }) + +export const newHdAccount = payload => ({ + type: T.NEW_HDACCOUNT, + payload +}) + export const setWrapper = payload => ({ type: T.SET_WRAPPER, payload: payload diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/reducers.js b/packages/blockchain-wallet-v4/src/redux/wallet/reducers.js index e432a1e65e3..358b0926555 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/reducers.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/reducers.js @@ -11,6 +11,11 @@ export const WRAPPER_INITIAL_STATE = Wrapper.fromJS( export const wrapperReducer = (state = WRAPPER_INITIAL_STATE, action) => { const { type } = action switch (type) { + // Merge wrapper from the Main Process. + case T.MERGE_WRAPPER: { + const redactedWrapper = Wrapper.redact(action.payload) + return state.mergeDeep(redactedWrapper) + } case T.SET_PAYLOAD_CHECKSUM: { const checksum = action.payload return set(Wrapper.payloadChecksum, checksum, state) diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/sagaRegister.js b/packages/blockchain-wallet-v4/src/redux/wallet/sagaRegister.js index 876401fb00d..35a86e7e0da 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/sagaRegister.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/sagaRegister.js @@ -1,11 +1,12 @@ -import { takeLatest } from 'redux-saga/effects' +import { takeEvery, takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, networks }) => { - const walletSagas = sagas({ api, networks }) +export default (...args) => { + const walletSagas = sagas(...args) return function * coreWalletSaga () { + yield takeEvery(AT.NEW_HDACCOUNT, walletSagas.newHDAccount) yield takeLatest(AT.SET_DEFAULT_ACCOUNT, walletSagas.refetchContextData) yield takeLatest(AT.SET_ADDRESS_ARCHIVED, walletSagas.refetchContextData) yield takeLatest(AT.SET_ACCOUNT_ARCHIVED, walletSagas.refetchContextData) diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/sagas.js b/packages/blockchain-wallet-v4/src/redux/wallet/sagas.js index 786ea5c1459..9a449b42e46 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/sagas.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/sagas.js @@ -30,7 +30,7 @@ import { generateMnemonic } from '../../walletCrypto' const taskToPromise = t => new Promise((resolve, reject) => t.fork(reject, resolve)) -export default ({ api, networks }) => { +export default ({ api, networks, securityModule }) => { const runTask = function * (task, setActionCreator) { let result = yield call( compose( @@ -77,10 +77,22 @@ export default ({ api, networks }) => { } const newHDAccount = function * ({ label, password }) { + const hdWallet = yield select(S.getDefaultHDWallet) + const index = hdWallet.accounts.size + + const key = yield call( + securityModule.deriveBIP32Key, + { + network: networks.btc, + secondPassword: password + }, + `m/44'/0'/${index}'` + ) + let wrapper = yield select(S.getWrapper) let nextWrapper = Wrapper.traverseWallet( Task.of, - Wallet.newHDAccount(label, password, networks.btc), + Wallet.newHDAccount(index, key, label, password), wrapper ) yield call(runTask, nextWrapper, A.wallet.setWrapper) @@ -132,14 +144,10 @@ export default ({ api, networks }) => { if (isEmpty(hdwallets)) { let mnemonic = yield call(generateMnemonic, api) - let upgradeWallet = Wallet.upgradeToHd( - mnemonic, - 'My Bitcoin Wallet', - password, - networks.btc - ) + const upgradeWallet = Wallet.newHDWallet(mnemonic, password) let nextWrapper = Wrapper.traverseWallet(Task.of, upgradeWallet, wrapper) yield call(runTask, nextWrapper, A.wallet.setWrapper) + yield put(A.wallet.newHdAccount({ label: `My Bitcoin Wallet`, password })) } else { throw new Error('Already an HD wallet') } diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/walletReducers.spec.js b/packages/blockchain-wallet-v4/src/redux/wallet/walletReducers.spec.js index 18c048cfe29..fc520a947dd 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/walletReducers.spec.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/walletReducers.spec.js @@ -1,3 +1,4 @@ +import { fromJS } from 'immutable' import { compose } from 'ramda' import { Wrapper, Wallet, AddressMap } from '../../types' import walletReducer from './reducers.js' @@ -20,6 +21,13 @@ describe('reducers', () => { describe('wallet', () => { const wrapped = Wrapper.fromJS(wrap(walletFixture)) + it('should handle MERGE_WRAPPER', () => { + const payload = fromJS({ wallet: { guid: `new guid` } }) + const action = Actions.mergeWrapper(payload) + const next = walletReducer(wrapped, action) + expect(next).toMatchSnapshot() + }) + it('should handle SET_WRAPPER', () => { let action = Actions.setWrapper(wrapped) let next = walletReducer(void 0, action) diff --git a/packages/blockchain-wallet-v4/src/redux/walletSync/middleware.js b/packages/blockchain-wallet-v4/src/redux/walletSync/middleware.js index e4cd9215e80..703b4d3cad9 100755 --- a/packages/blockchain-wallet-v4/src/redux/walletSync/middleware.js +++ b/packages/blockchain-wallet-v4/src/redux/walletSync/middleware.js @@ -101,7 +101,8 @@ export const getWalletAddresses = async (state, api) => { */ const walletSync = ({ isAuthenticated, - api + api, + mergeWrapper = false } = {}) => store => next => action => { const prevState = store.getState() const prevWallet = selectors.wallet.getWrapper(prevState) @@ -164,7 +165,11 @@ const walletSync = ({ action.type !== T.wallet.SET_PAYLOAD_CHECKSUM && action.type !== T.wallet.REFRESH_WRAPPER && prevWallet !== nextWallet: - sync() + if (mergeWrapper) { + store.dispatch(A.wallet.mergeWrapper(nextWallet)) + } else { + sync() + } break default: break diff --git a/packages/blockchain-wallet-v4/src/remote/index.js b/packages/blockchain-wallet-v4/src/remote/index.js index 845f7bcb156..5f45170cc5e 100755 --- a/packages/blockchain-wallet-v4/src/remote/index.js +++ b/packages/blockchain-wallet-v4/src/remote/index.js @@ -1,3 +1,5 @@ +// https://medium.com/@jaumepernas/your-data-is-loading-1425c6b76bf0 + import { taggedSum } from 'daggy' const Remote = taggedSum('Remote', { diff --git a/packages/blockchain-wallet-v4/src/signer/bch.js b/packages/blockchain-wallet-v4/src/signer/bch.js index bf558e8ceb2..29852202a14 100755 --- a/packages/blockchain-wallet-v4/src/signer/bch.js +++ b/packages/blockchain-wallet-v4/src/signer/bch.js @@ -50,10 +50,9 @@ export const sortSelection = selection => ({ outputs: Coin.bip69SortOutputs(selection.outputs) }) -// signHDWallet :: network -> password -> wrapper -> selection -> Task selection export const signHDWallet = curry( - (network, secondPassword, wrapper, selection, coinDust) => - addHDWalletWIFS(network, secondPassword, wrapper, selection).map( + (securityModule, network, secondPassword, wrapper, selection, coinDust) => + addHDWalletWIFS(securityModule, network, secondPassword, wrapper, selection).map( signWithWIF(network, coinDust) ) ) diff --git a/packages/blockchain-wallet-v4/src/signer/bsv.js b/packages/blockchain-wallet-v4/src/signer/bsv.js index e4ea42f82d3..38f5cc38d24 100644 --- a/packages/blockchain-wallet-v4/src/signer/bsv.js +++ b/packages/blockchain-wallet-v4/src/signer/bsv.js @@ -43,10 +43,9 @@ export const sortSelection = selection => ({ outputs: Coin.bip69SortOutputs(selection.outputs) }) -// signHDWallet :: network -> password -> wrapper -> selection -> Task selection export const signHDWallet = curry( - (network, secondPassword, wrapper, selection, coinDust) => - addHDWalletWIFS(network, secondPassword, wrapper, selection).map( + (securityModule, network, secondPassword, wrapper, selection, coinDust) => + addHDWalletWIFS(securityModule, network, secondPassword, wrapper, selection).map( signWithWIF(network, coinDust) ) ) diff --git a/packages/blockchain-wallet-v4/src/signer/btc.js b/packages/blockchain-wallet-v4/src/signer/btc.js index 745f7f43c41..ce8567bf170 100755 --- a/packages/blockchain-wallet-v4/src/signer/btc.js +++ b/packages/blockchain-wallet-v4/src/signer/btc.js @@ -37,10 +37,9 @@ export const sortSelection = selection => ({ outputs: Coin.bip69SortOutputs(selection.outputs) }) -// signHDWallet :: network -> password -> wrapper -> selection -> Task selection export const signHDWallet = curry( - (network, secondPassword, wrapper, selection) => - addHDWalletWIFS(network, secondPassword, wrapper, selection).map( + (securityModule, network, secondPassword, wrapper, selection) => + addHDWalletWIFS(securityModule, network, secondPassword, wrapper, selection).map( signWithWIF(network) ) ) diff --git a/packages/blockchain-wallet-v4/src/signer/eth.js b/packages/blockchain-wallet-v4/src/signer/eth.js index ac312205866..69638042caf 100755 --- a/packages/blockchain-wallet-v4/src/signer/eth.js +++ b/packages/blockchain-wallet-v4/src/signer/eth.js @@ -1,6 +1,8 @@ import BigNumber from 'bignumber.js' import EthereumTx from 'ethereumjs-tx' import EthereumAbi from 'ethereumjs-abi' + +import { returnTask } from '../utils/functional' import * as eth from '../utils/eth' import Task from 'data.task' import { curry } from 'ramda' @@ -13,9 +15,9 @@ const toHex = value => { } export const signErc20 = curry( - (network = 1, mnemonic, data, contractAddress) => { + (network = 1, securityModule, data, contractAddress) => { const { index, to, amount, nonce, gasPrice, gasLimit } = data - const privateKey = eth.getPrivateKey(mnemonic, index) + const privateKey = eth.getPrivateKey(securityModule, index) const transferMethodHex = '0xa9059cbb' const txParams = { to: contractAddress, @@ -36,9 +38,9 @@ export const signErc20 = curry( } ) -export const sign = curry((network = 1, mnemonic, data) => { +export const sign = curry((network = 1, securityModule, data) => { const { index, to, amount, nonce, gasPrice, gasLimit } = data - const privateKey = eth.getPrivateKey(mnemonic, index) + const privateKey = eth.getPrivateKey(securityModule, index) const txParams = { to, nonce: toHex(nonce), @@ -95,20 +97,25 @@ export const serialize = (network, raw, signature) => { return '0x' + tx.serialize().toString('hex') } -export const signLegacy = curry((network = 1, seedHex, data) => { - const { index, to, amount, nonce, gasPrice, gasLimit } = data - const privateKey = eth.getLegacyPrivateKey(seedHex, index) - const txParams = { - to, - nonce: toHex(nonce), - gasPrice: toHex(gasPrice), - gasLimit: toHex(gasLimit), - value: toHex(amount), - chainId: network || 1 - } +export const signLegacy = curry( + returnTask(async (network = 1, securityModule, secondPassword, data) => { + const { to, amount, nonce, gasPrice, gasLimit } = data - const tx = new EthereumTx(txParams) - tx.sign(privateKey) - const rawTx = '0x' + tx.serialize().toString('hex') - return Task.of(rawTx) -}) + const privateKey = await securityModule.deriveLegacyEthereumKey({ + secondPassword + }) + + const txParams = { + to, + nonce: toHex(nonce), + gasPrice: toHex(gasPrice), + gasLimit: toHex(gasLimit), + value: toHex(amount), + chainId: network || 1 + } + + const tx = new EthereumTx(txParams) + tx.sign(privateKey) + return '0x' + tx.serialize().toString('hex') + }) +) diff --git a/packages/blockchain-wallet-v4/src/signer/wifs.js b/packages/blockchain-wallet-v4/src/signer/wifs.js index e345c5ab4da..fb3cc4af141 100755 --- a/packages/blockchain-wallet-v4/src/signer/wifs.js +++ b/packages/blockchain-wallet-v4/src/signer/wifs.js @@ -4,12 +4,17 @@ import Task from 'data.task' import { Wrapper, Wallet } from '../types' import * as Coin from '../coinSelection/coin' -// addHDWalletWIFS :: network -> password -> wrapper -> selection -> Task selection export const addHDWalletWIFS = curry( - (network, secondPassword, wrapper, selection) => { + (securityModule, network, secondPassword, wrapper, selection) => { const wallet = Wrapper.selectWallet(wrapper) const deriveKey = coin => - Wallet.getHDPrivateKeyWIF(coin.path, secondPassword, network, wallet) + Wallet.getHDPrivateKeyWIF( + securityModule, + coin.path, + secondPassword, + network, + wallet + ) // .map(wif => Bitcoin.ECPair.fromWIF(wif, network)) .map(wif => set(Coin.priv, wif, coin)) const selectionWithKeys = traverseOf( diff --git a/packages/blockchain-wallet-v4/src/signer/xlm.js b/packages/blockchain-wallet-v4/src/signer/xlm.js index 4afd7c9d13e..c2d7c1206f5 100755 --- a/packages/blockchain-wallet-v4/src/signer/xlm.js +++ b/packages/blockchain-wallet-v4/src/signer/xlm.js @@ -2,8 +2,8 @@ import { getKeyPair } from '../utils/xlm' import * as StellarSdk from 'stellar-sdk' import Str from '@ledgerhq/hw-app-str' -export const sign = ({ transaction }, mnemonic) => { - const keyPair = getKeyPair(mnemonic) +export const sign = async ({ secondPassword, securityModule }, transaction) => { + const keyPair = await getKeyPair({ secondPassword, securityModule }) transaction.sign(keyPair) return transaction } diff --git a/packages/blockchain-wallet-v4/src/types/HDWallet.js b/packages/blockchain-wallet-v4/src/types/HDWallet.js index d1581d73a57..91c5f350e99 100755 --- a/packages/blockchain-wallet-v4/src/types/HDWallet.js +++ b/packages/blockchain-wallet-v4/src/types/HDWallet.js @@ -3,7 +3,6 @@ import { pipe, compose, curry, is, range, map } from 'ramda' import { view, over, traverseOf, traversed } from 'ramda-lens' import Bitcoin from 'bitcoinjs-lib' import BIP39 from 'bip39' -import * as crypto from '../walletCrypto' import Task from 'data.task' import Type from './Type' @@ -75,19 +74,10 @@ export const reviver = jsObject => { return new HDWallet(jsObject) } -export const deriveAccountNodeAtIndex = (seedHex, index, network) => { - let seed = BIP39.mnemonicToSeed(BIP39.entropyToMnemonic(seedHex)) - let masterNode = Bitcoin.HDNode.fromSeedBuffer(seed, network) - return masterNode - .deriveHardened(44) - .deriveHardened(0) - .deriveHardened(index) -} - -export const generateAccount = curry((index, label, network, seedHex) => { - let node = deriveAccountNodeAtIndex(seedHex, index, network) +export const generateAccount = (key, label) => { + const node = Bitcoin.HDNode.fromBase58(key) return HDAccount.fromJS(HDAccount.js(label, node, null)) -}) +} // encrypt :: Number -> String -> String -> HDWallet -> Task Error HDWallet export const encrypt = curry((iterations, sharedKey, password, hdWallet) => { diff --git a/packages/blockchain-wallet-v4/src/types/HDWallet.spec.js b/packages/blockchain-wallet-v4/src/types/HDWallet.spec.js index 9a7c12541ec..5fb7a93265f 100755 --- a/packages/blockchain-wallet-v4/src/types/HDWallet.spec.js +++ b/packages/blockchain-wallet-v4/src/types/HDWallet.spec.js @@ -18,27 +18,28 @@ describe('HDWallet', () => { }) }) - // describe('createNew', () => { - // const { wallet, mnemonic } = walletNewFixture - // const hdWallet = HDWallet.createNew(mnemonic) - - // it('should generate the correct seed hex', () => { - // expect(hdWallet.seedHex).toEqual(wallet.hd_wallets[0].seed_hex) - // }) - - // it('should have the correct first account', () => { - // let firstAccount = HDWallet.toJS(hdWallet).accounts[0] - // let accountFixtureNoLabels = R.set(R.lensProp('address_labels'), [], hdWalletFixture.accounts[0]) - // expect(firstAccount).toEqual(accountFixtureNoLabels) - // }) - - // it('should optionally set the first account label', () => { - // let label = 'another label' - // let hdWalletLabelled = HDWallet.createNew(mnemonic, { label }) - // let firstAccount = HDWallet.toJS(hdWalletLabelled).accounts[0] - // expect(firstAccount.label).toEqual(label) - // }) - // }) + it(`generateAccount`, () => { + expect( + HDWallet.generateAccount( + `xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi`, + `label` + ).toJS() + ).toEqual({ + label: 'label', + archived: false, + xpriv: + 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', + xpub: + 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8', + address_labels: {}, + cache: { + receiveAccount: + 'xpub68Gmy5EVb2BdFbj2LpWrk1M7obNuaPTpT5oh9QCCo5sRfqSHVYWex97WpDZzszdzHzxXDAzPLVSwybe4uPYkSk4G3gnrPqqkV9RyNzAcNJ1', + changeAccount: + 'xpub68Gmy5EVb2BdHTYHpekwGdcbBWax19w9HwA2DaADYvuCSSgt4YAErxxSN1KWSnmyqkwRNbnTj3XiUBKmHeC8rTjLRPjSULcDKQQgfgJDppq' + } + }) + }) describe('serializer', () => { it('compose(reviver, replacer) should be identity', () => { diff --git a/packages/blockchain-wallet-v4/src/types/KVStoreEntry.js b/packages/blockchain-wallet-v4/src/types/KVStoreEntry.js index a9ecbfb6dae..adb6119e20b 100755 --- a/packages/blockchain-wallet-v4/src/types/KVStoreEntry.js +++ b/packages/blockchain-wallet-v4/src/types/KVStoreEntry.js @@ -69,12 +69,27 @@ export const fromCredentials = curry((guid, sharedKey, password, network) => { return fromKeys(key, enc) }) +export const fromEntropy = ({ entropy, network }) => { + const d = BigInteger.fromBuffer(entropy) + const key = new Bitcoin.ECPair(d, null, { network }) + const enc = key.d.toBuffer(32) + return fromKeys(key, enc) +} + export const getMasterHDNode = curry((network, seedHex) => { const mnemonic = BIP39.entropyToMnemonic(seedHex) const masterhex = BIP39.mnemonicToSeed(mnemonic) return Bitcoin.HDNode.fromSeedBuffer(masterhex, network) }) +// BIP 43 purpose needs to be 31 bit or less. For lack of a BIP number +// we take the first 31 bits of the SHA256 hash of a reverse domain. +export const metadataPurpose = + crypto + .sha256('info.blockchain.metadata') + .slice(0, 4) + .readUInt32BE(0) & 0x7fffffff // 510742 + export const deriveMetadataNode = masterHDNode => { // BIP 43 purpose needs to be 31 bit or less. For lack of a BIP number // we take the first 31 bits of the SHA256 hash of a reverse domain. @@ -100,11 +115,6 @@ export const fromMasterHDNode = curry((masterHDNode, typeId) => { return fromMetadataHDNode(metadataHDNode, typeId) }) -export const fromHdWallet = curry((hdWallet, typeId) => { - const masterHdNode = getMasterHDNode(hdWallet.seedHex) - return fromMasterHDNode(masterHdNode, typeId) -}) - export const encrypt = curry((key, data) => crypto.encryptDataWithKey(data, key, null) ) diff --git a/packages/blockchain-wallet-v4/src/types/Serializer.js b/packages/blockchain-wallet-v4/src/types/Serializer.js index ac58af897d9..b73baf1e62e 100755 --- a/packages/blockchain-wallet-v4/src/types/Serializer.js +++ b/packages/blockchain-wallet-v4/src/types/Serializer.js @@ -19,6 +19,17 @@ import Remote from '../remote' const serializer = { replacer: function (key, value) { + // Without this, jsan swallows the stack trace. + if (value instanceof Error) { + return { + __serializedType__: `Error`, + data: { + message: value.message, + stack: value.stack + } + } + } + // Remove all functions from the state if (value && typeof value === 'function') { return '' @@ -34,13 +45,17 @@ const serializer = { return value }, reviver: function (key, value) { - if ( + if (value && value.type === 'Buffer') { + return Buffer.from(value.data) + } else if ( typeof value === 'object' && value !== null && '__serializedType__' in value ) { var data = value.data switch (value.__serializedType__) { + case `Error`: + return Object.assign(new Error(data.message), { stack: data.stack }) case 'Wrapper': return Wrapper.reviver(data) case 'Wallet': diff --git a/packages/blockchain-wallet-v4/src/types/Wallet.js b/packages/blockchain-wallet-v4/src/types/Wallet.js index 489144cf36c..c7f37de200e 100755 --- a/packages/blockchain-wallet-v4/src/types/Wallet.js +++ b/packages/blockchain-wallet-v4/src/types/Wallet.js @@ -6,19 +6,9 @@ import Maybe from 'data.maybe' import Bitcoin from 'bitcoinjs-lib' import memoize from 'fast-memoize' import BIP39 from 'bip39' -import { - compose, - curry, - map, - is, - pipe, - __, - concat, - split, - isNil, - flip -} from 'ramda' +import { compose, concat, curry, map, is, pipe, __, split, isNil } from 'ramda' import { traversed, traverseOf, over, view, set } from 'ramda-lens' + import * as crypto from '../walletCrypto' import { shift, shiftIProp } from './util' import Type from './Type' @@ -273,15 +263,6 @@ export const importLegacyAddress = curry( } ) -// upgradeToHd :: String -> String -> String? -> Task Error Wallet -export const upgradeToHd = curry( - (mnemonic, firstLabel, password, network, wallet) => { - return newHDWallet(mnemonic, password, wallet).chain( - newHDAccount(firstLabel, password, network) - ) - } -) - // newHDWallet :: String -> String? -> Wallet -> Task Error Wallet export const newHDWallet = curry((mnemonic, password, wallet) => { let hdWallet = HDWallet.createNew(mnemonic) @@ -293,26 +274,19 @@ export const newHDWallet = curry((mnemonic, password, wallet) => { ) }) -// newHDAccount :: String -> String? -> Wallet -> Task Error Wallet -export const newHDAccount = curry((label, password, network, wallet) => { - let hdWallet = HDWalletList.selectHDWallet(selectHdWallets(wallet)) - let index = hdWallet.accounts.size +export const newHDAccount = curry((index, key, label, password, wallet) => { let appendAccount = curry((w, account) => { - let accountsLens = compose( + const accountsLens = compose( hdWallets, HDWalletList.hdwallet, HDWallet.accounts ) - let accountWithIndex = set(HDAccount.index, index, account) + + const accountWithIndex = set(HDAccount.index, index, account) return over(accountsLens, accounts => accounts.push(accountWithIndex), w) }) - return applyCipher( - wallet, - password, - flip(crypto.decryptSecPass), - hdWallet.seedHex - ) - .map(HDWallet.generateAccount(index, label, network)) + + return Task.of(HDWallet.generateAccount(key, label)) .chain(applyCipher(wallet, password, HDAccount.encrypt)) .map(appendAccount(wallet)) }) diff --git a/packages/blockchain-wallet-v4/src/types/Wallet.spec.js b/packages/blockchain-wallet-v4/src/types/Wallet.spec.js index 467d8b6e19f..a428ea739fe 100755 --- a/packages/blockchain-wallet-v4/src/types/Wallet.spec.js +++ b/packages/blockchain-wallet-v4/src/types/Wallet.spec.js @@ -1,6 +1,7 @@ import * as R from 'ramda' import { Address, Wallet, AddressMap, serializer } from './index' import * as crypto from '../walletCrypto/index' +import { taskToPromise } from '../utils/functional' const walletFixture = require('./__mocks__/wallet.v3') const walletFixtureSecpass = require('./__mocks__/wallet.v3-secpass') @@ -107,6 +108,19 @@ describe('Wallet', () => { }) }) + it(`newHDAccount`, async () => { + const index = 1 + const key = `xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi` + const label = `label` + const password = undefined + + expect( + (await taskToPromise( + Wallet.newHDAccount(index, key, label, password, wallet) + )).toJS() + ).toMatchSnapshot() + }) + describe('setAddressLabel', () => { it('should set a new address label', () => { let addr = '14mQxLtEagsS8gYsdWJbzthFFuPDqDgtxQ' @@ -178,14 +192,4 @@ describe('Wallet', () => { expect(string2).toEqual(string) }) }) - - // describe('createNew', () => { - // const { mnemonic } = walletNewFixture - - // it('should create a new wallet', () => { - // let { guid, sharedKey } = walletNewFixture.wallet - // let wallet = Wallet.createNew(guid, sharedKey, mnemonic) - // expect(Wallet.toJS(wallet)).toEqual(walletNewFixture.wallet) - // }) - // }) }) diff --git a/packages/blockchain-wallet-v4/src/types/Wrapper.js b/packages/blockchain-wallet-v4/src/types/Wrapper.js index 49c99196937..36e735bbd30 100755 --- a/packages/blockchain-wallet-v4/src/types/Wrapper.js +++ b/packages/blockchain-wallet-v4/src/types/Wrapper.js @@ -48,6 +48,17 @@ export const selectRealAuthType = view(realAuthType) export const selectWallet = view(wallet) export const selectSyncPubKeys = view(syncPubKeys) +const keyPaths = [[`password`], [`wallet`, `hd_wallets`, 0, `seedHex`]] + +// Remove some properties before transferring between the Security and Main +// Processes. +export const redact = wrapper => + wrapper.withMutations(mutable => { + keyPaths.forEach(keyPath => { + mutable.deleteIn(keyPath) + }) + }) + // traverseWallet :: Monad m => (a -> m a) -> (Wallet -> m Wallet) -> Wrapper export const traverseWallet = curry((of, f, wrapper) => of(wrapper).chain(traverseOf(wallet, of, f)) diff --git a/packages/blockchain-wallet-v4/src/types/Wrapper.spec.js b/packages/blockchain-wallet-v4/src/types/Wrapper.spec.js index 54a367aa496..accf27e8fb8 100755 --- a/packages/blockchain-wallet-v4/src/types/Wrapper.spec.js +++ b/packages/blockchain-wallet-v4/src/types/Wrapper.spec.js @@ -5,6 +5,10 @@ const wrapperFixture = require('./__mocks__/wrapper.v3') describe('Wrapper', () => { const myWrapper = Wrapper.fromJS(wrapperFixture) + it(`redact`, () => { + expect(Wrapper.redact(myWrapper)).toMatchSnapshot() + }) + describe('serializer', () => { it('compose(replacer, reviver) should be identity', () => { const string = JSON.stringify(myWrapper) diff --git a/packages/blockchain-wallet-v4/src/types/__snapshots__/Wallet.spec.js.snap b/packages/blockchain-wallet-v4/src/types/__snapshots__/Wallet.spec.js.snap new file mode 100644 index 00000000000..74e7c8a5a1c --- /dev/null +++ b/packages/blockchain-wallet-v4/src/types/__snapshots__/Wallet.spec.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Wallet newHDAccount 1`] = ` +Object { + "address_book": Object {}, + "addresses": Object { + "14mQxLtEagsS8gYsdWJbzthFFuPDqDgtxQ": Object { + "addr": "14mQxLtEagsS8gYsdWJbzthFFuPDqDgtxQ", + "created_device_name": "javascript_web", + "created_device_version": "3.0", + "created_time": 1492721432222, + "label": "labeled_imported", + "priv": "BpD2ZuJjZ8PJPpDX6ZmsKFsXHkL7XV3dt385zghMfF6C", + "tag": 0, + }, + "19XmKRY66VnUn5irHAafyoTfiwFuGLUxKF": Object { + "addr": "19XmKRY66VnUn5irHAafyoTfiwFuGLUxKF", + "created_device_name": "javascript_web", + "created_device_version": "3.0", + "created_time": 1492721419269, + "label": "", + "priv": "38W3vsxt246Lhawvz81y2HQVnsJrBUX87jSTUUnixPsE", + "tag": 0, + }, + "1JD73aGSdeqKUjJ4ntP4eCyUiuZ3ogJE1u": Object { + "addr": "1JD73aGSdeqKUjJ4ntP4eCyUiuZ3ogJE1u", + "created_device_name": "javascript_web", + "created_device_version": "3.0", + "created_time": 1492721461228, + "label": "", + "priv": null, + "tag": 0, + }, + }, + "double_encryption": false, + "guid": "50dae286-e42e-4d67-8419-d5dcc563746c", + "hd_wallets": Array [ + Object { + "accounts": Array [ + Object { + "address_labels": Object { + "0": Object { + "index": 0, + "label": "labeled_address", + }, + }, + "archived": false, + "cache": Object { + "changeAccount": "xpub6F41z8MqNcJMwmeUExdCv7UXvYBEgQB29SWq9jyxuZ7WefmSTWcwXB6NRAJkGCkB3L1Eu4ttzWnPVKZ6REissrQ4i6p8gTi9j5YwDLxmZ8p", + "receiveAccount": "xpub6F41z8MqNcJMvKQgAd5QE2QYo32cocYigWp1D8726ykMmaMqvtqLkvuL1NqGuUJvU3aWyJaV2J4V6sD7Pv59J3tYGZdYRSx8gU7EG8ZuPSY", + }, + "index": 0, + "label": "My Bitcoin Wallet", + "xpriv": "xprv9yL1ousLjQQzGNBAYykaT8J3U626NV6zbLYkRv8rvUDpY4f1RnrvAXQneGXC9UNuNvGXX4j6oHBK5KiV2hKevRxY5ntis212oxjEL11ysuG", + "xpub": "xpub6CKNDRQEZmyHUrFdf1HapGEn27ramwpqxZUMEJYUUokoQrz9yLBAiKjGVWDuiCT39udj1r3whqQN89Tar5KrojH8oqSy7ytzJKW8gwmhwD3", + }, + Object { + "address_labels": Object {}, + "archived": false, + "cache": Object { + "changeAccount": "xpub68Gmy5EVb2BdHTYHpekwGdcbBWax19w9HwA2DaADYvuCSSgt4YAErxxSN1KWSnmyqkwRNbnTj3XiUBKmHeC8rTjLRPjSULcDKQQgfgJDppq", + "receiveAccount": "xpub68Gmy5EVb2BdFbj2LpWrk1M7obNuaPTpT5oh9QCCo5sRfqSHVYWex97WpDZzszdzHzxXDAzPLVSwybe4uPYkSk4G3gnrPqqkV9RyNzAcNJ1", + }, + "index": 1, + "label": "label", + "xpriv": "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + "xpub": "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + }, + ], + "default_account_idx": 0, + "mnemonic_verified": false, + "passphrase": "", + "seedHex": "6a4d9524d413fdf69ca1b5664d1d6db0", + }, + ], + "metadataHDNode": "xprv9tygGQP8be7uzNm5Czuy41juTK9pUKnWyZtDxgbmSEcCYa9VdvvtSknEyiKitqqm2TMv14NjXPQ68XLwSdH6Scc5GwXoZ31yRZZysxhVGU7", + "options": Object { + "fee_per_kb": 10000, + "html5_notifications": false, + "logout_time": 600000, + "pbkdf2_iterations": 5000, + }, + "sharedKey": "8a260b2b-5257-4357-ac56-7a7efca323ea", + "tx_names": Array [], + "tx_notes": Object {}, +} +`; diff --git a/packages/blockchain-wallet-v4/src/types/__snapshots__/Wrapper.spec.js.snap b/packages/blockchain-wallet-v4/src/types/__snapshots__/Wrapper.spec.js.snap new file mode 100644 index 00000000000..583ed7ac6d2 --- /dev/null +++ b/packages/blockchain-wallet-v4/src/types/__snapshots__/Wrapper.spec.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Wrapper redact 1`] = ` +Immutable.Map { + "sync_pubkeys": false, + "payload_checksum": "mypayloadchecksum", + "storage_token": "mytoken", + "version": 3, + "language": "en", + "wallet": Immutable.Map { + "addresses": Immutable.Map { + "19XmKRY66VnUn5irHAafyoTfiwFuGLUxKF": Immutable.Map { + "addr": "19XmKRY66VnUn5irHAafyoTfiwFuGLUxKF", + "priv": "38W3vsxt246Lhawvz81y2HQVnsJrBUX87jSTUUnixPsE", + "tag": 0, + "label": "", + "created_time": 1492721419269, + "created_device_name": "javascript_web", + "created_device_version": "3.0", + }, + "14mQxLtEagsS8gYsdWJbzthFFuPDqDgtxQ": Immutable.Map { + "addr": "14mQxLtEagsS8gYsdWJbzthFFuPDqDgtxQ", + "priv": "BpD2ZuJjZ8PJPpDX6ZmsKFsXHkL7XV3dt385zghMfF6C", + "tag": 0, + "label": "labeled_imported", + "created_time": 1492721432222, + "created_device_name": "javascript_web", + "created_device_version": "3.0", + }, + "1JD73aGSdeqKUjJ4ntP4eCyUiuZ3ogJE1u": Immutable.Map { + "addr": "1JD73aGSdeqKUjJ4ntP4eCyUiuZ3ogJE1u", + "priv": null, + "tag": 0, + "label": "", + "created_time": 1492721461228, + "created_device_name": "javascript_web", + "created_device_version": "3.0", + }, + }, + "tx_notes": Immutable.Map {}, + "guid": "50dae286-e42e-4d67-8419-d5dcc563746c", + "metadataHDNode": "xprv9tygGQP8be7uzNm5Czuy41juTK9pUKnWyZtDxgbmSEcCYa9VdvvtSknEyiKitqqm2TMv14NjXPQ68XLwSdH6Scc5GwXoZ31yRZZysxhVGU7", + "tx_names": Immutable.List [], + "double_encryption": false, + "address_book": Immutable.Map {}, + "hd_wallets": Immutable.List [ + Immutable.Map { + "accounts": Immutable.List [ + Immutable.Map { + "label": "My Bitcoin Wallet", + "archived": false, + "xpriv": "xprv9yL1ousLjQQzGNBAYykaT8J3U626NV6zbLYkRv8rvUDpY4f1RnrvAXQneGXC9UNuNvGXX4j6oHBK5KiV2hKevRxY5ntis212oxjEL11ysuG", + "xpub": "xpub6CKNDRQEZmyHUrFdf1HapGEn27ramwpqxZUMEJYUUokoQrz9yLBAiKjGVWDuiCT39udj1r3whqQN89Tar5KrojH8oqSy7ytzJKW8gwmhwD3", + "address_labels": Immutable.Map { + "0": Immutable.Map { + "index": 0, + "label": "labeled_address", + }, + }, + "cache": Immutable.Map { + "receiveAccount": "xpub6F41z8MqNcJMvKQgAd5QE2QYo32cocYigWp1D8726ykMmaMqvtqLkvuL1NqGuUJvU3aWyJaV2J4V6sD7Pv59J3tYGZdYRSx8gU7EG8ZuPSY", + "changeAccount": "xpub6F41z8MqNcJMwmeUExdCv7UXvYBEgQB29SWq9jyxuZ7WefmSTWcwXB6NRAJkGCkB3L1Eu4ttzWnPVKZ6REissrQ4i6p8gTi9j5YwDLxmZ8p", + }, + "index": 0, + }, + ], + "passphrase": "", + "mnemonic_verified": false, + "default_account_idx": 0, + }, + ], + "sharedKey": "8a260b2b-5257-4357-ac56-7a7efca323ea", + "options": Immutable.Map { + "pbkdf2_iterations": 5000, + "fee_per_kb": 10000, + "html5_notifications": false, + "logout_time": 600000, + }, + }, + "war_checksum": "mychecksum", + "pbkdf2_iterations": "", +} +`; diff --git a/packages/blockchain-wallet-v4/src/utils/eth.js b/packages/blockchain-wallet-v4/src/utils/eth.js index 72a5567e509..a8f4bedd8c1 100755 --- a/packages/blockchain-wallet-v4/src/utils/eth.js +++ b/packages/blockchain-wallet-v4/src/utils/eth.js @@ -1,7 +1,5 @@ import * as Exchange from '../exchange' import { prop, path } from 'ramda' -import BIP39 from 'bip39' -import Bitcoin from 'bitcoinjs-lib' import EthHd from 'ethereumjs-wallet/hdkey' import EthUtil from 'ethereumjs-util' import BigNumber from 'bignumber.js' @@ -11,43 +9,26 @@ import BigNumber from 'bignumber.js' */ export const isValidAddress = address => /^0x[a-fA-F0-9]{40}$/.test(address) -/** - * @param {string} mnemonic - * @param {integer} index - */ -export const getPrivateKey = (mnemonic, index) => { - const seed = BIP39.mnemonicToSeed(mnemonic) - const account = Bitcoin.HDNode.fromSeedBuffer(seed) - .deriveHardened(44) - .deriveHardened(60) - .deriveHardened(0) - .derive(0) - .derive(index) - .toBase58() - return EthHd.fromExtendedKey(account) - .getWallet() - .getPrivateKey() -} +export const getPrivateKey = async ( + { deriveBIP32Key }, + secondPassword, + index +) => { + const key = await deriveBIP32Key( + { secondPassword }, + `m/44'/60'/0'/0/${index}` + ) -// Derivation error using seedHex directly instead of seed derived from mnemonic derived from seedHex -export const getLegacyPrivateKey = seedHex => { - return deriveChildLegacy(0, seedHex) + return EthHd.fromExtendedKey(key) .getWallet() .getPrivateKey() } -const deriveChildLegacy = (index, seed) => { - const derivationPath = "m/44'/60'/0'/0" - return EthHd.fromMasterSeed(seed) - .derivePath(derivationPath) - .deriveChild(index) -} - export const privateKeyToAddress = pk => EthUtil.toChecksumAddress(EthUtil.privateToAddress(pk).toString('hex')) -export const deriveAddress = (mnemonic, index) => - privateKeyToAddress(getPrivateKey(mnemonic, index)) +export const deriveAddress = async (...args) => + privateKeyToAddress(await getPrivateKey(...args)) export const deriveAddressFromXpub = xpub => { const ethPublic = EthHd.fromExtendedKey(xpub) diff --git a/packages/blockchain-wallet-v4/src/utils/functional.js b/packages/blockchain-wallet-v4/src/utils/functional.js index e2516ce8370..6d4b9595f06 100755 --- a/packages/blockchain-wallet-v4/src/utils/functional.js +++ b/packages/blockchain-wallet-v4/src/utils/functional.js @@ -1,12 +1,23 @@ +import Task from 'data.task' import { compose } from 'ramda' import { call } from 'redux-saga/effects' // Used as default value for functions export const noop = () => {} -const taskToPromise = t => +export const promiseToTask = promise => + new Task((reject, resolve) => promise.then(resolve, reject)) + +export const taskToPromise = t => new Promise((resolve, reject) => t.fork(reject, resolve)) +// Transform a function that returns a promise into one that returns a Task. +export const returnTask = func => { + const newFunction = (...args) => promiseToTask(func(...args)) + Object.defineProperty(newFunction, `length`, { value: func.length }) + return newFunction +} + export const callTask = function * (task) { return yield call( compose( diff --git a/packages/blockchain-wallet-v4/src/utils/xlm.js b/packages/blockchain-wallet-v4/src/utils/xlm.js index 820dadc079e..6b1afddb9d1 100755 --- a/packages/blockchain-wallet-v4/src/utils/xlm.js +++ b/packages/blockchain-wallet-v4/src/utils/xlm.js @@ -2,8 +2,6 @@ import { BigNumber } from 'bignumber.js' import * as StellarSdk from 'stellar-sdk' import queryString from 'query-string' import { assoc } from 'ramda' -import BIP39 from 'bip39' -import * as ed25519 from 'ed25519-hd-key' export const calculateEffectiveBalance = (balance, reserve, fee) => new BigNumber(balance) @@ -48,9 +46,14 @@ export const decodeXlmURI = uri => { return { address: destination, amount, memo, note: msg } } -export const getKeyPair = mnemonic => { - const seed = BIP39.mnemonicToSeed(mnemonic) - const seedHex = seed.toString('hex') - const masterKey = ed25519.derivePath("m/44'/148'/0'", seedHex) +export const getKeyPair = async ({ + secondPassword, + securityModule: { deriveSLIP10ed25519Key } +}) => { + const masterKey = await deriveSLIP10ed25519Key( + { secondPassword }, + `m/44'/148'/0'` + ) + return StellarSdk.Keypair.fromRawEd25519Seed(masterKey.key) } diff --git a/packages/main-process/babel.config.js b/packages/main-process/babel.config.js index 7b2631c7211..e339b510b49 100644 --- a/packages/main-process/babel.config.js +++ b/packages/main-process/babel.config.js @@ -1,11 +1,21 @@ +const path = require(`path`) + +const resolve = directory => path.resolve(__dirname, directory) + module.exports = { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: [ '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', 'babel-plugin-styled-components', - ['module-resolver', { root: ['./src'], alias: { data: './src/data' } }], - ['react-intl', { messagesDir: './build/extractedMessages' }] + [ + 'module-resolver', + { + root: [resolve('src')], + alias: { data: resolve('src/data') } + } + ], + ['react-intl', { messagesDir: resolve('build/extractedMessages') }] ], ignore: [], env: { @@ -18,8 +28,14 @@ module.exports = { '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', 'babel-plugin-styled-components', - ['module-resolver', { root: ['./src'], alias: { data: './src/data' } }], - ['react-intl', { messagesDir: './build/extractedMessages' }] + [ + 'module-resolver', + { + root: [resolve('src')], + alias: { data: resolve('src/data') } + } + ], + ['react-intl', { messagesDir: resolve('build/extractedMessages') }] ] }, development: { @@ -31,7 +47,13 @@ module.exports = { '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', 'babel-plugin-styled-components', - ['module-resolver', { root: ['./src'], alias: { data: './src/data' } }], + [ + 'module-resolver', + { + root: [resolve('src')], + alias: { data: resolve('src/data') } + } + ], 'react-hot-loader/babel' ] } diff --git a/packages/main-process/package.json b/packages/main-process/package.json index d304a44ea17..ceb8e92c95e 100644 --- a/packages/main-process/package.json +++ b/packages/main-process/package.json @@ -1,5 +1,5 @@ { - "name": "blockchain-wallet-v4-frontend", + "name": "main-process", "version": "0.1.0", "description": "Frontend wallet application.", "license": "AGPL-3.0-or-later", @@ -9,23 +9,11 @@ }, "main": "index.js", "scripts": { - "analyze": "cross-env-shell ANALYZE=true NODE_ENV=production webpack-cli --config webpack.config.ci.js", - "build:dev": "cross-env-shell NODE_ENV=development webpack-cli --config webpack.config.dev.js --progress --colors", - "build:prod": "cross-env-shell NODE_ENV=production webpack-cli --config webpack.config.dev.js --progress --colors", - "build:staging": "cross-env-shell NODE_ENV=staging webpack-cli --config webpack.config.dev.js --progress --colors", - "build:testnet": "cross-env-shell NODE_ENV=testnet webpack-cli --config webpack.config.dev.js --progress --colors", "ci:coverage:frontend": "yarn coverage --runInBand", "ci:test:frontend": "yarn test --runInBand", - "ci:compile": "cross-env-shell NODE_ENV=production webpack-cli --config webpack.config.ci.js --display-error-details", "clean": "cross-env rimraf node_modules && rimraf build", "coverage": "cross-env ./../../node_modules/.bin/jest --coverage", - "debug:prod": "cross-env-shell NODE_ENV=production webpack-dev-server --config webpack.debug.js --progress --colors", "link:resolved:paths": "ln -sf $(pwd)/src/** ./node_modules && ln -sf $(pwd)/../../packages/** ./node_modules", - "manage:translations": "yarn build:prod && node ./translationRunner.js", - "start:dev": "cross-env-shell NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", - "start:prod": "cross-env-shell DISABLE_SSL=true NODE_ENV=production webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", - "start:staging": "cross-env-shell NODE_ENV=staging webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", - "start:testnet": "cross-env-shell NODE_ENV=testnet webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", "test": "cross-env ./../../node_modules/.bin/jest --silent", "test:build": "echo 'No precomplilation required for tests to execute.'", "test:debug": "cross-env node --inspect-brk ./../../node_modules/.bin/jest --runInBand", @@ -143,6 +131,7 @@ "rxjs": "6.5.2", "sanitize-html": "1.20.1", "styled-components": "4.2.0", + "web-microkernel": "1.0.0", "zxcvbn": "4.4.2" } } diff --git a/packages/main-process/src/IPC/Middleware.js b/packages/main-process/src/IPC/Middleware.js new file mode 100644 index 00000000000..26a7189715c --- /dev/null +++ b/packages/main-process/src/IPC/Middleware.js @@ -0,0 +1,54 @@ +import * as router from 'connected-react-router' + +import * as coreTypes from 'blockchain-wallet-v4/src/redux/actionTypes' +import * as types from '../data/actionTypes' + +const alreadyForwarded = ({ meta }) => meta && meta.forwarded + +const dispatchToSecurityProcess = ({ securityProcess }, action) => { + securityProcess.dispatch(action) +} + +const dispatchToRootProcess = ({ rootProcessDispatch }, action) => { + rootProcessDispatch(action) +} + +const ROOT_LOCATION_CHANGE = ({ rootProcessDispatch }, { payload }) => { + rootProcessDispatch({ type: router.LOCATION_CHANGE, payload }) +} + +const handlers = { + // Security Center needs settings. + [coreTypes.settings.FETCH_SETTINGS_FAILURE]: dispatchToSecurityProcess, + [coreTypes.settings.FETCH_SETTINGS_LOADING]: dispatchToSecurityProcess, + [coreTypes.settings.FETCH_SETTINGS_SUCCESS]: dispatchToSecurityProcess, + + // Tell the Security Process to merge our wrapper with its own. + [coreTypes.wallet.MERGE_WRAPPER]: dispatchToSecurityProcess, + + // Report a location change to the Root Process instead of processing it + // ourselves. + ROOT_LOCATION_CHANGE, + + // Inform the Root Process about routing changes so that it can switch the + // appropriate process to the foreground. + [router.LOCATION_CHANGE]: dispatchToRootProcess, + + // Tell the Security Process to reload itself when we do. + [types.auth.LOGOUT]: dispatchToSecurityProcess +} + +export default ({ imports }) => () => next => action => { + const { type } = action + + if (!alreadyForwarded(action)) { + if (type in handlers) { + handlers[type](imports, action) + } else if (type.startsWith(`@DATA.PREFERENCES.`)) { + // The Security Process handles persistence for preferences. + dispatchToSecurityProcess(imports, action) + } + } + + return next(action) +} diff --git a/packages/main-process/src/IPC/index.js b/packages/main-process/src/IPC/index.js new file mode 100644 index 00000000000..a043739b6c4 --- /dev/null +++ b/packages/main-process/src/IPC/index.js @@ -0,0 +1,28 @@ +import { serializer } from 'blockchain-wallet-v4/src/types' +import Middleware from './Middleware' +import * as kernel from 'web-microkernel' + +export default configureStore => () => + new Promise(async resolve => { + const exportedFunction = async imports => { + const middleware = Middleware({ imports }) + const root = await configureStore({ imports, middleware }) + + const dispatch = action => { + root.store.dispatch({ + ...action, + meta: { ...action.meta, forwarded: true } + }) + } + + resolve(root) + return { dispatch } + } + + const connection = await kernel.ChildProcess( + { reviver: serializer.reviver }, + exportedFunction + ) + + connection.addEventListener(`error`, console.error) + }) diff --git a/packages/main-process/src/components/TransactionListItem/__snapshots__/index.spec.js.snap b/packages/main-process/src/components/TransactionListItem/__snapshots__/index.spec.js.snap index bdf8f699027..a00c843b01d 100644 --- a/packages/main-process/src/components/TransactionListItem/__snapshots__/index.spec.js.snap +++ b/packages/main-process/src/components/TransactionListItem/__snapshots__/index.spec.js.snap @@ -170,6 +170,7 @@ exports[`ListItemContainer renders correctly 1`] = ` "deleteHdAddressLabel": [Function], "deleteLegacyAddress": [Function], "deleteWrapper": [Function], + "mergeWrapper": [Function], "refreshWrapper": [Function], "setAccountArchived": [Function], "setAccountLabel": [Function], diff --git a/packages/main-process/src/data/analytics/sagas.js b/packages/main-process/src/data/analytics/sagas.js index feafb079173..a875b91dcaf 100644 --- a/packages/main-process/src/data/analytics/sagas.js +++ b/packages/main-process/src/data/analytics/sagas.js @@ -44,18 +44,7 @@ export default ({ api }) => { } const generateUniqueUserID = function * () { - const defaultHDWallet = yield select( - selectors.core.wallet.getDefaultHDWallet - ) - const userId = yield call(waitForUserId) - if (userId) return userId - const { seedHex } = defaultHDWallet - const mnemonic = BIP39.entropyToMnemonic(seedHex) - const masterhex = BIP39.mnemonicToSeed(mnemonic) - const masterHDNode = Bitcoin.HDNode.fromSeedBuffer(masterhex) - let hash = crypto.sha256('info.blockchain.matomo') - let purpose = hash.slice(0, 4).readUInt32BE(0) & 0x7fffffff - return masterHDNode.deriveHardened(purpose).getAddress() + return yield call(waitForUserId) || `` } const initUserSession = function * () { diff --git a/packages/main-process/src/data/auth/actionTypes.js b/packages/main-process/src/data/auth/actionTypes.js index 0de985229bb..14e453788a5 100644 --- a/packages/main-process/src/data/auth/actionTypes.js +++ b/packages/main-process/src/data/auth/actionTypes.js @@ -3,6 +3,7 @@ export const DEAUTHORIZE_BROWSER = 'DEAUTHORIZE_BROWSER' export const LOGIN = 'LOGIN' export const LOGIN_FAILURE = 'LOGIN_FAILURE' export const LOGIN_LOADING = 'LOGIN_LOADING' +export const LOGIN_ROUTINE = 'LOGIN_ROUTINE' export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' export const LOGOUT = 'LOGOUT' export const LOGOUT_CLEAR_REDUX_STORE = 'LOGOUT_CLEAR_REDUX_STORE' diff --git a/packages/main-process/src/data/auth/actions.js b/packages/main-process/src/data/auth/actions.js index 63600f6a11a..f1845c62e56 100644 --- a/packages/main-process/src/data/auth/actions.js +++ b/packages/main-process/src/data/auth/actions.js @@ -7,6 +7,12 @@ export const login = (guid, password, code, sharedKey, mobileLogin) => ({ payload: { guid, password, code, sharedKey, mobileLogin } }) export const loginLoading = () => ({ type: AT.LOGIN_LOADING }) + +export const loginRoutine = (mobileLogin = false, firstLogin = false) => ({ + type: AT.LOGIN_ROUTINE, + payload: { firstLogin, mobileLogin } +}) + export const loginSuccess = () => ({ type: AT.LOGIN_SUCCESS, payload: {} }) export const loginFailure = err => ({ type: AT.LOGIN_FAILURE, diff --git a/packages/main-process/src/data/auth/sagaRegister.js b/packages/main-process/src/data/auth/sagaRegister.js index 7bff80754c1..6d1e80ba8dc 100644 --- a/packages/main-process/src/data/auth/sagaRegister.js +++ b/packages/main-process/src/data/auth/sagaRegister.js @@ -2,12 +2,13 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, coreSagas }) => { - const authSagas = sagas({ api, coreSagas }) +export default (...args) => { + const authSagas = sagas(...args) return function * authSaga () { yield takeLatest(AT.DEAUTHORIZE_BROWSER, authSagas.deauthorizeBrowser) yield takeLatest(AT.LOGIN, authSagas.login) + yield takeLatest(AT.LOGIN_ROUTINE, authSagas.loginRoutineSaga) yield takeLatest(AT.LOGOUT, authSagas.logout) yield takeLatest( AT.LOGOUT_CLEAR_REDUX_STORE, diff --git a/packages/main-process/src/data/auth/sagas.js b/packages/main-process/src/data/auth/sagas.js index cde0d1b4948..717fb471036 100644 --- a/packages/main-process/src/data/auth/sagas.js +++ b/packages/main-process/src/data/auth/sagas.js @@ -109,14 +109,8 @@ export default ({ api, coreSagas }) => { )).getOrElse(false) if (userFlowSupported) yield put(actions.modules.profile.signIn()) } - const loginRoutineSaga = function * (mobileLogin, firstLogin) { + const loginRoutineSaga = function * ({ payload: { mobileLogin, firstLogin } }) { try { - // If needed, the user should upgrade its wallet before being able to open the wallet - const isHdWallet = yield select(selectors.core.wallet.isHdWallet) - if (!isHdWallet) { - yield call(upgradeWalletSaga) - } - yield put(actions.auth.authenticate()) yield call(coreSagas.kvStore.root.fetchRoot, askSecondPasswordEnhancer) // If there was no eth metadata kv store entry, we need to create one and that requires the second password. yield call( @@ -141,9 +135,6 @@ export default ({ api, coreSagas }) => { const guid = yield select(selectors.core.wallet.getGuid) // store guid in cache for future login yield put(actions.cache.guidEntered(guid)) - // reset auth type and clear previous login form state - yield put(actions.auth.setAuthType(0)) - yield put(actions.form.destroy('login')) // set payload language to settings language const language = yield select(selectors.preferences.getLanguage) yield put(actions.modules.settings.updateLanguage(language)) @@ -224,7 +215,7 @@ export default ({ api, coreSagas }) => { password, code }) - yield call(loginRoutineSaga, mobileLogin) + yield put(actions.auth.loginRoutine(mobileLogin)) } catch (error) { const initialError = prop('initial_error', error) const authRequired = prop('authorization_required', error) @@ -247,7 +238,7 @@ export default ({ api, coreSagas }) => { session, password }) - yield call(loginRoutineSaga, mobileLogin) + yield put(actions.auth.loginRoutine(mobileLogin)) } catch (error) { if (error && error.auth_type > 0) { yield put(actions.auth.setAuthType(error.auth_type)) @@ -333,7 +324,7 @@ export default ({ api, coreSagas }) => { yield put(actions.auth.registerLoading()) yield call(coreSagas.wallet.createWalletSaga, action.payload) yield put(actions.alerts.displaySuccess(C.REGISTER_SUCCESS)) - yield call(loginRoutineSaga, false, true) + yield put(actions.auth.loginRoutine(false, true)) yield put(actions.auth.registerSuccess()) } catch (e) { yield put(actions.auth.registerFailure()) @@ -347,7 +338,7 @@ export default ({ api, coreSagas }) => { yield put(actions.alerts.displayInfo(C.RESTORE_WALLET_INFO)) yield call(coreSagas.wallet.restoreWalletSaga, action.payload) yield put(actions.alerts.displaySuccess(C.RESTORE_SUCCESS)) - yield call(loginRoutineSaga, false, true) + yield put(actions.auth.loginRoutine(false, true)) yield put(actions.auth.restoreSuccess()) } catch (e) { yield put(actions.auth.restoreFailure()) diff --git a/packages/main-process/src/data/auth/sagas.spec.js b/packages/main-process/src/data/auth/sagas.spec.js index a5dedcd54f8..3497b89edb9 100644 --- a/packages/main-process/src/data/auth/sagas.spec.js +++ b/packages/main-process/src/data/auth/sagas.spec.js @@ -48,7 +48,7 @@ describe('authSagas', () => { }) describe('login flow', () => { - const { login, loginRoutineSaga, pollingSession } = authSagas({ + const { login, pollingSession } = authSagas({ api, coreSagas }) @@ -93,11 +93,11 @@ describe('authSagas', () => { }) }) - it('should call login routine', () => { + it('should put login routine', () => { const { mobileLogin } = payload saga .next() - .call(loginRoutineSaga, mobileLogin) + .put(actions.auth.loginRoutine(mobileLogin)) .next() .isDone() }) @@ -185,9 +185,9 @@ describe('authSagas', () => { }) }) - it('should call login routine', () => { + it('should put login routine', () => { const { mobileLogin } = payload - saga.next().call(loginRoutineSaga, mobileLogin) + saga.next().put(actions.auth.loginRoutine(mobileLogin)) }) it('should follow 2FA flow on auth error', () => { @@ -352,7 +352,11 @@ describe('authSagas', () => { }) const mobileLogin = true const firstLogin = false - const saga = testSaga(loginRoutineSaga, mobileLogin, firstLogin) + + const saga = testSaga(loginRoutineSaga, { + payload: { mobileLogin, firstLogin } + }) + const beforeHdCheck = 'beforeHdCheck' it('should check if wallet is an hd wallet', () => { @@ -369,13 +373,9 @@ describe('authSagas', () => { .restore(beforeHdCheck) }) - it('should put authenticate action', () => { - saga.next(true).put(actions.auth.authenticate()) - }) - it('should fetch root', () => { saga - .next() + .next(true) .call(coreSagas.kvStore.root.fetchRoot, askSecondPasswordEnhancer) }) @@ -444,14 +444,6 @@ describe('authSagas', () => { saga.next(guid).put(actions.cache.guidEntered(guid)) }) - it('should reset auth state', () => { - saga.next().put(actions.auth.setAuthType(0)) - }) - - it('should clear login form', () => { - saga.next().put(actions.form.destroy('login')) - }) - it('should select current language', () => { saga.next().select(selectors.preferences.getLanguage) }) @@ -492,7 +484,10 @@ describe('authSagas', () => { it("should not display success if it's first login", () => { const firstLogin = true - return expectSaga(loginRoutineSaga, mobileLogin, firstLogin) + + return expectSaga(loginRoutineSaga, { + payload: { mobileLogin, firstLogin } + }) .provide([ // Every async or value returning yield has to be mocked // for saga to progress @@ -527,7 +522,7 @@ describe('authSagas', () => { }) describe('register flow', () => { - const { loginRoutineSaga, register } = authSagas({ + const { register } = authSagas({ api, coreSagas }) @@ -550,10 +545,10 @@ describe('authSagas', () => { saga.next().put(actions.alerts.displaySuccess(C.REGISTER_SUCCESS)) }) - it('should call login routine saga with falsy mobileLogin and truthy firstLogin', () => { + it('should put login routine action with falsy mobileLogin and truthy firstLogin', () => { const mobileLogin = false const firstLogin = true - saga.next().call(loginRoutineSaga, mobileLogin, firstLogin) + saga.next().put(actions.auth.loginRoutine(mobileLogin, firstLogin)) }) it('should finally trigger action that restore is successful', () => { @@ -586,7 +581,7 @@ describe('authSagas', () => { }) describe('restore flow', () => { - const { loginRoutineSaga, restore } = authSagas({ + const { restore } = authSagas({ api, coreSagas }) @@ -615,10 +610,10 @@ describe('authSagas', () => { saga.next().put(actions.alerts.displaySuccess(C.RESTORE_SUCCESS)) }) - it('should call login routine saga with falsy mobileLogin and truthy firstLogin', () => { + it('should put login routine action with falsy mobileLogin and truthy firstLogin', () => { const mobileLogin = false const firstLogin = true - saga.next().call(loginRoutineSaga, mobileLogin, firstLogin) + saga.next().put(actions.auth.loginRoutine(mobileLogin, firstLogin)) }) it('should finally trigger action that restore is successful', () => { diff --git a/packages/main-process/src/data/components/importBtcAddress/sagaRegister.js b/packages/main-process/src/data/components/importBtcAddress/sagaRegister.js index 1e44b52fd4a..c4fbfe71692 100644 --- a/packages/main-process/src/data/components/importBtcAddress/sagaRegister.js +++ b/packages/main-process/src/data/components/importBtcAddress/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, coreSagas, networks }) => { - const importBtcAddressSagas = sagas({ api, coreSagas, networks }) +export default (...args) => { + const importBtcAddressSagas = sagas(...args) return function * importBtcAddressSaga () { yield takeLatest( diff --git a/packages/main-process/src/data/components/sagaRegister.js b/packages/main-process/src/data/components/sagaRegister.js index 3fb331706d6..bbc8f65793b 100644 --- a/packages/main-process/src/data/components/sagaRegister.js +++ b/packages/main-process/src/data/components/sagaRegister.js @@ -30,36 +30,36 @@ import transactionReport from './transactionReport/sagaRegister' import uploadDocuments from './uploadDocuments/sagaRegister' import veriff from './veriff/sagaRegister' -export default ({ api, coreSagas, networks }) => +export default (...args) => function * componentsSaga () { yield fork(activityList()) yield fork(bchTransactions()) yield fork(btcTransactions()) - yield fork(coinify({ api, coreSagas, networks })) + yield fork(coinify(...args)) yield fork(ethTransactions()) yield fork(xlmTransactions()) - yield fork(exchange({ api, coreSagas, networks })) - yield fork(exchangeHistory({ api, coreSagas })) - yield fork(identityVerification({ api, coreSagas })) - yield fork(lockbox({ api, coreSagas })) - yield fork(importBtcAddress({ api, coreSagas, networks })) - yield fork(manageAddresses({ api, networks })) + yield fork(exchange(...args)) + yield fork(exchangeHistory(...args)) + yield fork(identityVerification(...args)) + yield fork(lockbox(...args)) + yield fork(importBtcAddress(...args)) + yield fork(manageAddresses(...args)) yield fork(onboarding()) - yield fork(onfido({ api, coreSagas })) - yield fork(priceChart({ coreSagas })) - yield fork(priceTicker({ coreSagas })) + yield fork(onfido(...args)) + yield fork(priceChart(...args)) + yield fork(priceTicker(...args)) yield fork(refresh()) - yield fork(requestBtc({ networks })) - yield fork(requestBch({ networks })) - yield fork(requestEth({ networks })) + yield fork(requestBtc(...args)) + yield fork(requestBch(...args)) + yield fork(requestEth(...args)) yield fork(requestXlm()) - yield fork(sendBch({ coreSagas, networks })) - yield fork(sendBtc({ coreSagas, networks })) - yield fork(sendEth({ api, coreSagas, networks })) - yield fork(sendXlm({ api, coreSagas })) - yield fork(settings({ coreSagas })) - yield fork(signMessage({ coreSagas })) - yield fork(transactionReport({ coreSagas })) - yield fork(uploadDocuments({ api })) - yield fork(veriff({ api, coreSagas })) + yield fork(sendBch(...args)) + yield fork(sendBtc(...args)) + yield fork(sendEth(...args)) + yield fork(sendXlm(...args)) + yield fork(settings(...args)) + yield fork(signMessage(...args)) + yield fork(transactionReport(...args)) + yield fork(uploadDocuments(...args)) + yield fork(veriff(...args)) } diff --git a/packages/main-process/src/data/components/sagas.js b/packages/main-process/src/data/components/sagas.js index 76bb3c9021e..7ab7b7d8a10 100644 --- a/packages/main-process/src/data/components/sagas.js +++ b/packages/main-process/src/data/components/sagas.js @@ -28,7 +28,7 @@ import transactionReport from './transactionReport/sagas' import uploadDocuments from './uploadDocuments/sagas' import veriff from './veriff/sagas' -export default ({ api, coreSagas, networks }) => ({ +export default ({ api, coreSagas, imports, networks }) => ({ activityList: activityList(), bchTransactions: bchTransactions(), btcTransactions: btcTransactions(), @@ -38,7 +38,7 @@ export default ({ api, coreSagas, networks }) => ({ exchange: exchange({ api, coreSagas, networks }), exchangeHistory: exchangeHistory({ api, coreSagas }), identityVerification: identityVerification({ api, coreSagas }), - importBtcAddress: importBtcAddress({ api, coreSagas, networks }), + importBtcAddress: importBtcAddress({ api, coreSagas, imports, networks }), manageAddresses: manageAddresses({ api, networks }), onboarding: onboarding(), onfido: onfido({ api }), diff --git a/packages/main-process/src/data/modules/sagaRegister.js b/packages/main-process/src/data/modules/sagaRegister.js index d4d3986916f..47079cd578b 100644 --- a/packages/main-process/src/data/modules/sagaRegister.js +++ b/packages/main-process/src/data/modules/sagaRegister.js @@ -7,13 +7,13 @@ import securityCenter from './securityCenter/sagaRegister' import transferEth from './transferEth/sagaRegister' import sfox from './sfox/sagaRegister' -export default ({ api, coreSagas, networks }) => +export default (...args) => function * modulesSaga () { - yield fork(addressesBch({ coreSagas, networks })) - yield fork(profile({ api, coreSagas, networks })) - yield fork(rates({ api })) - yield fork(settings({ api, coreSagas })) - yield fork(securityCenter({ coreSagas })) - yield fork(transferEth({ coreSagas, networks })) - yield fork(sfox({ api, coreSagas, networks })) + yield fork(addressesBch(...args)) + yield fork(profile(...args)) + yield fork(rates(...args)) + yield fork(settings(...args)) + yield fork(securityCenter(...args)) + yield fork(transferEth(...args)) + yield fork(sfox(...args)) } diff --git a/packages/main-process/src/data/modules/sagas.js b/packages/main-process/src/data/modules/sagas.js index 00b1640f76d..ad3d01b8bbf 100644 --- a/packages/main-process/src/data/modules/sagas.js +++ b/packages/main-process/src/data/modules/sagas.js @@ -6,12 +6,12 @@ import securityCenter from './securityCenter/sagas' import transferEth from './transferEth/sagas' import sfox from './sfox/sagas' -export default ({ api, coreSagas, networks }) => ({ - addressesBch: addressesBch({ coreSagas }), - profile: profile({ api, coreSagas, networks }), - rates: rates({ api }), - settings: settings({ api, coreSagas }), - securityCenter: securityCenter({ coreSagas }), - transferEth: transferEth({ coreSagas, networks }), - sfox: sfox({ api, coreSagas }) +export default (...args) => ({ + addressesBch: addressesBch(...args), + profile: profile(...args), + rates: rates(...args), + settings: settings(...args), + securityCenter: securityCenter(...args), + transferEth: transferEth(...args), + sfox: sfox(...args) }) diff --git a/packages/main-process/src/data/modules/settings/sagaRegister.js b/packages/main-process/src/data/modules/settings/sagaRegister.js index 7c093289516..3bea440ef08 100644 --- a/packages/main-process/src/data/modules/settings/sagaRegister.js +++ b/packages/main-process/src/data/modules/settings/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, coreSagas }) => { - const settingsSagas = sagas({ api, coreSagas }) +export default (...args) => { + const settingsSagas = sagas(...args) return function * settingsModuleSaga () { yield takeLatest(AT.INIT_SETTINGS_INFO, settingsSagas.initSettingsInfo) @@ -11,7 +11,6 @@ export default ({ api, coreSagas }) => { AT.INIT_SETTINGS_PREFERENCES, settingsSagas.initSettingsPreferences ) - yield takeLatest(AT.SHOW_BACKUP_RECOVERY, settingsSagas.showBackupRecovery) yield takeLatest( AT.SHOW_GOOGLE_AUTHENTICATOR_SECRET_URL, settingsSagas.showGoogleAuthenticatorSecretUrl diff --git a/packages/main-process/src/data/modules/settings/sagas.js b/packages/main-process/src/data/modules/settings/sagas.js index 325a4145031..4b9f89dfcb6 100644 --- a/packages/main-process/src/data/modules/settings/sagas.js +++ b/packages/main-process/src/data/modules/settings/sagas.js @@ -3,7 +3,6 @@ import profileSagas from 'data/modules/profile/sagas' import * as actions from '../../actions' import * as selectors from '../../selectors' import * as C from 'services/AlertService' -import { addLanguageToUrl } from 'services/LocalesService' import { askSecondPasswordEnhancer, promptForSecondPassword @@ -19,7 +18,7 @@ export const ipRestrictionError = export const logLocation = 'modules/settings/sagas' -export default ({ api, coreSagas }) => { +export default ({ api, coreSagas, imports, securityModule }) => { const { syncUserWithWallet } = profileSagas({ api, coreSagas @@ -45,26 +44,6 @@ export default ({ api, coreSagas }) => { } } - const recoverySaga = function * ({ password }) { - const getMnemonic = s => selectors.core.wallet.getMnemonic(s, password) - try { - const mnemonicT = yield select(getMnemonic) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) - const mnemonicArray = mnemonic.split(' ') - yield put( - actions.modules.settings.addMnemonic({ mnemonic: mnemonicArray }) - ) - } catch (e) { - yield put( - actions.logs.logErrorMessage(logLocation, 'showBackupRecovery', e) - ) - } - } - - const showBackupRecovery = function * () { - yield call(askSecondPasswordEnhancer(recoverySaga), {}) - } - const showGoogleAuthenticatorSecretUrl = function * () { try { const googleAuthenticatorSecretUrl = yield call( @@ -142,7 +121,7 @@ export default ({ api, coreSagas }) => { const updateLanguage = function * (action) { try { yield call(coreSagas.settings.setLanguage, action.payload) - addLanguageToUrl(action.payload.language) + imports.addLanguageToUrl(action.payload.language) } catch (e) { yield put(actions.logs.logErrorMessage(logLocation, 'updateLanguage', e)) } @@ -327,18 +306,18 @@ export default ({ api, coreSagas }) => { try { const password = yield call(promptForSecondPassword) if (isLegacy) { - const getSeedHex = state => - selectors.core.wallet.getSeedHex(state, password) - const seedHexT = yield select(getSeedHex) - const seedHex = yield call(() => taskToPromise(seedHexT)) - const legPriv = utils.eth.getLegacyPrivateKey(seedHex).toString('hex') + const legPriv = utils.eth + .getLegacyPrivateKey(securityModule) + .toString('hex') yield put(actions.modules.settings.addShownEthPrivateKey(legPriv)) } else { - const getMnemonic = state => - selectors.core.wallet.getMnemonic(state, password) - const mnemonicT = yield select(getMnemonic) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) - let priv = utils.eth.getPrivateKey(mnemonic, 0).toString('hex') + let priv = (yield call( + utils.eth.getPrivateKey, + securityModule, + password, + 0 + )).toString('hex') + yield put(actions.modules.settings.addShownEthPrivateKey(priv)) } } catch (e) { @@ -351,11 +330,7 @@ export default ({ api, coreSagas }) => { const showXlmPrivateKey = function * () { try { const password = yield call(promptForSecondPassword) - const getMnemonic = state => - selectors.core.wallet.getMnemonic(state, password) - const mnemonicT = yield select(getMnemonic) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) - const keyPair = utils.xlm.getKeyPair(mnemonic) + const keyPair = yield call(utils.xlm.getKeyPair, securityModule, password) yield put( actions.modules.settings.addShownXlmPrivateKey(keyPair.secret()) ) @@ -369,7 +344,6 @@ export default ({ api, coreSagas }) => { return { initSettingsInfo, initSettingsPreferences, - showBackupRecovery, showGoogleAuthenticatorSecretUrl, updateMobile, resendMobile, @@ -387,7 +361,6 @@ export default ({ api, coreSagas }) => { enableTwoStepGoogleAuthenticator, enableTwoStepYubikey, newHDAccount, - recoverySaga, showBtcPrivateKey, showEthPrivateKey, showXlmPrivateKey diff --git a/packages/main-process/src/data/modules/settings/sagas.spec.js b/packages/main-process/src/data/modules/settings/sagas.spec.js index 5f8393ffdee..b975a6cee0e 100644 --- a/packages/main-process/src/data/modules/settings/sagas.spec.js +++ b/packages/main-process/src/data/modules/settings/sagas.spec.js @@ -201,11 +201,6 @@ describe('settingsSagas', () => { saga.next().call(coreSagas.settings.setLanguage, action.payload) }) - it('should add the language to the url', () => { - saga.next() - expect(contains(action.payload.language, window.location.href)).toBe(true) - }) - describe('error handling', () => { const error = new Error('ERROR') it('should log the error', () => { @@ -674,21 +669,4 @@ describe('settingsSagas', () => { saga.next(MOCK_PASSWORD).select(selectors.core.wallet.getWallet) }) }) - - describe('showEthPrivateKey', () => { - const getMnemonic = () => jest.fn() - const { showEthPrivateKey } = settingsSagas({ coreSagas }) - - let action = { payload: { isLegacy: false } } - - it('should get the mnemonic', () => { - return expectSaga(showEthPrivateKey, action) - .provide([ - [matchers.call.fn(promptForSecondPassword), 'password'], - [select(getMnemonic), 'mnemonicT'], - [matchers.call.fn(() => taskToPromise), 'mnemonic'] - ]) - .run() - }) - }) }) diff --git a/packages/main-process/src/data/preferences/sagaRegister.js b/packages/main-process/src/data/preferences/sagaRegister.js index 4810f281be6..2c1035ebbf0 100644 --- a/packages/main-process/src/data/preferences/sagaRegister.js +++ b/packages/main-process/src/data/preferences/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default () => { - const preferencesSagas = sagas() +export default ({ imports }) => { + const preferencesSagas = sagas({ imports }) return function * preferencesSaga () { yield takeLatest(AT.SET_LANGUAGE, preferencesSagas.setLanguage) diff --git a/packages/main-process/src/data/preferences/sagas.js b/packages/main-process/src/data/preferences/sagas.js index 611eb8d13c5..3b8de2a470b 100644 --- a/packages/main-process/src/data/preferences/sagas.js +++ b/packages/main-process/src/data/preferences/sagas.js @@ -3,13 +3,13 @@ import * as actions from '../actions.js' import * as C from 'services/AlertService' import { addLanguageToUrl } from 'services/LocalesService' -export default () => { +export default ({ imports }) => { const logLocation = 'preferences/sagas' const setLanguage = function * (action) { const { language, showAlert } = action.payload try { - addLanguageToUrl(language) + imports.addLanguageToUrl(language) if (showAlert) { yield put(actions.alerts.displaySuccess(C.LANGUAGE_UPDATE_SUCCESS)) } diff --git a/packages/main-process/src/data/rootSaga.js b/packages/main-process/src/data/rootSaga.js index 49b086cfd5a..d75470860fc 100644 --- a/packages/main-process/src/data/rootSaga.js +++ b/packages/main-process/src/data/rootSaga.js @@ -1,6 +1,5 @@ -import { all, call, delay, fork, put } from 'redux-saga/effects' +import { all, fork } from 'redux-saga/effects' import { coreSagasFactory, coreRootSagaFactory } from 'blockchain-wallet-v4/src' -import * as actions from './actions' import alerts from './alerts/sagaRegister' import analytics from './analytics/sagaRegister' import auth from './auth/sagaRegister' @@ -11,71 +10,21 @@ import preferences from './preferences/sagaRegister' import goals from './goals/sagaRegister' import router from './router/sagaRegister' import wallet from './wallet/sagaRegister' -import { tryParseLanguageFromUrl } from 'services/LocalesService' -const logLocation = 'data/rootSaga' - -const welcomeSaga = function * () { - try { - const version = APP_VERSION - const style1 = 'background: #F00; color: #FFF; font-size: 24px;' - const style2 = 'font-size: 18px;' - /* eslint-disable */ - console.log('=======================================================') - console.log(`%c Wallet version ${version}`, style2) - console.log('=======================================================') - console.log('%c STOP!!', style1) - console.log('%c This browser feature is intended for developers.', style2) - console.log('%c If someone told you to copy-paste something here,', style2) - console.log( - '%c it is a scam and will give them access to your money!', - style2 - ) - /* eslint-enable */ - } catch (e) { - yield put(actions.logs.logErrorMessage(logLocation, 'welcomeSaga', e)) - } -} - -const languageInitSaga = function * () { - try { - yield delay(250) - const lang = tryParseLanguageFromUrl() - if (lang.language) { - yield put(actions.preferences.setLanguage(lang.language, false)) - if (lang.cultureCode) { - yield put(actions.preferences.setCulture(lang.cultureCode)) - } - } - } catch (e) { - yield put(actions.logs.logErrorMessage(logLocation, 'languageInitSaga', e)) - } -} - -export default function * rootSaga ({ - api, - bchSocket, - btcSocket, - ethSocket, - ratesSocket, - networks, - options -}) { - const coreSagas = coreSagasFactory({ api, networks, options }) +export default function * rootSaga (args) { + const coreSagas = coreSagasFactory(args) yield all([ - call(welcomeSaga), fork(alerts), - fork(analytics({ api })), - fork(auth({ api, coreSagas })), - fork(components({ api, coreSagas, networks, options })), - fork(modules({ api, coreSagas, networks })), - fork(preferences()), - fork(goals({ api })), - fork(wallet({ coreSagas })), - fork(middleware({ api, bchSocket, btcSocket, ethSocket, ratesSocket })), - fork(coreRootSagaFactory({ api, networks, options })), - fork(router()), - call(languageInitSaga) + fork(analytics({ ...args, coreSagas })), + fork(auth({ ...args, coreSagas })), + fork(components({ ...args, coreSagas })), + fork(modules({ ...args, coreSagas })), + fork(preferences({ ...args, coreSagas })), + fork(goals({ ...args, coreSagas })), + fork(wallet({ ...args, coreSagas })), + fork(middleware({ ...args, coreSagas })), + fork(coreRootSagaFactory({ ...args, coreSagas })), + fork(router({ ...args, coreSagas })) ]) } diff --git a/packages/main-process/src/index.dev.js b/packages/main-process/src/index.dev.js index 453f445412a..e99cc60c38a 100644 --- a/packages/main-process/src/index.dev.js +++ b/packages/main-process/src/index.dev.js @@ -8,21 +8,26 @@ import configureStore from 'store' import App from 'scenes/app.js' import Error from './index.error' -const renderApp = (Component, store, history, persistor) => { - const render = (Component, store, history, persistor) => { +const renderApp = (Component, root) => { + const render = (Component, { imports, securityModule, store, history }) => { ReactDOM.render( - + , document.getElementById('app') ) } - render(App, store, history, persistor) + render(App, root) if (module.hot) { module.hot.accept('./scenes/app.js', () => - render(require('./scenes/app.js').default, store, history, persistor) + render(require('./scenes/app.js').default, root) ) } } @@ -35,7 +40,7 @@ const renderError = e => { configureStore() .then(root => { - renderApp(App, root.store, root.history, root.persistor) + renderApp(App, root) }) .catch(e => { renderError(e) diff --git a/packages/main-process/src/index.html b/packages/main-process/src/index.html deleted file mode 100644 index 7858507df44..00000000000 --- a/packages/main-process/src/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - Blockchain Wallet - Exchange Cryptocurrency - - - -
- - diff --git a/packages/main-process/src/index.prod.js b/packages/main-process/src/index.prod.js index ba3478f7cd0..96967f535dc 100644 --- a/packages/main-process/src/index.prod.js +++ b/packages/main-process/src/index.prod.js @@ -1,14 +1,18 @@ import React from 'react' import ReactDOM from 'react-dom' -import './favicons' import configureStore from 'store' import App from 'scenes/app.js' import Error from './index.error' -const renderApp = (Component, store, history, persistor) => { +const renderApp = (Component, { imports, securityModule, store, history }) => { ReactDOM.render( - , + , document.getElementById('app') ) } @@ -19,7 +23,7 @@ const renderError = () => { configureStore() .then(root => { - renderApp(App, root.store, root.history, root.persistor) + renderApp(App, root) }) .catch(e => { // eslint-disable-next-line no-console diff --git a/packages/main-process/src/layouts/Wallet/Header/SecurityCenter/index.js b/packages/main-process/src/layouts/Wallet/Header/SecurityCenter/index.js index 7ac41a91d31..0493f92c44a 100644 --- a/packages/main-process/src/layouts/Wallet/Header/SecurityCenter/index.js +++ b/packages/main-process/src/layouts/Wallet/Header/SecurityCenter/index.js @@ -7,11 +7,21 @@ import { NavbarNavItemTextIcon } from 'components/Navbar' -const SecurityCenter = () => { +const SecurityCenter = props => { + const onSecurityCenterClick = event => { + props.dispatch({ + type: `ROOT_LOCATION_CHANGE`, + payload: { action: `PUSH`, location: { pathname: `/security-center` } } + }) + + event.preventDefault() + } + return ( diff --git a/packages/main-process/src/layouts/Wallet/Header/index.js b/packages/main-process/src/layouts/Wallet/Header/index.js index eaebb156514..d93eedeee6d 100644 --- a/packages/main-process/src/layouts/Wallet/Header/index.js +++ b/packages/main-process/src/layouts/Wallet/Header/index.js @@ -18,7 +18,8 @@ class HeaderContainer extends React.PureComponent { } const mapDispatchToProps = dispatch => ({ - actions: bindActionCreators(actions.components.layoutWallet, dispatch) + actions: bindActionCreators(actions.components.layoutWallet, dispatch), + dispatch }) export default withRouter( diff --git a/packages/main-process/src/modals/Wallet/index.js b/packages/main-process/src/modals/Wallet/index.js index 74785f29af5..32b26dacc44 100644 --- a/packages/main-process/src/modals/Wallet/index.js +++ b/packages/main-process/src/modals/Wallet/index.js @@ -1,5 +1,4 @@ import PairingCode from './PairingCode' import ShowXPub from './ShowXPub' -import UpgradeWallet from './UpgradeWallet' -export { PairingCode, ShowXPub, UpgradeWallet } +export { PairingCode, ShowXPub } diff --git a/packages/main-process/src/modals/index.js b/packages/main-process/src/modals/index.js index ac7e4a0b7d1..89c62ebc29a 100644 --- a/packages/main-process/src/modals/index.js +++ b/packages/main-process/src/modals/index.js @@ -73,7 +73,7 @@ import { TwoStepSetup, TwoStepYubico } from './Settings' -import { PairingCode, ShowXPub, UpgradeWallet } from './Wallet' +import { PairingCode, ShowXPub } from './Wallet' import { RequestXlm, SendXlm, @@ -147,7 +147,6 @@ const Modals = () => ( - diff --git a/packages/main-process/src/scenes/Settings/General/index.js b/packages/main-process/src/scenes/Settings/General/index.js index cfae104eab7..af827217acf 100644 --- a/packages/main-process/src/scenes/Settings/General/index.js +++ b/packages/main-process/src/scenes/Settings/General/index.js @@ -1,14 +1,9 @@ import React from 'react' import styled from 'styled-components' -import { FormattedMessage } from 'react-intl' - -import { Banner, Text } from 'blockchain-info-components' import About from './About' -import PairingCode from './PairingCode' import PrivacyPolicy from './PrivacyPolicy' import TermsOfService from './TermsOfService' -import WalletId from './WalletId' const Wrapper = styled.section` padding: 30px; @@ -19,21 +14,6 @@ const Wrapper = styled.section` const General = () => { return ( - - - -   - - - - - diff --git a/packages/main-process/src/scenes/app.js b/packages/main-process/src/scenes/app.js index 4b124f4cc36..443317c09f4 100644 --- a/packages/main-process/src/scenes/app.js +++ b/packages/main-process/src/scenes/app.js @@ -1,8 +1,7 @@ -import React from 'react' +import React, { useEffect } from 'react' import { Redirect, Switch } from 'react-router-dom' import { connect, Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' -import { PersistGate } from 'redux-persist/integration/react' import { map, values } from 'ramda' import { createGlobalStyle } from 'styled-components' @@ -56,22 +55,32 @@ const GlobalStyle = createGlobalStyle` } ` +const SetForegroundProcess = ({ children, imports }) => { + useEffect(() => { + imports.setForegroundProcess() + }) + + return {children} +} + class App extends React.PureComponent { render () { const { + imports, + securityModule, store, history, - persistor, isAuthenticated, supportedCoins } = this.props + return ( - - - - + + + + @@ -159,14 +168,14 @@ class App extends React.PureComponent { )} - - - - - - - - + + + + + + + + ) diff --git a/packages/main-process/src/store/index.js b/packages/main-process/src/store/index.js index 4e94ede5097..92df3976d09 100644 --- a/packages/main-process/src/store/index.js +++ b/packages/main-process/src/store/index.js @@ -1,11 +1,9 @@ -import { createStore, applyMiddleware, compose } from 'redux' +import { combineReducers, createStore, applyMiddleware, compose } from 'redux' +import { REHYDRATE } from 'redux-persist' import createSagaMiddleware from 'redux-saga' -import { persistStore, persistCombineReducers } from 'redux-persist' -import storage from 'redux-persist/lib/storage' -import getStoredStateMigrateV4 from 'redux-persist/lib/integration/getStoredStateMigrateV4' import { createHashHistory } from 'history' import { connectRouter, routerMiddleware } from 'connected-react-router' -import { head } from 'ramda' +import { dissoc, head, merge } from 'ramda' import Bitcoin from 'bitcoinjs-lib' import BitcoinCash from 'bitcoinforksjs-lib' @@ -16,8 +14,11 @@ import { ApiSocket, HorizonStreamingService } from 'blockchain-wallet-v4/src/network' +import httpService from 'blockchain-wallet-v4/src/network/api/http' +import Settings from 'blockchain-wallet-v4/src/network/api/settings' import { serializer } from 'blockchain-wallet-v4/src/types' import { actions, rootSaga, rootReducer, selectors } from 'data' +import IPC from '../IPC' import { autoDisconnection, streamingXlm, @@ -29,6 +30,7 @@ import { const devToolsConfig = { maxAge: 1000, + name: `Main Process`, serialize: serializer, actionsBlacklist: [ // '@@redux-form/INITIALIZE', @@ -43,7 +45,8 @@ const devToolsConfig = { ] } -const configureStore = () => { +export default IPC(async ({ imports, middleware: IPCmiddleware }) => { + const { options } = imports const history = createHashHistory() const sagaMiddleware = createSagaMiddleware() const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ @@ -52,109 +55,109 @@ const configureStore = () => { const walletPath = 'wallet.payload' const kvStorePath = 'wallet.kvstore' const isAuthenticated = selectors.auth.isAuthenticated + const apiKey = '1770d5d9-bcea-4d28-ad21-6cbd5be018a8' + // TODO: deprecate when wallet-options-v4 is updated on prod + const socketUrl = head(options.domains.webSocket.split('/inv')) + const horizonUrl = options.domains.horizon + const btcSocket = new Socket({ + options, + url: `${socketUrl}/inv` + }) + const bchSocket = new Socket({ + options, + url: `${socketUrl}/bch/inv` + }) + const ethSocket = new Socket({ + options, + url: `${socketUrl}/eth/inv` + }) + const ratesSocket = new ApiSocket({ + options, + url: `${socketUrl}/nabu-gateway/markets/quotes`, + maxReconnects: 3 + }) + const xlmStreamingService = new HorizonStreamingService({ + url: horizonUrl + }) - return fetch('/Resources/wallet-options-v4.json') - .then(res => res.json()) - .then(options => { - const apiKey = '1770d5d9-bcea-4d28-ad21-6cbd5be018a8' - // TODO: deprecate when wallet-options-v4 is updated on prod - const socketUrl = head(options.domains.webSocket.split('/inv')) - const horizonUrl = options.domains.horizon - const btcSocket = new Socket({ - options, - url: `${socketUrl}/inv` - }) - const bchSocket = new Socket({ - options, - url: `${socketUrl}/bch/inv` - }) - const ethSocket = new Socket({ - options, - url: `${socketUrl}/eth/inv` - }) - const ratesSocket = new ApiSocket({ - options, - url: `${socketUrl}/nabu-gateway/markets/quotes`, - maxReconnects: 3 - }) - const xlmStreamingService = new HorizonStreamingService({ - url: horizonUrl - }) - const getAuthCredentials = () => - selectors.modules.profile.getAuthCredentials(store.getState()) - const reauthenticate = () => - store.dispatch(actions.modules.profile.signIn()) - const networks = { - btc: Bitcoin.networks[options.platforms.web.coins.BTC.config.network], - bch: - BitcoinCash.networks[options.platforms.web.coins.BTC.config.network], - eth: options.platforms.web.coins.ETH.config.network, - xlm: options.platforms.web.coins.XLM.config.network - } - const api = createWalletApi({ - options, - apiKey, - getAuthCredentials, - reauthenticate, - networks - }) - const persistWhitelist = ['session', 'preferences', 'cache'] + const getAuthCredentials = () => + selectors.modules.profile.getAuthCredentials(store.getState()) + const reauthenticate = () => store.dispatch(actions.modules.profile.signIn()) + const networks = { + btc: Bitcoin.networks[options.platforms.web.coins.BTC.config.network], + bch: BitcoinCash.networks[options.platforms.web.coins.BTC.config.network], + eth: options.platforms.web.coins.ETH.config.network, + xlm: options.platforms.web.coins.XLM.config.network + } - // TODO: remove getStoredStateMigrateV4 someday (at least a year from now) - const store = createStore( - connectRouter(history)( - persistCombineReducers( - { - getStoredState: getStoredStateMigrateV4({ - whitelist: persistWhitelist - }), - key: 'root', - storage, - whitelist: persistWhitelist - }, - rootReducer - ) - ), - composeEnhancers( - applyMiddleware( - sagaMiddleware, - routerMiddleware(history), - coreMiddleware.kvStore({ isAuthenticated, api, kvStorePath }), - webSocketBtc(btcSocket), - webSocketBch(bchSocket), - webSocketEth(ethSocket), - streamingXlm(xlmStreamingService, api), - webSocketRates(ratesSocket), - coreMiddleware.walletSync({ isAuthenticated, api, walletPath }), - autoDisconnection() - ) - ) - ) - const persistor = persistStore(store, null) + const http = httpService({ apiKey, imports }) + const securityModule = imports.securityProcess.securityModule - sagaMiddleware.run(rootSaga, { - api, - bchSocket, - btcSocket, - ethSocket, - ratesSocket, - networks, - options - }) + const baseApi = createWalletApi({ + getAuthCredentials, + http, + imports, + options, + reauthenticate, + networks, + securityModule + }) - // expose globals here - window.createTestXlmAccounts = () => { - store.dispatch(actions.core.data.xlm.createTestAccounts()) - } + const rootUrl = options.domains.root + const api = { ...baseApi, ...Settings({ ...http, rootUrl, securityModule }) } + const combinedReducer = combineReducers(rootReducer) - store.dispatch(actions.goals.defineGoals()) + const reducer = (state, action) => { + const { payload, type } = action - return { - store, - history, - persistor - } - }) -} + return type === REHYDRATE + ? merge(state, dissoc(`_persist`, payload)) + : combinedReducer(state, action) + } + + // TODO: remove getStoredStateMigrateV4 someday (at least a year from now) + const store = createStore( + connectRouter(history)(reducer), + composeEnhancers( + applyMiddleware( + IPCmiddleware, + sagaMiddleware, + routerMiddleware(history), + coreMiddleware.kvStore({ isAuthenticated, api, kvStorePath }), + webSocketBtc(btcSocket), + webSocketBch(bchSocket), + webSocketEth(ethSocket), + streamingXlm(xlmStreamingService, api), + webSocketRates(ratesSocket), + coreMiddleware.walletSync({ isAuthenticated, mergeWrapper: true }), + autoDisconnection() + ) + ) + ) + + sagaMiddleware.run(rootSaga, { + api, + bchSocket, + btcSocket, + ethSocket, + imports, + ratesSocket, + networks, + options, + securityModule + }) + + // expose globals here + window.createTestXlmAccounts = () => { + store.dispatch(actions.core.data.xlm.createTestAccounts()) + } + + store.dispatch(actions.goals.defineGoals()) -export default configureStore + return { + imports, + securityModule, + store, + history + } +}) diff --git a/packages/main-process/src/store/index.spec.js b/packages/main-process/src/store/index.spec.js deleted file mode 100644 index 223a9549862..00000000000 --- a/packages/main-process/src/store/index.spec.js +++ /dev/null @@ -1,172 +0,0 @@ -import configureStore from './index' -import * as Redux from 'redux' -import * as Middleware from '../middleware' -import { - createWalletApi, - ApiSocket, - Socket -} from 'blockchain-wallet-v4/src/network' -import { persistStore } from 'redux-persist' -import * as coreMiddleware from 'blockchain-wallet-v4/src/redux/middleware' -// setup mocks -jest.mock('redux-saga', () => () => ({ - run: () => jest.fn() -})) - -jest.mock('redux-persist', () => ({ - persistCombineReducers: jest.fn(), - persistStore: jest.fn() -})) - -jest.mock('connected-react-router', () => ({ - connectRouter: () => () => jest.fn(), - routerMiddleware: jest.fn() -})) - -jest.mock('blockchain-wallet-v4/src/network', () => ({ - Socket: jest.fn().mockImplementation(() => 'FAKE_SOCKET'), - ApiSocket: jest.fn().mockImplementation(() => 'FAKE_API_SOCKET'), - createWalletApi: jest.fn().mockImplementation(() => 'FAKE_WALLET_API'), - HorizonStreamingService: jest.fn() -})) - -jest.mock('blockchain-wallet-v4/src/redux/middleware', () => ({ - kvStore: jest.fn(), - walletSync: jest.fn() -})) - -jest.mock('../middleware', () => ({ - webSocketBtc: jest.fn(), - webSocketBch: jest.fn(), - webSocketEth: jest.fn(), - streamingXlm: jest.fn(), - webSocketRates: jest.fn(), - autoDisconnection: jest.fn() -})) - -describe('App Store Config', () => { - let apiKey = '1770d5d9-bcea-4d28-ad21-6cbd5be018a8' - let fakeWalletOptions = { - domains: { webSocket: 'MOCK_SOCKET', root: 'MOCK_ROOT' }, - platforms: { - web: { - coins: { - BTC: { config: { network: 'bitcoin' } }, - ETH: { config: { network: 1 } }, - XLM: { config: { network: 'public' } } - } - } - } - } - let mockNetworks = { - bch: { - bech32: 'bc', - bip32: { private: 76066276, public: 76067358 }, - messagePrefix: '\u0018Bitcoin Signed Message:\n', - pubKeyHash: 0, - scriptHash: 5, - wif: 128 - }, - btc: { - bip32: { private: 76066276, public: 76067358 }, - messagePrefix: '\u0018Bitcoin Signed Message:\n', - pubKeyHash: 0, - scriptHash: 5, - wif: 128 - }, - eth: 1 - } - let createStoreSpy, - applyMiddlewareSpy, - composeSpy, - kvStoreSpy, - walletSyncSpy, - autoDisconnectSpy, - btcSocketSpy, - bchSocketSpy, - ethSocketSpy - - beforeAll(() => { - // setup fetch mock - fetch.resetMocks() - fetch.mockResponseOnce(JSON.stringify(fakeWalletOptions)) - - // setup spies - createStoreSpy = jest.spyOn(Redux, 'createStore') - applyMiddlewareSpy = jest.spyOn(Redux, 'applyMiddleware') - composeSpy = jest.spyOn(Redux, 'compose').mockImplementation(jest.fn()) - kvStoreSpy = jest.spyOn(coreMiddleware, 'kvStore') - walletSyncSpy = jest.spyOn(coreMiddleware, 'walletSync') - btcSocketSpy = jest.spyOn(Middleware, 'webSocketBtc') - bchSocketSpy = jest.spyOn(Middleware, 'webSocketBch') - ethSocketSpy = jest.spyOn(Middleware, 'webSocketEth') - autoDisconnectSpy = jest.spyOn(Middleware, 'autoDisconnection') - }) - - it('the entire app should bootstrap correctly', async () => { - // bootstrap - let mockStore = await configureStore() - - // assertions - // wallet options - expect(fetch.mock.calls.length).toEqual(1) - expect(fetch.mock.calls[0][0]).toEqual('/Resources/wallet-options-v4.json') - // socket registration - expect(Socket.mock.calls.length).toEqual(3) - expect(Socket.mock.calls[0][0]).toEqual({ - options: fakeWalletOptions, - url: `${fakeWalletOptions.domains.webSocket}/inv` - }) - expect(Socket.mock.calls[1][0]).toEqual({ - options: fakeWalletOptions, - url: `${fakeWalletOptions.domains.webSocket}/bch/inv` - }) - expect(Socket.mock.calls[2][0]).toEqual({ - options: fakeWalletOptions, - url: `${fakeWalletOptions.domains.webSocket}/eth/inv` - }) - expect(ApiSocket).toHaveBeenCalledTimes(1) - expect(ApiSocket).toHaveBeenCalledWith({ - options: fakeWalletOptions, - url: `${fakeWalletOptions.domains.webSocket}/nabu-gateway/markets/quotes`, - maxReconnects: 3 - }) - // build api - expect(createWalletApi.mock.calls.length).toBe(1) - expect(createWalletApi.mock.calls[0][0]).toMatchObject({ - options: fakeWalletOptions, - networks: mockNetworks, - apiKey: apiKey - }) - // middleware registration - expect(kvStoreSpy).toHaveBeenCalledTimes(1) - expect(kvStoreSpy).toHaveBeenCalledWith({ - isAuthenticated: expect.any(Function), - api: 'FAKE_WALLET_API', - kvStorePath: 'wallet.kvstore' - }) - expect(btcSocketSpy).toHaveBeenCalledTimes(1) - expect(btcSocketSpy).toHaveBeenCalledWith(expect.any(Object)) - expect(bchSocketSpy).toHaveBeenCalledTimes(1) - expect(bchSocketSpy).toHaveBeenCalledWith(expect.any(Object)) - expect(ethSocketSpy).toHaveBeenCalledTimes(1) - expect(ethSocketSpy).toHaveBeenCalledWith(expect.any(Object)) - expect(walletSyncSpy).toHaveBeenCalledTimes(1) - expect(walletSyncSpy).toHaveBeenCalledWith({ - isAuthenticated: expect.any(Function), - api: 'FAKE_WALLET_API', - walletPath: 'wallet.payload' - }) - expect(autoDisconnectSpy).toHaveBeenCalledTimes(1) - // middleware compose - expect(composeSpy).toHaveBeenCalledTimes(1) - expect(applyMiddlewareSpy).toHaveBeenCalledTimes(1) - // store creation - expect(createStoreSpy).toHaveBeenCalledTimes(1) - expect(persistStore.mock.calls.length).toBe(1) - expect(persistStore.mock.calls[0][0]).toEqual(expect.any(Object)) - expect(persistStore.mock.calls[0][1]).toEqual(null) - expect(mockStore.history).toBeDefined() - expect(mockStore.store).toBeDefined() - }) -}) diff --git a/packages/main-process/src/template.html b/packages/main-process/src/template.html new file mode 100644 index 00000000000..7aa7e721812 --- /dev/null +++ b/packages/main-process/src/template.html @@ -0,0 +1,12 @@ + + + + + + + +
+ + diff --git a/packages/main-process/webpack.config.ci.js b/packages/main-process/webpack.config.ci.js new file mode 100644 index 00000000000..81cb14cf4a2 --- /dev/null +++ b/packages/main-process/webpack.config.ci.js @@ -0,0 +1,80 @@ +/* eslint-disable */ +const babelConfig = require(`./babel.config.js`) +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin +const CleanWebpackPlugin = require('clean-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const path = require(`path`) +const Webpack = require('webpack') + +const runBundleAnalyzer = process.env.ANALYZE +const src = path.join(__dirname, `src`) + +module.exports = ({ envConfig, PATHS }) => ({ + name: `main`, + entry: ['@babel/polyfill', path.join(src, 'index.js')], + output: { + path: PATHS.ciBuild, + chunkFilename: '[name].[chunkhash:10].js', + publicPath: '/main/', + crossOriginLoading: 'anonymous' + }, + module: { + rules: [ + { + test: /\.js$/, + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { loader: 'babel-loader', options: babelConfig } + ] + }, + { + test: /\.(eot|ttf|otf|woff|woff2)$/, + use: { + loader: 'file-loader', + options: { + name: 'fonts/[name]-[hash].[ext]' + } + } + }, + { + test: /\.(png|jpg|gif|svg|ico|webmanifest|xml)$/, + use: { + loader: 'file-loader', + options: { + name: 'img/[name].[ext]' + } + } + }, + { + test: /\.(pdf)$/, + use: { + loader: 'file-loader', + options: { + name: 'resources/[name]-[hash].[ext]' + } + } + }, + { + test: /\.css$/, + use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] + } + ] + }, + plugins: [ + new CleanWebpackPlugin(), + new Webpack.DefinePlugin({ + APP_VERSION: JSON.stringify(require(PATHS.pkgJson).version), + NETWORK_TYPE: JSON.stringify(envConfig.NETWORK_TYPE) + }), + new HtmlWebpackPlugin({ + template: path.join(src, 'template.html'), + filename: 'index.html' + }), + new Webpack.IgnorePlugin({ + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/ + }), + ...(runBundleAnalyzer ? [new BundleAnalyzerPlugin({})] : []) + ] +}) diff --git a/packages/main-process/webpack.config.dev.js b/packages/main-process/webpack.config.dev.js new file mode 100644 index 00000000000..dc033c2b086 --- /dev/null +++ b/packages/main-process/webpack.config.dev.js @@ -0,0 +1,86 @@ +/* eslint-disable */ +const babelConfig = require(`./babel.config.js`) +const CleanWebpackPlugin = require('clean-webpack-plugin') +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const path = require(`path`) +const Webpack = require('webpack') + +const src = path.join(__dirname, `src`) + +module.exports = ({ envConfig, localhostUrl, PATHS }) => ({ + name: `main`, + entry: [ + '@babel/polyfill', + 'react-hot-loader/patch', + `webpack-dev-server/client?${localhostUrl}`, + 'webpack/hot/only-dev-server', + path.join(src, 'index.js') + ], + output: { + path: PATHS.appBuild, + chunkFilename: '[name].[chunkhash:10].js', + publicPath: localhostUrl + `/main/`, + crossOriginLoading: 'anonymous' + }, + module: { + rules: [ + { + test: /\.js$/, + include: /src|blockchain-info-components.src|blockchain-wallet-v4.src/, + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { loader: 'babel-loader', options: babelConfig } + ] + }, + { + test: /\.(eot|ttf|otf|woff|woff2)$/, + use: { + loader: 'file-loader', + options: { + name: 'fonts/[name]-[hash].[ext]' + } + } + }, + { + test: /\.(png|jpg|gif|svg|ico|webmanifest|xml)$/, + use: { + loader: 'file-loader', + options: { + name: 'img/[name].[ext]' + } + } + }, + { + test: /\.(pdf)$/, + use: { + loader: 'file-loader', + options: { + name: 'resources/[name]-[hash].[ext]' + } + } + }, + { + test: /\.css$/, + use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] + } + ] + }, + plugins: [ + new CleanWebpackPlugin(), + new CaseSensitivePathsPlugin(), + new Webpack.DefinePlugin({ + APP_VERSION: JSON.stringify(require(PATHS.pkgJson).version), + NETWORK_TYPE: JSON.stringify(envConfig.NETWORK_TYPE) + }), + new HtmlWebpackPlugin({ + template: path.join(src, 'template.html'), + filename: 'index.html' + }), + new Webpack.IgnorePlugin({ + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/ + }), + new Webpack.HotModuleReplacementPlugin() + ] +}) diff --git a/packages/main-process/webpack.debug.js b/packages/main-process/webpack.debug.js new file mode 100644 index 00000000000..3ef5f8b1813 --- /dev/null +++ b/packages/main-process/webpack.debug.js @@ -0,0 +1,78 @@ +/* eslint-disable */ +const babelConfig = require(`./babel.config.js`) +const CleanWebpackPlugin = require('clean-webpack-plugin') +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const Webpack = require('webpack') +const path = require('path') + +const src = path.join(__dirname, `src`) + +module.exports = ({ envConfig, localhostUrl, PATHS }) => ({ + name: `main`, + entry: ['@babel/polyfill', path.join(src, 'index.js')], + output: { + path: PATHS.ciBuild, + chunkFilename: '[name].[chunkhash:10].js', + publicPath: localhostUrl + `/main/`, + crossOriginLoading: 'anonymous' + }, + module: { + rules: [ + { + test: /\.js$/, + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { loader: 'babel-loader', options: babelConfig } + ] + }, + { + test: /\.(eot|ttf|otf|woff|woff2)$/, + use: { + loader: 'file-loader', + options: { + name: 'fonts/[name]-[hash].[ext]' + } + } + }, + { + test: /\.(png|jpg|gif|svg|ico|webmanifest|xml)$/, + use: { + loader: 'file-loader', + options: { + name: 'img/[name].[ext]' + } + } + }, + { + test: /\.(pdf)$/, + use: { + loader: 'file-loader', + options: { + name: 'resources/[name]-[hash].[ext]' + } + } + }, + { + test: /\.css$/, + use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] + } + ] + }, + plugins: [ + new CleanWebpackPlugin(), + new CaseSensitivePathsPlugin(), + new Webpack.DefinePlugin({ + APP_VERSION: JSON.stringify(require(PATHS.pkgJson).version), + NETWORK_TYPE: JSON.stringify(envConfig.NETWORK_TYPE) + }), + new HtmlWebpackPlugin({ + template: path.join(src, 'template.html'), + filename: 'index.html' + }), + new Webpack.IgnorePlugin({ + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/ + }) + ] +}) diff --git a/packages/root-process/babel.config.js b/packages/root-process/babel.config.js new file mode 100644 index 00000000000..16bedc9a2f6 --- /dev/null +++ b/packages/root-process/babel.config.js @@ -0,0 +1,8 @@ +module.exports = { + presets: [['@babel/preset-env', { modules: false }]], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-object-rest-spread', + 'babel-plugin-styled-components' + ] +} diff --git a/packages/root-process/package.json b/packages/root-process/package.json new file mode 100644 index 00000000000..9a189776c97 --- /dev/null +++ b/packages/root-process/package.json @@ -0,0 +1,20 @@ +{ + "name": "root-process", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "test": "echo 'No tests to execute.'", + "test:build": "echo 'No precomplilation required for tests to execute.'" + }, + "devDependencies": { + "eslint": "5.16.0", + "webpack": "4.31.0", + "webpack-cli": "3.3.2", + "webpack-dev-server": "3.4.0" + }, + "dependencies": { + "axios": "0.19.0", + "web-microkernel": "1.0.0" + } +} diff --git a/packages/root-process/src/assets/images/favicon/android-chrome-192x192.png b/packages/root-process/src/assets/images/favicon/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..4a892a3cdd9de192019f4da3e08727056e95000b GIT binary patch literal 4868 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 literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/android-chrome-512x512.png b/packages/root-process/src/assets/images/favicon/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..27eb4cdfc77fba9f1884d3b9b4b47f9c23a6fb05 GIT binary patch literal 13373 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 literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/apple-touch-icon.png b/packages/root-process/src/assets/images/favicon/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b19393522885b83a30093199815e9b80454a4f66 GIT binary patch literal 2421 zcmZ`(dpMN&7k^u+EnU_sR;iyJ&!%>%F|Sy<7t7>65u*~}Ww3*o6*7(7Ds8P?GD9xI zh{)|7iZCui$vd){ahb-J%HYMg48zPc{AQnL|M~s#JJ0!k&U3!c`JD4S=Q-!xadx!X zx_Q@T006e)Y_0Gr%lhYitER%uT2d7P>jO?ZoCbiJ2O3{})K$K|uPxpI0Alt705uT+ z~1OSjR0DyV}fMqrS=!E4qx*P+5Z%Ul)U9H#F*3dagN58P}Wp{c_TXJdRjRF>< z=?#ZBGCjYjRIa`m9=%u59GzGD$FmkhFsfW#<$WCgqoSEVIu5_?XLa+IR%E%)yYd=d zEz9JK%d)hpR;f&$QPcLYx;3M=UAiLAtZmPP+q2+~6}jROtK$)ilg;AD;D(Xxugim?L zCl_}*TSE#r0YH#Ji$;F)byxRE3HPpwoC%vdC#T;sG&CHyHnp17) z)=15*K5_Zx&7A6FCWn$!6_wXc`lCHN+Q)2pj%*YB6Ort|y9=jz&e z-5&C7LpkFM%U_jBGxwMa>HOyRlf`g{dj@}0ymZ7px>+zO9G&n;=SyUY%#sE|246TW z@g%1aGWp%3Gv~rO588)&W(+h8P5f{IT30E$lsN$NMHwY8aFqNC?q|VAkz_>yx?OJ) zOcqqPeiSdUTDdq%foOi&uTa1koR%q+g@e-tgVX1zjQqiA9F>u$!gz-7BNkI27R!~` zmMP;V6kSmoHKAb3)Xz=mL?%75k184cqm+n#FclscpOmG5f~ za*_`>WT3&EcLu%*-x|XeK_t$#8l2nKD_&bkNoh5NE>Cw?>t{Ue-B93|n`^l7Obt;g zO5|^V@kQ@kpVN`8taSJu!L(Hxc=6;&i`3J;(w9H+P?$bjYZATo< zf36dtH4Rdc6>p}f0>PEO58-YqegEsloJ>!HN~pyMjlb=k#X~-Y}HmzwN1b zXiLodT>oClrb3>U0wJL%A~581U2lzGNmlgQ{*ax2hp-2@B7OAE#g5i`ADZOG#SiE= zVC(Nja1^;v*E=?A7;fZjsaETmb~{eAIS_1^W$`xZ?;o3Vz1_2ha|D4KhMv7^ip@*8 z;GVrs>P>4dHJsVxMvHpw73~&lSH20N#(6;DN4i7s=aH>TEw@D7iZldJ9L?RGH$V^B z@Jm784gJ52O6xIddK_$O*)sqR|<^O?FIzsqVU6n!|m|35**y`?9RcSd2c*HmG> zSeXC#5JWYQBQ}-=(Lpv~tBVmB{Ojy7!Cza#?4Qd#Y{0Wz%yQ`nw>Q+u2y6{Ef{W-d+vA;Ho*KG$eP2(-lC_R|;gi%H z&BH#lx)xUZ4cbQV)0m%Rr#kek%9LK!C`i$r-<3tU5-ijol$jWKy)_ArOV|`wWC%K3 z4jKTrh;o`}nxRm3T?9;L#OS4{FiVgVbu^5VTx1dW4rUK2%19ov^f-K~u zDu_d=0~uV?UX!R1*D?`G%v}|?fd(h+ zOw+xBZy&b51b&Jzl}OfuE?o0|o)w5Q_9-dthT0XZd%E-L77NU3Ql-$r8c+0d8Xr1@$Y6jH)cm)9rNH~w&NAp z?XEDQ*5VV>nbSbvnwpoz(G}&xBFcv`*%5=5p8{5|%)A$tYnvQ)R+ulBCLtr};5XSX zc~?cr@+EzUWBvspNJSS^^-o8Lef%cpsJL}bNLNU36-$07+Dv9ZMV5q!vr1VX7%;4JMVrpPYyQNUzDACh<%Ige zPvs;ru&{iRvtN_G_PL4jC78zUR?@<%M$NT?w~m418Cm zy2C$4T6;wL`9%6#`d;%_832nh`NbGxYHVWeiZ!uRDG2_I!CGQ4y+qkb1`RfzIB7FhidhX|iE#HP;IB?*NQxAe0 z!vkst8i#x}v<$TD(s!t7>041vJN(oPU|0Q~A>L~a!qMYw?4jBgoI3=dI+~FSXO3j5 PN&q-(N2|&+-nae>jad + + + + + #2d89ef + + + diff --git a/packages/root-process/src/assets/images/favicon/favicon-16x16.png b/packages/root-process/src/assets/images/favicon/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..34b445a571253c553832f31064a3378314becc89 GIT binary patch literal 798 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>VBS&fig zQ+M6ncLUU>>+Oy6@SV&`KZJLBjxAi3$%bI!_D4C|ix?0Lel;Tgl) zXAJ9}Gpv6O6alfFdh=h(%CTRGmqG%XgsABu^K|5Q)pa{oz8*20W~;oSYNl zJlC^a&N=a>#M8^9%%)RV`_juZ8-r4U*4{iL z-Ze=edv6Tm8uMva%hHejn{aTVjVMV;EJ?LW zE=mPb3`Pcq7P>t*I;7bhncr0V4trO$q6BL!5%1yU4}S(1~=kdvRD?>|kl2q+p} z6%tVrlvu7%P?VpRnUkteQdy9ykXcZY%)n4F=kX^Vj>0evjZ^-o&v-r!VqjM0)=TCV zRu=Z2EW#|T;L>1nIE7hxbBMy}8&^&oIdeqj2>a;|wWF{uCm literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/favicon-32x32.png b/packages/root-process/src/assets/images/favicon/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..664c498f8187e374fdde95b111fc8791bc100a41 GIT binary patch literal 1079 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+0810!pIPl)UP|NqNpuBw{9 zv2xy~s@Y(2-lmd8>ww(KdE0;_k8Pr7ZV!+VRJBbzbia^gl1E;5Q1w>zkX;^my+Ku5 zJ#xDPtG2o4cI!m$5x0s3DhRCHsu8kFIdG?#c?yv0meUnbwM{*Amu=!9AQy-@ioA~6^1A)S!$DhaO2fyKq;%V13)Rx;>A{J z2RVy?o-?d_212V|F>HIwu>L8O zyZ%1VFF*r`*-;8zr$A~_^|R#%iNl(a5DXKEMgqu8B7r$SG~t#8W;^$%R#e)82;n|zbQC1=}SJ0BKdwz^bQ zGo^niYi{?)X^-dltFX=$Q3}?0UgITN64l`|(c-zWNonMntogH_+vvV%x)2@HRa@Em zIP5x0Uaj-bWe;<*tJ#~+Cj-7mWccc4s zw*%`KHtYz~YyKkihK-^7w)N9bLci2}*>}`;#NJE!G~ zF!(Crw--f2ZhlH;S|x4`Ew0}fff^)1HU#IVm6RtIr81P4m+NKbWfvzW7NqLs7p2dB zXCnnv#063ml39|I%8-+vp6@?RvIr;|UKJ8i5|mi3P*9YgmYI{PP*Pcts*qVwlFYzR zG3W6o9*)8=4UJR&r_Xpk4Pszc=GIH*7FHJao-D#Ftl-jMayW%qd2@)u=^Iy09657D i<_P=g29E_^dJM0`1xr3TnN9^-!QkoY=d#Wzp$PzrZN?n{ literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/favicon.ico b/packages/root-process/src/assets/images/favicon/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..775e9c6cbbf4a93f6aa4f89835653a597b4899ae GIT binary patch literal 15086 zcmd6uYitx%6vr==1R9NfU|YY@1eF^7Kw=_nDJ}Mc0!1T)8u(yhTLQ$0K`GL%CM;l- zh{hO;1T5Q^7+mO`$5nqbi0b0kStBAU3WZZcF*>9Iy*bBZ9B>B zna4fn{QmdedF-7jV~bb{TfLfr&1L7GV(bORSZ;1~JDss#VP85}Jid1cV;@6>Jg5UM z>`f@RUtFGym}LJAG_x1STUgt;lLf>A>%Ti$akBFojW5{PV&MJ)n8Q#6Y4JN*jo^UJ zSbz(%2b|0n4Qk=dC*`QJ85vT zhPmp8B(T%@kaP`6WG4;v$?AtWU>7y`n^~RN8s>_f?saBrm}_>@fcJ8v{V-qbW@xZ6 zws_aN%v$G%%LXXkTwDLu%>#ZiqBsd(g-*M%Zj2ezlj#;|TR1L$QWb}@Fclf51Gr@KZgQYBgd9g-z(h|B* zWxOANy{B{gswpUwBj=CYO290h%T#B~+ge<)HnUA9Kk5iM3~2C7vkSnFMU#ecPb;zq z4DjpLAj7Ul1F{?C7d3oewX9d|In9k3VwPI9{SYuXe&}3Kt-*6Vqc1w%rOeXpgc@-C z7&w1JHdm;_u^%MCu-oNCSrTb=R z7(SkUQzyHu2Dy8%4WBtJ8Ht`Bq?}6Iq6T39$soH#L;Ppas0P{RH$B(bV`cdR9q+$_ z>}oX}t0)+Vo~!YgrTar22R18RmrP?%qygFC{G5uOr&3O(?Ws5# zOlOa$!2mnyl4?jAyHvwRy9)KTJF%0UYCo_E$5PVSc@3_!hiY|<8DgGkA(q;E6EPUI zHk)m6Yh6{HVGR%iV;SYqK)L1NzPvJL$#zE_+pjRt@Mg-|W1G*0a@(sx?5} zg~C2VgT1OLv4*Oqg~m=A9FEcyS6y{YJPmjZ>rnlLYFq79#h{@LN+4Qby-NJ8Z8ozY zgo*@#F+q5c7O|sCBi5Y~v9?7Ki*=xc89R(N#8?s9Nyb*7eZW{6+EK=qqIENtg4PD# zAE8BX_yg_`fH81~0E~e<1Yiu@Apm2eXqPhPMhiVL<^+BB9qb8ZZ(~Ola?^YIq%o+- zoiqjwxlL!#k=rx|1Gx<|n2=jHgNeB{GnB1=G^=5M>Uo*-n_}s+?j0BId)pMR4{zk> zWW&6U2THhZe`>dB4Bb0>;N4UhiZ33+t~J>?kk`x2D~0RLV3^KL@5p8`M01aDxoQ43 z-1x46c9@-e7Q^o6GtwZhPbPPaq3J#C48E^cy2plE{-Jk*GS+wZmIHDxKc$dc7sKr% zkK?<`uzPoYKQKhUm56+Az;ldyU6@<#d$MrTfz+NvW0G>yoapu4`F-%MNjwIv@9Dzl z6d0tri2>Rn9s}f8)&A5EX#YD`IlCR={G)twR%B3{n`gLp+c_+~4#`}9kK2hF*O zg=aXCIQKNe2QdcSxp{`q_H8;J9cz>$k%hk4c2R4aQ%!HQ_#LMYrMc>? zr+`76$H8^nN5{2TahhgwMP!-=rxI{lAv(o?Qx7-|fm0GVJuwPX6>gZeu!%DvGqd16 zJT?k-urB(bZ*2MkV`6N~fm0Qj6Q(L~o&qNha-qcLDVD&7%4NKn6~g~k4T=TURl14Q z0xfI3&D4S?tw}%?p@yT zBlny;F+So#-$C*vD;MKuYmU3CH1Cf)S5DpKVxaFVe~iP&KwrA|K#V`{bbkFyG{&Hm zpBN|x?@KW{x99zJxAWu>@?$$j+wURoi|b!jzUpqby&lUw?Z4__*N(gmd>@pr$tVDy zAjLpmx|T>{1iMO~F5rD>onr95k{EG)DaO$~MHcW0k{@S-9hDnrj2rTiTJR}?GQs)c zy5iWE#&FdhDg>WFEVwQ`PTS$bFt-blCs)8+E6(S_oU0q>abZrD7F@@Dcns@cUGzcU F{{ioqUfBQu literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/mstile-144x144.png b/packages/root-process/src/assets/images/favicon/mstile-144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c4f925b28752157c7d42d018955f7c1f1b66f5 GIT binary patch literal 2488 zcmcIkcT`hp7JnL;AePV~EMh=m2`VBWjEIR2kWge4!z7duK?qeqQE3S}Ah1Z0VF6J} z2$Dggjsy~llqfFXupqrlk&;FYHG&QH19s2Z-E;QOJ?Fmr{qFC(_jm7m?|U!d!g;IR zvZ}HW1nsu5#@U1QuN#*P4EXj;g+#DO`IwzEgCKUw&adv$Al4vT+n<9Vsx|~g#X`_$ zKt)YJP>4PRO%tJV@})%A!6hq2zI>bPna8#-`-5!|9x; zf^uqJ355=f6S~2$0oY25%B!K|Z87D5yqUF`mbK4XeN~8as?uV&GD#L#ztRWvg2SqKQ>H1a26u7)`2Dw+sl) zVWWwa+Rim7d;lQBXdHt}6(9$Mp*h7`q;in9ALk#MQ>0C-(j-*wx3~g$U_ej}=O32K z(RJp6T?5w&^iNf47FUjtYQQc~*k|Dvnq9Qd+%F`j2n-XN4a7?fT`N@0{6exf7*#Xh z&4F2}KWs3nxIle!wW;@;@>UVxH|&%rjNT>ncwF0&d))Q)i&h@HN4OON4-)>}Z-v^9 z)$DG5&B(}B$nF*bP~5`@{$GrM-9NpBIo*OS9>``jHv_EgP|ev)2FhEtn_wgDzW~VG zAPdIWnip`tX634Q+fMc zHdEGpVm4ssZSi6pLh6U4zYQ!8)u&mm-NzHb4Q}(WwZaXEVcO9kfCt;0!^6j=gkxry(ZhxP3B$69(wY4U9xq+1*~sZ< zDo^|G!Yr?o2M#vdDlD@;HG2HFs zW}74?F9*eoDE}3W1XZj(!y9l3D>G#;{hLm2XE>ykeGxZEq&kc!2-j}$^{eaFgzSa^ z*d9LB13!tP=>Tyv_`8S~SpEeT+VcMF4n|!CO0>r(aS_vtN_)1^@h;X!-a&YUUT*l2e3m7YqKu@- zet-piLb{}^_%WOij>MIMgmEn7Xo&kp5AC4+zJb&6F?5I2dw2CzKN=;mVqX{D0*)hmFw0%S4rxOQ7((%))a~>zwIF)-sI|lhd2@v z1`TV@Y^zW{a8?o?>6SLM9#sn0Y?@z%tDi+ZSdk(mix$>W`2=ai8?rII;S3#AHm1=qQ!EU7{U~zj)fMdLDaDfn0yK zmaINZcU;EIGR#vVjVb3kVDq2%8Kp#~o0|I4z2$2aE#ZL`_E@vh_|azwNBeJ&BI58W zZHu4HbbQqiZ!RVk)7PToii$CIa<7t?bsuHiVlnm1DRE~{i&g4ZAnuZ0ZCALyg_;r> z1IWtVoV3a7mMUSyO~9xv`d zUdLn~q;Gsaz6qaDTz{*@}Izibc z!UMlZH%W~{TNd!fl1ecysr0+(&gCFn6{^a5&;mIb;ma_@C=pJgxKDKv4n#T}eYh(| zJ_DS@jM+m98FP)wq_IXt60dPDNw9-)tP^82Tihjc-9bmE$?jdVjldp)Y3|e`?^M)O zv`trnDQe)NjOsjLnk7Vycm3UeuWznbtB1dfvVL?{51LYz4_-7k&HybS8~k}3%lvZm FKLA25t!4lK literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/mstile-150x150.png b/packages/root-process/src/assets/images/favicon/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..6326cd191c61c0540b8e08d90e01f177f02e9714 GIT binary patch literal 2502 zcmbtUd00~E9zKmNwvbjS{na5VrIdcr83Zq!1yfkh6Twdl87Wee9_e2tu|&Q0zqr zngvqqFa({!LePj81eqs6(0Y1ug%bgSey}~bk7ffB&ZQMF(~6j>#Sy7sEn=ns0Im|x z-Od3@RGP{{l1l(TckgQsa8rxU-YsFK6$7&4z*%N$A;2F0C#&}y2O#*cfbMYm>2_L$ zp35`90kJsF+U{Iwar{?v?+fr!EMEY|Gk zd0_o?GzN6maensGkr>br?-7Idhz6U)9gYHna2`=09}v?+QJ~h;JrZ!^Ly>?RyCe2r zEvy?8>xO_M0Be^UVt@Ln9kU*A_2@j*?_FR7DYUjgEGu{^Es)3x+hxE4Kyf-z2_2+R zF0fSOBzGmd1L$Ba?vQ@V0h|=V1tmGc-ftG@Rwz+*09HZAe@KyJ5GWeg+DRvCzmX#G zS0%o^@SCL~s$|XIR5^eHq8TqCh16=qx39d`0YFr3pnYXRr*&BE&y|B92dqSZDj5K& zQU(-arOdig#Q{J!aGZsSd?;@aU`wg&8~OiN@NGeJa8l3;44t2!kK_H>4*sM{26ewP zWQbIEf2xciH)<5?b{UN9e5XIWLvLuCbihDwh_4ik7|y&W#w1h(uLi+NxT0TXZ$L znyb4siiL_|vVh#!)M(gPgES!3{Oa!d?oYhupXzuoc)a@B`udvs8Gb`UW0OK45H?F? z+lR(?>6vVmyw*qkRkr$PUbFqqh|2Csc~4E$F4Er@-hy*1IPPdie&Ops5ekAO3+{Yay2XFnQoym&4U=x31Yo^w;;dma) zg4erbL-O14)Wk%NZG~aa^#;qvO?hAX4$xaBUftF?SHxaSQr~h>^E=D^kK0a`Dh75q zZwo7w>4+2E+a!S^vN-EbYCT$+>dKZewFpG$N_VN z6T&`dIf<*4C*94xjtKFVz4Lj-yYvH&d7*jmede`410J9;Stm*yFghA**>r;KU~T|e zG^b5irx)}9y?kG5P@=6mdNpd^(ig6dG&rIozLIXUoYG3DEbTs=v&yC?Bp4a-eiHFa zmy&IJTDj>g&T_f{cje}&PWu&UN|oF_pI8##;8SHgQkeVhRa_>qdW1Dx zc7uxAo&Pilu0jXFMsGOTL7JOts5$*_J_+>wQ0Z%29q~s#{T`s1Vm2-!I2^@H(T52E z?TxEZ>CP(4d~;%uW(snL#!czluz!89X3D878fMB&AkX5yG*c#@=GScuZn5Z-=W$

>fvR*`86=K@$A5TBZdl8jgCoK{r)*&2oSm^dB@<=(68_Gcd&qnqI zbd8=mLqQD&O9&M>DkqpSkkrF%ur8><_4QhK zgG!Whm4jQSSInt>6mIQZ2G7pyYW+pNBM)}9s@-Avw*Z&qiFR%)m|Wl~o3YDzG1ugt zeL0iAR~E+#XG^5*t5F^rOB^`~xdGUk4x^5O4>{=rPGHhhX$E`=?SfxsIohH30es>) z+a4$O>$J!30|t<;{OeMu11v=|NV<(agHP%UvdFpq(S#PwUZLTJTTEbpZoQ{y851_Y zSi;e0qdgIVL4sHL_>n;eVd`l?}yY1)D=Nkg5m iZe!gNL9G3~UyPuU^{W_@B>W990&=j~PtGHG{rYc9Td0Bn literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/mstile-310x150.png b/packages/root-process/src/assets/images/favicon/mstile-310x150.png new file mode 100644 index 0000000000000000000000000000000000000000..43794bc7ac6b041f61765d4723026bfdf283986c GIT binary patch literal 2678 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 literal 0 HcmV?d00001 diff --git a/packages/root-process/src/assets/images/favicon/safari-pinned-tab.svg b/packages/root-process/src/assets/images/favicon/safari-pinned-tab.svg new file mode 100644 index 00000000000..daecb91da10 --- /dev/null +++ b/packages/root-process/src/assets/images/favicon/safari-pinned-tab.svg @@ -0,0 +1,18 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/packages/root-process/src/assets/images/favicon/site.webmanifest b/packages/root-process/src/assets/images/favicon/site.webmanifest new file mode 100644 index 00000000000..1007f33d605 --- /dev/null +++ b/packages/root-process/src/assets/images/favicon/site.webmanifest @@ -0,0 +1,26 @@ +{ + "name": "Blockchain Wallet", + "short_name": "Blockchain", + "icons": [ + { + "src": "/img/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/img/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone", + "prefer_related_applications": true, + "related_applications": [ + { + "platform": "play", + "id": "piuk.blockchain.android" + } + ] +} diff --git a/packages/root-process/src/favicons.js b/packages/root-process/src/favicons.js new file mode 100644 index 00000000000..91eb9fac4b6 --- /dev/null +++ b/packages/root-process/src/favicons.js @@ -0,0 +1,14 @@ +require('./assets/images/favicon/favicon.ico') +require('./assets/images/favicon/favicon-16x16.png') +require('./assets/images/favicon/favicon-32x32.png') +require('./assets/images/favicon/android-chrome-192x192.png') +require('./assets/images/favicon/android-chrome-512x512.png') +require('./assets/images/favicon/apple-touch-icon.png') +require('./assets/images/favicon/mstile-70x70.png') +require('./assets/images/favicon/mstile-144x144.png') +require('./assets/images/favicon/mstile-150x150.png') +require('./assets/images/favicon/mstile-310x310.png') +require('./assets/images/favicon/mstile-310x150.png') +require('./assets/images/favicon/safari-pinned-tab.svg') +require('./assets/images/favicon/site.webmanifest') +require('./assets/images/favicon/browserconfig.xml') diff --git a/packages/root-process/src/index.js b/packages/root-process/src/index.js new file mode 100644 index 00000000000..aac8c8c960c --- /dev/null +++ b/packages/root-process/src/index.js @@ -0,0 +1,203 @@ +import axios from 'axios' +import * as kernel from 'web-microkernel' + +import './favicons' + +const LOCATION_CHANGE = `@@router/LOCATION_CHANGE` + +const securityProcessPaths = [ + `/authorize-approve`, + `/help`, + `/login`, + `/logout`, + `/mobile-login`, + `/recover`, + `/reminder`, + `/reset-2fa`, + `/reset-two-factor`, + `/security-center`, + `/signup`, + `/verify-email` +] + +const pathnameIsInSecurityProcess = pathname => + securityProcessPaths.some(path => pathname.startsWith(path)) + +;(async () => { + const rootProcess = kernel.RootProcess() + rootProcess.addEventListener(`error`, console.error) + const { createProcess, setForeground } = rootProcess + const optionsPromise = fetch('/Resources/wallet-options-v4.json') + + const mainProcessPromise = createProcess({ + name: `main`, + sandbox: `allow-forms allow-popups allow-popups-to-escape-sandbox allow-scripts`, + src: MAIN_DOMAIN + `/index.html#/login` + }) + + const pathname = window.location.hash.slice(1) + + const securityProcess = await createProcess({ + name: `security`, + // `allow-popups allow-popups-to-escape-sandbox`: Allow downloading of the + // backup phrase recovery PDF. + sandbox: `allow-forms allow-popups allow-popups-to-escape-sandbox allow-scripts`, + src: SECURITY_DOMAIN + `/index.html#${pathname}` + }) + + setForeground(securityProcess, `lightgreen`) + + const replaceUrl = url => { + const data = {} + const title = `` + window.history.replaceState(data, title, url) + } + + // update url with new language without forcing browser reload + const addLanguageToUrl = language => { + replaceUrl(`/${language}/${window.location.hash}`) + } + + const sanitizedAxios = kernel.sanitizeFunction(axios) + + const localStorageProxy = { + getItem: key => localStorage.getItem(key), + setItem: (key, value) => localStorage.setItem(key, value), + removeItem: key => localStorage.removeItem(key) + } + + const logout = () => { + // router will fallback to /login route + replaceUrl(`#`) + window.location.reload(true) + } + + let mainProcessExports + const mainProcessActions = [] + + const processMainActionsQueue = () => { + if (mainProcessExports) { + while (mainProcessActions.length > 0) { + const action = mainProcessActions.shift() + mainProcessExports.dispatch(action) + } + } + } + + const mainProcessDispatch = action => { + mainProcessActions.push(action) + processMainActionsQueue() + } + + const options = await (await optionsPromise).json() + + const setForegroundProcess = () => { + const pathname = window.location.hash.slice(1) + + if (pathnameIsInSecurityProcess(pathname)) { + setForeground(securityProcess) + } else { + setForeground(mainProcess) + } + } + + const replaceFragment = identifier => { + const [withoutFragment] = window.location.href.split(`#`) + replaceUrl(`${withoutFragment}#${identifier}`) + } + + // We remember the most recent Main Process location so we can revert the + // address bar to it when the back button is pressed in the Security Center. + let mainProcessPathname = `/login` + + const dispatchFromSecurityProcess = action => { + if (action.type === LOCATION_CHANGE) { + const { pathname } = action.payload.location + + if (pathnameIsInSecurityProcess(pathname)) { + replaceFragment(pathname) + } else { + // If the Security Center is parking at /home then show the most recent + // location in the Main Process. + if (pathname === `/home`) { + replaceFragment(mainProcessPathname) + setForegroundProcess() + + // Now that the Security Process is in the background it's safe to + // park it. + securityProcessExports.dispatch(action) + } else { + replaceFragment(pathname) + mainProcessDispatch(action) + } + } + } + } + + const securityProcessExports = await securityProcess({ + addLanguageToUrl, + axios: sanitizedAxios, + localStorage: localStorageProxy, + logout, + mainProcessDispatch, + options, + pathname: window.location.pathname, + rootProcessDispatch: dispatchFromSecurityProcess, + setForegroundProcess + }) + + const dispatchFromMainProcess = action => { + if (action.type === LOCATION_CHANGE) { + const { pathname } = action.payload.location + + // If mainProcessPathname is `/login` then the user just logged in and we + // want the Security Process to park itself at '/home' while the Main + // Process has the focus. + if ( + pathnameIsInSecurityProcess(pathname) || + mainProcessPathname === `/login` + ) { + securityProcessExports.dispatch(action) + } + + // The Main Process doesn't have a /security-center route. + if (pathname !== `/security-center`) { + mainProcessPathname = pathname + } + + replaceFragment(pathname) + } + } + + const mainProcess = await mainProcessPromise + + mainProcessExports = await mainProcess({ + addLanguageToUrl, + axios: sanitizedAxios, + options, + pathname: window.location.pathname, + rootProcessDispatch: dispatchFromMainProcess, + securityProcess: securityProcessExports, + setForegroundProcess + }) + + processMainActionsQueue() + + window.addEventListener(`popstate`, () => { + const pathname = window.location.hash.slice(1) + + const action = { + type: LOCATION_CHANGE, + meta: { forwarded: true }, + payload: { action: `PUSH`, location: { hash: ``, pathname, search: `` } } + } + + if (pathnameIsInSecurityProcess(pathname)) { + securityProcessExports.dispatch(action) + } else { + mainProcessDispatch(action) + } + + setForegroundProcess() + }) +})().catch(console.error) diff --git a/packages/root-process/src/template.html b/packages/root-process/src/template.html new file mode 100644 index 00000000000..227a56e5a4b --- /dev/null +++ b/packages/root-process/src/template.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + Blockchain Wallet - Exchange Cryptocurrency + + + + + + diff --git a/packages/root-process/webpack.config.ci.js b/packages/root-process/webpack.config.ci.js new file mode 100644 index 00000000000..867aa79232d --- /dev/null +++ b/packages/root-process/webpack.config.ci.js @@ -0,0 +1,49 @@ +'use strict' +const babelConfig = require(`./babel.config.js`) +const CleanWebpackPlugin = require('clean-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const path = require(`path`) +const Webpack = require('webpack') + +const src = path.join(__dirname, `src`) + +module.exports = ({ PATHS }) => ({ + name: `root`, + entry: ['@babel/polyfill', path.join(src, 'index.js')], + module: { + rules: [ + { + test: /\.js$/, + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { loader: 'babel-loader', options: babelConfig } + ] + }, + { + test: /\.(png|jpg|gif|svg|ico|webmanifest|xml)$/, + use: { + loader: 'file-loader', + options: { + name: 'img/[name].[ext]' + } + } + } + ] + }, + output: { + filename: `index.js`, + path: PATHS.ciBuild, + publicPath: '/' + }, + plugins: [ + new CleanWebpackPlugin(), + new HtmlWebpackPlugin({ + template: path.join(src, 'template.html'), + filename: 'index.html' + }), + new Webpack.DefinePlugin({ + MAIN_DOMAIN: `"/main"`, + SECURITY_DOMAIN: `"/security"` + }) + ] +}) diff --git a/packages/root-process/webpack.config.dev.js b/packages/root-process/webpack.config.dev.js new file mode 100644 index 00000000000..2f03a859288 --- /dev/null +++ b/packages/root-process/webpack.config.dev.js @@ -0,0 +1,48 @@ +'use strict' + +const CleanWebpackPlugin = require('clean-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const path = require(`path`) +const Webpack = require('webpack') + +const src = path.join(__dirname, `src`) + +module.exports = ({ localhostUrl, mainDomain, PATHS, securityDomain }) => ({ + name: `root`, + entry: [ + 'react-hot-loader/patch', + `webpack-dev-server/client?${localhostUrl}`, + 'webpack/hot/only-dev-server', + path.join(src, 'index.js') + ], + mode: `development`, + module: { + rules: [ + { + test: /\.(png|jpg|gif|svg|ico|webmanifest|xml)$/, + use: { + loader: 'file-loader', + options: { + name: 'img/[name].[ext]' + } + } + } + ] + }, + output: { + filename: `index.js`, + path: PATHS.appBuild + }, + plugins: [ + new CleanWebpackPlugin(), + new HtmlWebpackPlugin({ + template: path.join(src, 'template.html'), + filename: 'index.html' + }), + new Webpack.DefinePlugin({ + MAIN_DOMAIN: `"http://${mainDomain}"`, + SECURITY_DOMAIN: `"http://${securityDomain}"` + }), + new Webpack.HotModuleReplacementPlugin() + ] +}) diff --git a/packages/root-process/webpack.debug.js b/packages/root-process/webpack.debug.js new file mode 100644 index 00000000000..d074216c37a --- /dev/null +++ b/packages/root-process/webpack.debug.js @@ -0,0 +1,48 @@ +'use strict' +const babelConfig = require(`./babel.config.js`) +const CleanWebpackPlugin = require('clean-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const path = require(`path`) +const Webpack = require('webpack') + +const src = path.join(__dirname, `src`) + +module.exports = ({ localhostUrl, mainDomain, PATHS, securityDomain }) => ({ + name: `root`, + entry: ['@babel/polyfill', path.join(src, 'index.js')], + module: { + rules: [ + { + test: /\.js$/, + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { loader: 'babel-loader', options: babelConfig } + ] + }, + { + test: /\.(png|jpg|gif|svg|ico|webmanifest|xml)$/, + use: { + loader: 'file-loader', + options: { + name: 'img/[name].[ext]' + } + } + } + ] + }, + output: { + filename: `index.js`, + path: PATHS.ciBuild + }, + plugins: [ + new CleanWebpackPlugin(), + new HtmlWebpackPlugin({ + template: path.join(src, 'template.html'), + filename: 'index.html' + }), + new Webpack.DefinePlugin({ + MAIN_DOMAIN: `"http://${mainDomain}"`, + SECURITY_DOMAIN: `"http://${securityDomain}"` + }) + ] +}) diff --git a/packages/security-process/babel.config.js b/packages/security-process/babel.config.js index 7b2631c7211..e339b510b49 100644 --- a/packages/security-process/babel.config.js +++ b/packages/security-process/babel.config.js @@ -1,11 +1,21 @@ +const path = require(`path`) + +const resolve = directory => path.resolve(__dirname, directory) + module.exports = { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: [ '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', 'babel-plugin-styled-components', - ['module-resolver', { root: ['./src'], alias: { data: './src/data' } }], - ['react-intl', { messagesDir: './build/extractedMessages' }] + [ + 'module-resolver', + { + root: [resolve('src')], + alias: { data: resolve('src/data') } + } + ], + ['react-intl', { messagesDir: resolve('build/extractedMessages') }] ], ignore: [], env: { @@ -18,8 +28,14 @@ module.exports = { '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', 'babel-plugin-styled-components', - ['module-resolver', { root: ['./src'], alias: { data: './src/data' } }], - ['react-intl', { messagesDir: './build/extractedMessages' }] + [ + 'module-resolver', + { + root: [resolve('src')], + alias: { data: resolve('src/data') } + } + ], + ['react-intl', { messagesDir: resolve('build/extractedMessages') }] ] }, development: { @@ -31,7 +47,13 @@ module.exports = { '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', 'babel-plugin-styled-components', - ['module-resolver', { root: ['./src'], alias: { data: './src/data' } }], + [ + 'module-resolver', + { + root: [resolve('src')], + alias: { data: resolve('src/data') } + } + ], 'react-hot-loader/babel' ] } diff --git a/packages/security-process/package.json b/packages/security-process/package.json index d304a44ea17..d4f2253ce0e 100644 --- a/packages/security-process/package.json +++ b/packages/security-process/package.json @@ -1,5 +1,5 @@ { - "name": "blockchain-wallet-v4-frontend", + "name": "security-process", "version": "0.1.0", "description": "Frontend wallet application.", "license": "AGPL-3.0-or-later", @@ -19,13 +19,8 @@ "ci:compile": "cross-env-shell NODE_ENV=production webpack-cli --config webpack.config.ci.js --display-error-details", "clean": "cross-env rimraf node_modules && rimraf build", "coverage": "cross-env ./../../node_modules/.bin/jest --coverage", - "debug:prod": "cross-env-shell NODE_ENV=production webpack-dev-server --config webpack.debug.js --progress --colors", "link:resolved:paths": "ln -sf $(pwd)/src/** ./node_modules && ln -sf $(pwd)/../../packages/** ./node_modules", "manage:translations": "yarn build:prod && node ./translationRunner.js", - "start:dev": "cross-env-shell NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", - "start:prod": "cross-env-shell DISABLE_SSL=true NODE_ENV=production webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", - "start:staging": "cross-env-shell NODE_ENV=staging webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", - "start:testnet": "cross-env-shell NODE_ENV=testnet webpack-dev-server --config webpack.config.dev.js --progress --colors --watch --devtool cheap-module-source-map", "test": "cross-env ./../../node_modules/.bin/jest --silent", "test:build": "echo 'No precomplilation required for tests to execute.'", "test:debug": "cross-env node --inspect-brk ./../../node_modules/.bin/jest --runInBand", @@ -143,6 +138,7 @@ "rxjs": "6.5.2", "sanitize-html": "1.20.1", "styled-components": "4.2.0", + "web-microkernel": "1.0.0", "zxcvbn": "4.4.2" } } diff --git a/packages/security-process/src/IPC/Middleware.js b/packages/security-process/src/IPC/Middleware.js new file mode 100644 index 00000000000..ee9ee7b7cc4 --- /dev/null +++ b/packages/security-process/src/IPC/Middleware.js @@ -0,0 +1,71 @@ +import * as router from 'connected-react-router' +import { REHYDRATE } from 'redux-persist' + +import * as coreTypes from 'blockchain-wallet-v4/src/redux/actionTypes' +import * as Wrapper from 'blockchain-wallet-v4/src/types/Wrapper' +import * as types from '../data/actionTypes' + +const alreadyForwarded = ({ meta }) => meta && meta.forwarded + +const dispatchToMainProcess = ({ mainProcessDispatch }, action) => { + mainProcessDispatch(action) +} + +const dispatchToRootProcess = ({ rootProcessDispatch }, action) => { + rootProcessDispatch(action) +} + +const ROOT_LOCATION_CHANGE = ({ rootProcessDispatch }, { payload }) => { + rootProcessDispatch({ type: router.LOCATION_CHANGE, payload }) +} + +const handlers = { + // Dispatched by createRoot which requires the seed. + [coreTypes.kvStore.root.UPDATE_METADATA_ROOT]: dispatchToMainProcess, + + // Dispatched by createXlm which requires the seed. + [coreTypes.kvStore.xlm.CREATE_METADATA_XLM]: dispatchToMainProcess, + + // Report failure of wallet synchronization. + [coreTypes.walletSync.SYNC_ERROR]: dispatchToMainProcess, + + // Report success of wallet synchronization. + [coreTypes.walletSync.SYNC_SUCCESS]: dispatchToMainProcess, + + // We handle persistence for the Main Process. + [REHYDRATE]: dispatchToMainProcess, + + // Report a location change to the Root Process instead of processing it + // ourselves. + ROOT_LOCATION_CHANGE, + + // Inform the Root Process about routing changes so that it can switch the + // appropriate process to the foreground. + [router.LOCATION_CHANGE]: dispatchToRootProcess, + + // Proceed with the login routine after receiving the payload. + [types.auth.AUTHENTICATE]: dispatchToMainProcess +} + +// Used to set the wrapper in /recover. +handlers[coreTypes.wallet.REFRESH_WRAPPER] = ( + { mainProcessDispatch }, + action +) => { + const redactedWrapper = Wrapper.redact(action.payload) + mainProcessDispatch({ ...action, payload: redactedWrapper }) +} + +// Send the wrapper to the Main Process after logging in. +handlers[coreTypes.wallet.SET_WRAPPER] = + handlers[coreTypes.wallet.REFRESH_WRAPPER] + +export default ({ imports }) => () => next => action => { + const { type } = action + + if (!alreadyForwarded(action) && type in handlers) { + handlers[type](imports, action) + } + + return next(action) +} diff --git a/packages/security-process/src/IPC/index.js b/packages/security-process/src/IPC/index.js new file mode 100644 index 00000000000..f047575c961 --- /dev/null +++ b/packages/security-process/src/IPC/index.js @@ -0,0 +1,28 @@ +import { serializer } from 'blockchain-wallet-v4/src/types' +import * as kernel from 'web-microkernel' + +import Middleware from './Middleware' + +export default configureStore => () => + new Promise(async resolve => { + const exportedFunction = async imports => { + const middleware = Middleware({ imports }) + const root = await configureStore({ imports, middleware }) + resolve(root) + + const dispatch = action => + root.store.dispatch({ + ...action, + meta: { ...action.meta, forwarded: true } + }) + + return { dispatch, securityModule: root.securityModule } + } + + const childProcess = await kernel.ChildProcess( + { reviver: serializer.reviver }, + exportedFunction + ) + + childProcess.addEventListener(`error`, console.error) + }) diff --git a/packages/security-process/src/data/actionTypes.js b/packages/security-process/src/data/actionTypes.js index 0b00bb13607..12924bf15e1 100644 --- a/packages/security-process/src/data/actionTypes.js +++ b/packages/security-process/src/data/actionTypes.js @@ -3,11 +3,8 @@ import * as alerts from './alerts/actionTypes' import * as analytics from './analytics/actionTypes' import * as auth from './auth/actionTypes' import * as cache from './cache/actionTypes' -import * as components from './components/actionTypes' import { actionTypes as form } from './form/actionTypes' -import * as goals from './goals/actionTypes' import * as logs from './logs/actionTypes' -import * as middleware from './middleware/actionTypes' import * as modals from './modals/actionTypes' import * as modules from './modules/actionTypes' import * as preferences from './preferences/actionTypes' @@ -18,13 +15,10 @@ export { analytics, cache, core, - components, form, alerts, auth, - goals, logs, - middleware, modals, modules, preferences, diff --git a/packages/security-process/src/data/actions.js b/packages/security-process/src/data/actions.js index df3f3836e4c..8f716519a00 100644 --- a/packages/security-process/src/data/actions.js +++ b/packages/security-process/src/data/actions.js @@ -6,7 +6,6 @@ import * as cache from './cache/actions' import * as components from './components/actions' import * as goals from './goals/actions' import * as logs from './logs/actions' -import * as middleware from './middleware/actions' import * as modals from './modals/actions' import * as modules from './modules/actions' import * as form from './form/actions' @@ -26,7 +25,6 @@ export { goals, logs, form, - middleware, modals, modules, preferences, diff --git a/packages/security-process/src/data/auth/actionTypes.js b/packages/security-process/src/data/auth/actionTypes.js index 0de985229bb..14e453788a5 100644 --- a/packages/security-process/src/data/auth/actionTypes.js +++ b/packages/security-process/src/data/auth/actionTypes.js @@ -3,6 +3,7 @@ export const DEAUTHORIZE_BROWSER = 'DEAUTHORIZE_BROWSER' export const LOGIN = 'LOGIN' export const LOGIN_FAILURE = 'LOGIN_FAILURE' export const LOGIN_LOADING = 'LOGIN_LOADING' +export const LOGIN_ROUTINE = 'LOGIN_ROUTINE' export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' export const LOGOUT = 'LOGOUT' export const LOGOUT_CLEAR_REDUX_STORE = 'LOGOUT_CLEAR_REDUX_STORE' diff --git a/packages/security-process/src/data/auth/actions.js b/packages/security-process/src/data/auth/actions.js index 63600f6a11a..f1845c62e56 100644 --- a/packages/security-process/src/data/auth/actions.js +++ b/packages/security-process/src/data/auth/actions.js @@ -7,6 +7,12 @@ export const login = (guid, password, code, sharedKey, mobileLogin) => ({ payload: { guid, password, code, sharedKey, mobileLogin } }) export const loginLoading = () => ({ type: AT.LOGIN_LOADING }) + +export const loginRoutine = (mobileLogin = false, firstLogin = false) => ({ + type: AT.LOGIN_ROUTINE, + payload: { firstLogin, mobileLogin } +}) + export const loginSuccess = () => ({ type: AT.LOGIN_SUCCESS, payload: {} }) export const loginFailure = err => ({ type: AT.LOGIN_FAILURE, diff --git a/packages/security-process/src/data/auth/sagaRegister.js b/packages/security-process/src/data/auth/sagaRegister.js index 7bff80754c1..6d1e80ba8dc 100644 --- a/packages/security-process/src/data/auth/sagaRegister.js +++ b/packages/security-process/src/data/auth/sagaRegister.js @@ -2,12 +2,13 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, coreSagas }) => { - const authSagas = sagas({ api, coreSagas }) +export default (...args) => { + const authSagas = sagas(...args) return function * authSaga () { yield takeLatest(AT.DEAUTHORIZE_BROWSER, authSagas.deauthorizeBrowser) yield takeLatest(AT.LOGIN, authSagas.login) + yield takeLatest(AT.LOGIN_ROUTINE, authSagas.loginRoutineSaga) yield takeLatest(AT.LOGOUT, authSagas.logout) yield takeLatest( AT.LOGOUT_CLEAR_REDUX_STORE, diff --git a/packages/security-process/src/data/auth/sagas.js b/packages/security-process/src/data/auth/sagas.js index cde0d1b4948..8f8c2bc9f1a 100644 --- a/packages/security-process/src/data/auth/sagas.js +++ b/packages/security-process/src/data/auth/sagas.js @@ -5,7 +5,6 @@ import * as C from 'services/AlertService' import * as CC from 'services/ConfirmService' import { actions, actionTypes, model, selectors } from 'data' import { - askSecondPasswordEnhancer, confirm, promptForSecondPassword, forceSyncWallet @@ -28,7 +27,7 @@ export const wrongAuthCodeErrorMessage = 'Authentication code is incorrect' const { LOGIN_EVENTS } = model.analytics -export default ({ api, coreSagas }) => { +export default ({ api, coreSagas, imports }) => { const upgradeWallet = function * () { try { let password = yield call(promptForSecondPassword) @@ -109,7 +108,8 @@ export default ({ api, coreSagas }) => { )).getOrElse(false) if (userFlowSupported) yield put(actions.modules.profile.signIn()) } - const loginRoutineSaga = function * (mobileLogin, firstLogin) { + + const loginRoutineSaga = function * ({ payload: { mobileLogin, firstLogin } }) { try { // If needed, the user should upgrade its wallet before being able to open the wallet const isHdWallet = yield select(selectors.core.wallet.isHdWallet) @@ -117,42 +117,17 @@ export default ({ api, coreSagas }) => { yield call(upgradeWalletSaga) } yield put(actions.auth.authenticate()) - yield call(coreSagas.kvStore.root.fetchRoot, askSecondPasswordEnhancer) - // If there was no eth metadata kv store entry, we need to create one and that requires the second password. - yield call( - coreSagas.kvStore.eth.fetchMetadataEth, - askSecondPasswordEnhancer - ) - yield call( - coreSagas.kvStore.xlm.fetchMetadataXlm, - askSecondPasswordEnhancer + + imports.mainProcessDispatch( + actions.auth.loginRoutine(mobileLogin, firstLogin) ) - yield call(coreSagas.kvStore.bch.fetchMetadataBch) - yield call(coreSagas.kvStore.lockbox.fetchMetadataLockbox) - yield put(actions.router.push('/home')) - yield call(coreSagas.settings.fetchSettings) - yield call(coreSagas.data.xlm.fetchLedgerDetails) - yield call(coreSagas.data.xlm.fetchData) - yield call(authNabu) - yield call(upgradeAddressLabelsSaga) - yield put(actions.auth.loginSuccess()) - yield put(actions.auth.startLogoutTimer()) - yield call(startSockets) + const guid = yield select(selectors.core.wallet.getGuid) // store guid in cache for future login yield put(actions.cache.guidEntered(guid)) // reset auth type and clear previous login form state yield put(actions.auth.setAuthType(0)) yield put(actions.form.destroy('login')) - // set payload language to settings language - const language = yield select(selectors.preferences.getLanguage) - yield put(actions.modules.settings.updateLanguage(language)) - yield put(actions.analytics.initUserSession()) - yield fork(transferEthSaga) - yield call(saveGoals, firstLogin) - yield put(actions.goals.runGoals()) - yield fork(checkDataErrors) - yield fork(logoutRoutine, yield call(setLogoutEventListener)) } catch (e) { yield put( actions.logs.logErrorMessage(logLocation, 'loginRoutineSaga', e) @@ -224,7 +199,7 @@ export default ({ api, coreSagas }) => { password, code }) - yield call(loginRoutineSaga, mobileLogin) + yield put(actions.auth.loginRoutine(mobileLogin)) } catch (error) { const initialError = prop('initial_error', error) const authRequired = prop('authorization_required', error) @@ -247,7 +222,7 @@ export default ({ api, coreSagas }) => { session, password }) - yield call(loginRoutineSaga, mobileLogin) + yield put(actions.auth.loginRoutine(mobileLogin)) } catch (error) { if (error && error.auth_type > 0) { yield put(actions.auth.setAuthType(error.auth_type)) @@ -333,7 +308,7 @@ export default ({ api, coreSagas }) => { yield put(actions.auth.registerLoading()) yield call(coreSagas.wallet.createWalletSaga, action.payload) yield put(actions.alerts.displaySuccess(C.REGISTER_SUCCESS)) - yield call(loginRoutineSaga, false, true) + yield put(actions.auth.loginRoutine(false, true)) yield put(actions.auth.registerSuccess()) } catch (e) { yield put(actions.auth.registerFailure()) @@ -347,7 +322,7 @@ export default ({ api, coreSagas }) => { yield put(actions.alerts.displayInfo(C.RESTORE_WALLET_INFO)) yield call(coreSagas.wallet.restoreWalletSaga, action.payload) yield put(actions.alerts.displaySuccess(C.RESTORE_SUCCESS)) - yield call(loginRoutineSaga, false, true) + yield put(actions.auth.loginRoutine(false, true)) yield put(actions.auth.restoreSuccess()) } catch (e) { yield put(actions.auth.restoreFailure()) @@ -456,12 +431,7 @@ export default ({ api, coreSagas }) => { )).getOrElse(false) if (userFlowSupported) { yield put(actions.modules.profile.clearSession()) - yield put(actions.middleware.webSocket.rates.stopSocket()) } - yield put(actions.middleware.webSocket.bch.stopSocket()) - yield put(actions.middleware.webSocket.btc.stopSocket()) - yield put(actions.middleware.webSocket.eth.stopSocket()) - yield put(actions.middleware.webSocket.xlm.stopStreams()) // only show browser de-auth page to accounts with verified email isEmailVerified.getOrElse(0) ? yield put(actions.router.push('/logout')) @@ -485,8 +455,7 @@ export default ({ api, coreSagas }) => { } const logoutClearReduxStore = function * () { // router will fallback to /login route - yield window.history.pushState('', '', '#') - yield window.location.reload(true) + yield imports.logout() } return { diff --git a/packages/security-process/src/data/auth/sagas.spec.js b/packages/security-process/src/data/auth/sagas.spec.js index a5dedcd54f8..96ee9c782a3 100644 --- a/packages/security-process/src/data/auth/sagas.spec.js +++ b/packages/security-process/src/data/auth/sagas.spec.js @@ -1,8 +1,8 @@ import { select } from 'redux-saga/effects' import { expectSaga, testSaga } from 'redux-saga-test-plan' -import { fork, call } from 'redux-saga-test-plan/matchers' +import { call } from 'redux-saga-test-plan/matchers' -import { askSecondPasswordEnhancer, confirm } from 'services/SagaService' +import { confirm } from 'services/SagaService' import { coreSagasFactory, Remote } from 'blockchain-wallet-v4/src' import * as selectors from '../selectors' import * as actions from '../actions' @@ -30,25 +30,15 @@ const VULNERABLE_ADDRESS = '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH' describe('authSagas', () => { // Mocking Math.random() to have identical popup ids for action testing const originalMath = Object.create(Math) - let pushStateSpy - let locationReloadSpy beforeAll(() => { Math.random = () => 0.5 - pushStateSpy = jest - .spyOn(window.history, 'pushState') - .mockImplementation(() => {}) - locationReloadSpy = jest - .spyOn(window.location, 'reload') - .mockImplementation(() => {}) }) afterAll(() => { global.Math = originalMath - pushStateSpy.restore() - locationReloadSpy.restore() }) describe('login flow', () => { - const { login, loginRoutineSaga, pollingSession } = authSagas({ + const { login, pollingSession } = authSagas({ api, coreSagas }) @@ -93,11 +83,11 @@ describe('authSagas', () => { }) }) - it('should call login routine', () => { + it('should dispatch login routine action', () => { const { mobileLogin } = payload saga .next() - .call(loginRoutineSaga, mobileLogin) + .put(actions.auth.loginRoutine(mobileLogin)) .next() .isDone() }) @@ -185,9 +175,9 @@ describe('authSagas', () => { }) }) - it('should call login routine', () => { + it('should put login routine', () => { const { mobileLogin } = payload - saga.next().call(loginRoutineSaga, mobileLogin) + saga.next().put(actions.auth.loginRoutine(mobileLogin)) }) it('should follow 2FA flow on auth error', () => { @@ -335,104 +325,14 @@ describe('authSagas', () => { }) describe('login routine', () => { - const { - authNabu, - checkDataErrors, - loginRoutineSaga, - logoutRoutine, - saveGoals, - setLogoutEventListener, - startSockets, - transferEthSaga, - upgradeWalletSaga, - upgradeAddressLabelsSaga - } = authSagas({ + const { loginRoutineSaga } = authSagas({ api, coreSagas }) - const mobileLogin = true - const firstLogin = false - const saga = testSaga(loginRoutineSaga, mobileLogin, firstLogin) - const beforeHdCheck = 'beforeHdCheck' - - it('should check if wallet is an hd wallet', () => { - saga - .next() - .select(selectors.core.wallet.isHdWallet) - .save(beforeHdCheck) - }) - - it('should call upgradeWalletSaga if wallet is not hd', () => { - saga - .next(false) - .call(upgradeWalletSaga) - .restore(beforeHdCheck) - }) + const saga = testSaga(loginRoutineSaga) it('should put authenticate action', () => { - saga.next(true).put(actions.auth.authenticate()) - }) - - it('should fetch root', () => { - saga - .next() - .call(coreSagas.kvStore.root.fetchRoot, askSecondPasswordEnhancer) - }) - - it('should fetch eth metadata', () => { - saga - .next() - .call(coreSagas.kvStore.eth.fetchMetadataEth, askSecondPasswordEnhancer) - }) - - it('should fetch xlm metadata', () => { - saga - .next() - .call(coreSagas.kvStore.xlm.fetchMetadataXlm, askSecondPasswordEnhancer) - }) - - it('should fetch bch metadata', () => { - saga.next().call(coreSagas.kvStore.bch.fetchMetadataBch) - }) - - it('should fetch lockbox metadata', () => { - saga.next().call(coreSagas.kvStore.lockbox.fetchMetadataLockbox) - }) - - it('should redirect to home route', () => { - saga.next().put(actions.router.push('/home')) - }) - - it('should fetch settings', () => { - saga.next().call(coreSagas.settings.fetchSettings) - }) - - it('should fetch xlm ledger details', () => { - saga.next().call(coreSagas.data.xlm.fetchLedgerDetails) - }) - - it('should fetch xlm accounts', () => { - saga.next().call(coreSagas.data.xlm.fetchData) - }) - - it('should call auth nabu saga', () => { - saga.next().call(authNabu) - }) - - it('should call upgrade address labels saga', () => { - saga.next().call(upgradeAddressLabelsSaga) - }) - - it('should trigger login success action', () => { - saga.next().put(actions.auth.loginSuccess()) - }) - - it('should start logout timer', () => { - saga.next().put(actions.auth.startLogoutTimer()) - }) - - it('should start sockets', () => { - saga.next().call(startSockets) + saga.next().put(actions.auth.authenticate()) }) it('should select guid from state', () => { @@ -452,60 +352,6 @@ describe('authSagas', () => { saga.next().put(actions.form.destroy('login')) }) - it('should select current language', () => { - saga.next().select(selectors.preferences.getLanguage) - }) - - it('should trigger update language action with selected language', () => { - const language = 'en' - saga.next(language).put(actions.modules.settings.updateLanguage(language)) - }) - - it('should init analytics user session', () => { - saga.next().put(actions.analytics.initUserSession()) - }) - - it('should launch transferEth saga', () => { - saga.next().fork(transferEthSaga) - }) - - it('should save goals', () => { - saga.next().call(saveGoals, false) - }) - - it('should run goals', () => { - saga.next().put(actions.goals.runGoals()) - }) - - it('should check for data errors', () => { - saga.next().fork(checkDataErrors) - }) - - it('should start listening for logout event', () => { - saga.next().call(setLogoutEventListener) - }) - - it('should launch logout routine saga upon logout event', () => { - const stubLogoutEvent = {} - saga.next(stubLogoutEvent).fork(logoutRoutine, stubLogoutEvent) - }) - - it("should not display success if it's first login", () => { - const firstLogin = true - return expectSaga(loginRoutineSaga, mobileLogin, firstLogin) - .provide([ - // Every async or value returning yield has to be mocked - // for saga to progress - [select(selectors.core.wallet.isHdWallet), true], - [select(selectors.core.wallet.getGuid), 12], - [fork.fn(transferEthSaga), jest.fn], - [call.fn(setLogoutEventListener), jest.fn], - [fork.fn(logoutRoutine), jest.fn] - ]) - .not.put(actions.alerts.displaySuccess(C.LOGIN_SUCCESS)) - .run() - }) - describe('error handling', () => { it('should log error', () => { const error = {} @@ -527,7 +373,7 @@ describe('authSagas', () => { }) describe('register flow', () => { - const { loginRoutineSaga, register } = authSagas({ + const { register } = authSagas({ api, coreSagas }) @@ -550,10 +396,10 @@ describe('authSagas', () => { saga.next().put(actions.alerts.displaySuccess(C.REGISTER_SUCCESS)) }) - it('should call login routine saga with falsy mobileLogin and truthy firstLogin', () => { + it('should dispatch login routine action with falsy mobileLogin and truthy firstLogin', () => { const mobileLogin = false const firstLogin = true - saga.next().call(loginRoutineSaga, mobileLogin, firstLogin) + saga.next().put(actions.auth.loginRoutine(mobileLogin, firstLogin)) }) it('should finally trigger action that restore is successful', () => { @@ -586,7 +432,7 @@ describe('authSagas', () => { }) describe('restore flow', () => { - const { loginRoutineSaga, restore } = authSagas({ + const { restore } = authSagas({ api, coreSagas }) @@ -615,10 +461,10 @@ describe('authSagas', () => { saga.next().put(actions.alerts.displaySuccess(C.RESTORE_SUCCESS)) }) - it('should call login routine saga with falsy mobileLogin and truthy firstLogin', () => { + it('should dispatch a login routine saga with falsy mobileLogin and truthy firstLogin', () => { const mobileLogin = false const firstLogin = true - saga.next().call(loginRoutineSaga, mobileLogin, firstLogin) + saga.next().put(actions.auth.loginRoutine(mobileLogin, firstLogin)) }) it('should finally trigger action that restore is successful', () => { @@ -990,21 +836,11 @@ describe('authSagas', () => { describe('logout routine', () => { const { logout } = authSagas({ api, - coreSagas + coreSagas, + imports: { logout: jest.fn() } }) - it('should stop rates scoket if user flow is supported', () => { - return expectSaga(logout) - .provide([ - [select(selectors.core.settings.getEmailVerified), Remote.of(true)], - [select(selectors.modules.profile.userFlowSupported), Remote.of(true)] - ]) - .put(actions.modules.profile.clearSession()) - .put(actions.middleware.webSocket.rates.stopSocket()) - .run() - }) - - it('should not stop rates scoket if user flow is supported', () => { + it('should redirect to logout if email is verified', () => { return expectSaga(logout) .provide([ [select(selectors.core.settings.getEmailVerified), Remote.of(true)], @@ -1013,78 +849,20 @@ describe('authSagas', () => { Remote.of(false) ] ]) - .not.put(actions.modules.profile.clearSession()) - .not.put(actions.middleware.webSocket.rates.stopSocket()) - .run() - }) - - it('should stop sockets and redirect to logout if email is verified', () => { - return expectSaga(logout) - .provide([ - [select(selectors.core.settings.getEmailVerified), Remote.of(true)], - [ - select(selectors.modules.profile.userFlowSupported), - Remote.of(false) - ] - ]) - .put(actions.middleware.webSocket.bch.stopSocket()) - .put(actions.middleware.webSocket.btc.stopSocket()) - .put(actions.middleware.webSocket.eth.stopSocket()) - .put(actions.middleware.webSocket.xlm.stopStreams()) .put(actions.router.push('/logout')) .run() }) - - it('should stop sockets and clear redux store if email is not verified', async () => { - return expectSaga(logout) - .provide([ - [select(selectors.core.settings.getEmailVerified), Remote.of(false)], - [ - select(selectors.modules.profile.userFlowSupported), - Remote.of(false) - ] - ]) - .put(actions.middleware.webSocket.bch.stopSocket()) - .put(actions.middleware.webSocket.btc.stopSocket()) - .put(actions.middleware.webSocket.eth.stopSocket()) - .put(actions.middleware.webSocket.xlm.stopStreams()) - .run() - .then(() => { - expect(pushStateSpy).toHaveBeenCalledTimes(1) - expect(pushStateSpy).toHaveBeenCalledWith('', '', '#') - }) - }) }) describe('deauthorization of browser', () => { const { deauthorizeBrowser } = authSagas({ api, - coreSagas + coreSagas, + imports: { logout: jest.fn() } }) const saga = testSaga(deauthorizeBrowser) const beforeCatch = 'beforeCatch' - const pageReloadTest = () => - it('should push login to url to history and reload window', () => { - pushStateSpy.mockReset() - locationReloadSpy.mockReset() - - saga - .next() - .inspect(gen => { - // Inside the called saga - gen.next() - expect(pushStateSpy).toHaveBeenCalledTimes(1) - expect(pushStateSpy).toHaveBeenCalledWith('', '', '#') - - gen.next() - expect(locationReloadSpy).toHaveBeenCalledTimes(1) - expect(locationReloadSpy).toHaveBeenCalledWith(true) - }) - .next() - .isDone() - }) - it('should select guid', () => { saga.next().select(selectors.core.wallet.getGuid) }) @@ -1106,8 +884,6 @@ describe('authSagas', () => { .save(beforeCatch) }) - pageReloadTest() - describe('error handling', () => { beforeAll(() => { saga.restore(beforeCatch) @@ -1131,8 +907,6 @@ describe('authSagas', () => { .next() .put(actions.alerts.displayError(C.DEAUTHORIZE_BROWSER_ERROR)) }) - - pageReloadTest() }) }) diff --git a/packages/security-process/src/data/components/actions.js b/packages/security-process/src/data/components/actions.js index c01e1ebdf3d..c1b4d155991 100644 --- a/packages/security-process/src/data/components/actions.js +++ b/packages/security-process/src/data/components/actions.js @@ -1,65 +1,3 @@ -import * as activityList from './activityList/actions' -import * as bchTransactions from './bchTransactions/actions' -import * as btcTransactions from './btcTransactions/actions' -import * as coinify from './coinify/actions' -import * as ethTransactions from './ethTransactions/actions' -import * as xlmTransactions from './xlmTransactions/actions' -import * as exchange from './exchange/actions' -import * as exchangeHistory from './exchangeHistory/actions' -import * as identityVerification from './identityVerification/actions' -import * as importBtcAddress from './importBtcAddress/actions' -import * as layoutWallet from './layoutWallet/actions' -import * as lockbox from './lockbox/actions' -import * as manageAddresses from './manageAddresses/actions' -import * as onboarding from './onboarding/actions' -import * as onfido from './onfido/actions' import * as priceChart from './priceChart/actions' -import * as priceTicker from './priceTicker/actions' -import * as refresh from './refresh/actions' -import * as requestBtc from './requestBtc/actions' -import * as requestBch from './requestBch/actions' -import * as requestEth from './requestEth/actions' -import * as requestXlm from './requestXlm/actions' -import * as sendBch from './sendBch/actions' -import * as sendBtc from './sendBtc/actions' -import * as sendEth from './sendEth/actions' -import * as sendXlm from './sendXlm/actions' -import * as settings from './settings/actions' -import * as signMessage from './signMessage/actions' -import * as transactionReport from './transactionReport/actions' -import * as uploadDocuments from './uploadDocuments/actions' -import * as veriff from './veriff/actions' -export { - activityList, - bchTransactions, - btcTransactions, - coinify, - ethTransactions, - xlmTransactions, - exchange, - exchangeHistory, - identityVerification, - importBtcAddress, - manageAddresses, - onboarding, - onfido, - layoutWallet, - lockbox, - priceChart, - priceTicker, - refresh, - requestBtc, - requestBch, - requestEth, - requestXlm, - sendBch, - sendBtc, - sendEth, - sendXlm, - settings, - signMessage, - transactionReport, - uploadDocuments, - veriff -} +export { priceChart } diff --git a/packages/security-process/src/data/components/sagaRegister.js b/packages/security-process/src/data/components/sagaRegister.js index 3fb331706d6..994cd40585a 100644 --- a/packages/security-process/src/data/components/sagaRegister.js +++ b/packages/security-process/src/data/components/sagaRegister.js @@ -1,65 +1,7 @@ import { fork } from 'redux-saga/effects' -import activityList from './activityList/sagaRegister' -import bchTransactions from './bchTransactions/sagaRegister' -import btcTransactions from './btcTransactions/sagaRegister' -import coinify from './coinify/sagaRegister' -import ethTransactions from './ethTransactions/sagaRegister' -import xlmTransactions from './xlmTransactions/sagaRegister' -import exchange from './exchange/sagaRegister' -import exchangeHistory from './exchangeHistory/sagaRegister' -import identityVerification from './identityVerification/sagaRegister' -import importBtcAddress from './importBtcAddress/sagaRegister' -import lockbox from './lockbox/sagaRegister' -import manageAddresses from './manageAddresses/sagaRegister' -import onboarding from './onboarding/sagaRegister' -import onfido from './onfido/sagaRegister' import priceChart from './priceChart/sagaRegister' -import priceTicker from './priceTicker/sagaRegister' -import refresh from './refresh/sagaRegister' -import requestBtc from './requestBtc/sagaRegister' -import requestBch from './requestBch/sagaRegister' -import requestEth from './requestEth/sagaRegister' -import requestXlm from './requestXlm/sagaRegister' -import sendBch from './sendBch/sagaRegister' -import sendBtc from './sendBtc/sagaRegister' -import sendEth from './sendEth/sagaRegister' -import sendXlm from './sendXlm/sagaRegister' -import settings from './settings/sagaRegister' -import signMessage from './signMessage/sagaRegister' -import transactionReport from './transactionReport/sagaRegister' -import uploadDocuments from './uploadDocuments/sagaRegister' -import veriff from './veriff/sagaRegister' export default ({ api, coreSagas, networks }) => function * componentsSaga () { - yield fork(activityList()) - yield fork(bchTransactions()) - yield fork(btcTransactions()) - yield fork(coinify({ api, coreSagas, networks })) - yield fork(ethTransactions()) - yield fork(xlmTransactions()) - yield fork(exchange({ api, coreSagas, networks })) - yield fork(exchangeHistory({ api, coreSagas })) - yield fork(identityVerification({ api, coreSagas })) - yield fork(lockbox({ api, coreSagas })) - yield fork(importBtcAddress({ api, coreSagas, networks })) - yield fork(manageAddresses({ api, networks })) - yield fork(onboarding()) - yield fork(onfido({ api, coreSagas })) yield fork(priceChart({ coreSagas })) - yield fork(priceTicker({ coreSagas })) - yield fork(refresh()) - yield fork(requestBtc({ networks })) - yield fork(requestBch({ networks })) - yield fork(requestEth({ networks })) - yield fork(requestXlm()) - yield fork(sendBch({ coreSagas, networks })) - yield fork(sendBtc({ coreSagas, networks })) - yield fork(sendEth({ api, coreSagas, networks })) - yield fork(sendXlm({ api, coreSagas })) - yield fork(settings({ coreSagas })) - yield fork(signMessage({ coreSagas })) - yield fork(transactionReport({ coreSagas })) - yield fork(uploadDocuments({ api })) - yield fork(veriff({ api, coreSagas })) } diff --git a/packages/security-process/src/data/goals/sagas.spec.js b/packages/security-process/src/data/goals/sagas.spec.js index 8a196711908..7197b99c1b4 100644 --- a/packages/security-process/src/data/goals/sagas.spec.js +++ b/packages/security-process/src/data/goals/sagas.spec.js @@ -371,41 +371,6 @@ describe('goals sagas', () => { }) }) - describe('runKycGoal saga', () => { - it('should not show kyc if current tier is >= goal tier', () => { - const saga = testSaga(runKycGoal, { id: mockGoalId, data: { tier: 2 } }) - - saga - .next() - .put(actions.goals.deleteGoal(mockGoalId)) - .next() - .call(waitForUserData) - .next() - .select(selectors.modules.profile.getUserTiers) - .next(Remote.of({ current: 2 })) - .isDone() - }) - - it('should show kyc if current tier is < goal tier', () => { - const goalTier = 2 - const saga = testSaga(runKycGoal, { - id: mockGoalId, - data: { tier: goalTier } - }) - saga - .next() - .put(actions.goals.deleteGoal(mockGoalId)) - .next() - .call(waitForUserData) - .next() - .select(selectors.modules.profile.getUserTiers) - .next(Remote.of({ current: 1 })) - .put(actions.components.identityVerification.verifyIdentity(goalTier)) - .next() - .isDone() - }) - }) - describe('runSwapGetStartedGoal saga', () => { it('should not show modal if it has already been seen', () => { const saga = testSaga(runSwapGetStartedGoal, { id: mockGoalId }) diff --git a/packages/security-process/src/data/model.js b/packages/security-process/src/data/model.js index 1f1a5645183..be167d78dc8 100644 --- a/packages/security-process/src/data/model.js +++ b/packages/security-process/src/data/model.js @@ -1,8 +1,6 @@ import * as analytics from './analytics/model' -import * as components from './components/model' import * as form from './form/model' import * as logs from './logs/model' import * as profile from './modules/profile/model' -import * as rates from './modules/rates/model' -export { analytics, components, form, logs, profile, rates } +export { analytics, form, logs, profile } diff --git a/packages/security-process/src/data/modules/actionTypes.js b/packages/security-process/src/data/modules/actionTypes.js index 46a21d9a2e8..c16a6c816a6 100644 --- a/packages/security-process/src/data/modules/actionTypes.js +++ b/packages/security-process/src/data/modules/actionTypes.js @@ -1,17 +1,4 @@ -import * as addressesBch from './addressesBch/actionTypes' -import * as profile from './profile/actionTypes' -import * as rates from './rates/actionTypes' import * as settings from './settings/actionTypes' import * as securityCenter from './securityCenter/actionTypes' -import * as transferEth from './transferEth/actionTypes' -import * as sfox from './sfox/actionTypes' -export { - addressesBch, - profile, - rates, - settings, - securityCenter, - transferEth, - sfox -} +export { settings, securityCenter } diff --git a/packages/security-process/src/data/modules/actions.js b/packages/security-process/src/data/modules/actions.js index 8b38e8dccf9..ef7ee4890db 100644 --- a/packages/security-process/src/data/modules/actions.js +++ b/packages/security-process/src/data/modules/actions.js @@ -1,17 +1,5 @@ -import * as addressesBch from './addressesBch/actions' import * as profile from './profile/actions' -import * as rates from './rates/actions' import * as settings from './settings/actions' import * as securityCenter from './securityCenter/actions' -import * as transferEth from './transferEth/actions' -import * as sfox from './sfox/actions' -export { - addressesBch, - profile, - rates, - settings, - securityCenter, - transferEth, - sfox -} +export { profile, settings, securityCenter } diff --git a/packages/security-process/src/data/modules/sagaRegister.js b/packages/security-process/src/data/modules/sagaRegister.js index d4d3986916f..52ef9761af5 100644 --- a/packages/security-process/src/data/modules/sagaRegister.js +++ b/packages/security-process/src/data/modules/sagaRegister.js @@ -1,19 +1,11 @@ import { fork } from 'redux-saga/effects' -import addressesBch from './addressesBch/sagaRegister' import profile from './profile/sagaRegister' -import rates from './rates/sagaRegister' import settings from './settings/sagaRegister' import securityCenter from './securityCenter/sagaRegister' -import transferEth from './transferEth/sagaRegister' -import sfox from './sfox/sagaRegister' -export default ({ api, coreSagas, networks }) => +export default (...args) => function * modulesSaga () { - yield fork(addressesBch({ coreSagas, networks })) - yield fork(profile({ api, coreSagas, networks })) - yield fork(rates({ api })) - yield fork(settings({ api, coreSagas })) - yield fork(securityCenter({ coreSagas })) - yield fork(transferEth({ coreSagas, networks })) - yield fork(sfox({ api, coreSagas, networks })) + yield fork(profile(...args)) + yield fork(settings(...args)) + yield fork(securityCenter(...args)) } diff --git a/packages/security-process/src/data/modules/sagas.js b/packages/security-process/src/data/modules/sagas.js index 00b1640f76d..c1004a8d461 100644 --- a/packages/security-process/src/data/modules/sagas.js +++ b/packages/security-process/src/data/modules/sagas.js @@ -1,17 +1,7 @@ -import addressesBch from './addressesBch/sagas' -import profile from './profile/sagas' -import rates from './rates/sagas' import settings from './settings/sagas' import securityCenter from './securityCenter/sagas' -import transferEth from './transferEth/sagas' -import sfox from './sfox/sagas' -export default ({ api, coreSagas, networks }) => ({ - addressesBch: addressesBch({ coreSagas }), - profile: profile({ api, coreSagas, networks }), - rates: rates({ api }), - settings: settings({ api, coreSagas }), - securityCenter: securityCenter({ coreSagas }), - transferEth: transferEth({ coreSagas, networks }), - sfox: sfox({ api, coreSagas }) +export default (...args) => ({ + settings: settings(...args), + securityCenter: securityCenter(...args) }) diff --git a/packages/security-process/src/data/modules/selectors.js b/packages/security-process/src/data/modules/selectors.js index 195f92e0ac1..54292cb538f 100644 --- a/packages/security-process/src/data/modules/selectors.js +++ b/packages/security-process/src/data/modules/selectors.js @@ -1,5 +1,3 @@ import * as profile from './profile/selectors' -import * as rates from './rates/selectors' -import * as sfox from './sfox/selectors' -export { profile, rates, sfox } +export { profile } diff --git a/packages/security-process/src/data/modules/settings/actions.js b/packages/security-process/src/data/modules/settings/actions.js index 8618fa91037..50addc82bb9 100644 --- a/packages/security-process/src/data/modules/settings/actions.js +++ b/packages/security-process/src/data/modules/settings/actions.js @@ -38,11 +38,6 @@ export const verifyMobile = code => ({ payload: { code } }) -export const updateLanguage = language => ({ - type: AT.UPDATE_LANGUAGE, - payload: { language } -}) - export const updateCurrency = currency => ({ type: AT.UPDATE_CURRENCY, payload: { currency } diff --git a/packages/security-process/src/data/modules/settings/sagaRegister.js b/packages/security-process/src/data/modules/settings/sagaRegister.js index 7c093289516..16ba971f84f 100644 --- a/packages/security-process/src/data/modules/settings/sagaRegister.js +++ b/packages/security-process/src/data/modules/settings/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default ({ api, coreSagas }) => { - const settingsSagas = sagas({ api, coreSagas }) +export default (...args) => { + const settingsSagas = sagas(...args) return function * settingsModuleSaga () { yield takeLatest(AT.INIT_SETTINGS_INFO, settingsSagas.initSettingsInfo) @@ -19,7 +19,6 @@ export default ({ api, coreSagas }) => { yield takeLatest(AT.UPDATE_MOBILE, settingsSagas.updateMobile) yield takeLatest(AT.RESEND_MOBILE, settingsSagas.resendMobile) yield takeLatest(AT.VERIFY_MOBILE, settingsSagas.verifyMobile) - yield takeLatest(AT.UPDATE_LANGUAGE, settingsSagas.updateLanguage) yield takeLatest(AT.UPDATE_CURRENCY, settingsSagas.updateCurrency) yield takeLatest(AT.UPDATE_AUTO_LOGOUT, settingsSagas.updateAutoLogout) yield takeLatest(AT.UPDATE_LOGGING_LEVEL, settingsSagas.updateLoggingLevel) @@ -44,8 +43,5 @@ export default ({ api, coreSagas }) => { settingsSagas.enableTwoStepYubikey ) yield takeLatest(AT.NEW_HD_ACCOUNT, settingsSagas.newHDAccount) - yield takeLatest(AT.SHOW_BTC_PRIV_KEY, settingsSagas.showBtcPrivateKey) - yield takeLatest(AT.SHOW_ETH_PRIV_KEY, settingsSagas.showEthPrivateKey) - yield takeLatest(AT.SHOW_XLM_PRIV_KEY, settingsSagas.showXlmPrivateKey) } } diff --git a/packages/security-process/src/data/modules/settings/sagas.js b/packages/security-process/src/data/modules/settings/sagas.js index 325a4145031..d1b0cf40b8d 100644 --- a/packages/security-process/src/data/modules/settings/sagas.js +++ b/packages/security-process/src/data/modules/settings/sagas.js @@ -3,12 +3,7 @@ import profileSagas from 'data/modules/profile/sagas' import * as actions from '../../actions' import * as selectors from '../../selectors' import * as C from 'services/AlertService' -import { addLanguageToUrl } from 'services/LocalesService' -import { - askSecondPasswordEnhancer, - promptForSecondPassword -} from 'services/SagaService' -import { Types, utils } from 'blockchain-wallet-v4/src' +import { askSecondPasswordEnhancer } from 'services/SagaService' import { contains, toLower, prop, propEq, head } from 'ramda' export const taskToPromise = t => @@ -137,17 +132,6 @@ export default ({ api, coreSagas }) => { } } - // We prefer local storage language and update this in background for - // things like emails and external communication with the user - const updateLanguage = function * (action) { - try { - yield call(coreSagas.settings.setLanguage, action.payload) - addLanguageToUrl(action.payload.language) - } catch (e) { - yield put(actions.logs.logErrorMessage(logLocation, 'updateLanguage', e)) - } - } - const updateCurrency = function * (action) { try { yield call(coreSagas.settings.setCurrency, action.payload) @@ -307,65 +291,6 @@ export default ({ api, coreSagas }) => { yield put(actions.modals.closeModal()) } - const showBtcPrivateKey = function * (action) { - const { addr } = action.payload - try { - const password = yield call(promptForSecondPassword) - const wallet = yield select(selectors.core.wallet.getWallet) - const privT = Types.Wallet.getPrivateKeyForAddress(wallet, password, addr) - const priv = yield call(() => taskToPromise(privT)) - yield put(actions.modules.settings.addShownBtcPrivateKey(priv)) - } catch (e) { - yield put( - actions.logs.logErrorMessage(logLocation, 'showBtcPrivateKey', e) - ) - } - } - - const showEthPrivateKey = function * (action) { - const { isLegacy } = action.payload - try { - const password = yield call(promptForSecondPassword) - if (isLegacy) { - const getSeedHex = state => - selectors.core.wallet.getSeedHex(state, password) - const seedHexT = yield select(getSeedHex) - const seedHex = yield call(() => taskToPromise(seedHexT)) - const legPriv = utils.eth.getLegacyPrivateKey(seedHex).toString('hex') - yield put(actions.modules.settings.addShownEthPrivateKey(legPriv)) - } else { - const getMnemonic = state => - selectors.core.wallet.getMnemonic(state, password) - const mnemonicT = yield select(getMnemonic) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) - let priv = utils.eth.getPrivateKey(mnemonic, 0).toString('hex') - yield put(actions.modules.settings.addShownEthPrivateKey(priv)) - } - } catch (e) { - yield put( - actions.logs.logErrorMessage(logLocation, 'showEthPrivateKey', e) - ) - } - } - - const showXlmPrivateKey = function * () { - try { - const password = yield call(promptForSecondPassword) - const getMnemonic = state => - selectors.core.wallet.getMnemonic(state, password) - const mnemonicT = yield select(getMnemonic) - const mnemonic = yield call(() => taskToPromise(mnemonicT)) - const keyPair = utils.xlm.getKeyPair(mnemonic) - yield put( - actions.modules.settings.addShownXlmPrivateKey(keyPair.secret()) - ) - } catch (e) { - yield put( - actions.logs.logErrorMessage(logLocation, 'showXlmPrivateKey', e) - ) - } - } - return { initSettingsInfo, initSettingsPreferences, @@ -374,7 +299,6 @@ export default ({ api, coreSagas }) => { updateMobile, resendMobile, verifyMobile, - updateLanguage, updateCurrency, updateAutoLogout, updateLoggingLevel, @@ -387,9 +311,6 @@ export default ({ api, coreSagas }) => { enableTwoStepGoogleAuthenticator, enableTwoStepYubikey, newHDAccount, - recoverySaga, - showBtcPrivateKey, - showEthPrivateKey, - showXlmPrivateKey + recoverySaga } } diff --git a/packages/security-process/src/data/modules/settings/sagas.spec.js b/packages/security-process/src/data/modules/settings/sagas.spec.js index 5f8393ffdee..8ce1b44a2d4 100644 --- a/packages/security-process/src/data/modules/settings/sagas.spec.js +++ b/packages/security-process/src/data/modules/settings/sagas.spec.js @@ -1,24 +1,17 @@ -import { select } from 'redux-saga/effects' -import { promptForSecondPassword } from 'services/SagaService' +import { assocPath } from 'ramda' import { testSaga, expectSaga } from 'redux-saga-test-plan' -import * as matchers from 'redux-saga-test-plan/matchers' import { coreSagasFactory, Remote } from 'blockchain-wallet-v4/src' import * as actions from '../../actions' +import { createMockWalletState, walletV3 } from 'blockchain-wallet-v4/data' import * as selectors from '../../selectors.js' -import settingsSagas, { - logLocation, - ipRestrictionError, - taskToPromise -} from './sagas' +import settingsSagas, { logLocation, ipRestrictionError } from './sagas' import * as C from 'services/AlertService' -import { contains } from 'ramda' jest.mock('blockchain-wallet-v4/src/redux/sagas') const coreSagas = coreSagasFactory() const SECRET_GOOGLE_AUTHENTICATOR_URL = 'some_url' -const MOCK_PASSWORD = 'password' const MOCK_GUID = '50dae286-e42e-4d67-8419-d5dcc563746c' describe('settingsSagas', () => { @@ -76,6 +69,79 @@ describe('settingsSagas', () => { }) }) + describe(`recoverySaga`, () => { + it(`without second password`, () => { + const { recoverySaga } = settingsSagas({ coreSagas }) + const mockState = createMockWalletState(walletV3) + + return expectSaga(recoverySaga, {}) + .withState(mockState) + .put({ + type: `@COMPONENT.ADD_MNEMONIC`, + payload: { + phrase: { + mnemonic: [ + `fuel`, + `cloth`, + `used`, + `increase`, + `solution`, + `dutch`, + `void`, + `tourist`, + `shadow`, + `sound`, + `soldier`, + `chalk` + ] + } + } + }) + .run() + }) + + it(`with second password`, () => { + const securityModule = { + decryptWithSecondPassword: () => `5de57bbf395cec88fd672fc4d9fb3a12` + } + + const { recoverySaga } = settingsSagas({ coreSagas, securityModule }) + + const encryptedState = assocPath( + [`hd_wallets`, 0, `seed_hex`], + `bKcNwis6TlK2SHRvxqESO+afPtLiNgcoLmqab/F816AVUgnbu+Gc3Bdcf5MAVjBew3mrhpS2Wbrtlg/DzBFMkA==`, + walletV3 + ) + + const mockState = createMockWalletState(encryptedState) + + return expectSaga(recoverySaga, { password: `password` }) + .withState(mockState) + .put({ + type: `@COMPONENT.ADD_MNEMONIC`, + payload: { + phrase: { + mnemonic: [ + `fuel`, + `cloth`, + `used`, + `increase`, + `solution`, + `dutch`, + `void`, + `tourist`, + `shadow`, + `sound`, + `soldier`, + `chalk` + ] + } + } + }) + .run() + }) + }) + describe('showGoogleAuthenticatorSecretUrl', () => { let { showGoogleAuthenticatorSecretUrl } = settingsSagas({ coreSagas }) @@ -190,36 +256,6 @@ describe('settingsSagas', () => { }) }) - describe('updateLanguage', () => { - let { updateLanguage } = settingsSagas({ coreSagas }) - - let action = { payload: { language: 'ES' } } - - let saga = testSaga(updateLanguage, action) - - it('should call setLanguage', () => { - saga.next().call(coreSagas.settings.setLanguage, action.payload) - }) - - it('should add the language to the url', () => { - saga.next() - expect(contains(action.payload.language, window.location.href)).toBe(true) - }) - - describe('error handling', () => { - const error = new Error('ERROR') - it('should log the error', () => { - saga - .restart() - .next() - .throw(error) - .put( - actions.logs.logErrorMessage(logLocation, 'updateLanguage', error) - ) - }) - }) - }) - describe('updateCurrency', () => { let { updateCurrency } = settingsSagas({ coreSagas }) @@ -658,37 +694,4 @@ describe('settingsSagas', () => { }) }) }) - - describe('showBtcPrivateKey', () => { - const { showBtcPrivateKey } = settingsSagas({ coreSagas }) - - let action = { payload: { addr: 'address' } } - - let saga = testSaga(showBtcPrivateKey, action) - - it('should call promptForSecondPassword', () => { - saga.next().call(promptForSecondPassword) - }) - - it('should select the wallet', () => { - saga.next(MOCK_PASSWORD).select(selectors.core.wallet.getWallet) - }) - }) - - describe('showEthPrivateKey', () => { - const getMnemonic = () => jest.fn() - const { showEthPrivateKey } = settingsSagas({ coreSagas }) - - let action = { payload: { isLegacy: false } } - - it('should get the mnemonic', () => { - return expectSaga(showEthPrivateKey, action) - .provide([ - [matchers.call.fn(promptForSecondPassword), 'password'], - [select(getMnemonic), 'mnemonicT'], - [matchers.call.fn(() => taskToPromise), 'mnemonic'] - ]) - .run() - }) - }) }) diff --git a/packages/security-process/src/data/preferences/sagaRegister.js b/packages/security-process/src/data/preferences/sagaRegister.js index 4810f281be6..cba35c94a80 100644 --- a/packages/security-process/src/data/preferences/sagaRegister.js +++ b/packages/security-process/src/data/preferences/sagaRegister.js @@ -2,8 +2,8 @@ import { takeLatest } from 'redux-saga/effects' import * as AT from './actionTypes' import sagas from './sagas' -export default () => { - const preferencesSagas = sagas() +export default (...args) => { + const preferencesSagas = sagas(...args) return function * preferencesSaga () { yield takeLatest(AT.SET_LANGUAGE, preferencesSagas.setLanguage) diff --git a/packages/security-process/src/data/preferences/sagas.js b/packages/security-process/src/data/preferences/sagas.js index 611eb8d13c5..30d821e4122 100644 --- a/packages/security-process/src/data/preferences/sagas.js +++ b/packages/security-process/src/data/preferences/sagas.js @@ -1,9 +1,8 @@ import { put } from 'redux-saga/effects' import * as actions from '../actions.js' import * as C from 'services/AlertService' -import { addLanguageToUrl } from 'services/LocalesService' -export default () => { +export default ({ imports: { addLanguageToUrl } }) => { const logLocation = 'preferences/sagas' const setLanguage = function * (action) { diff --git a/packages/security-process/src/data/rootReducer.js b/packages/security-process/src/data/rootReducer.js index 5bdefdee9f0..e85a94f7e4a 100644 --- a/packages/security-process/src/data/rootReducer.js +++ b/packages/security-process/src/data/rootReducer.js @@ -2,7 +2,6 @@ import { coreReducers, paths } from 'blockchain-wallet-v4/src' import alertsReducer from './alerts/reducers' import analyticsReducer from './analytics/reducers' import authReducer from './auth/reducers' -import componentsReducer from './components/reducers' import formReducer from './form/reducers' import cacheReducer from './cache/reducers' import goalsReducer from './goals/reducers' @@ -10,31 +9,24 @@ import logsReducer from './logs/reducers' import modalsReducer from './modals/reducers' import preferencesReducer from './preferences/reducers' import profileReducer from './modules/profile/reducers' -import ratesReducer from './modules/rates/reducers' import sessionReducer from './session/reducers' import wizardReducer from './wizard/reducers' import settingsReducer from './modules/settings/reducers' -import sfoxSignupReducer from './modules/sfox/reducers' -import qaReducer from './modules/qa/reducers' const rootReducer = { alerts: alertsReducer, analytics: analyticsReducer, auth: authReducer, - components: componentsReducer, form: formReducer, goals: goalsReducer, modals: modalsReducer, logs: logsReducer, preferences: preferencesReducer, profile: profileReducer, - rates: ratesReducer, cache: cacheReducer, session: sessionReducer, wizard: wizardReducer, securityCenter: settingsReducer, - sfoxSignup: sfoxSignupReducer, - qa: qaReducer, [paths.dataPath]: coreReducers.data, [paths.walletPath]: coreReducers.wallet, [paths.settingsPath]: coreReducers.settings, diff --git a/packages/security-process/src/data/rootSaga.js b/packages/security-process/src/data/rootSaga.js index 49b086cfd5a..09691b353c8 100644 --- a/packages/security-process/src/data/rootSaga.js +++ b/packages/security-process/src/data/rootSaga.js @@ -5,7 +5,6 @@ import alerts from './alerts/sagaRegister' import analytics from './analytics/sagaRegister' import auth from './auth/sagaRegister' import components from './components/sagaRegister' -import middleware from './middleware/sagaRegister' import modules from './modules/sagaRegister' import preferences from './preferences/sagaRegister' import goals from './goals/sagaRegister' @@ -37,10 +36,10 @@ const welcomeSaga = function * () { } } -const languageInitSaga = function * () { +const languageInitSaga = function * ({ imports }) { try { yield delay(250) - const lang = tryParseLanguageFromUrl() + const lang = tryParseLanguageFromUrl(imports) if (lang.language) { yield put(actions.preferences.setLanguage(lang.language, false)) if (lang.cultureCode) { @@ -52,30 +51,21 @@ const languageInitSaga = function * () { } } -export default function * rootSaga ({ - api, - bchSocket, - btcSocket, - ethSocket, - ratesSocket, - networks, - options -}) { - const coreSagas = coreSagasFactory({ api, networks, options }) +export default function * rootSaga (args) { + const coreSagas = coreSagasFactory(args) yield all([ call(welcomeSaga), fork(alerts), - fork(analytics({ api })), - fork(auth({ api, coreSagas })), - fork(components({ api, coreSagas, networks, options })), - fork(modules({ api, coreSagas, networks })), - fork(preferences()), - fork(goals({ api })), - fork(wallet({ coreSagas })), - fork(middleware({ api, bchSocket, btcSocket, ethSocket, ratesSocket })), - fork(coreRootSagaFactory({ api, networks, options })), - fork(router()), - call(languageInitSaga) + fork(analytics({ ...args, coreSagas })), + fork(auth({ ...args, coreSagas })), + fork(components({ ...args, coreSagas })), + fork(modules({ ...args, coreSagas })), + fork(preferences({ ...args, coreSagas })), + fork(goals({ ...args, coreSagas })), + fork(wallet({ ...args, coreSagas })), + fork(coreRootSagaFactory({ ...args, coreSagas })), + fork(router({ ...args, coreSagas })), + call(languageInitSaga, { ...args, coreSagas }) ]) } diff --git a/packages/security-process/src/data/sagas.js b/packages/security-process/src/data/sagas.js index 4f37cc8ca6b..f218dae6a03 100644 --- a/packages/security-process/src/data/sagas.js +++ b/packages/security-process/src/data/sagas.js @@ -1,19 +1,8 @@ import * as analytics from './analytics/sagas' import * as auth from './auth/sagas' -import * as components from './components/sagas' -import * as middleware from './middleware/sagas' import * as modules from './modules/sagas' import * as preferences from './preferences/sagas' import * as router from './router/sagas' import * as wallet from './wallet/sagas' -export { - analytics, - auth, - components, - middleware, - modules, - preferences, - router, - wallet -} +export { analytics, auth, modules, preferences, router, wallet } diff --git a/packages/security-process/src/data/selectors.js b/packages/security-process/src/data/selectors.js index de0ff9d95c2..57619be389e 100644 --- a/packages/security-process/src/data/selectors.js +++ b/packages/security-process/src/data/selectors.js @@ -3,8 +3,6 @@ import * as alerts from './alerts/selectors' import * as analytics from './analytics/selectors' import * as auth from './auth/selectors' import * as cache from './cache/selectors' -import * as components from './components/selectors' -import * as exchange from './exchange/selectors' import * as form from './form/selectors' import * as goals from './goals/selectors' import * as logs from './logs/selectors' @@ -20,8 +18,6 @@ export { analytics, auth, cache, - components, - exchange, form, core, goals, diff --git a/packages/security-process/src/index.dev.js b/packages/security-process/src/index.dev.js index 453f445412a..23bd834997f 100644 --- a/packages/security-process/src/index.dev.js +++ b/packages/security-process/src/index.dev.js @@ -2,27 +2,35 @@ import React from 'react' import ReactDOM from 'react-dom' import { AppContainer } from 'react-hot-loader' -import './favicons' import configureStore from 'store' import App from 'scenes/app.js' import Error from './index.error' -const renderApp = (Component, store, history, persistor) => { - const render = (Component, store, history, persistor) => { +const renderApp = (Component, root) => { + const render = ( + Component, + { imports, securityModule, store, history, persistor } + ) => { ReactDOM.render( - + , document.getElementById('app') ) } - render(App, store, history, persistor) + render(App, root) if (module.hot) { module.hot.accept('./scenes/app.js', () => - render(require('./scenes/app.js').default, store, history, persistor) + render(require('./scenes/app.js').default, root) ) } } @@ -35,7 +43,7 @@ const renderError = e => { configureStore() .then(root => { - renderApp(App, root.store, root.history, root.persistor) + renderApp(App, root) }) .catch(e => { renderError(e) diff --git a/packages/security-process/src/index.html b/packages/security-process/src/index.html deleted file mode 100644 index 7858507df44..00000000000 --- a/packages/security-process/src/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - Blockchain Wallet - Exchange Cryptocurrency - - - -

- - diff --git a/packages/security-process/src/index.prod.js b/packages/security-process/src/index.prod.js index ba3478f7cd0..340fcfbdde7 100644 --- a/packages/security-process/src/index.prod.js +++ b/packages/security-process/src/index.prod.js @@ -1,14 +1,22 @@ import React from 'react' import ReactDOM from 'react-dom' -import './favicons' import configureStore from 'store' import App from 'scenes/app.js' import Error from './index.error' -const renderApp = (Component, store, history, persistor) => { +const renderApp = ( + Component, + { imports, securityModule, store, history, persistor } +) => { ReactDOM.render( - , + , document.getElementById('app') ) } @@ -19,7 +27,7 @@ const renderError = () => { configureStore() .then(root => { - renderApp(App, root.store, root.history, root.persistor) + renderApp(App, root) }) .catch(e => { // eslint-disable-next-line no-console diff --git a/packages/security-process/src/layouts/Security/Header/index.js b/packages/security-process/src/layouts/Security/Header/index.js new file mode 100644 index 00000000000..016547d6ea6 --- /dev/null +++ b/packages/security-process/src/layouts/Security/Header/index.js @@ -0,0 +1,63 @@ +import React from 'react' +import { connect } from 'react-redux' +import { Link } from 'react-router-dom' +import styled from 'styled-components' + +import { Icon, Image } from 'blockchain-info-components' +import { Navbar, NavbarBrand } from 'components/Navbar' + +const White = styled.div` + color: white; + + a:link { + color: white; + } + + a:visited { + color: white; + } +` + +const Dashboard = styled.div` + padding-right: 25px; +` + +const Header = props => { + const onCloseClick = event => { + props.dispatch({ + type: `ROOT_LOCATION_CHANGE`, + payload: { + action: `PUSH`, + location: { hash: ``, pathname: `/home`, search: `` } + } + }) + + event.preventDefault() + } + + return ( + + + + + + + + + + + + + + + ) +} + +export default connect()(Header) diff --git a/packages/security-process/src/layouts/Security/index.js b/packages/security-process/src/layouts/Security/index.js new file mode 100644 index 00000000000..be832963b3a --- /dev/null +++ b/packages/security-process/src/layouts/Security/index.js @@ -0,0 +1,104 @@ +import { replace } from 'ramda' +import React from 'react' +import { connect } from 'react-redux' +import { Redirect, Route } from 'react-router-dom' +import styled from 'styled-components' + +import Alerts from 'components/Alerts' +import Header from './Header' +import AnalyticsTracker from 'providers/AnalyticsTracker' +import ErrorBoundary from 'providers/ErrorBoundaryProvider' +import Modals from 'modals' +import { selectors } from 'data' + +const Wrapper = styled.div` + height: auto; + min-height: 100%; + width: 100%; + overflow: auto; + + @media (min-width: 768px) { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + height: 100%; + } +` +const HeaderContainer = styled.div` + position: relative; + width: 100%; + + @media (min-width: 768px) { + top: 0; + left: 0; + } +` +const ContentContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow-y: auto; + margin: 0 25px; + + @media (min-width: 768px) { + height: 100%; + } + + @media (min-height: 1000px) { + height: 100%; + margin-top: 200px; + justify-content: flex-start; + } + + @media (min-height: 1400px) { + height: 100%; + margin-top: 500px; + justify-content: flex-start; + } +` + +class SecurityLayoutContainer extends React.PureComponent { + render () { + const { + component: Component, + isAuthenticated, + location, + ...rest + } = this.props + + return isAuthenticated ? ( + + ( + + + + + + +
+ + + + + + + )} + /> + + ) : ( + + ) + } +} + +const mapStateToProps = state => ({ + isAuthenticated: selectors.auth.isAuthenticated(state) +}) + +export default connect(mapStateToProps)(SecurityLayoutContainer) diff --git a/packages/security-process/src/modals/Settings/index.js b/packages/security-process/src/modals/Settings/index.js index eeb2f162e5d..177173acad6 100644 --- a/packages/security-process/src/modals/Settings/index.js +++ b/packages/security-process/src/modals/Settings/index.js @@ -1,4 +1,3 @@ -import AutoDisconnection from './AutoDisconnection' import ConfirmDisable2FA from './ConfirmDisable2FA' import SecondPassword from './SecondPassword' import TwoStepGoogleAuthenticator from './TwoStepGoogleAuthenticator' @@ -6,7 +5,6 @@ import TwoStepSetup from './TwoStepSetup' import TwoStepYubico from './TwoStepYubico' export { - AutoDisconnection, ConfirmDisable2FA, SecondPassword, TwoStepGoogleAuthenticator, diff --git a/packages/main-process/src/modals/Wallet/UpgradeWallet/index.js b/packages/security-process/src/modals/Wallet/UpgradeWallet/index.js similarity index 100% rename from packages/main-process/src/modals/Wallet/UpgradeWallet/index.js rename to packages/security-process/src/modals/Wallet/UpgradeWallet/index.js diff --git a/packages/main-process/src/modals/Wallet/UpgradeWallet/template.js b/packages/security-process/src/modals/Wallet/UpgradeWallet/template.js similarity index 100% rename from packages/main-process/src/modals/Wallet/UpgradeWallet/template.js rename to packages/security-process/src/modals/Wallet/UpgradeWallet/template.js diff --git a/packages/security-process/src/modals/Wallet/index.js b/packages/security-process/src/modals/Wallet/index.js index 74785f29af5..84ab9913233 100644 --- a/packages/security-process/src/modals/Wallet/index.js +++ b/packages/security-process/src/modals/Wallet/index.js @@ -1,5 +1,4 @@ import PairingCode from './PairingCode' -import ShowXPub from './ShowXPub' import UpgradeWallet from './UpgradeWallet' -export { PairingCode, ShowXPub, UpgradeWallet } +export { PairingCode, UpgradeWallet } diff --git a/packages/security-process/src/modals/index.js b/packages/security-process/src/modals/index.js index ac7e4a0b7d1..b9e5f4a0f06 100644 --- a/packages/security-process/src/modals/index.js +++ b/packages/security-process/src/modals/index.js @@ -1,159 +1,31 @@ import React from 'react' -import { - DeleteAddressLabel, - ShowUsedAddresses, - UpgradeAddressLabels -} from './Addresses' -import { RequestBch, SendBch } from './Bch' -import { - AddBtcWallet, - ImportBtcAddress, - RequestBtc, - SendBtc, - ShowBtcPrivateKey, - VerifyMessage -} from './Btc' -import { - CoinifyBuyViaCard, - CoinifyDeleteBank, - CoinifyTradeDetails -} from './Coinify' -import { - PaxWelcome, - RequestEth, - SendEth, - ShowEthPrivateKey, - TransferEth -} from './Eth' -import { - EthAirdrop, - ExchangeConfirm, - ExchangeResults, - KycDocResubmit, - IdentityVerification, - ShapeshiftTradeDetails, - SunRiverLinkError, - SwapUpgrade, - UserExists -} from './Exchange' -import { Confirm, PromptInput, Support } from './Generic' -import { - LockboxAppManager, - LockboxFirmware, - LockboxSetup, - LockboxConnectionPrompt, - LockboxShowXPubs -} from './Lockbox' +import { Confirm, PromptInput } from './Generic' import { MobileNumberChange, MobileNumberVerify } from './Mobile' -import { - AirdropClaim, - AirdropSuccess, - CoinifyUpgrade, - LinkFromPitAccount, - LinkToPitAccount, - SwapGetStarted, - UpgradeForAirdrop, - Welcome -} from './Onboarding' -import Onfido from './Onfido' import QRCode from './QRCode' import { - SfoxEnterMicroDeposits, - SfoxExchangeData, - SfoxTradeDetails -} from './Sfox' -import SignMessage from './SignMessage' -import { EditTxDescription, TransactionReport } from './Transactions' -import { - AutoDisconnection, ConfirmDisable2FA, SecondPassword, TwoStepGoogleAuthenticator, TwoStepSetup, TwoStepYubico } from './Settings' -import { PairingCode, ShowXPub, UpgradeWallet } from './Wallet' -import { - RequestXlm, - SendXlm, - ShowXlmPrivateKey, - SunRiverWelcome, - XlmCreateAccountLearn, - XlmReserveLearn -} from './Xlm' +import { PairingCode, UpgradeWallet } from './Wallet' const Modals = () => (
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
) diff --git a/packages/security-process/src/scenes/Home/index.js b/packages/security-process/src/scenes/Home/index.js index 8d200bb5e57..7f0c455f6af 100644 --- a/packages/security-process/src/scenes/Home/index.js +++ b/packages/security-process/src/scenes/Home/index.js @@ -1,70 +1,3 @@ import React from 'react' -import styled from 'styled-components' -import ReactHighcharts from 'react-highcharts' -import PriceChart from './PriceChart' -import Balances from './Balances' -import Banners from './Banners' -import ThePit from './ThePit' - -ReactHighcharts.Highcharts.setOptions({ lang: { thousandsSep: ',' } }) - -const Wrapper = styled.div` - width: 100%; - height: 100%; - padding: 25px; - @media (min-width: 992px) { - padding: 15px 30px; - } -` -const ColumnWrapper = styled.section` - display: flex; - flex-direction: column; - justify-content: flex-start; - width: 100%; - @media (min-width: 992px) { - flex-direction: row; - } -` -const Column = styled.div` - flex-direction: column; - justify-content: flex-start; - align-items: center; - height: 100%; - width: 100%; - display: flex; - max-width: 600px; - box-sizing: border-box; - padding-bottom: 25px; - @media (max-height: 800px), (max-width: 991px) { - height: auto; - display: block; - } -` -const ColumnLeft = styled(Column)` - @media (min-width: 992px) { - padding-right: 30px; - } -` -const ColumnRight = styled(Column)` - & > :not(:first-child) { - margin-top: 20px; - } -` - -const Home = () => ( - - - - - - - - - - - - -) - -export default Home +export default () =>

Security Home

diff --git a/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/PairingCode/index.js b/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/PairingCode/index.js new file mode 100644 index 00000000000..bb637a8a36b --- /dev/null +++ b/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/PairingCode/index.js @@ -0,0 +1,89 @@ +import React from 'react' +import styled from 'styled-components' +import { FormattedMessage } from 'react-intl' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' + +import { actions } from 'data' +import { Badge, Button, Text } from 'blockchain-info-components' +import { + SettingComponent, + SettingContainer, + SettingDescription, + SettingHeader, + SettingSummary +} from 'components/Setting' + +const BadgesContainer = styled.div` + display: block; + padding-top: 10px; + & > * { + display: inline; + margin-right: 5px; + } +` + +class PairingCode extends React.PureComponent { + constructor (props) { + super(props) + this.onShowCode = this.onShowCode.bind(this) + } + + onShowCode () { + this.props.actions.showModal('PairingCode') + } + + render () { + return ( + + + + + + + + + + + + + + + + + + + + + + ) + } +} + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators(actions.modals, dispatch) +}) + +export default connect( + null, + mapDispatchToProps +)(PairingCode) diff --git a/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/WalletId/index.js b/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/WalletId/index.js new file mode 100644 index 00000000000..7f054bab0b5 --- /dev/null +++ b/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/WalletId/index.js @@ -0,0 +1,49 @@ +import React from 'react' +import { FormattedMessage } from 'react-intl' +import { connect } from 'react-redux' + +import { selectors } from 'data' +import { Text } from 'blockchain-info-components' +import { + SettingComponent, + SettingContainer, + SettingDescription, + SettingHeader, + SettingSummary +} from 'components/Setting' + +const WalletId = props => { + return ( + + + + + + + + + + + + + + {props.guid} + + + ) +} + +const mapStateToProps = state => ({ + guid: selectors.core.wallet.getGuid(state) +}) + +export default connect(mapStateToProps)(WalletId) diff --git a/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/index.js b/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/index.js index c5434ae3591..5efc0352f19 100644 --- a/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/index.js +++ b/packages/security-process/src/scenes/SecurityCenter/AdvancedSecurity/index.js @@ -1,11 +1,15 @@ import React from 'react' +import { FormattedMessage } from 'react-intl' import styled from 'styled-components' +import { Banner, Text } from 'blockchain-info-components' import APIAccess from './APIAccess' import IPWhitelist from './IPWhitelist' import LoginIpRestriction from './LoginIpRestriction' +import PairingCode from './PairingCode' import PasswordStretching from './PasswordStretching' import WalletAccessTor from './WalletAccessTor' +import WalletId from './WalletId' import TwoStepVerificationRemember from './TwoStepVerificationRemember' import WalletPassword from './WalletPassword' import SecondPasswordWallet from './SecondPasswordWallet' @@ -19,6 +23,21 @@ export default class AdvancedSecurity extends React.PureComponent { render () { return ( + + + +   + + + + + diff --git a/packages/security-process/src/scenes/SecurityCenter/BasicSecurity/WalletRecoveryPhrase/RecordBackupPhrase/FirstStep/index.js b/packages/security-process/src/scenes/SecurityCenter/BasicSecurity/WalletRecoveryPhrase/RecordBackupPhrase/FirstStep/index.js index f2b1e97662c..5ae71ead7a7 100644 --- a/packages/security-process/src/scenes/SecurityCenter/BasicSecurity/WalletRecoveryPhrase/RecordBackupPhrase/FirstStep/index.js +++ b/packages/security-process/src/scenes/SecurityCenter/BasicSecurity/WalletRecoveryPhrase/RecordBackupPhrase/FirstStep/index.js @@ -71,6 +71,8 @@ const FirstStep = props => { href={recoveryPdf} download='recovery_phrase.pdf' data-e2e='downloadRecoveryPhraseLink' + target='_blank' + rel='noopener noreferrer' >