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..6e79f9286c2 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": { @@ -166,7 +177,7 @@ "express": "4.16.4", "file-loader": "3.0.1", "generate-changelog": "1.7.1", - "html-webpack-plugin": "3.2.0", + "html-webpack-plugin": "4.0.0-beta.8", "husky": "2.3.0", "identity-obj-proxy": "3.0.0", "istanbul": "github:Xesenix/istanbul", 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..ef9707d03d4 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,6 @@ export default ({ api }) => { } const calculateSignature = function * ( - password, transaction, transport, scrambleKey, @@ -75,9 +74,7 @@ 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 xlmSigner.sign({ transaction }, securityModule) case ADDRESS_TYPES.LOCKBOX: return yield call( xlmSigner.signWithLockbox, @@ -257,21 +254,17 @@ export default ({ api }) => { return makePayment(merge(p, { transaction })) }, - * 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') - } + * sign (transport, scrambleKey) { + const transaction = prop('transaction', p) + const signed = yield call( + calculateSignature, + securityModule, + 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..6211d6c7718 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/actionTypes.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/actionTypes.js @@ -1,4 +1,5 @@ // setters +export const MERGE_WRAPPER = '@CORE.MERGE_WRAPPER' 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..4e6c7b28f3c 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/actions.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/actions.js @@ -1,6 +1,9 @@ import * as T from './actionTypes' // setters + +export const mergeWrapper = payload => ({ type: T.MERGE_WRAPPER, 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/sagas.js b/packages/blockchain-wallet-v4/src/redux/wallet/sagas.js index 786ea5c1459..a412f675269 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( @@ -47,14 +47,14 @@ export default ({ api, networks }) => { if (isEncrypted) { const task = Wrapper.traverseWallet( Task.of, - Wallet.decrypt(password), + Wallet.decrypt(securityModule, password), wrapper ) yield call(runTask, task, A.wallet.setWrapper) } else { const task = Wrapper.traverseWallet( Task.of, - Wallet.encrypt(password), + Wallet.encrypt(securityModule, password), wrapper ) yield call(runTask, task, A.wallet.setWrapper) @@ -70,7 +70,7 @@ export default ({ api, networks }) => { Date.now(), password, bipPass, - { network, api } + { network, api, securityModule } ) const wrapperT = walletT.map(wallet => set(Wrapper.wallet, wallet, wrapper)) yield call(runTask, wrapperT, A.wallet.setWrapper) @@ -80,7 +80,7 @@ export default ({ api, networks }) => { let wrapper = yield select(S.getWrapper) let nextWrapper = Wrapper.traverseWallet( Task.of, - Wallet.newHDAccount(label, password, networks.btc), + Wallet.newHDAccount(securityModule, label, password, networks.btc), wrapper ) yield call(runTask, nextWrapper, A.wallet.setWrapper) @@ -208,9 +208,19 @@ export default ({ api, networks }) => { const isEncrypted = yield select(S.isSecondPasswordOn) if (isEncrypted) { const task = Task.of(wrapper) - .chain(Wrapper.traverseWallet(Task.of, Wallet.decrypt(password))) + .chain( + Wrapper.traverseWallet( + Task.of, + Wallet.decrypt(securityModule, password) + ) + ) .map(Wrapper.setBothPbkdf2Iterations(iterations)) - .chain(Wrapper.traverseWallet(Task.of, Wallet.encrypt(password))) + .chain( + Wrapper.traverseWallet( + Task.of, + Wallet.encrypt(securityModule, password) + ) + ) yield call(runTask, task, A.wallet.setWrapper) } else { const newWrapper = Wrapper.setBothPbkdf2Iterations(iterations, wrapper) diff --git a/packages/blockchain-wallet-v4/src/redux/wallet/selectors.js b/packages/blockchain-wallet-v4/src/redux/wallet/selectors.js index ecaf3c3782d..2fae2a97bde 100755 --- a/packages/blockchain-wallet-v4/src/redux/wallet/selectors.js +++ b/packages/blockchain-wallet-v4/src/redux/wallet/selectors.js @@ -91,18 +91,6 @@ export const getHDAccounts = compose( Wallet.selectHDAccounts, getWallet ) -export const getSeedHex = curry((state, password) => - compose( - Wallet.getSeedHex(password), - getWallet - )(state) -) -export const getMnemonic = curry((state, password) => - compose( - Wallet.getMnemonic(password), - getWallet - )(state) -) export const getDefaultAccount = compose( HDWallet.selectDefaultAccount, getDefaultHDWallet 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..d689cfaee3a 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 = ({ transaction }, securityModule) => { + const keyPair = getKeyPair(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..13cfa225b02 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' @@ -84,10 +83,24 @@ export const deriveAccountNodeAtIndex = (seedHex, index, network) => { .deriveHardened(index) } -export const generateAccount = curry((index, label, network, seedHex) => { - let node = deriveAccountNodeAtIndex(seedHex, index, network) +export const generateAccount = async ( + { deriveBIP32Key }, + secondPassword, + index, + label, + network +) => { + const key = await deriveBIP32Key( + { + network, + secondPassword + }, + `m/44'/0'/${index}'` + ) + + 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/KVStoreEntry.js b/packages/blockchain-wallet-v4/src/types/KVStoreEntry.js index a9ecbfb6dae..ef1ca2b0f95 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. 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..7744c1a76ab 100755 --- a/packages/blockchain-wallet-v4/src/types/Wallet.js +++ b/packages/blockchain-wallet-v4/src/types/Wallet.js @@ -5,20 +5,10 @@ import Task from 'data.task' 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 { promiseToTask } from '../utils/functional' import * as crypto from '../walletCrypto' import { shift, shiftIProp } from './util' import Type from './Type' @@ -293,29 +283,27 @@ 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 - let appendAccount = curry((w, account) => { - let accountsLens = compose( - hdWallets, - HDWalletList.hdwallet, - HDWallet.accounts +export const newHDAccount = curry( + (securityModule, label, password, network, wallet) => { + let hdWallet = HDWalletList.selectHDWallet(selectHdWallets(wallet)) + let index = hdWallet.accounts.size + let appendAccount = curry((w, account) => { + let accountsLens = compose( + hdWallets, + HDWalletList.hdwallet, + HDWallet.accounts + ) + let accountWithIndex = set(HDAccount.index, index, account) + return over(accountsLens, accounts => accounts.push(accountWithIndex), w) + }) + + return promiseToTask( + HDWallet.generateAccount(securityModule, password, index, label, network) ) - let 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)) - .chain(applyCipher(wallet, password, HDAccount.encrypt)) - .map(appendAccount(wallet)) -}) + .chain(applyCipher(securityModule, wallet, password, HDAccount.encrypt)) + .map(appendAccount(wallet)) + } +) // setLegacyAddressLabel :: String -> String -> Wallet -> Wallet export const setLegacyAddressLabel = curry((address, label, wallet) => { @@ -624,17 +612,6 @@ export const getSeedHex = curry((secondPassword, wallet) => { } }) -// getMnemonic :: String -> Wallet -> Task Error String -export const getMnemonic = curry((secondPassword, wallet) => { - const eitherToTask = e => e.fold(Task.rejected, Task.of) - const entropyToMnemonic = compose( - eitherToTask, - Either.try(BIP39.entropyToMnemonic) - ) - const seedHex = getSeedHex(secondPassword, wallet) - return seedHex.chain(entropyToMnemonic) -}) - export const js = ( guid, sharedKey, 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__/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..a6d2c4ca735 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' @@ -12,42 +10,29 @@ import BigNumber from 'bignumber.js' export const isValidAddress = address => /^0x[a-fA-F0-9]{40}$/.test(address) /** - * @param {string} mnemonic + * @param {function} deriveBIP32Key * @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..529cf4c4ff6 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 ( + { deriveSLIP10ed25519Key }, + secondPassword +) => { + 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..af7fce0581d 100644 --- a/packages/main-process/babel.config.js +++ b/packages/main-process/babel.config.js @@ -1,39 +1,51 @@ -module.exports = { - presets: ['@babel/preset-env', '@babel/preset-react'], - plugins: [ +module.exports = (api, baseDirectory = `.`) => { + // api isn't set when called from Webpack. + if (api) { + api.cache.forever() + } + + const babelPlugins = [ '@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' }] - ], - ignore: [], - env: { - production: { - presets: [ - ['@babel/preset-env', { modules: false }], - '@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' }] - ] - }, - development: { - presets: [ - ['@babel/preset-env', { modules: false }], - '@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-hot-loader/babel' + [ + 'module-resolver', + { + root: [`${baseDirectory}/src`], + alias: { data: `${baseDirectory}/src/data` } + } + ] + ] + + return { + presets: ['@babel/preset-env', '@babel/preset-react'], + plugins: babelPlugins.concat([ + [ + 'react-intl', + { messagesDir: `${baseDirectory}/build/extractedMessages` } ] + ]), + ignore: [], + env: { + production: { + presets: [ + ['@babel/preset-env', { modules: false }], + '@babel/preset-react' + ], + plugins: babelPlugins.concat([ + [ + 'react-intl', + { messagesDir: `${baseDirectory}/build/extractedMessages` } + ] + ]) + }, + development: { + presets: [ + ['@babel/preset-env', { modules: false }], + '@babel/preset-react' + ], + plugins: babelPlugins.concat('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..e4314e07a50 100644 --- a/packages/main-process/src/data/auth/sagas.js +++ b/packages/main-process/src/data/auth/sagas.js @@ -109,14 +109,13 @@ 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 +140,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 +220,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 +243,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 +329,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 +343,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..0db4fa8fa99 100644 --- a/packages/main-process/src/data/modules/sagaRegister.js +++ b/packages/main-process/src/data/modules/sagaRegister.js @@ -7,12 +7,12 @@ import securityCenter from './securityCenter/sagaRegister' import transferEth from './transferEth/sagaRegister' import sfox from './sfox/sagaRegister' -export default ({ api, coreSagas, networks }) => +export default ({ api, coreSagas, imports, networks }) => function * modulesSaga () { yield fork(addressesBch({ coreSagas, networks })) yield fork(profile({ api, coreSagas, networks })) yield fork(rates({ api })) - yield fork(settings({ api, coreSagas })) + yield fork(settings({ api, coreSagas, imports })) yield fork(securityCenter({ coreSagas })) yield fork(transferEth({ coreSagas, networks })) yield fork(sfox({ api, coreSagas, networks })) diff --git a/packages/main-process/src/data/modules/sagas.js b/packages/main-process/src/data/modules/sagas.js index 00b1640f76d..e120e6fc45d 100644 --- a/packages/main-process/src/data/modules/sagas.js +++ b/packages/main-process/src/data/modules/sagas.js @@ -6,11 +6,11 @@ import securityCenter from './securityCenter/sagas' import transferEth from './transferEth/sagas' import sfox from './sfox/sagas' -export default ({ api, coreSagas, networks }) => ({ +export default ({ api, coreSagas, imports, networks }) => ({ addressesBch: addressesBch({ coreSagas }), profile: profile({ api, coreSagas, networks }), rates: rates({ api }), - settings: settings({ api, coreSagas }), + settings: settings({ api, coreSagas, imports }), securityCenter: securityCenter({ coreSagas }), transferEth: transferEth({ coreSagas, networks }), sfox: sfox({ api, coreSagas }) diff --git a/packages/main-process/src/data/modules/settings/sagaRegister.js b/packages/main-process/src/data/modules/settings/sagaRegister.js index 7c093289516..f59d82b1e37 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 ({ api, coreSagas, imports }) => { + const settingsSagas = sagas({ api, coreSagas, imports }) 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..497cad8e1ac 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)) } @@ -325,20 +304,13 @@ export default ({ api, coreSagas }) => { 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') + 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 = utils.eth.getPrivateKey(securityModule, 0).toString('hex') yield put(actions.modules.settings.addShownEthPrivateKey(priv)) } } catch (e) { @@ -350,12 +322,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 = utils.xlm.getKeyPair(securityModule) yield put( actions.modules.settings.addShownXlmPrivateKey(keyPair.secret()) ) @@ -369,7 +336,6 @@ export default ({ api, coreSagas }) => { return { initSettingsInfo, initSettingsPreferences, - showBackupRecovery, showGoogleAuthenticatorSecretUrl, updateMobile, resendMobile, @@ -387,7 +353,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..19f18d6744f 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,37 @@ 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, + imports, ratesSocket, networks, - options + options, + securityModule }) { - const coreSagas = coreSagasFactory({ api, networks, options }) + const coreSagas = coreSagasFactory({ + api, + imports, + networks, + options, + securityModule + }) 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(components({ api, coreSagas, imports, networks, options })), + fork(modules({ api, coreSagas, imports, networks })), + fork(preferences({ imports })), fork(goals({ api })), fork(wallet({ coreSagas })), fork(middleware({ api, bchSocket, btcSocket, ethSocket, ratesSocket })), - fork(coreRootSagaFactory({ api, networks, options })), - fork(router()), - call(languageInitSaga) + fork(coreRootSagaFactory({ api, imports, networks, options })), + fork(router()) ]) } 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 index 7858507df44..7aa7e721812 100644 --- a/packages/main-process/src/index.html +++ b/packages/main-process/src/index.html @@ -1,27 +1,12 @@ - - - - - - - - - - - - - - - - - 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/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/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 00000000000..4a892a3cdd9 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/android-chrome-192x192.png differ 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 00000000000..27eb4cdfc77 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/android-chrome-512x512.png differ 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 00000000000..b1939352288 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/apple-touch-icon.png differ diff --git a/packages/root-process/src/assets/images/favicon/browserconfig.xml b/packages/root-process/src/assets/images/favicon/browserconfig.xml new file mode 100644 index 00000000000..ad1ae18c66a --- /dev/null +++ b/packages/root-process/src/assets/images/favicon/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #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 00000000000..34b445a5712 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/favicon-16x16.png differ 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 00000000000..664c498f818 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/favicon-32x32.png differ 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 00000000000..775e9c6cbbf Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/favicon.ico differ 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 00000000000..f3c4f925b28 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/mstile-144x144.png differ 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 00000000000..6326cd191c6 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/mstile-150x150.png differ 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 00000000000..43794bc7ac6 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/mstile-310x150.png differ diff --git a/packages/root-process/src/assets/images/favicon/mstile-310x310.png b/packages/root-process/src/assets/images/favicon/mstile-310x310.png new file mode 100644 index 00000000000..42b16ef69f6 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/mstile-310x310.png differ diff --git a/packages/root-process/src/assets/images/favicon/mstile-70x70.png b/packages/root-process/src/assets/images/favicon/mstile-70x70.png new file mode 100644 index 00000000000..0fd28a25991 Binary files /dev/null and b/packages/root-process/src/assets/images/favicon/mstile-70x70.png differ 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.html b/packages/root-process/src/index.html new file mode 100644 index 00000000000..227a56e5a4b --- /dev/null +++ b/packages/root-process/src/index.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + Blockchain Wallet - Exchange Cryptocurrency + + + + + + diff --git a/packages/root-process/src/index.js b/packages/root-process/src/index.js new file mode 100644 index 00000000000..a0d1f733b39 --- /dev/null +++ b/packages/root-process/src/index.js @@ -0,0 +1,199 @@ +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`, + src: `/main.html#/login` + }) + + const pathname = window.location.hash.slice(1) + + const securityProcess = await createProcess({ + name: `security`, + src: `/security.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/security-process/babel.config.js b/packages/security-process/babel.config.js index 7b2631c7211..af7fce0581d 100644 --- a/packages/security-process/babel.config.js +++ b/packages/security-process/babel.config.js @@ -1,39 +1,51 @@ -module.exports = { - presets: ['@babel/preset-env', '@babel/preset-react'], - plugins: [ +module.exports = (api, baseDirectory = `.`) => { + // api isn't set when called from Webpack. + if (api) { + api.cache.forever() + } + + const babelPlugins = [ '@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' }] - ], - ignore: [], - env: { - production: { - presets: [ - ['@babel/preset-env', { modules: false }], - '@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' }] - ] - }, - development: { - presets: [ - ['@babel/preset-env', { modules: false }], - '@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-hot-loader/babel' + [ + 'module-resolver', + { + root: [`${baseDirectory}/src`], + alias: { data: `${baseDirectory}/src/data` } + } + ] + ] + + return { + presets: ['@babel/preset-env', '@babel/preset-react'], + plugins: babelPlugins.concat([ + [ + 'react-intl', + { messagesDir: `${baseDirectory}/build/extractedMessages` } ] + ]), + ignore: [], + env: { + production: { + presets: [ + ['@babel/preset-env', { modules: false }], + '@babel/preset-react' + ], + plugins: babelPlugins.concat([ + [ + 'react-intl', + { messagesDir: `${baseDirectory}/build/extractedMessages` } + ] + ]) + }, + development: { + presets: [ + ['@babel/preset-env', { modules: false }], + '@babel/preset-react' + ], + plugins: babelPlugins.concat('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..1a22bb96e8f --- /dev/null +++ b/packages/security-process/src/IPC/Middleware.js @@ -0,0 +1,72 @@ +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, + [types.auth.LOGIN_ROUTINE]: 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..839975298a7 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,50 +108,16 @@ export default ({ api, coreSagas }) => { )).getOrElse(false) if (userFlowSupported) yield put(actions.modules.profile.signIn()) } - const loginRoutineSaga = function * (mobileLogin, firstLogin) { + + const loginRoutineSaga = function * () { 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( - coreSagas.kvStore.eth.fetchMetadataEth, - askSecondPasswordEnhancer - ) - yield call( - coreSagas.kvStore.xlm.fetchMetadataXlm, - askSecondPasswordEnhancer - ) - 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 +189,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 +212,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 +298,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 +312,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 +421,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 +445,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..03e2dfdd888 100644 --- a/packages/security-process/src/data/modules/settings/sagas.js +++ b/packages/security-process/src/data/modules/settings/sagas.js @@ -1,15 +1,13 @@ +import BIP39 from 'bip39' +import { view } from 'ramda-lens' import { put, call, select } from 'redux-saga/effects' 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 { contains, toLower, prop, propEq, head } from 'ramda' +import { askSecondPasswordEnhancer } from 'services/SagaService' +import { Types } from 'blockchain-wallet-v4/src' +import { contains, toLower, pipe, prop, propEq, head } from 'ramda' export const taskToPromise = t => new Promise((resolve, reject) => t.fork(reject, resolve)) @@ -19,7 +17,7 @@ export const ipRestrictionError = export const logLocation = 'modules/settings/sagas' -export default ({ api, coreSagas }) => { +export default ({ api, coreSagas, securityModule }) => { const { syncUserWithWallet } = profileSagas({ api, coreSagas @@ -45,11 +43,23 @@ export default ({ api, coreSagas }) => { } } + const getSeedHex = pipe( + selectors.core.wallet.getDefaultHDWallet, + view(Types.HDWallet.seedHex) + ) + + const getSeedEntropy = function * (secondPassword) { + const seedHex = yield select(getSeedHex) + + return secondPassword + ? securityModule.decryptWithSecondPassword({ secondPassword }, seedHex) + : seedHex + } + 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 entropy = yield call(getSeedEntropy, password) + const mnemonic = BIP39.entropyToMnemonic(entropy) const mnemonicArray = mnemonic.split(' ') yield put( actions.modules.settings.addMnemonic({ mnemonic: mnemonicArray }) @@ -137,17 +147,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 +306,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 +314,6 @@ export default ({ api, coreSagas }) => { updateMobile, resendMobile, verifyMobile, - updateLanguage, updateCurrency, updateAutoLogout, updateLoggingLevel, @@ -387,9 +326,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..81ce55375c3 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) { @@ -54,28 +53,25 @@ const languageInitSaga = function * () { export default function * rootSaga ({ api, - bchSocket, - btcSocket, - ethSocket, - ratesSocket, + imports, networks, - options + options, + securityModule }) { - const coreSagas = coreSagasFactory({ api, networks, options }) + const coreSagas = coreSagasFactory({ api, networks, options, securityModule }) yield all([ call(welcomeSaga), fork(alerts), fork(analytics({ api })), - fork(auth({ api, coreSagas })), + fork(auth({ api, coreSagas, imports })), fork(components({ api, coreSagas, networks, options })), - fork(modules({ api, coreSagas, networks })), - fork(preferences()), + fork(modules({ api, coreSagas, networks, securityModule })), + fork(preferences({ imports })), fork(goals({ api })), fork(wallet({ coreSagas })), - fork(middleware({ api, bchSocket, btcSocket, ethSocket, ratesSocket })), fork(coreRootSagaFactory({ api, networks, options })), fork(router()), - call(languageInitSaga) + call(languageInitSaga, { imports }) ]) } 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 index 7858507df44..7aa7e721812 100644 --- a/packages/security-process/src/index.html +++ b/packages/security-process/src/index.html @@ -1,27 +1,12 @@ - - - - - - - - - - - - - - - - - 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..acfc273ed17 --- /dev/null +++ b/packages/security-process/src/layouts/Security/index.js @@ -0,0 +1,102 @@ +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 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/security-process/src/modals/Wallet/index.js b/packages/security-process/src/modals/Wallet/index.js index 74785f29af5..7efa1127f6d 100644 --- a/packages/security-process/src/modals/Wallet/index.js +++ b/packages/security-process/src/modals/Wallet/index.js @@ -1,5 +1,3 @@ import PairingCode from './PairingCode' -import ShowXPub from './ShowXPub' -import UpgradeWallet from './UpgradeWallet' -export { PairingCode, ShowXPub, UpgradeWallet } +export { PairingCode } diff --git a/packages/security-process/src/modals/index.js b/packages/security-process/src/modals/index.js index ac7e4a0b7d1..8968f037efc 100644 --- a/packages/security-process/src/modals/index.js +++ b/packages/security-process/src/modals/index.js @@ -1,159 +1,30 @@ 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 } 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/app.js b/packages/security-process/src/scenes/app.js index 4b124f4cc36..e80f2dba6f8 100644 --- a/packages/security-process/src/scenes/app.js +++ b/packages/security-process/src/scenes/app.js @@ -1,9 +1,8 @@ -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' import { selectors } from 'data' @@ -12,34 +11,21 @@ import { MediaContextProvider } from 'providers/MatchMediaProvider' import AnalyticsTracker from 'providers/AnalyticsTracker' import TranslationsProvider from 'providers/TranslationsProvider' import PublicLayout from 'layouts/Public' +import SecurityLayout from 'layouts/Security' import ThemeProvider from 'providers/ThemeProvider' -import WalletLayout from 'layouts/Wallet' -import Addresses from './Settings/Addresses' import AuthorizeLogin from './AuthorizeLogin' -import BuySell from './BuySell' -import Exchange from './Exchange' -import ExchangeHistory from './ExchangeHistory' -import ExchangeProfile from './ExchangeProfile' -import General from './Settings/General' import Help from './Help' -import Home from './Home' -import Lockbox from './Lockbox' import Login from './Login' import Logout from './Logout' import MobileLogin from './MobileLogin' -import Preferences from './Settings/Preferences' -import Profile from './Settings/Profile' import Recover from './Recover' import Register from './Register' import Reminder from './Reminder' import Reset2FA from './Reset2FA' import Reset2FAToken from './Reset2FAToken' import SecurityCenter from './SecurityCenter' -import ThePit from './ThePit' -import Transactions from './Transactions' -import UploadDocuments from './UploadDocuments' -import UploadDocumentsSuccess from './UploadDocuments/Success' +import Home from './Home' import VerifyEmailToken from './VerifyEmailToken' const GlobalStyle = createGlobalStyle` @@ -56,15 +42,17 @@ const GlobalStyle = createGlobalStyle` } ` +const SetForegroundProcess = ({ children, imports }) => { + useEffect(() => { + imports.setForegroundProcess() + }) + + return {children} +} + class App extends React.PureComponent { render () { - const { - store, - history, - persistor, - isAuthenticated, - supportedCoins - } = this.props + const { imports, store, history, persistor, isAuthenticated } = this.props return ( @@ -72,93 +60,43 @@ class App extends React.PureComponent { - - - - - - - - - - - - - - - - - - - - - - - - - - - - {values( - map( - coin => - coin.txListAppRoute && - coin.invited && ( - - ), - supportedCoins - ) - )} - {isAuthenticated ? ( - - ) : ( - - )} - + + + + + + + + + + + + + + + + {isAuthenticated ? ( + + ) : ( + + )} + + @@ -174,10 +112,7 @@ class App extends React.PureComponent { } const mapStateToProps = state => ({ - isAuthenticated: selectors.auth.isAuthenticated(state), - supportedCoins: selectors.core.walletOptions - .getSupportedCoins(state) - .getOrFail() + isAuthenticated: selectors.auth.isAuthenticated(state) }) export default connect(mapStateToProps)(App) diff --git a/packages/security-process/src/services/LocalesService/index.js b/packages/security-process/src/services/LocalesService/index.js index ee80f9bd066..f6bb577f0d9 100644 --- a/packages/security-process/src/services/LocalesService/index.js +++ b/packages/security-process/src/services/LocalesService/index.js @@ -50,13 +50,8 @@ export function convertCultureCodeToLanguage (cultureCode) { return Maybe.Just(selectedLanguage.language) } -// update url with new language without forcing browser reload -export function addLanguageToUrl (language) { - window.history.pushState({}, '', `/${language}/${window.location.hash}`) -} - -export function tryParseLanguageFromUrl () { - const path = window.location.pathname.replace(/\//g, '') +export function tryParseLanguageFromUrl ({ pathname }) { + const path = pathname.replace(/\//g, '') if (path && path.length) { return languages[findIndex(propEq('language', path))(languages)] diff --git a/packages/security-process/src/store/index.js b/packages/security-process/src/store/index.js index 4e94ede5097..5e220c4b9ce 100644 --- a/packages/security-process/src/store/index.js +++ b/packages/security-process/src/store/index.js @@ -1,7 +1,6 @@ import { createStore, applyMiddleware, compose } from 'redux' 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' @@ -13,22 +12,19 @@ import { coreMiddleware } from 'blockchain-wallet-v4/src' import { createWalletApi, Socket, - ApiSocket, - HorizonStreamingService + ApiSocket } 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 SecurityModule from 'blockchain-wallet-v4/src/SecurityModule' import { serializer } from 'blockchain-wallet-v4/src/types' import { actions, rootSaga, rootReducer, selectors } from 'data' -import { - autoDisconnection, - streamingXlm, - webSocketBch, - webSocketBtc, - webSocketEth, - webSocketRates -} from '../middleware' +import IPC from '../IPC' const devToolsConfig = { maxAge: 1000, + name: `Security Process`, serialize: serializer, actionsBlacklist: [ // '@@redux-form/INITIALIZE', @@ -43,7 +39,8 @@ const devToolsConfig = { ] } -const configureStore = () => { +export default IPC(async ({ imports, middleware: IPCmiddleware }) => { + const { options, localStorage } = imports const history = createHashHistory() const sagaMiddleware = createSagaMiddleware() const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ @@ -52,109 +49,103 @@ 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 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 + }) - 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 }) - sagaMiddleware.run(rootSaga, { - api, - bchSocket, - btcSocket, - ethSocket, - ratesSocket, - networks, - options - }) + const baseApi = createWalletApi({ + http, + options, + getAuthCredentials, + reauthenticate, + networks + }) + const persistWhitelist = ['session', 'preferences', 'cache'] - // expose globals here - window.createTestXlmAccounts = () => { - store.dispatch(actions.core.data.xlm.createTestAccounts()) - } + // TODO: remove getStoredStateMigrateV4 someday (at least a year from now) + const store = createStore( + connectRouter(history)( + persistCombineReducers( + { + getStoredState: getStoredStateMigrateV4({ + storage: localStorage, + whitelist: persistWhitelist + }), + key: 'root', + storage: localStorage, + whitelist: persistWhitelist + }, + rootReducer + ) + ), + composeEnhancers( + applyMiddleware( + IPCmiddleware, + sagaMiddleware, + routerMiddleware(history), + coreMiddleware.kvStore({ isAuthenticated, api: baseApi, kvStorePath }), + coreMiddleware.walletSync({ isAuthenticated, api: baseApi, walletPath }) + ) + ) + ) - store.dispatch(actions.goals.defineGoals()) + const rootUrl = options.domains.root + const securityModule = SecurityModule({ http, rootUrl, store }) + const api = { ...baseApi, ...Settings({ ...http, rootUrl, securityModule }) } + const persistor = persistStore(store, null) - return { - store, - history, - persistor - } - }) -} + sagaMiddleware.run(rootSaga, { + api, + bchSocket, + btcSocket, + ethSocket, + imports, + ratesSocket, + networks, + options, + securityModule + }) + + // expose globals here + window.createTestXlmAccounts = () => { + store.dispatch(actions.core.data.xlm.createTestAccounts()) + } -export default configureStore + return { + api, + imports, + securityModule, + store, + history, + persistor + } +}) diff --git a/packages/web-microkernel/babel.config.js b/packages/web-microkernel/babel.config.js new file mode 100644 index 00000000000..c76a0d028a8 --- /dev/null +++ b/packages/web-microkernel/babel.config.js @@ -0,0 +1,12 @@ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current' + } + } + ] + ] +} diff --git a/packages/web-microkernel/package.json b/packages/web-microkernel/package.json new file mode 100644 index 00000000000..1056301fc5b --- /dev/null +++ b/packages/web-microkernel/package.json @@ -0,0 +1,22 @@ +{ + "name": "web-microkernel", + "version": "1.0.0", + "main": "src/index.js", + "license": "MIT", + "scripts": { + "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", + "test:watch": "cross-env ./../../node_modules/.bin/jest --watchAll" + }, + "dependencies": { + "lodash": "4.17.11" + }, + "devDependencies": { + "@babel/core": "7.4.4", + "@babel/preset-env": "7.4.4", + "babel-jest": "24.8.0", + "jest": "24.8.0", + "realistic-structured-clone": "2.0.2" + } +} diff --git a/packages/web-microkernel/src/Core.js b/packages/web-microkernel/src/Core.js new file mode 100644 index 00000000000..5584a990e78 --- /dev/null +++ b/packages/web-microkernel/src/Core.js @@ -0,0 +1,493 @@ +import * as _ from 'lodash' + +const fromEntries = entries => + Object.assign({}, ...entries.map(([key, value]) => ({ [key]: value }))) + +export default ({ ErrorEvent, EventTarget, getRandomValues }) => { + const canSerialize = value => Boolean(types.find(({ test }) => test(value))) + + const inspectionCutoff = 40 + + const inspect = value => { + const stringified = JSON.stringify(value) || String(value) + + return stringified.length > inspectionCutoff + ? `${stringified.slice(0, inspectionCutoff)}...` + : stringified + } + + // Create an unforgeable key. + const Key = () => { + const array = new Uint32Array(1) + getRandomValues(array) + return array[0].toString(36) + } + + const decoders = {} + const types = [] + + // The types are defined in the same order they are tested. + + types.push( + { + name: `boolean`, + test: _.isBoolean, + encode: (context, boolean) => boolean, + decode: (context, code) => code + }, + + { + name: `null`, + test: _.isNull, + encode: () => null, + decode: () => null + }, + + { + name: `number`, + test: _.isNumber, + encode: (context, number) => number, + decode: (context, code) => code + }, + + { + name: `string`, + test: _.isString, + encode: (context, string) => string, + decode: (context, code) => code + }, + + { + name: `undefined`, + test: _.isUndefined, + encode: () => undefined, + decode: () => undefined + }, + + { + name: `array`, + test: Array.isArray, + + encode: (context, array) => + array.map((value, index) => + context.memoizedEncode(context, value, index) + ), + + decode: (context, codes) => + codes.map((code, index) => context.memoizedDecode(context, code, index)) + } + ) + + // error + + const isError = value => value instanceof Error + + const encodeError = (context, error) => ({ + // The `message` property has to be handled separately. + message: context.memoizedEncode(context, error.message), + + // Encode all properties (not just standard `message` and `name`) for + // the benefit of Axios. + entries: encodeObject(context, error), + + stack: error.stack + }) + + const decodeError = (context, { entries, message, stack }) => + Object.assign( + Error(context.memoizedDecode(context, message)), + decodeObject(context, entries), + { stack } + ) + + types.push({ + name: `error`, + test: isError, + encode: encodeError, + decode: decodeError + }) + + // function + + const encodeFunction = ( + { allowFunctions, isExport, functionData, functions, processName }, + func + ) => { + if (!functionData.has(func)) { + if (!allowFunctions && !isExport) { + throw new TypeError( + `Refusing to encode functions outside of exported function.` + ) + } + + const address = [processName, Key()] + functionData.set(func, { address, isExport }) + functions[String(address)] = func + } + + return { + ...functionData.get(func), + length: func.length + } + } + + const decodeFunction = (context, { address, isExport, length }) => { + const { functionData, functions, reportExceptionsIn, returns } = context + + if (!(String(address) in functions)) { + const proxyFunction = (...args) => + new Promise( + reportExceptionsIn((resolve, reject) => { + const returnKey = Key() + returns[returnKey] = { resolve, reject } + const [processName, key] = address + + // Function application isn't a type so encode it manually. + context.postMessage(processName, [ + `functionApply`, + { + args: encode({ ...context, allowFunctions: isExport }, args), + key, + returnAddress: [context.processName, returnKey] + } + ]) + }) + ) + + Object.defineProperty(proxyFunction, `length`, { value: length }) + functionData.set(proxyFunction, { address, isExport }) + functions[String(address)] = proxyFunction + } + + return functions[String(address)] + } + + decoders.functionApply = (context, { args, key, returnAddress }) => { + const { functionData, functions, postMessage, reportExceptionsIn } = context + const decodedArgs = decode(context, args) + const address = [context.processName, key] + const func = functions[String(address)] + const { isExport } = functionData.get(func) + const [processName, returnKey] = returnAddress + + const functionReturn = type => + reportExceptionsIn(valueOrReason => { + const encodedValueOrReason = encode( + { ...context, allowFunctions: isExport }, + valueOrReason + ) + + // Function return isn't a type so encode it manually. + postMessage(processName, [ + `functionReturn`, + { + key: returnKey, + [type]: encodedValueOrReason + } + ]) + }) + + Promise.resolve(func(...decodedArgs)).then( + functionReturn(`value`), + functionReturn(`reason`) + ) + } + + decoders.functionReturn = (context, { key, reason, value }) => { + const { returns } = context + const { reject, resolve } = returns[key] + delete returns[key] + + if (reason) { + reject(decode(context, reason)) + } else { + resolve(decode(context, value)) + } + } + + types.push({ + name: `function`, + test: _.isFunction, + encode: encodeFunction, + decode: decodeFunction + }) + + // map + + const encodeMap = (context, map) => + [...map.entries()].map(([key, value]) => [ + context.memoizedEncode(context, key), + context.memoizedEncode(context, value, key) + ]) + + const decodeMap = (context, pairs) => + new Map( + pairs.map(([encodedKey, encodedValue]) => { + const key = context.memoizedDecode(context, encodedKey) + const value = context.memoizedDecode(context, encodedValue, key) + return [key, value] + }) + ) + + types.push({ + name: `map`, + test: _.isMap, + encode: encodeMap, + decode: decodeMap + }) + + // set + + const encodeSet = (context, set) => + [...set].map(value => context.memoizedEncode(context, value)) + + const decodeSet = (context, codes) => + new Set(codes.map(code => context.memoizedDecode(context, code))) + + types.push({ + name: `set`, + test: _.isSet, + encode: encodeSet, + decode: decodeSet + }) + + // object + + const encodeObject = (context, object) => + Object.entries(object).map(([key, value]) => [ + key, + context.memoizedEncode(context, value, key) + ]) + + const decodeObject = (context, entries) => + fromEntries( + entries.map(([key, encodedValue]) => [ + key, + context.memoizedDecode(context, encodedValue, key) + ]) + ) + + types.push({ + name: `object`, + test: _.isPlainObject, + encode: encodeObject, + decode: decodeObject + }) + + // end of type definitions + + types.forEach(({ name, decode }) => { + decoders[name] = decode + }) + + const encodeWithType = (context, value, key = ``) => { + let json + + try { + json = value.toJSON(key) + } catch (exception) { + const type = types.find(({ test }) => test(value)) + + if (type) { + return [type.name, type.encode(context, value)] + } else { + throw new TypeError(`Don't know how to encode "${inspect(value)}".`) + } + } + + return encodeWithType(context, json, key) + } + + const memoizeResolver = (context, value) => value + + const encode = (context, value) => { + const memoizedEncode = _.memoize(encodeWithType, memoizeResolver) + + try { + return memoizedEncode({ ...context, memoizedEncode }, value) + } catch (exception) { + throw Object.assign( + new Error(`Exception while encoding "${inspect(value)}"`), + { exception, value } + ) + } + } + + const decodeFromType = (context, code, key = ``) => { + const [type, encoding] = code + const decode = decoders[type] + + if (decode === undefined) { + throw new TypeError(`Don't know how to decode type ${inspect(type)}.`) + } + + const value = decode(context, encoding) + const revived = context.reviver(key, value) + + // Freeze the newly created value because it's read-only: Changes wouldn't + // otherwise propogate back to the original value in the other realm. + try { + return Object.freeze(revived) + } catch (exception) { + // Some types cannot be frozen (e.g., array buffer views with elements). + return revived + } + } + + const decode = (context, code) => { + const memoizedDecode = _.memoize(decodeFromType, memoizeResolver) + return memoizedDecode({ ...context, memoizedDecode }, code) + } + + const defaultReviver = (key, value) => value + + const ReportExceptionsIn = eventTarget => callback => (...args) => { + try { + return callback(...args) + } catch (exception) { + eventTarget.dispatchEvent( + new ErrorEvent(`error`, { + error: exception, + message: exception.message + }) + ) + } + } + + const Context = () => { + const eventTarget = new EventTarget() + const reportExceptionsIn = ReportExceptionsIn(eventTarget) + + return { + eventTarget, + functionData: new Map(), + functions: {}, + reportExceptionsIn, + returns: {} + } + } + + const ChildProcess = ( + { addListener, postMessage, processName, reviver = defaultReviver }, + exportedFunction + ) => { + const context = { + ...Context(), + postMessage, + processName, + reviver + } + + const messageListener = context.reportExceptionsIn(({ data }) => { + decode(context, data) + }) + + addListener(messageListener) + postMessage(null, encode({ ...context, isExport: true }, exportedFunction)) + return context.eventTarget + } + + const CreateProcess = ({ insertHTML, newProcesses }) => ({ name, src }) => + new Promise(resolve => { + const element = insertHTML( + `` + ) + + newProcesses.set(element.contentWindow, { element, resolve }) + }) + + const RootProcess = ({ + addListener, + hide, + insertHTML, + postMessage, + reviver = defaultReviver, + show + }) => { + const context = { + ...Context(), + postMessage, + processName: null, + reviver + } + + const newProcesses = new Map() + const processElements = new WeakMap() + + const messageListener = context.reportExceptionsIn(({ data, source }) => { + const value = decode(context, data) + + if (newProcesses.has(source)) { + const { element, resolve } = newProcesses.get(source) + newProcesses.delete(source) + processElements.set(value, element) + + if (!foregroundElement) { + setForeground(value) + } + + resolve(value) + } + }) + + addListener(messageListener) + let foregroundElement + + const setForeground = (process, color) => { + if (foregroundElement) { + hide(foregroundElement) + } + + foregroundElement = processElements.get(process) + show(foregroundElement, color) + } + + const createProcess = CreateProcess({ insertHTML, newProcesses }) + return Object.assign(context.eventTarget, { createProcess, setForeground }) + } + + const isSanitary = value => !_.isFunction(value) && canSerialize(value) + const sanitizeArray = array => array.filter(isSanitary).map(sanitize) + + const sanitizeError = error => + Object.assign(new Error(error.message), sanitizeObject(error)) + + const sanitizeObject = object => + fromEntries( + Object.entries(object) + .filter(([, value]) => isSanitary(value)) + .map(([key, value]) => [key, sanitize(value)]) + ) + + const sanitize = value => + isError(value) + ? sanitizeError(value) + : Array.isArray(value) + ? sanitizeArray(value) + : _.isPlainObject(value) + ? sanitizeObject(value) + : isSanitary(value) + ? value + : null + + const sanitizeFunction = callback => async (...args) => { + try { + return sanitize(await callback(...args.map(sanitize))) + } catch (exception) { + throw sanitize(exception) + } + } + + return { + canSerialize, + ChildProcess, + inspect, + RootProcess, + sanitize, + sanitizeFunction + } +} diff --git a/packages/web-microkernel/src/Core.spec.js b/packages/web-microkernel/src/Core.spec.js new file mode 100644 index 00000000000..51dad8ed4bc --- /dev/null +++ b/packages/web-microkernel/src/Core.spec.js @@ -0,0 +1,291 @@ +import { EventTarget, MockWindow } from './mocks' +import Core from './Core.js' + +function ErrorEvent (type, properties) { + this.type = type + this.properties = properties +} + +let counter = 0 + +const getRandomValues = typedArray => { + typedArray.fill(counter) + counter++ +} + +const core = Core({ ErrorEvent, EventTarget, getRandomValues }) + +it(`stringifies a value for debugging`, () => { + expect( + core.inspect([{ a: `This is a very, very, very long string.` }]) + ).toEqual(`[{"a":"This is a very, very, very long s...`) +}) + +const windowMethods = ({ source, windows }) => ({ + addListener: listener => source.addEventListener(`message`, listener), + + postMessage: (processName, message) => { + // console.log(`${source.name} -> ${processName}`, JSON.stringify(message)) + windows.get(processName).postMessage(message, `*`, [], source) + } +}) + +// Create two test windows and serialize a value across them. +const serialize = async (exports, { reviver } = {}) => + new Promise(async (resolve, reject) => { + let childWindow + let name + const rootWindow = MockWindow(`root`) + const windows = new Map([[null, rootWindow]]) + + const insertHTML = html => { + name = html.match(/name="(.+)"/)[1] + childWindow = MockWindow(name) + windows.set(name, childWindow) + return { contentWindow: childWindow } + } + + const rootProcess = core.RootProcess({ + ...windowMethods({ source: rootWindow, windows }), + hide: () => {}, + insertHTML, + reviver, + show: () => {} + }) + + rootProcess.addEventListener(`error`, reject) + const childProcessFunctionPromise = rootProcess.createProcess(``) + + const childProcess = core.ChildProcess( + { + ...windowMethods({ source: childWindow, windows }), + processName: name, + reviver + }, + () => exports + ) + + childProcess.addEventListener(`error`, reject) + const childProcessFunction = await childProcessFunctionPromise + + resolve({ + childProcess, + rootProcess, + value: await childProcessFunction() + }) + }) + +describe(`serializes values across realm boundaries`, () => { + it(`creates consistent object references`, async () => { + const object = { a: 1, b: 2, c: 3 } + const exports = [object, object] + const { value } = await serialize(exports) + const [left, right] = value + expect(left).toBe(right) + }) + + it(`freezes serialized values`, async () => { + const exports = [{ a: 1 }] + const { value } = await serialize(exports) + expect(Object.isFrozen(value)).toEqual(true) + expect(Object.isFrozen(value[0])).toEqual(true) + }) + + it(`toJSON`, async () => { + const wat = Symbol(`wat`) + + const exports = { + value: wat, + + toJSON: () => ({ + type: `symbol`, + description: `wat` + }) + } + + const reviver = (key, value) => + Object(value) === value && + value.type === `symbol` && + value.description === `wat` + ? wat + : value + + const { value } = await serialize(exports, { reviver }) + expect(value).toEqual(wat) + }) + + it(`unsupported type`, async () => { + await expect(serialize(Symbol(`description`))).rejects.toEqual({ + error: new Error(`Exception while encoding "Symbol(description)"`), + message: 'Exception while encoding "Symbol(description)"' + }) + }) + + describe(`types`, () => { + it(`array`, async () => { + const exports = [1, 2, 3] + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + + it(`boolean`, async () => { + const exports = true + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + + it(`Error`, async () => { + const exports = Error(`message`) + exports.name = `name` + + // Axios adds extra properties to errors. + exports.extra = `extra` + + const { value } = await serialize(exports) + expect(value instanceof Error).toEqual(true) + expect(value).toEqual(exports) + }) + + describe(`function`, () => { + it(`has the same arguments length`, async () => { + const exports = (a, b) => a + b + const { value } = await serialize(exports) + expect(value.length).toEqual(exports.length) + }) + + it(`encodes arguments`, async () => { + const exports = async (a, b) => [a, b] + const { value } = await serialize(exports) + const object = {} + const [serializedA, serializedB] = await value(object, object) + expect(serializedA).not.toBe(object) + expect(serializedA).toBe(serializedB) + }) + + it(`returns value`, async () => { + const exports = async (a, b) => a + b + const { value } = await serialize(exports) + expect(await value(1, 2)).toEqual(3) + }) + + it(`allows functions only within exports`, async () => { + const compose = async (f, g) => x => f(g(x)) + const { rootProcess, value } = await serialize(compose) + + const errorPromise = new Promise(resolve => { + rootProcess.addEventListener(`error`, resolve) + }) + + const winner = await Promise.race([ + value(Math.cos, Math.sin), + errorPromise + ]) + + expect(winner.error.exception.message).toEqual( + `Refusing to encode functions outside of exported function.` + ) + }) + + it(`consistent identity`, async () => { + const func = async (a, b) => a + b + const exports = [func, func] + const { value } = await serialize(exports) + const [left, right] = value + expect(left).toBe(right) + }) + + it(`throws exception`, async () => { + const exports = async () => { + throw new Error(`a tantrum`) + } + + const { value } = await serialize(exports) + await expect(value()).rejects.toEqual(new Error(`a tantrum`)) + }) + }) + + it(`map`, async () => { + const exports = new Map([[`a`, 1], [`b`, 2], [`c`, 3]]) + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + + it(`null`, async () => { + const exports = null + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + + it(`number`, async () => { + const exports = 42 + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + + it(`object`, async () => { + const exports = { a: 1, b: 2, c: 3 } + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + + it(`set`, async () => { + const exports = new Set([1, 2, 3]) + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + + it(`undefined`, async () => { + const exports = undefined + const { value } = await serialize(exports) + expect(value).toEqual(exports) + }) + }) +}) + +describe(`sanitize`, () => { + it(`canSerialize`, () => { + expect(core.canSerialize(42)).toEqual(true) + expect(core.canSerialize(Symbol(`description`))).toEqual(false) + }) + + it(`removes non-serializable types`, () => { + expect(core.sanitize(Math.sin)).toEqual(null) + expect(core.sanitize({ a: 1, b: Math.sin })).toEqual({ a: 1 }) + expect(core.sanitize([Math.sin])).toEqual([]) + }) + + it(`is mutable`, () => { + const immutable = Object.freeze({ a: { b: 1 } }) + const mutable = core.sanitize(immutable) + mutable.a.b = 2 + expect(mutable.a.b).toEqual(2) + }) + + it(`error`, () => { + const error = Object.assign(new Error(`message`), { + answer: 42, + sin: Math.sin + }) + + const sanitized = core.sanitize(error) + expect(sanitized instanceof Error).toEqual(true) + + expect(Object.entries(sanitized)).toEqual( + Object.entries(Object.assign(new Error(`message`), { answer: 42 })) + ) + }) + + it(`sanitizes functions`, async () => { + const trickyFunction = async value => ({ + ...value, + sin: Math.sin + }) + + expect( + await core.sanitizeFunction(trickyFunction)({ + answer: 42, + cos: Math.cos + }) + ).toEqual({ answer: 42 }) + }) +}) diff --git a/packages/web-microkernel/src/index.js b/packages/web-microkernel/src/index.js new file mode 100644 index 00000000000..d6e342f1435 --- /dev/null +++ b/packages/web-microkernel/src/index.js @@ -0,0 +1,63 @@ +import Core from './Core' + +const core = Core({ + ErrorEvent, + EventTarget, + getRandomValues: typedArray => window.crypto.getRandomValues(typedArray) +}) + +const tag = `web-microkernel` + +const addListener = listener => { + const multiplexed = ({ data: { data, type }, source }) => { + if (type === tag) { + listener({ data, source }) + } + } + + window.addEventListener(`message`, multiplexed) +} + +const postMessage = frames => (processName, message) => { + const target = processName === null ? window.parent : frames[processName] + const multiplexed = { type: tag, data: message } + target.postMessage(multiplexed, `*`) +} + +const ChildProcess = (options, exportedFunction) => + core.ChildProcess( + { + addListener, + postMessage: postMessage(window.parent.frames), + processName: window.name, + ...options + }, + exportedFunction + ) + +const hide = element => { + element.style.display = `none` +} + +const insertHTML = HTML => { + const body = document.body + body.insertAdjacentHTML(`beforeend`, HTML) + return body.lastChild +} + +const show = element => { + element.style.display = `block` +} + +const RootProcess = options => + core.RootProcess({ + addListener, + hide, + insertHTML, + postMessage: postMessage(window.frames), + show, + ...options + }) + +const { sanitizeFunction } = core +export { ChildProcess, RootProcess, sanitizeFunction } diff --git a/packages/web-microkernel/src/mocks.js b/packages/web-microkernel/src/mocks.js new file mode 100644 index 00000000000..474a22776d9 --- /dev/null +++ b/packages/web-microkernel/src/mocks.js @@ -0,0 +1,37 @@ +import structuredClone from 'realistic-structured-clone' + +export function EventTarget() { + const listeners = {} + + return { + addEventListener: (type, listener) => { + if (!(type in listeners)) { + listeners[type] = new Set() + } + + listeners[type].add(listener) + }, + + dispatchEvent: event => { + const typeListeners = listeners[event.type] || new Set() + ;[...typeListeners].forEach(listener => listener(event.properties)) + }, + + removeEventListener: (type, listener) => { + listeners[type].delete(listener) + } + } +} + +export const MockWindow = name => { + const target = new EventTarget() + + const postMessage = (message, targetOrigin, transfer, source) => { + target.dispatchEvent({ + type: `message`, + properties: { data: structuredClone(message), source } + }) + } + + return { ...target, name, postMessage } +} diff --git a/packages/web-microkernel/wallaby.js b/packages/web-microkernel/wallaby.js new file mode 100644 index 00000000000..e655d0d3e51 --- /dev/null +++ b/packages/web-microkernel/wallaby.js @@ -0,0 +1,13 @@ +module.exports = function() { + return { + files: ['src/**/*.js', '!src/*.spec.js'], + tests: ['src/*.spec.js'], + + env: { + type: 'node', + runner: 'node' + }, + + testFramework: 'jest' + } +} diff --git a/packages/web-microkernel/yarn.lock b/packages/web-microkernel/yarn.lock new file mode 100644 index 00000000000..a4651646950 --- /dev/null +++ b/packages/web-microkernel/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +lodash-es@4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0" + integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q== diff --git a/webpack.config.ci.js b/webpack.config.ci.js index f669fd2af77..9ca3752f633 100644 --- a/webpack.config.ci.js +++ b/webpack.config.ci.js @@ -5,7 +5,11 @@ const CleanWebpackPlugin = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const UglifyJSPlugin = require('uglifyjs-webpack-plugin') const Webpack = require('webpack') -const PATHS = require('./../../config/paths') +const path = require('path') +const PATHS = require('./config/paths') + +const mainProcessBabelConfig = require(`./packages/main-process/babel.config`) +const securityProcessBabelConfig = require(`./packages/security-process/babel.config`) let envConfig = {} let manifestCacheBust = new Date().getTime() @@ -17,7 +21,9 @@ module.exports = { fs: 'empty' }, entry: { - app: ['@babel/polyfill', PATHS.src + '/index.js'] + index: ['@babel/polyfill', `./packages/root-process/src/index.js`], + main: ['@babel/polyfill', './packages/main-process/src/index.js'], + security: ['@babel/polyfill', './packages/security-process/src/index.js'] }, output: { path: PATHS.ciBuild, @@ -30,9 +36,27 @@ module.exports = { rules: [ { test: /\.js$/, + include: path.resolve(__dirname, `packages/security-process/src`), + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { + loader: 'babel-loader', + options: securityProcessBabelConfig( + null, + `./packages/security-process` + ) + } + ] + }, + { + test: /\.js$/, + exclude: path.resolve(__dirname, `packages/security-process/src`), use: [ { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, - 'babel-loader' + { + loader: 'babel-loader', + options: mainProcessBabelConfig(null, `./packages/main-process`) + } ] }, { @@ -68,6 +92,9 @@ module.exports = { } ] }, + // performance: { + // hints: `error` + // }, plugins: [ new CleanWebpackPlugin(), new Webpack.DefinePlugin({ @@ -75,9 +102,20 @@ module.exports = { NETWORK_TYPE: JSON.stringify(envConfig.NETWORK_TYPE) }), new HtmlWebpackPlugin({ - template: PATHS.src + '/index.html', + chunks: [`index`], + template: './packages/root-process/src/index.html', filename: 'index.html' }), + new HtmlWebpackPlugin({ + chunks: [`main`], + template: './packages/main-process/src/index.html', + filename: 'main.html' + }), + new HtmlWebpackPlugin({ + chunks: [`security`], + template: './packages/security-process/src/index.html', + filename: 'security.html' + }), new Webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ @@ -104,35 +142,6 @@ module.exports = { concatenateModules: true, runtimeChunk: { name: `manifest.${manifestCacheBust}` - }, - splitChunks: { - cacheGroups: { - default: { - chunks: 'initial', - name: 'app', - priority: -20, - reuseExistingChunk: true - }, - vendor: { - chunks: 'initial', - name: 'vendor', - priority: -10, - test: function(module) { - // ensure other packages in mono repo don't get put into vendor bundle - return ( - module.resource && - module.resource.indexOf('blockchain-wallet-v4-frontend/src') === - -1 && - module.resource.indexOf( - 'node_modules/blockchain-info-components/src' - ) === -1 && - module.resource.indexOf( - 'node_modules/blockchain-wallet-v4/src' - ) === -1 - ) - } - } - } } } } diff --git a/webpack.config.dev.js b/webpack.config.dev.js index df8586df032..c4ca0895d9e 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -7,8 +7,11 @@ const UglifyJSPlugin = require('uglifyjs-webpack-plugin') const Webpack = require('webpack') const path = require('path') const fs = require('fs') -const PATHS = require('../../config/paths') -const mockWalletOptions = require('../../config/mocks/wallet-options-v4.json') +const PATHS = require('./config/paths') +const mockWalletOptions = require('./config/mocks/wallet-options-v4.json') + +const mainProcessBabelConfig = require(`./packages/main-process/babel.config`) +const securityProcessBabelConfig = require(`./packages/security-process/babel.config`) let envConfig = {} let manifestCacheBust = new Date().getTime() @@ -53,12 +56,26 @@ module.exports = { fs: 'empty' }, entry: { - app: [ + index: [ '@babel/polyfill', 'react-hot-loader/patch', 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/only-dev-server', - PATHS.src + '/index.js' + './packages/root-process/src/index.js' + ], + main: [ + '@babel/polyfill', + 'react-hot-loader/patch', + 'webpack-dev-server/client?http://localhost:8080', + 'webpack/hot/only-dev-server', + './packages/main-process/src/index.js' + ], + security: [ + '@babel/polyfill', + 'react-hot-loader/patch', + 'webpack-dev-server/client?http://localhost:8080', + 'webpack/hot/only-dev-server', + './packages/security-process/src/index.js' ] }, output: { @@ -71,10 +88,30 @@ module.exports = { rules: [ { test: /\.js$/, - include: /src|blockchain-info-components.src|blockchain-wallet-v4.src/, + include: path.resolve(__dirname, `packages/security-process/src`), use: [ { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, - 'babel-loader' + { + loader: 'babel-loader', + options: securityProcessBabelConfig( + null, + `./packages/security-process` + ) + } + ] + }, + { + test: /\.js$/, + exclude: [ + path.resolve(__dirname, `packages/security-process/src`), + /\/node_modules\// + ], + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { + loader: 'babel-loader', + options: mainProcessBabelConfig(null, `./packages/main-process`) + } ] }, { @@ -110,6 +147,9 @@ module.exports = { } ] }, + performance: { + hints: false + }, plugins: [ new CleanWebpackPlugin(), new CaseSensitivePathsPlugin(), @@ -118,9 +158,20 @@ module.exports = { NETWORK_TYPE: JSON.stringify(envConfig.NETWORK_TYPE) }), new HtmlWebpackPlugin({ - template: PATHS.src + '/index.html', + chunks: [`index`], + template: './packages/root-process/src/index.html', filename: 'index.html' }), + new HtmlWebpackPlugin({ + chunks: [`main`], + template: './packages/main-process/src/index.html', + filename: 'main.html' + }), + new HtmlWebpackPlugin({ + chunks: [`security`], + template: './packages/security-process/src/index.html', + filename: 'security.html' + }), new Webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ @@ -129,54 +180,9 @@ module.exports = { ], optimization: { namedModules: true, - minimizer: [ - new UglifyJSPlugin({ - uglifyOptions: { - warnings: false, - compress: { - warnings: false, - keep_fnames: true - }, - mangle: { - keep_fnames: true - } - }, - parallel: true, - cache: true - }) - ], concatenateModules: false, runtimeChunk: { name: `manifest.${manifestCacheBust}` - }, - splitChunks: { - cacheGroups: { - default: { - chunks: 'initial', - name: 'app', - priority: -20, - reuseExistingChunk: true - }, - vendor: { - chunks: 'initial', - name: 'vendor', - priority: -10, - test: function(module) { - // ensure other packages in mono repo don't get put into vendor bundle - return ( - module.resource && - module.resource.indexOf('blockchain-wallet-v4-frontend/src') === - -1 && - module.resource.indexOf( - 'node_modules/blockchain-info-components/src' - ) === -1 && - module.resource.indexOf( - 'node_modules/blockchain-wallet-v4/src' - ) === -1 - ) - } - } - } } }, devServer: { @@ -253,18 +259,18 @@ module.exports = { headers: { 'Access-Control-Allow-Origin': '*', 'Content-Security-Policy': [ - "img-src 'self' data: blob:", - "script-src 'self' 'unsafe-eval'", - "style-src 'self' 'unsafe-inline'", + `img-src ${localhostUrl} data: blob:`, + `script-src ${localhostUrl} 'unsafe-eval'`, + `style-src ${localhostUrl} 'unsafe-inline'`, `frame-src ${envConfig.COINIFY_PAYMENT_DOMAIN} ${ envConfig.WALLET_HELPER_DOMAIN - } ${envConfig.ROOT_URL} https://magic.veriff.me https://localhost:8080`, + } ${envConfig.ROOT_URL} https://magic.veriff.me ${localhostUrl}`, `child-src ${envConfig.COINIFY_PAYMENT_DOMAIN} ${ envConfig.WALLET_HELPER_DOMAIN } blob:`, [ 'connect-src', - "'self'", + localhostUrl, 'ws://localhost:8080', 'wss://localhost:8080', 'wss://api.ledgerwallet.com', @@ -291,8 +297,8 @@ module.exports = { 'https://shapeshift.io' ].join(' '), "object-src 'none'", - "media-src 'self' https://storage.googleapis.com/bc_public_assets/ data: mediastream: blob:", - "font-src 'self'" + `media-src ${localhostUrl} https://storage.googleapis.com/bc_public_assets/ data: mediastream: blob:`, + `font-src ${localhostUrl}` ].join('; ') } } diff --git a/webpack.debug.js b/webpack.debug.js index db0abff74a5..a3a4cb600be 100644 --- a/webpack.debug.js +++ b/webpack.debug.js @@ -7,12 +7,11 @@ const UglifyJSPlugin = require('uglifyjs-webpack-plugin') const Webpack = require('webpack') const path = require('path') const fs = require('fs') -const PATHS = require('../../config/paths') -const mockWalletOptions = require('../../config/mocks/wallet-options-v4.json') -const iSignThisDomain = - mockWalletOptions.platforms.web.coinify.config.iSignThisDomain -const coinifyPaymentDomain = - mockWalletOptions.platforms.web.coinify.config.coinifyPaymentDomain +const PATHS = require('./config/paths') +const mockWalletOptions = require('./config/mocks/wallet-options-v4.json') + +const mainProcessBabelConfig = require(`./packages/main-process/babel.config`) +const securityProcessBabelConfig = require(`./packages/security-process/babel.config`) let envConfig = {} let manifestCacheBust = new Date().getTime() @@ -57,7 +56,9 @@ module.exports = { fs: 'empty' }, entry: { - app: ['@babel/polyfill', PATHS.src + '/index.js'] + index: ['@babel/polyfill', `./packages/root-process/src/index.js`], + main: ['@babel/polyfill', './packages/main-process/src/index.js'], + security: ['@babel/polyfill', './packages/security-process/src/index.js'] }, output: { path: PATHS.ciBuild, @@ -69,9 +70,27 @@ module.exports = { rules: [ { test: /\.js$/, + include: path.resolve(__dirname, `packages/security-process/src`), + use: [ + { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, + { + loader: 'babel-loader', + options: securityProcessBabelConfig( + null, + `./packages/security-process` + ) + } + ] + }, + { + test: /\.js$/, + exclude: [path.resolve(__dirname, `packages/security-process/src`)], use: [ { loader: 'thread-loader', options: { workerParallelJobs: 50 } }, - 'babel-loader' + { + loader: 'babel-loader', + options: mainProcessBabelConfig(null, `./packages/main-process`) + } ] }, { @@ -108,7 +127,7 @@ module.exports = { ] }, performance: { - hints: false + hints: `error` }, plugins: [ new CleanWebpackPlugin(), @@ -118,9 +137,20 @@ module.exports = { NETWORK_TYPE: JSON.stringify(envConfig.NETWORK_TYPE) }), new HtmlWebpackPlugin({ - template: PATHS.src + '/index.html', + chunks: [`index`], + template: './packages/root-process/src/index.html', filename: 'index.html' }), + new HtmlWebpackPlugin({ + chunks: [`main`], + template: './packages/main-process/src/index.html', + filename: 'main.html' + }), + new HtmlWebpackPlugin({ + chunks: [`security`], + template: './packages/security-process/src/index.html', + filename: 'security.html' + }), new Webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/ @@ -146,35 +176,6 @@ module.exports = { concatenateModules: true, runtimeChunk: { name: `manifest.${manifestCacheBust}` - }, - splitChunks: { - cacheGroups: { - default: { - chunks: 'initial', - name: 'app', - priority: -20, - reuseExistingChunk: true - }, - vendor: { - chunks: 'initial', - name: 'vendor', - priority: -10, - test: function(module) { - // ensure other packages in mono repo don't get put into vendor bundle - return ( - module.resource && - module.resource.indexOf('blockchain-wallet-v4-frontend/src') === - -1 && - module.resource.indexOf( - 'node_modules/blockchain-info-components/src' - ) === -1 && - module.resource.indexOf( - 'node_modules/blockchain-wallet-v4/src' - ) === -1 - ) - } - } - } } }, devServer: { @@ -201,11 +202,13 @@ module.exports = { walletHelper: envConfig.WALLET_HELPER_DOMAIN, veriff: envConfig.VERIFF_URL, comWalletApp: envConfig.COM_WALLET_APP, + coinifyPaymentDomain: envConfig.COINIFY_PAYMENT_DOMAIN, comRoot: envConfig.COM_ROOT, ledgerSocket: envConfig.LEDGER_SOCKET_URL, ledger: localhostUrl + '/ledger', // will trigger reverse proxy horizon: envConfig.HORIZON_URL, - coinify: envConfig.COINIFY_URL + coinify: envConfig.COINIFY_URL, + thePit: envConfig.THE_PIT_URL } if (process.env.NODE_ENV === 'testnet') { @@ -249,18 +252,18 @@ module.exports = { headers: { 'Access-Control-Allow-Origin': '*', 'Content-Security-Policy': [ - "img-src 'self' data: blob:", - "script-src 'self'", - "style-src 'self' 'unsafe-inline'", - `frame-src ${iSignThisDomain} ${coinifyPaymentDomain} ${ + `img-src ${localhostUrl} data: blob:`, + `script-src ${localhostUrl}`, + `style-src ${localhostUrl} 'unsafe-inline'`, + `frame-src ${envConfig.COINIFY_PAYMENT_DOMAIN} ${ envConfig.WALLET_HELPER_DOMAIN - } ${envConfig.ROOT_URL} https://localhost:8080 http://localhost:8080`, - `child-src ${iSignThisDomain} ${coinifyPaymentDomain} ${ + } ${envConfig.ROOT_URL} https://magic.veriff.me ${localhostUrl}`, + `child-src ${envConfig.COINIFY_PAYMENT_DOMAIN} ${ envConfig.WALLET_HELPER_DOMAIN } blob:`, [ 'connect-src', - "'self'", + localhostUrl, 'ws://localhost:8080', 'wss://localhost:8080', 'wss://api.ledgerwallet.com', @@ -273,6 +276,7 @@ module.exports = { envConfig.VERIFF_URL, envConfig.LEDGER_SOCKET_URL, envConfig.HORIZON_URL, + 'https://friendbot.stellar.org', 'https://app-api.coinify.com', 'https://app-api.sandbox.coinify.com', 'https://api.sfox.com', @@ -286,8 +290,8 @@ module.exports = { 'https://shapeshift.io' ].join(' '), "object-src 'none'", - "media-src 'self' https://storage.googleapis.com/bc_public_assets/ data: mediastream: blob:", - "font-src 'self'" + `media-src ${localhostUrl} https://storage.googleapis.com/bc_public_assets/ data: mediastream: blob:`, + `font-src ${localhostUrl}` ].join('; ') } } diff --git a/yarn.lock b/yarn.lock index 09e48bfffcf..c22a159cb83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3037,6 +3037,14 @@ accepts@~1.3.4, accepts@~1.3.5: mime-types "~2.1.18" negotiator "0.6.1" +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + acorn-dynamic-import@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" @@ -4744,6 +4752,11 @@ base32.js@^0.1.0: resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.1.0.tgz#b582dec693c2f11e893cf064ee6ac5b6131a2202" integrity sha1-tYLexpPC8R6JPPBk7mrFthMaIgI= +base64-arraybuffer-es6@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.5.0.tgz#27877d01148bcfb3919c17ecf64ea163d9bdba62" + integrity sha512-UCIPaDJrNNj5jG2ZL+nzJ7czvZV/ZYX6LaIRgfVU1k1edJOQg7dkbiSKzwHkNp6aHEHER/PhlFBrMYnlvJJQEw== + base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -5011,6 +5024,22 @@ body-parser@1.18.3: raw-body "2.3.3" type-is "~1.6.16" +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + bonjour@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" @@ -5318,6 +5347,11 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cacache@^10.0.4: version "10.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" @@ -5444,7 +5478,7 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.0.0.tgz#fb7eb569b72ad7a45812f93fd9430a3e410b3dd3" integrity sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw== -camel-case@3.0.x: +camel-case@3.0.x, camel-case@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= @@ -5700,6 +5734,25 @@ chokidar@^2.0.4, chokidar@^2.1.5: optionalDependencies: fsevents "^1.2.7" +chokidar@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" + integrity sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + chownr@^1.0.1, chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -5757,7 +5810,7 @@ classnames@^2.2.5: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== -clean-css@4.2.x: +clean-css@4.2.x, clean-css@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g== @@ -6118,6 +6171,13 @@ content-disposition@0.5.2: resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -6145,6 +6205,11 @@ cookie@0.3.1, cookie@^0.3.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -6213,6 +6278,11 @@ core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.5.7: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== +core-js@^2.5.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== + core-js@^2.6.3, core-js@^2.6.5: version "2.6.5" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" @@ -6887,7 +6957,7 @@ del@^3.0.0: pify "^3.0.0" rimraf "^2.2.8" -del@^4.0.0, del@^4.1.0: +del@^4.0.0, del@^4.1.0, del@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== @@ -8160,6 +8230,42 @@ express@4.16.4, express@^4.16.3, express@^4.16.4: utils-merge "1.0.1" vary "~1.1.2" +express@^4.17.0: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -8443,6 +8549,19 @@ finalhandler@1.1.1: statuses "~1.4.0" unpipe "~1.0.0" +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-babel-config@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.1.0.tgz#acc01043a6749fec34429be6b64f542ebb5d6355" @@ -9423,7 +9542,7 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= -he@1.2.x, he@^1.1.1: +he@1.2.x, he@^1.1.1, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -9542,7 +9661,7 @@ html-entities@^1.2.0, html-entities@^1.2.1: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= -html-minifier@^3.2.3, html-minifier@^3.5.20: +html-minifier@^3.5.20: version "3.5.21" resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== @@ -9555,22 +9674,34 @@ html-minifier@^3.2.3, html-minifier@^3.5.20: relateurl "0.2.x" uglify-js "3.4.x" +html-minifier@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-4.0.0.tgz#cca9aad8bce1175e02e17a8c33e46d8988889f56" + integrity sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig== + dependencies: + camel-case "^3.0.0" + clean-css "^4.2.1" + commander "^2.19.0" + he "^1.2.0" + param-case "^2.1.1" + relateurl "^0.2.7" + uglify-js "^3.5.1" + html-tags@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" integrity sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos= -html-webpack-plugin@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" - integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s= +html-webpack-plugin@4.0.0-beta.8: + version "4.0.0-beta.8" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.8.tgz#d9a8d4322d8cf310f1568f6f4f585a80df0ad378" + integrity sha512-n5S2hJi3/vioRvEDswZP2WFgZU8TUqFoYIrkg5dt+xDC4TigQEhIcl4Y81Qs2La/EqKWuJZP8+ikbHGVmzQ4Mg== dependencies: - html-minifier "^3.2.3" - loader-utils "^0.2.16" - lodash "^4.17.3" - pretty-error "^2.0.2" - tapable "^1.0.0" - toposort "^1.0.0" + html-minifier "^4.0.0" + loader-utils "^1.2.3" + lodash "^4.17.11" + pretty-error "^2.1.1" + tapable "^1.1.3" util.promisify "1.0.0" html-webpack-plugin@^4.0.0-beta.2: @@ -9632,6 +9763,28 @@ http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-parser-js@>=0.4.0: version "0.5.0" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.0.tgz#d65edbede84349d0dc30320815a15d39cc3cbbd8" @@ -9883,6 +10036,11 @@ inherits@2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" @@ -9972,7 +10130,7 @@ inquirer@^5.2.0: strip-ansi "^4.0.0" through "^2.3.6" -internal-ip@^4.2.0: +internal-ip@^4.2.0, internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== @@ -10049,7 +10207,7 @@ ipaddr.js@1.8.0: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= -ipaddr.js@^1.9.0: +ipaddr.js@1.9.0, ipaddr.js@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== @@ -11593,16 +11751,6 @@ loader-utils@1.1.0, loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" -loader-utils@^0.2.16: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" @@ -11723,7 +11871,7 @@ lodash@3.x, lodash@^3.3.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= -lodash@4.17.11, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: +lodash@4.17.11, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -12133,7 +12281,7 @@ mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== -mime-types@2.1.24: +mime-types@2.1.24, mime-types@~2.1.24: version "2.1.24" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== @@ -12152,11 +12300,21 @@ mime@1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + mime@^2.0.3, mime@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" integrity sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg== +mime@^2.4.2: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -12442,6 +12600,11 @@ negotiator@0.6.1: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + neo-async@^2.5.0, neo-async@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835" @@ -13240,7 +13403,7 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" -param-case@2.1.x: +param-case@2.1.x, param-case@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= @@ -13380,6 +13543,11 @@ parseurl@~1.3.2: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -13884,7 +14052,7 @@ prettier@1.17.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.1.tgz#ed64b4e93e370cb8a25b9ef7fef3e4fd1c0995db" integrity sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg== -pretty-error@^2.0.2, pretty-error@^2.1.1: +pretty-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= @@ -14022,6 +14190,14 @@ proxy-addr@~2.0.4: forwarded "~0.1.2" ipaddr.js "1.8.0" +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -14245,6 +14421,11 @@ range-parser@^1.0.3, range-parser@~1.2.0: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + raw-body@2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" @@ -14255,6 +14436,16 @@ raw-body@2.3.3: iconv-lite "0.4.23" unpipe "1.0.0" +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + raw-loader@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" @@ -14833,6 +15024,16 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" +realistic-structured-clone@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.2.tgz#2f8ec225b1f9af20efc79ac96a09043704414959" + integrity sha512-5IEvyfuMJ4tjQOuKKTFNvd+H9GSbE87IcendSBannE28PTrbolgaVg5DdEApRKhtze794iXqVUFKV60GLCNKEg== + dependencies: + core-js "^2.5.3" + domexception "^1.0.1" + typeson "^5.8.2" + typeson-registry "^1.0.0-alpha.20" + realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -15142,7 +15343,7 @@ rehype-parse@^6.0.0: parse5 "^5.0.0" xtend "^4.0.1" -relateurl@0.2.x: +relateurl@0.2.x, relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= @@ -15828,6 +16029,25 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + serialize-javascript@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe" @@ -15872,6 +16092,16 @@ serve-static@1.13.2: parseurl "~1.3.2" send "0.16.2" +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -15907,6 +16137,11 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + sha.js@^2.3.6, sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -16385,7 +16620,7 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.4.0 < 2": +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -16913,6 +17148,11 @@ tapable@^1.0.0, tapable@^1.1.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.0.tgz#0d076a172e3d9ba088fd2272b2668fb8d194b78c" integrity sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA== +tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + tar@^4: version "4.4.6" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" @@ -17113,16 +17353,16 @@ toggle-selection@^1.0.3: resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + toml@^2.3.0: version "2.3.3" resolved "https://registry.yarnpkg.com/toml/-/toml-2.3.3.tgz#8d683d729577cb286231dfc7a8affe58d31728fb" integrity sha512-O7L5hhSQHxuufWUdcTRPfuTh3phKfAZ/dqfxZFoxPCj2RYmpaSGLEIs016FCXItQwNr08yefUB5TSjzRYnajTA== -toposort@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" - integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= - toposort@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" @@ -17252,6 +17492,14 @@ type-is@~1.6.16: media-typer "0.3.0" mime-types "~2.1.18" +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typed-styles@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" @@ -17286,6 +17534,20 @@ typescript-tuple@^2.1.0: dependencies: typescript-compare "^0.0.2" +typeson-registry@^1.0.0-alpha.20: + version "1.0.0-alpha.28" + resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.28.tgz#cc76b71cdafa7f39c0d39fba9573114675ff098b" + integrity sha512-WQD4dPXak+20mUiFFeaI0xh+dqiMQs2fmPFzX6akg1+WN74ocVFjYFwRbWNdtLuCUomxXrxAMP9dxjCakZQHvQ== + dependencies: + base64-arraybuffer-es6 "0.5.0" + typeson "5.13.0" + whatwg-url "7.0.0" + +typeson@5.13.0, typeson@^5.8.2: + version "5.13.0" + resolved "https://registry.yarnpkg.com/typeson/-/typeson-5.13.0.tgz#dc65b23ea1978a315ed4c95e58a5b6936bcc3591" + integrity sha512-xcSaSt+hY/VcRYcqZuVkJwMjDXXJb4CZd51qDocpYw8waA314ygyOPlKhsGsw4qKuJ0tfLLUrxccrm+xvyS0AQ== + u2f-api@0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/u2f-api/-/u2f-api-0.2.7.tgz#17bf196b242f6bf72353d9858e6a7566cc192720" @@ -17312,6 +17574,14 @@ uglify-js@3.4.x, uglify-js@^3.1.4: commander "~2.17.1" source-map "~0.6.1" +uglify-js@^3.5.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" + integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + dependencies: + commander "~2.20.0" + source-map "~0.6.1" + uglify-js@^3.5.12: version "3.5.12" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.12.tgz#6b759cabc08c3e91fe82323d6387019f0c5864cd" @@ -17956,6 +18226,16 @@ webpack-dev-middleware@^3.6.2: range-parser "^1.0.3" webpack-log "^2.0.0" +webpack-dev-middleware@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz#ef751d25f4e9a5c8a35da600c5fda3582b5c6cff" + integrity sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.2" + range-parser "^1.2.1" + webpack-log "^2.0.0" + webpack-dev-server@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.3.1.tgz#7046e49ded5c1255a82c5d942bcdda552b72a62d" @@ -17992,6 +18272,42 @@ webpack-dev-server@3.3.1: webpack-log "^2.0.0" yargs "12.0.5" +webpack-dev-server@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.4.0.tgz#d0928cb9dc77ca554593396f509b5ab4fa529c73" + integrity sha512-yOFL4Ol5B/aCRk0QN/DnSk1vjcJ0qLnEGE0l8pFjw6kYbkbEtt84D750yWHbyKWK9qVOGERk2oZGpEgoXo3cyw== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.6" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.0" + html-entities "^1.2.1" + http-proxy-middleware "^0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + killable "^1.0.1" + loglevel "^1.6.1" + opn "^5.5.0" + portfinder "^1.0.20" + schema-utils "^1.0.0" + selfsigned "^1.10.4" + semver "^6.0.0" + serve-index "^1.9.1" + sockjs "0.3.19" + sockjs-client "1.3.0" + spdy "^4.0.0" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.0" + webpack-log "^2.0.0" + yargs "12.0.5" + webpack-hot-middleware@^2.24.3: version "2.24.3" resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.24.3.tgz#5bb76259a8fc0d97463ab517640ba91d3382d4a6" @@ -18121,19 +18437,19 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" integrity sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw== -whatwg-url@^6.4.1: - version "6.5.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== +whatwg-url@7.0.0, whatwg-url@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1" webidl-conversions "^4.0.2" -whatwg-url@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" - integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1"