From d5ece361aab72324e60bd2398e2f328ff1cdcc73 Mon Sep 17 00:00:00 2001 From: Daniel Ternyak Date: Mon, 8 Jan 2018 23:04:20 -0600 Subject: [PATCH 01/35] Revert "Implement `offline-plugin` for Service Workers / App Cache (#701)" (#760) This reverts commit ef506c54d6ee94ec5756c8d403ffdbe9b94881d4. --- common/index.tsx | 4 +--- package.json | 1 - webpack_config/webpack.prod.js | 13 +++++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/common/index.tsx b/common/index.tsx index 4616d967..17c51cdd 100644 --- a/common/index.tsx +++ b/common/index.tsx @@ -5,15 +5,13 @@ import 'sass/styles.scss'; import 'babel-polyfill'; import 'whatwg-fetch'; import React from 'react'; -import OfflineRuntime from 'offline-plugin/runtime'; import { render } from 'react-dom'; import Root from './Root'; import { configuredStore } from './store'; import consoleAdvertisement from './utils/consoleAdvertisement'; -OfflineRuntime.install(); - const appEl = document.getElementById('app'); + render(, appEl); if (module.hot) { diff --git a/package.json b/package.json index a0cd8336..46434f58 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,6 @@ "node-sass": "4.7.2", "nodemon": "1.14.9", "null-loader": "0.1.1", - "offline-plugin": "4.9.0", "prettier": "1.9.2", "progress": "2.0.0", "react-hot-loader": "3.1.3", diff --git a/webpack_config/webpack.prod.js b/webpack_config/webpack.prod.js index 23cfd648..f13548cc 100644 --- a/webpack_config/webpack.prod.js +++ b/webpack_config/webpack.prod.js @@ -6,7 +6,7 @@ const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); const BabelMinifyPlugin = require('babel-minify-webpack-plugin'); -const OfflinePlugin = require('offline-plugin'); +// const OfflinePlugin = require('offline-plugin') const base = require('./webpack.base'); const config = require('./config'); const rimraf = require('rimraf'); @@ -75,10 +75,15 @@ base.plugins.push( new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.[chunkhash:8].js' - }), - new OfflinePlugin({ - appShell: '/' }) + // For progressive web apps + // new OfflinePlugin({ + // relativePaths: false, + // AppCache: false, + // ServiceWorker: { + // events: true + // } + // }) ); // minimize webpack output From c54ba441fa2f4d382c3947dd92b7434e5d26c397 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 9 Jan 2018 08:27:47 -0600 Subject: [PATCH 02/35] fix(package): update rc-slider to version 8.6.0 (#761) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46434f58..918b0429 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "qrcode": "1.2.0", "qrcode.react": "0.7.2", "query-string": "5.0.1", - "rc-slider": "8.5.0", + "rc-slider": "8.6.0", "react": "16.2.0", "react-dom": "16.2.0", "react-markdown": "2.5.1", From 6e2b74c79a9a88d97ad200c3974bdcb8aaf196e8 Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Tue, 9 Jan 2018 15:13:14 -0500 Subject: [PATCH 03/35] Use network unit everywhere, fix network redux state (#765) * Use network unit in confirmation modal. Make sure network is set at init. * Fix token display * Ensure that when the node changes, the network also changes. Show network unit in unit dropdown. * Type saga, fix tests. --- common/actions/config/actionCreators.ts | 10 +++- common/actions/config/actionTypes.ts | 3 +- .../ConfirmationModal/components/Amount.tsx | 17 ++++-- .../components/UnitDropDown/UnitDropDown.tsx | 11 ++-- common/reducers/config.ts | 1 + common/sagas/config.ts | 55 ++++++++++------- common/selectors/config.ts | 5 +- common/store.ts | 5 ++ spec/reducers/config.spec.ts | 12 +++- spec/sagas/config.spec.ts | 60 ++++++++++--------- 10 files changed, 109 insertions(+), 70 deletions(-) diff --git a/common/actions/config/actionCreators.ts b/common/actions/config/actionCreators.ts index 4a71626c..adb7b467 100644 --- a/common/actions/config/actionCreators.ts +++ b/common/actions/config/actionCreators.ts @@ -1,6 +1,6 @@ import * as interfaces from './actionTypes'; import { TypeKeys } from './constants'; -import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data'; +import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config/data'; export type TForceOfflineConfig = typeof forceOfflineConfig; export function forceOfflineConfig(): interfaces.ForceOfflineAction { @@ -25,10 +25,14 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction { } export type TChangeNode = typeof changeNode; -export function changeNode(nodeSelection: string, node: NodeConfig): interfaces.ChangeNodeAction { +export function changeNode( + nodeSelection: string, + node: NodeConfig, + network: NetworkConfig +): interfaces.ChangeNodeAction { return { type: TypeKeys.CONFIG_NODE_CHANGE, - payload: { nodeSelection, node } + payload: { nodeSelection, node, network } }; } diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index e42d47eb..4157ca52 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -1,5 +1,5 @@ import { TypeKeys } from './constants'; -import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data'; +import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config/data'; /*** Toggle Offline ***/ export interface ToggleOfflineAction { @@ -24,6 +24,7 @@ export interface ChangeNodeAction { payload: { nodeSelection: string; node: NodeConfig; + network: NetworkConfig; }; } diff --git a/common/components/ConfirmationModal/components/Amount.tsx b/common/components/ConfirmationModal/components/Amount.tsx index 44ac8c2f..a02d6e72 100644 --- a/common/components/ConfirmationModal/components/Amount.tsx +++ b/common/components/ConfirmationModal/components/Amount.tsx @@ -7,10 +7,12 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { getDecimal, getUnit } from 'selectors/transaction'; +import { getNetworkConfig } from 'selectors/config'; interface StateProps { unit: string; decimal: number; + network: AppState['config']['network']; } class AmountClass extends Component { @@ -20,14 +22,16 @@ class AmountClass extends Component { withSerializedTransaction={serializedTransaction => { const transactionInstance = makeTransaction(serializedTransaction); const { value, data } = getTransactionFields(transactionInstance); - const { decimal, unit } = this.props; + const { decimal, unit, network } = this.props; + const isToken = unit !== 'ether'; + const handledValue = isToken + ? TokenValue(ERC20.transfer.decodeInput(data)._value) + : Wei(value); return ( ); @@ -39,5 +43,6 @@ class AmountClass extends Component { export const Amount = connect((state: AppState) => ({ decimal: getDecimal(state), - unit: getUnit(state) + unit: getUnit(state), + network: getNetworkConfig(state) }))(AmountClass); diff --git a/common/components/UnitDropDown/UnitDropDown.tsx b/common/components/UnitDropDown/UnitDropDown.tsx index 1572545c..389f18d4 100644 --- a/common/components/UnitDropDown/UnitDropDown.tsx +++ b/common/components/UnitDropDown/UnitDropDown.tsx @@ -7,6 +7,7 @@ import { Query } from 'components/renderCbs'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { getUnit } from 'selectors/transaction'; +import { getNetworkConfig } from 'selectors/config'; interface DispatchProps { setUnitMeta: TSetUnitMeta; @@ -17,6 +18,7 @@ interface StateProps { tokens: TokenBalance[]; allTokens: MergedToken[]; showAllTokens?: boolean; + network: AppState['config']['network']; } const StringDropdown = Dropdown as new () => Dropdown; @@ -24,7 +26,7 @@ const ConditionalStringDropDown = withConditional(StringDropdown); class UnitDropdownClass extends Component { public render() { - const { tokens, allTokens, showAllTokens, unit } = this.props; + const { tokens, allTokens, showAllTokens, unit, network } = this.props; const focusedTokens = showAllTokens ? allTokens : tokens; return (
@@ -32,8 +34,8 @@ class UnitDropdownClass extends Component { params={['readOnly']} withQuery={({ readOnly }) => ( state.config; let hasCheckedOnline = false; export function* pollOfflineStatus(): SagaIterator { while (true) { - const node = yield select(getNodeConfig); - const isOffline = yield select(getOffline); - const isForcedOffline = yield select(getForceOffline); + const node: NodeConfig = yield select(getNodeConfig); + const isOffline: boolean = yield select(getOffline); + const isForcedOffline: boolean = yield select(getForceOffline); // If they're forcing themselves offline, exit the loop. It will be // kicked off again if they toggle it in handleTogglePollOfflineStatus. @@ -104,7 +104,7 @@ export function* handlePollOfflineStatus(): SagaIterator { } export function* handleTogglePollOfflineStatus(): SagaIterator { - const isForcedOffline = yield select(getForceOffline); + const isForcedOffline: boolean = yield select(getForceOffline); if (isForcedOffline) { yield fork(handlePollOfflineStatus); } else { @@ -119,13 +119,20 @@ export function* reload(): SagaIterator { } export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIterator { - const currentNode = yield select(getNode); - const currentConfig = yield select(getNodeConfig); - const currentNetwork = currentConfig.network; + const currentNode: string = yield select(getNode); + const currentConfig: NodeConfig = yield select(getNodeConfig); + const customNets: CustomNetworkConfig[] = yield select(getCustomNetworkConfigs); + const currentNetwork = + getNetworkConfigFromId(currentConfig.network, customNets) || NETWORKS[currentConfig.network]; + + function* bailOut(message: string) { + yield put(showNotification('danger', message, 5000)); + yield put(changeNode(currentNode, currentConfig, currentNetwork)); + } let actionConfig = NODES[action.payload]; if (!actionConfig) { - const customConfigs = yield select(getCustomNodeConfigs); + const customConfigs: CustomNodeConfig[] = yield select(getCustomNodeConfigs); const config = getCustomNodeConfigFromId(action.payload, customConfigs); if (config) { actionConfig = makeNodeConfigFromCustomConfig(config); @@ -133,11 +140,7 @@ export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIte } if (!actionConfig) { - yield put( - showNotification('danger', `Attempted to switch to unknown node '${action.payload}'`, 5000) - ); - yield put(changeNode(currentNode, currentConfig)); - return; + return yield* bailOut(`Attempted to switch to unknown node '${action.payload}'`); } // Grab latest block from the node, before switching, to confirm it's online @@ -157,18 +160,24 @@ export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIte } if (timeout) { - yield put(showNotification('danger', translate('ERROR_32'), 5000)); - yield put(changeNode(currentNode, currentConfig)); - return; + return yield* bailOut(translateRaw('ERROR_32')); + } + + const actionNetwork = getNetworkConfigFromId(actionConfig.network, customNets); + + if (!actionNetwork) { + return yield* bailOut( + `Unknown custom network for your node '${action.payload}', try re-adding it` + ); } yield put(setLatestBlock(latestBlock)); - yield put(changeNode(action.payload, actionConfig)); + yield put(changeNode(action.payload, actionConfig, actionNetwork)); - const currentWallet = yield select(getWalletInst); + const currentWallet: IWallet | null = yield select(getWalletInst); // if there's no wallet, do not reload as there's no component state to resync - if (currentWallet && currentNetwork !== actionConfig.network) { + if (currentWallet && currentConfig.network !== actionConfig.network) { yield call(reload); } } diff --git a/common/selectors/config.ts b/common/selectors/config.ts index 5cd1ce6b..9a8e5c40 100644 --- a/common/selectors/config.ts +++ b/common/selectors/config.ts @@ -8,7 +8,6 @@ import { } from 'config/data'; import { INode } from 'libs/nodes/INode'; import { AppState } from 'reducers'; -import { getNetworkConfigFromId } from 'utils/network'; import { getUnit } from 'selectors/transaction/meta'; import { isEtherUnit } from 'libs/units'; import { SHAPESHIFT_TOKEN_WHITELIST } from 'api/shapeshift'; @@ -25,8 +24,8 @@ export function getNodeLib(state: AppState): INode { return getNodeConfig(state).lib; } -export function getNetworkConfig(state: AppState): NetworkConfig | undefined { - return getNetworkConfigFromId(getNodeConfig(state).network, getCustomNetworkConfigs(state)); +export function getNetworkConfig(state: AppState): NetworkConfig { + return state.config.network; } export function getNetworkContracts(state: AppState): NetworkContract[] | null { diff --git a/common/store.ts b/common/store.ts index fa97bba3..bbc1a92b 100644 --- a/common/store.ts +++ b/common/store.ts @@ -18,6 +18,7 @@ import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage'; import RootReducer from './reducers'; import promiseMiddleware from 'redux-promise-middleware'; import { getNodeConfigFromId } from 'utils/node'; +import { getNetworkConfigFromId } from 'utils/network'; import sagas from './sagas'; import { gasPricetoBase } from 'libs/units'; @@ -72,6 +73,10 @@ const configureStore = () => { // If we couldn't find it, revert to defaults if (savedNode) { savedConfigState.node = savedNode; + const network = getNetworkConfigFromId(savedNode.network, savedConfigState.customNetworks); + if (network) { + savedConfigState.network = network; + } } else { savedConfigState.nodeSelection = configInitialState.nodeSelection; } diff --git a/spec/reducers/config.spec.ts b/spec/reducers/config.spec.ts index 0e908ac0..33b40017 100644 --- a/spec/reducers/config.spec.ts +++ b/spec/reducers/config.spec.ts @@ -1,6 +1,6 @@ import { config, INITIAL_STATE } from 'reducers/config'; import * as configActions from 'actions/config'; -import { NODES } from 'config/data'; +import { NODES, NETWORKS } from 'config/data'; import { makeCustomNodeId, makeNodeConfigFromCustomConfig } from 'utils/node'; const custNode = { @@ -21,8 +21,10 @@ describe('config reducer', () => { it('should handle CONFIG_NODE_CHANGE', () => { const key = Object.keys(NODES)[0]; + const node = NODES[key]; + const network = NETWORKS[node.network]; - expect(config(undefined, configActions.changeNode(key, NODES[key]))).toEqual({ + expect(config(undefined, configActions.changeNode(key, node, network))).toEqual({ ...INITIAL_STATE, node: NODES[key], nodeSelection: key @@ -85,7 +87,11 @@ describe('config reducer', () => { const addedState = config(undefined, configActions.addCustomNode(custNode)); const addedAndActiveState = config( addedState, - configActions.changeNode(customNodeId, makeNodeConfigFromCustomConfig(custNode)) + configActions.changeNode( + customNodeId, + makeNodeConfigFromCustomConfig(custNode), + NETWORKS[custNode.network] + ) ); const removedState = config(addedAndActiveState, configActions.removeCustomNode(custNode)); diff --git a/spec/sagas/config.spec.ts b/spec/sagas/config.spec.ts index ffa11969..fce3d408 100644 --- a/spec/sagas/config.spec.ts +++ b/spec/sagas/config.spec.ts @@ -13,20 +13,21 @@ import { unsetWeb3NodeOnWalletEvent, equivalentNodeOrDefault } from 'sagas/config'; -import { NODES, NodeConfig } from 'config/data'; +import { NODES, NodeConfig, NETWORKS } from 'config/data'; import { getNode, getNodeConfig, getOffline, getForceOffline, - getCustomNodeConfigs + getCustomNodeConfigs, + getCustomNetworkConfigs } from 'selectors/config'; import { INITIAL_STATE as configInitialState } from 'reducers/config'; import { getWalletInst } from 'selectors/wallet'; import { Web3Wallet } from 'libs/wallet'; import { RPCNode } from 'libs/nodes'; import { showNotification } from 'actions/notifications'; -import translate from 'translations'; +import { translateRaw } from 'translations'; // init module configuredStore.getState(); @@ -181,10 +182,13 @@ describe('handleNodeChangeIntent*', () => { // normal operation variables const defaultNode = configInitialState.nodeSelection; const defaultNodeConfig = NODES[defaultNode]; + const customNetworkConfigs = []; + const defaultNodeNetwork = NETWORKS[defaultNodeConfig.network]; const newNode = Object.keys(NODES).reduce( (acc, cur) => (NODES[acc].network === defaultNodeConfig.network ? cur : acc) ); const newNodeConfig = NODES[newNode]; + const newNodeNetwork = NETWORKS[newNodeConfig.network]; const changeNodeIntentAction = changeNodeIntent(newNode); const truthyWallet = true; const latestBlock = '0xa'; @@ -198,6 +202,14 @@ describe('handleNodeChangeIntent*', () => { const data = {} as any; data.gen = cloneableGenerator(handleNodeChangeIntent)(changeNodeIntentAction); + function shouldBailOut(gen, nextVal, errMsg) { + expect(gen.next(nextVal).value).toEqual(put(showNotification('danger', errMsg, 5000))); + expect(gen.next().value).toEqual( + put(changeNode(defaultNode, defaultNodeConfig, defaultNodeNetwork)) + ); + expect(gen.next().done).toEqual(true); + } + beforeAll(() => { originalRandom = Math.random; Math.random = () => 0.001; @@ -215,17 +227,17 @@ describe('handleNodeChangeIntent*', () => { expect(data.gen.next(defaultNode).value).toEqual(select(getNodeConfig)); }); - it('should race getCurrentBlock and delay', () => { - expect(data.gen.next(defaultNodeConfig).value).toMatchSnapshot(); + it('should select getCustomNetworkConfigs', () => { + expect(data.gen.next(defaultNodeConfig).value).toEqual(select(getCustomNetworkConfigs)); }); - it('should put showNotification and put changeNode if timeout', () => { + it('should race getCurrentBlock and delay', () => { + expect(data.gen.next(customNetworkConfigs).value).toMatchSnapshot(); + }); + + it('should show error and revert to previous node if check times out', () => { data.clone1 = data.gen.clone(); - expect(data.clone1.next(raceFailure).value).toEqual( - put(showNotification('danger', translate('ERROR_32'), 5000)) - ); - expect(data.clone1.next().value).toEqual(put(changeNode(defaultNode, defaultNodeConfig))); - expect(data.clone1.next().done).toEqual(true); + shouldBailOut(data.clone1, raceFailure, translateRaw('ERROR_32')); }); it('should put setLatestBlock', () => { @@ -234,7 +246,7 @@ describe('handleNodeChangeIntent*', () => { it('should put changeNode', () => { expect(data.gen.next().value).toEqual( - put(changeNode(changeNodeIntentAction.payload, newNodeConfig)) + put(changeNode(changeNodeIntentAction.payload, newNodeConfig, newNodeNetwork)) ); }); @@ -272,30 +284,24 @@ describe('handleNodeChangeIntent*', () => { it('should select getCustomNodeConfig and match race snapshot', () => { data.customNode.next(); data.customNode.next(defaultNode); - expect(data.customNode.next(defaultNodeConfig).value).toEqual(select(getCustomNodeConfigs)); + data.customNode.next(defaultNodeConfig); + expect(data.customNode.next(customNetworkConfigs).value).toEqual(select(getCustomNodeConfigs)); expect(data.customNode.next(customNodeConfigs).value).toMatchSnapshot(); }); // test custom node not found - it('should select getCustomNodeConfig, put showNotification, put changeNode', () => { + it('should handle unknown / missing custom node', () => { data.customNodeNotFound.next(); data.customNodeNotFound.next(defaultNode); - expect(data.customNodeNotFound.next(defaultNodeConfig).value).toEqual( + data.customNodeNotFound.next(defaultNodeConfig); + expect(data.customNodeNotFound.next(customNetworkConfigs).value).toEqual( select(getCustomNodeConfigs) ); - expect(data.customNodeNotFound.next(customNodeConfigs).value).toEqual( - put( - showNotification( - 'danger', - `Attempted to switch to unknown node '${customNodeNotFoundAction.payload}'`, - 5000 - ) - ) + shouldBailOut( + data.customNodeNotFound, + customNodeConfigs, + `Attempted to switch to unknown node '${customNodeNotFoundAction.payload}'` ); - expect(data.customNodeNotFound.next().value).toEqual( - put(changeNode(defaultNode, defaultNodeConfig)) - ); - expect(data.customNodeNotFound.next().done).toEqual(true); }); }); From 26619e28cc7c2fbea89380cd1d30ab93751700be Mon Sep 17 00:00:00 2001 From: Jack Clancy Date: Wed, 10 Jan 2018 00:17:52 -0500 Subject: [PATCH 04/35] Enforce HTTPS / Prevent Reverse Tabnabbing (#773) * working version of test custom rule config * setting no imports to false so tests will pass * adding anchor blank noopener rule, rule currently off to allow tests to pass * removing copied code from tslint-microsoft-contrib * adding tslint-microsoft-contrib to dev deps * extending tslint for external http rule * locking tslint-microsoft-contrib version and turning on target blank noopener rule * final fixes for pull #663 * add noopener noreferrer as needed * fixing false positives for a tags without href * really fix linting errors * fix imports * remove accidently(?) added LedgerNano duplicate file --- .../components/BalanceSidebar/AccountInfo.tsx | 12 ++- common/components/BalanceSidebar/Promos.tsx | 1 + common/components/ErrorScreen/index.tsx | 2 +- .../TransactionSucceeded.tsx | 7 +- .../Header/components/GasPriceDropdown.tsx | 6 +- .../Header/components/NavigationLink.tsx | 8 +- .../components/DeterministicWalletsModal.tsx | 10 +- .../WalletDecrypt/components/LedgerNano.tsx | 10 +- .../WalletDecrypt/components/Mnemonic.tsx | 2 +- .../WalletDecrypt/components/Trezor.tsx | 6 +- common/components/ui/Help.tsx | 2 +- common/components/ui/NewTabLink.tsx | 2 +- common/containers/Tabs/Help/index.tsx | 3 +- .../Tabs/Swap/components/BitcoinQR.tsx | 2 +- .../Tabs/Swap/components/CurrentRates.tsx | 7 +- .../Swap/components/SwapInfoHeaderTitle.tsx | 7 +- .../Tabs/Swap/components/SwapProgress.tsx | 4 +- common/index.html | 8 +- .../noExternalHttpLinkRule.js | 95 ++++++++++++++++++ .../noExternalHttpLinkRule.ts | 96 +++++++++++++++++++ package.json | 1 + tslint.json | 6 +- 22 files changed, 266 insertions(+), 31 deletions(-) create mode 100644 custom_linting_rules/noExternalHttpLinkRule.js create mode 100644 custom_linting_rules/noExternalHttpLinkRule.ts diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index 074a2188..e363b4e3 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -93,14 +93,22 @@ export default class AccountInfo extends React.Component {
    {!!blockExplorer && (
  • - + {`${network.name} (${blockExplorer.name})`}
  • )} {!!tokenExplorer && (
  • - + {`Tokens (${tokenExplorer.name})`}
  • diff --git a/common/components/BalanceSidebar/Promos.tsx b/common/components/BalanceSidebar/Promos.tsx index 06693d12..6c4ac07c 100644 --- a/common/components/BalanceSidebar/Promos.tsx +++ b/common/components/BalanceSidebar/Promos.tsx @@ -58,6 +58,7 @@ export default class Promos extends React.Component<{}, State> { className="Promos-promo" key={promo.href} target="_blank" + rel="noopener noreferrer" href={promo.href} style={{ backgroundColor: promo.color }} > diff --git a/common/components/ErrorScreen/index.tsx b/common/components/ErrorScreen/index.tsx index bb263c4f..643283b3 100644 --- a/common/components/ErrorScreen/index.tsx +++ b/common/components/ErrorScreen/index.tsx @@ -19,7 +19,7 @@ const ErrorScreen: React.SFC = ({ error }) => { Please contact{' '} support@myetherwallet.com diff --git a/common/components/ExtendedNotifications/TransactionSucceeded.tsx b/common/components/ExtendedNotifications/TransactionSucceeded.tsx index 8385db35..074da8d0 100644 --- a/common/components/ExtendedNotifications/TransactionSucceeded.tsx +++ b/common/components/ExtendedNotifications/TransactionSucceeded.tsx @@ -14,7 +14,12 @@ const TransactionSucceeded = ({ txHash, blockExplorer }: TransactionSucceededPro return ( diff --git a/common/components/Header/components/GasPriceDropdown.tsx b/common/components/Header/components/GasPriceDropdown.tsx index 7110a9af..a2610815 100644 --- a/common/components/Header/components/GasPriceDropdown.tsx +++ b/common/components/Header/components/GasPriceDropdown.tsx @@ -61,7 +61,11 @@ export default class GasPriceDropdown extends Component { 21 GWEI.

    - + Read more

    diff --git a/common/components/Header/components/NavigationLink.tsx b/common/components/Header/components/NavigationLink.tsx index 8d6f0a48..42ca117e 100644 --- a/common/components/Header/components/NavigationLink.tsx +++ b/common/components/Header/components/NavigationLink.tsx @@ -35,7 +35,13 @@ class NavigationLink extends React.Component { const linkEl = link.external || !link.to ? ( - + {translate(link.name)} ) : ( diff --git a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx index 15e502ae..769d088a 100644 --- a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx +++ b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx @@ -292,7 +292,11 @@ class DeterministicWalletsModalClass extends React.Component { )} - + @@ -310,7 +314,9 @@ function mapStateToProps(state: AppState) { }; } -export const DeterministicWalletsModal = connect(mapStateToProps, { +const DeterministicWalletsModal = connect(mapStateToProps, { getDeterministicWallets, setDesiredToken })(DeterministicWalletsModalClass); + +export default DeterministicWalletsModal; diff --git a/common/components/WalletDecrypt/components/LedgerNano.tsx b/common/components/WalletDecrypt/components/LedgerNano.tsx index ef993076..7d97c49e 100644 --- a/common/components/WalletDecrypt/components/LedgerNano.tsx +++ b/common/components/WalletDecrypt/components/LedgerNano.tsx @@ -1,7 +1,7 @@ import './LedgerNano.scss'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; -import { DeterministicWalletsModal } from './DeterministicWalletsModal'; +import DeterministicWalletsModal from './DeterministicWalletsModal'; import { LedgerWallet } from 'libs/wallet'; import Ledger3 from 'vendor/ledger3'; import LedgerEth from 'vendor/ledger-eth'; @@ -81,7 +81,7 @@ export class LedgerNanoSDecrypt extends Component { className="LedgerDecrypt-buy btn btn-sm btn-default" href="https://www.ledgerwallet.com/r/fa4b?path=/products/" target="_blank" - rel="noopener" + rel="noopener noreferrer" > {translate('Don’t have a Ledger? Order one now!')} @@ -92,9 +92,9 @@ export class LedgerNanoSDecrypt extends Component { Guides:
    How to use MyEtherWallet with your Nano S @@ -103,7 +103,7 @@ export class LedgerNanoSDecrypt extends Component { How to secure your tokens with your Nano S diff --git a/common/components/WalletDecrypt/components/Mnemonic.tsx b/common/components/WalletDecrypt/components/Mnemonic.tsx index 0a880807..b9fd2ca8 100644 --- a/common/components/WalletDecrypt/components/Mnemonic.tsx +++ b/common/components/WalletDecrypt/components/Mnemonic.tsx @@ -2,7 +2,7 @@ import { mnemonicToSeed, validateMnemonic } from 'bip39'; import DPATHS from 'config/dpaths'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; -import { DeterministicWalletsModal } from './DeterministicWalletsModal'; +import DeterministicWalletsModal from './DeterministicWalletsModal'; import { formatMnemonic } from 'utils/formatters'; const DEFAULT_PATH = DPATHS.MNEMONIC[0].value; diff --git a/common/components/WalletDecrypt/components/Trezor.tsx b/common/components/WalletDecrypt/components/Trezor.tsx index aea42c2c..26971a5b 100644 --- a/common/components/WalletDecrypt/components/Trezor.tsx +++ b/common/components/WalletDecrypt/components/Trezor.tsx @@ -3,7 +3,7 @@ import { TrezorWallet } from 'libs/wallet'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; import TrezorConnect from 'vendor/trezor-connect'; -import { DeterministicWalletsModal } from './DeterministicWalletsModal'; +import DeterministicWalletsModal from './DeterministicWalletsModal'; import './Trezor.scss'; import { Spinner } from 'components/ui'; const DEFAULT_PATH = DPATHS.TREZOR[0].value; @@ -53,7 +53,7 @@ export class TrezorDecrypt extends Component { className="TrezorDecrypt-buy btn btn-sm btn-default" href="https://trezor.io/?a=myetherwallet.com" target="_blank" - rel="noopener" + rel="noopener noreferrer" > {translate('Don’t have a TREZOR? Order one now!')} @@ -65,7 +65,7 @@ export class TrezorDecrypt extends Component { How to use TREZOR with MyEtherWallet diff --git a/common/components/ui/Help.tsx b/common/components/ui/Help.tsx index 3cea97b1..de018200 100644 --- a/common/components/ui/Help.tsx +++ b/common/components/ui/Help.tsx @@ -11,7 +11,7 @@ interface Props { const Help = ({ size = 'x1', link }: Props) => { return ( - + ); diff --git a/common/components/ui/NewTabLink.tsx b/common/components/ui/NewTabLink.tsx index b93da2d0..cbd11930 100644 --- a/common/components/ui/NewTabLink.tsx +++ b/common/components/ui/NewTabLink.tsx @@ -36,7 +36,7 @@ interface NewTabLinkProps extends AAttributes { } const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => ( - + {content || children} ); diff --git a/common/containers/Tabs/Help/index.tsx b/common/containers/Tabs/Help/index.tsx index 4a74cd4a..6bba17dc 100644 --- a/common/containers/Tabs/Help/index.tsx +++ b/common/containers/Tabs/Help/index.tsx @@ -17,6 +17,7 @@ const Help = () => ( {translate('HELP_Warning')} @@ -25,7 +26,7 @@ const Help = () => (
  • This page is deprecated. Please check out our more up-to-date and searchable{' '} - + Knowledge Base.{' '}

    diff --git a/common/containers/Tabs/Swap/components/BitcoinQR.tsx b/common/containers/Tabs/Swap/components/BitcoinQR.tsx index 39f29758..27cd3a51 100644 --- a/common/containers/Tabs/Swap/components/BitcoinQR.tsx +++ b/common/containers/Tabs/Swap/components/BitcoinQR.tsx @@ -21,7 +21,7 @@ export default class BitcoinQR extends Component { Orders that take too long will have to be processed manually & and may delay the amount of time it takes to receive your coins.
    - + Please use the recommended TX fees seen here.

    diff --git a/common/containers/Tabs/Swap/components/CurrentRates.tsx b/common/containers/Tabs/Swap/components/CurrentRates.tsx index 6c7686c3..58ca0540 100644 --- a/common/containers/Tabs/Swap/components/CurrentRates.tsx +++ b/common/containers/Tabs/Swap/components/CurrentRates.tsx @@ -88,7 +88,12 @@ export default class CurrentRates extends Component {
    {children} - +
    diff --git a/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx b/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx index d0b37a97..27e86ed8 100644 --- a/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx +++ b/common/containers/Tabs/Swap/components/SwapInfoHeaderTitle.tsx @@ -27,7 +27,12 @@ export default class SwapInfoHeaderTitle extends Component{translate('SWAP_information')}
  • diff --git a/common/containers/Tabs/Swap/components/SwapProgress.tsx b/common/containers/Tabs/Swap/components/SwapProgress.tsx index ce390c59..a84789ab 100644 --- a/common/containers/Tabs/Swap/components/SwapProgress.tsx +++ b/common/containers/Tabs/Swap/components/SwapProgress.tsx @@ -49,7 +49,7 @@ export default class SwapProgress extends Component { if (destinationId !== 'BTC') { link = bityConfig.ETHTxExplorer(outputTx); linkElement = ( - + {notificationMessage} ); @@ -57,7 +57,7 @@ export default class SwapProgress extends Component { } else { link = bityConfig.BTCTxExplorer(outputTx); linkElement = ( - + {notificationMessage} ); diff --git a/common/index.html b/common/index.html index d77f5fbf..69e84eb1 100644 --- a/common/index.html +++ b/common/index.html @@ -22,7 +22,7 @@

    If you are not sure why you are seeing this message, or are unsure of how to enable Javascript, please visit - enable-javascript.com + enable-javascript.com to learn more.

@@ -41,18 +41,18 @@ to a laptop or computer to continue using MyEtherWallet.

- + Firefox - Chrome - + Opera diff --git a/custom_linting_rules/noExternalHttpLinkRule.js b/custom_linting_rules/noExternalHttpLinkRule.js new file mode 100644 index 00000000..45741cfd --- /dev/null +++ b/custom_linting_rules/noExternalHttpLinkRule.js @@ -0,0 +1,95 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +exports.__esModule = true; +var ts = require("typescript"); +var Lint = require("tslint"); +var ErrorTolerantWalker_1 = require("../node_modules/tslint-microsoft-contrib/utils/ErrorTolerantWalker"); +var JsxAttribute_1 = require("../node_modules/tslint-microsoft-contrib/utils/JsxAttribute"); +var FAILURE_STRING = 'Anchor tags with an external link must use https'; +/** + * Implementation of the no-external-http-link rule. + */ +var Rule = /** @class */ (function (_super) { + __extends(Rule, _super); + function Rule() { + return _super !== null && _super.apply(this, arguments) || this; + } + Rule.prototype.apply = function (sourceFile) { + if (sourceFile.languageVariant === ts.LanguageVariant.JSX) { + return this.applyWithWalker(new NoExternalHttpLinkRuleWalker(sourceFile, this.getOptions())); + } + else { + return []; + } + }; + Rule.metadata = { + ruleName: 'tno-external-http-link', + type: 'functionality', + description: 'Anchor tags with an external link must use https', + options: null, + optionsDescription: '', + typescriptOnly: true, + issueClass: 'SDL', + issueType: 'Error', + severity: 'Critical', + level: 'Mandatory', + group: 'Security', + commonWeaknessEnumeration: '242,676' + }; + return Rule; +}(Lint.Rules.AbstractRule)); +exports.Rule = Rule; +var NoExternalHttpLinkRuleWalker = /** @class */ (function (_super) { + __extends(NoExternalHttpLinkRuleWalker, _super); + function NoExternalHttpLinkRuleWalker() { + return _super !== null && _super.apply(this, arguments) || this; + } + NoExternalHttpLinkRuleWalker.prototype.visitJsxElement = function (node) { + var openingElement = node.openingElement; + this.validateOpeningElement(openingElement); + _super.prototype.visitJsxElement.call(this, node); + }; + NoExternalHttpLinkRuleWalker.prototype.visitJsxSelfClosingElement = function (node) { + this.validateOpeningElement(node); + _super.prototype.visitJsxSelfClosingElement.call(this, node); + }; + NoExternalHttpLinkRuleWalker.prototype.validateOpeningElement = function (openingElement) { + if (openingElement.tagName.getText() === 'a') { + var allAttributes = JsxAttribute_1.getJsxAttributesFromJsxElement(openingElement); + var href = allAttributes.href; + if (href !== null && !isSafeHrefAttributeValue(href) && JsxAttribute_1.getStringLiteral(href) !== 'undefined') { + this.addFailureAt(openingElement.getStart(), openingElement.getWidth(), FAILURE_STRING); + } + } + }; + return NoExternalHttpLinkRuleWalker; +}(ErrorTolerantWalker_1.ErrorTolerantWalker)); +function isSafeHrefAttributeValue(attribute) { + if (JsxAttribute_1.isEmpty(attribute)) { + return false; + } + if (attribute.initializer.kind === ts.SyntaxKind.JsxExpression) { + var expression = attribute.initializer; + if (expression.expression !== null && + expression.expression.kind !== ts.SyntaxKind.StringLiteral) { + return true; // attribute value is not a string literal, so do not validate + } + } + var stringValue = JsxAttribute_1.getStringLiteral(attribute); + if (stringValue === '#') { + return true; + } + else if (stringValue === null || stringValue.length === 0) { + return false; + } + return stringValue.indexOf('https://') >= 0; +} diff --git a/custom_linting_rules/noExternalHttpLinkRule.ts b/custom_linting_rules/noExternalHttpLinkRule.ts new file mode 100644 index 00000000..8f88e2c2 --- /dev/null +++ b/custom_linting_rules/noExternalHttpLinkRule.ts @@ -0,0 +1,96 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; + +import { ErrorTolerantWalker } from '../node_modules/tslint-microsoft-contrib/utils/ErrorTolerantWalker'; +import { ExtendedMetadata } from '../node_modules/tslint-microsoft-contrib/utils/ExtendedMetadata'; +import { Utils } from '../node_modules/tslint-microsoft-contrib/utils/Utils'; + +import { + getJsxAttributesFromJsxElement, + getStringLiteral, + isEmpty +} from '../node_modules/tslint-microsoft-contrib/utils/JsxAttribute'; + +const FAILURE_STRING = 'Anchor tags with an external link must use https'; + +/** + * Implementation of the no-external-http-link rule. + */ +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: ExtendedMetadata = { + ruleName: 'tno-external-http-link', + type: 'functionality', + description: 'Anchor tags with an external link must use https', + options: null, + optionsDescription: '', + typescriptOnly: true, + issueClass: 'SDL', + issueType: 'Error', + severity: 'Critical', + level: 'Mandatory', + group: 'Security', + commonWeaknessEnumeration: '242,676' + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + if (sourceFile.languageVariant === ts.LanguageVariant.JSX) { + return this.applyWithWalker(new NoExternalHttpLinkRuleWalker(sourceFile, this.getOptions())); + } else { + return []; + } + } +} + +class NoExternalHttpLinkRuleWalker extends ErrorTolerantWalker { + protected visitJsxElement(node: ts.JsxElement): void { + const openingElement: ts.JsxOpeningElement = node.openingElement; + this.validateOpeningElement(openingElement); + super.visitJsxElement(node); + } + + protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void { + this.validateOpeningElement(node); + super.visitJsxSelfClosingElement(node); + } + + private validateOpeningElement(openingElement: ts.JsxOpeningLikeElement): void { + if (openingElement.tagName.getText() === 'a') { + const allAttributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement( + openingElement + ); + const href: ts.JsxAttribute = allAttributes.href; + if ( + href !== null && + !isSafeHrefAttributeValue(href) && + getStringLiteral(href) !== 'undefined' + ) { + this.addFailureAt(openingElement.getStart(), openingElement.getWidth(), FAILURE_STRING); + } + } + } +} + +function isSafeHrefAttributeValue(attribute: ts.JsxAttribute): boolean { + if (isEmpty(attribute)) { + return false; + } + + if (attribute.initializer.kind === ts.SyntaxKind.JsxExpression) { + const expression: ts.JsxExpression = attribute.initializer; + if ( + expression.expression !== null && + expression.expression.kind !== ts.SyntaxKind.StringLiteral + ) { + return true; // attribute value is not a string literal, so do not validate + } + } + + const stringValue = getStringLiteral(attribute); + if (stringValue === '#') { + return true; + } else if (stringValue === null || stringValue.length === 0) { + return false; + } + + return stringValue.indexOf('https://') >= 0; +} diff --git a/package.json b/package.json index 918b0429..6b19cf05 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "ts-loader": "3.2.0", "tslint": "5.8.0", "tslint-config-prettier": "1.6.0", + "tslint-microsoft-contrib": "5.0.1", "tslint-react": "3.3.3", "types-rlp": "0.0.1", "typescript": "2.6.2", diff --git a/tslint.json b/tslint.json index 1f943613..122d5c9b 100644 --- a/tslint.json +++ b/tslint.json @@ -23,7 +23,9 @@ "no-var-requires": false, "jsx-wrap-multiline": false, "comment-format": false, - "ordered-imports": false + "ordered-imports": false, + "react-anchor-blank-noopener": true, + "no-external-http-link": true }, - "rulesDirectory": [] + "rulesDirectory": ["node_modules/tslint-microsoft-contrib", "custom_linting_rules"] } From fb0cce1d684d08eb00dd582296f6ed8db7368507 Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Wed, 10 Jan 2018 00:29:24 -0500 Subject: [PATCH 05/35] Rename Aux to AuxComponent for Windows (#771) * Rename reseved filename 'Aux' to 'AuxComponent' * fix prettier --- common/components/ui/{Aux.tsx => AuxComponent.tsx} | 0 common/components/ui/index.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename common/components/ui/{Aux.tsx => AuxComponent.tsx} (100%) diff --git a/common/components/ui/Aux.tsx b/common/components/ui/AuxComponent.tsx similarity index 100% rename from common/components/ui/Aux.tsx rename to common/components/ui/AuxComponent.tsx diff --git a/common/components/ui/index.ts b/common/components/ui/index.ts index 759477fb..452d4b04 100644 --- a/common/components/ui/index.ts +++ b/common/components/ui/index.ts @@ -11,5 +11,5 @@ export { default as Spinner } from './Spinner'; export { default as SwapDropdown } from './SwapDropdown'; export { default as Tooltip } from './Tooltip'; export * from './ConditionalInput'; -export * from './Aux'; +export * from './AuxComponent'; export * from './Expandable'; From 1f2d5b853d2e5b4127a69248d2bc9424926fb88e Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 10 Jan 2018 14:59:38 -0600 Subject: [PATCH 06/35] chore(package): update nodemon to version 1.14.10 (#779) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b19cf05..a8747bd9 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "lint-staged": "6.0.0", "minimist": "1.2.0", "node-sass": "4.7.2", - "nodemon": "1.14.9", + "nodemon": "1.14.10", "null-loader": "0.1.1", "prettier": "1.9.2", "progress": "2.0.0", From a84a6e98fcae9f288d6e4ccff7acd69d51ae055b Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Wed, 10 Jan 2018 15:52:17 -0600 Subject: [PATCH 07/35] chore(package): update jest to version 22.0.5 (#783) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8747bd9..0e746389 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "html-webpack-plugin": "2.30.1", "husky": "0.14.3", "image-webpack-loader": "3.4.2", - "jest": "22.0.4", + "jest": "22.0.5", "less": "2.7.3", "less-loader": "4.0.5", "lint-staged": "6.0.0", From af2e0b69e1cd307d91d25edd310ae064b8fa308b Mon Sep 17 00:00:00 2001 From: aitrean Date: Thu, 11 Jan 2018 01:44:13 -0500 Subject: [PATCH 08/35] Web Worker Decrypt (#680) 1. Attempt an empty password every time a keystore is uploaded. 2. Delegate scrypt decryption (ie ethereumjs-wallet.fromV3) to its own web worker and interface with it through an async typescript function that gets handled in the wallet saga. This keeps the UI unblocked when scrypt takes a long time to decrypt. 3. Add logic to show a spinner x number of milliseconds after file upload so the user will understand when a wallet is being decrypted. --- common/actions/wallet/actionCreators.ts | 13 ++++++ common/actions/wallet/actionTypes.ts | 13 +++++- common/actions/wallet/constants.ts | 5 ++- .../WalletDecrypt/WalletDecrypt.tsx | 20 ++++++++- .../WalletDecrypt/components/Keystore.tsx | 26 ++++++++--- .../libs/wallet/non-deterministic/helpers.ts | 16 +++++-- .../libs/wallet/non-deterministic/wallets.ts | 6 +-- common/libs/web-workers/scrypt-wrapper.ts | 23 ++++++++++ .../workers/scrypt-worker.worker.ts | 18 ++++++++ common/reducers/wallet.ts | 17 +++++++ common/sagas/wallet/wallet.ts | 44 ++++++++++++++++--- common/typescript/worker-loader.d.ts | 6 +++ jest_config/__mocks__/workerMock.js | 1 + jest_config/jest.config.json | 5 ++- package.json | 3 +- spec/sagas/wallet.spec.tsx | 27 ++++++++++-- webpack_config/webpack.base.js | 4 ++ 17 files changed, 219 insertions(+), 28 deletions(-) create mode 100644 common/libs/web-workers/scrypt-wrapper.ts create mode 100644 common/libs/web-workers/workers/scrypt-worker.worker.ts create mode 100644 common/typescript/worker-loader.d.ts create mode 100644 jest_config/__mocks__/workerMock.js diff --git a/common/actions/wallet/actionCreators.ts b/common/actions/wallet/actionCreators.ts index ce25069d..23ee04d4 100644 --- a/common/actions/wallet/actionCreators.ts +++ b/common/actions/wallet/actionCreators.ts @@ -43,12 +43,25 @@ export function setWallet(value: IWallet): types.SetWalletAction { }; } +export function setWalletPending(loadingStatus: boolean): types.SetWalletPendingAction { + return { + type: TypeKeys.WALLET_SET_PENDING, + payload: loadingStatus + }; +} + export function setBalancePending(): types.SetBalancePendingAction { return { type: TypeKeys.WALLET_SET_BALANCE_PENDING }; } +export function setPasswordPrompt(): types.SetPasswordPendingAction { + return { + type: TypeKeys.WALLET_SET_PASSWORD_PENDING + }; +} + export type TSetBalance = typeof setBalanceFullfilled; export function setBalanceFullfilled(value: Wei): types.SetBalanceFullfilledAction { return { diff --git a/common/actions/wallet/actionTypes.ts b/common/actions/wallet/actionTypes.ts index 2f02b354..f154f2e8 100644 --- a/common/actions/wallet/actionTypes.ts +++ b/common/actions/wallet/actionTypes.ts @@ -32,6 +32,11 @@ export interface ResetWalletAction { type: TypeKeys.WALLET_RESET; } +export interface SetWalletPendingAction { + type: TypeKeys.WALLET_SET_PENDING; + payload: boolean; +} + /*** Set Balance ***/ export interface SetBalancePendingAction { type: TypeKeys.WALLET_SET_BALANCE_PENDING; @@ -116,10 +121,15 @@ export interface SetWalletConfigAction { payload: WalletConfig; } +export interface SetPasswordPendingAction { + type: TypeKeys.WALLET_SET_PASSWORD_PENDING; +} + /*** Union Type ***/ export type WalletAction = | UnlockPrivateKeyAction | SetWalletAction + | SetWalletPendingAction | ResetWalletAction | SetBalancePendingAction | SetBalanceFullfilledAction @@ -132,4 +142,5 @@ export type WalletAction = | SetTokenBalanceRejectedAction | ScanWalletForTokensAction | SetWalletTokensAction - | SetWalletConfigAction; + | SetWalletConfigAction + | SetPasswordPendingAction; diff --git a/common/actions/wallet/constants.ts b/common/actions/wallet/constants.ts index bf2c547b..c1c2ff9d 100644 --- a/common/actions/wallet/constants.ts +++ b/common/actions/wallet/constants.ts @@ -10,11 +10,14 @@ export enum TypeKeys { WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING', WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED', WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED', + WALLET_SET_PENDING = 'WALLET_SET_PENDING', + WALLET_SET_NOT_PENDING = 'WALLET_SET_NOT_PENDING', WALLET_SET_TOKEN_BALANCE_PENDING = 'WALLET_SET_TOKEN_BALANCE_PENDING', WALLET_SET_TOKEN_BALANCE_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED', WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED', WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS', WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS', WALLET_SET_CONFIG = 'WALLET_SET_CONFIG', - WALLET_RESET = 'WALLET_RESET' + WALLET_RESET = 'WALLET_RESET', + WALLET_SET_PASSWORD_PENDING = 'WALLET_SET_PASSWORD_PENDING' } diff --git a/common/components/WalletDecrypt/WalletDecrypt.tsx b/common/components/WalletDecrypt/WalletDecrypt.tsx index 9a7191eb..8e0539e1 100644 --- a/common/components/WalletDecrypt/WalletDecrypt.tsx +++ b/common/components/WalletDecrypt/WalletDecrypt.tsx @@ -33,6 +33,7 @@ import { import { AppState } from 'reducers'; import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data'; import { IWallet } from 'libs/wallet'; +import { showNotification, TShowNotification } from 'actions/notifications'; import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg'; import LedgerIcon from 'assets/images/wallets/ledger.svg'; import MetamaskIcon from 'assets/images/wallets/metamask.svg'; @@ -49,10 +50,13 @@ interface Props { setWallet: TSetWallet; unlockWeb3: TUnlockWeb3; resetWallet: TResetWallet; + showNotification: TShowNotification; wallet: IWallet; hidden?: boolean; offline: boolean; disabledWallets?: string[]; + isWalletPending: AppState['wallet']['isWalletPending']; + isPasswordPending: AppState['wallet']['isPasswordPending']; } interface State { @@ -210,6 +214,15 @@ export class WalletDecrypt extends Component { value={this.state.value} onChange={this.onChange} onUnlock={this.onUnlock} + showNotification={this.props.showNotification} + isWalletPending={ + this.state.selectedWalletKey === 'keystore-file' ? this.props.isWalletPending : undefined + } + isPasswordPending={ + this.state.selectedWalletKey === 'keystore-file' + ? this.props.isPasswordPending + : undefined + } /> ); } @@ -376,7 +389,9 @@ export class WalletDecrypt extends Component { function mapStateToProps(state: AppState) { return { offline: state.config.offline, - wallet: state.wallet.inst + wallet: state.wallet.inst, + isWalletPending: state.wallet.isWalletPending, + isPasswordPending: state.wallet.isPasswordPending }; } @@ -387,5 +402,6 @@ export default connect(mapStateToProps, { unlockWeb3, setWallet, resetWallet, - resetTransactionState: reset + resetTransactionState: reset, + showNotification })(WalletDecrypt); diff --git a/common/components/WalletDecrypt/components/Keystore.tsx b/common/components/WalletDecrypt/components/Keystore.tsx index 98b95a2a..0b938124 100644 --- a/common/components/WalletDecrypt/components/Keystore.tsx +++ b/common/components/WalletDecrypt/components/Keystore.tsx @@ -1,6 +1,8 @@ import { isKeystorePassRequired } from 'libs/wallet'; import React, { Component } from 'react'; import translate, { translateRaw } from 'translations'; +import Spinner from 'components/ui/Spinner'; +import { TShowNotification } from 'actions/notifications'; export interface KeystoreValue { file: string; @@ -18,15 +20,23 @@ function isPassRequired(file: string): boolean { return passReq; } +function isValidFile(rawFile: File): boolean { + const fileType = rawFile.type; + return fileType === '' || fileType === 'application/json'; +} + export class KeystoreDecrypt extends Component { public props: { value: KeystoreValue; + isWalletPending: boolean; + isPasswordPending: boolean; onChange(value: KeystoreValue): void; onUnlock(): void; + showNotification(level: string, message: string): TShowNotification; }; public render() { - const { file, password } = this.props.value; + const { isWalletPending, isPasswordPending, value: { file, password } } = this.props; const passReq = isPassRequired(file); const unlockDisabled = !file || (passReq && !password); @@ -44,7 +54,8 @@ export class KeystoreDecrypt extends Component { {translate('ADD_Radio_2_short')} -
+ {isWalletPending ? : ''} +

{translate('ADD_Label_3')}

0 ? 'is-valid' : 'is-invalid'}`} @@ -97,10 +108,15 @@ export class KeystoreDecrypt extends Component { this.props.onChange({ ...this.props.value, file: keystore, - valid: keystore.length && !passReq + valid: keystore.length && !passReq, + password: '' }); + this.props.onUnlock(); }; - - fileReader.readAsText(inputFile, 'utf-8'); + if (isValidFile(inputFile)) { + fileReader.readAsText(inputFile, 'utf-8'); + } else { + this.props.showNotification('danger', translateRaw('ERROR_3')); + } }; } diff --git a/common/libs/wallet/non-deterministic/helpers.ts b/common/libs/wallet/non-deterministic/helpers.ts index f4645e6b..a51a6181 100644 --- a/common/libs/wallet/non-deterministic/helpers.ts +++ b/common/libs/wallet/non-deterministic/helpers.ts @@ -58,6 +58,10 @@ const isKeystorePassRequired = (file: string): boolean => { ); }; +const getUtcWallet = (file: string, password: string): Promise => { + return UtcWallet(file, password); +}; + const getPrivKeyWallet = (key: string, password: string) => key.length === 64 ? PrivKeyWallet(Buffer.from(key, 'hex')) @@ -79,12 +83,16 @@ const getKeystoreWallet = (file: string, password: string) => { case KeystoreTypes.v2Unencrypted: return PrivKeyWallet(Buffer.from(parsed.privKey, 'hex')); - case KeystoreTypes.utc: - return UtcWallet(file, password); - default: throw Error('Unknown wallet'); } }; -export { isKeystorePassRequired, getPrivKeyWallet, getKeystoreWallet }; +export { + isKeystorePassRequired, + determineKeystoreType, + getPrivKeyWallet, + getKeystoreWallet, + getUtcWallet, + KeystoreTypes +}; diff --git a/common/libs/wallet/non-deterministic/wallets.ts b/common/libs/wallet/non-deterministic/wallets.ts index 9227aed5..3d1260fc 100644 --- a/common/libs/wallet/non-deterministic/wallets.ts +++ b/common/libs/wallet/non-deterministic/wallets.ts @@ -1,7 +1,8 @@ -import { fromPrivateKey, fromEthSale, fromV3 } from 'ethereumjs-wallet'; +import { fromPrivateKey, fromEthSale } from 'ethereumjs-wallet'; import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty'; import { signWrapper } from './helpers'; import { decryptPrivKey } from 'libs/decrypt'; +import { fromV3 } from 'libs/web-workers/scrypt-wrapper'; import Web3Wallet from './web3'; import AddressOnlyWallet from './address'; @@ -16,8 +17,7 @@ const MewV1Wallet = (keystore: string, password: string) => const PrivKeyWallet = (privkey: Buffer) => signWrapper(fromPrivateKey(privkey)); -const UtcWallet = (keystore: string, password: string) => - signWrapper(fromV3(keystore, password, true)); +const UtcWallet = (keystore: string, password: string) => fromV3(keystore, password, true); export { EncryptedPrivateKeyWallet, diff --git a/common/libs/web-workers/scrypt-wrapper.ts b/common/libs/web-workers/scrypt-wrapper.ts new file mode 100644 index 00000000..4e6bf37b --- /dev/null +++ b/common/libs/web-workers/scrypt-wrapper.ts @@ -0,0 +1,23 @@ +import { IFullWallet, fromPrivateKey } from 'ethereumjs-wallet'; +import { toBuffer } from 'ethereumjs-util'; +import Worker from 'worker-loader!./workers/scrypt-worker.worker.ts'; + +export const fromV3 = ( + keystore: string, + password: string, + nonStrict: boolean +): Promise => { + return new Promise((resolve, reject) => { + const scryptWorker = new Worker(); + scryptWorker.postMessage({ keystore, password, nonStrict }); + scryptWorker.onmessage = event => { + const data: string = event.data; + try { + const wallet = fromPrivateKey(toBuffer(data)); + resolve(wallet); + } catch (e) { + reject(e); + } + }; + }); +}; diff --git a/common/libs/web-workers/workers/scrypt-worker.worker.ts b/common/libs/web-workers/workers/scrypt-worker.worker.ts new file mode 100644 index 00000000..38df1d1d --- /dev/null +++ b/common/libs/web-workers/workers/scrypt-worker.worker.ts @@ -0,0 +1,18 @@ +import { fromV3, IFullWallet } from 'ethereumjs-wallet'; + +const scryptWorker: Worker = self as any; +interface DecryptionParameters { + keystore: string; + password: string; + nonStrict: boolean; +} + +scryptWorker.onmessage = (event: MessageEvent) => { + const info: DecryptionParameters = event.data; + try { + const rawKeystore: IFullWallet = fromV3(info.keystore, info.password, info.nonStrict); + scryptWorker.postMessage(rawKeystore.getPrivateKeyString()); + } catch (e) { + scryptWorker.postMessage(e.message); + } +}; diff --git a/common/reducers/wallet.ts b/common/reducers/wallet.ts index b9581c89..2f001c88 100644 --- a/common/reducers/wallet.ts +++ b/common/reducers/wallet.ts @@ -4,6 +4,7 @@ import { SetWalletAction, WalletAction, SetWalletConfigAction, + SetWalletPendingAction, TypeKeys, SetTokenBalanceFulfilledAction } from 'actions/wallet'; @@ -21,7 +22,9 @@ export interface State { error: string | null; }; }; + isWalletPending: boolean; isTokensLoading: boolean; + isPasswordPending: boolean; tokensError: string | null; hasSavedWalletTokens: boolean; } @@ -31,6 +34,8 @@ export const INITIAL_STATE: State = { config: null, balance: { isPending: false, wei: null }, tokens: {}, + isWalletPending: false, + isPasswordPending: false, isTokensLoading: false, tokensError: null, hasSavedWalletTokens: true @@ -61,6 +66,14 @@ function setBalanceRejected(state: State): State { return { ...state, balance: { ...state.balance, isPending: false } }; } +function setWalletPending(state: State, action: SetWalletPendingAction): State { + return { ...state, isWalletPending: action.payload }; +} + +function setPasswordPending(state: State): State { + return { ...state, isPasswordPending: true }; +} + function setTokenBalancesPending(state: State): State { return { ...state, @@ -143,6 +156,8 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat return setBalanceFullfilled(state, action); case TypeKeys.WALLET_SET_BALANCE_REJECTED: return setBalanceRejected(state); + case TypeKeys.WALLET_SET_PENDING: + return setWalletPending(state, action); case TypeKeys.WALLET_SET_TOKEN_BALANCES_PENDING: return setTokenBalancesPending(state); case TypeKeys.WALLET_SET_TOKEN_BALANCES_FULFILLED: @@ -161,6 +176,8 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat return setWalletTokens(state); case TypeKeys.WALLET_SET_CONFIG: return setWalletConfig(state, action); + case TypeKeys.WALLET_SET_PASSWORD_PENDING: + return setPasswordPending(state); default: return state; } diff --git a/common/sagas/wallet/wallet.ts b/common/sagas/wallet/wallet.ts index dee1dc1a..ef647cbf 100644 --- a/common/sagas/wallet/wallet.ts +++ b/common/sagas/wallet/wallet.ts @@ -7,6 +7,7 @@ import { setTokenBalancesFulfilled, setTokenBalancesRejected, setWallet, + setWalletPending, setWalletConfig, UnlockKeystoreAction, UnlockMnemonicAction, @@ -16,7 +17,8 @@ import { TypeKeys, SetTokenBalancePendingAction, setTokenBalanceFulfilled, - setTokenBalanceRejected + setTokenBalanceRejected, + setPasswordPrompt } from 'actions/wallet'; import { Wei } from 'libs/units'; import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config'; @@ -27,12 +29,16 @@ import { MnemonicWallet, getPrivKeyWallet, getKeystoreWallet, + determineKeystoreType, + KeystoreTypes, + getUtcWallet, + signWrapper, Web3Wallet, WalletConfig } from 'libs/wallet'; import { NODES, initWeb3Node, Token } from 'config/data'; -import { SagaIterator } from 'redux-saga'; -import { apply, call, fork, put, select, takeEvery, take } from 'redux-saga/effects'; +import { SagaIterator, delay, Task } from 'redux-saga'; +import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects'; import { getNodeLib, getAllTokens } from 'selectors/config'; import { getTokens, @@ -168,18 +174,44 @@ export function* unlockPrivateKey(action: UnlockPrivateKeyAction): SagaIterator yield put(setWallet(wallet)); } +export function* startLoadingSpinner(): SagaIterator { + yield call(delay, 400); + yield put(setWalletPending(true)); +} + +export function* stopLoadingSpinner(loadingFork: Task | null): SagaIterator { + if (loadingFork !== null && loadingFork !== undefined) { + yield cancel(loadingFork); + } + yield put(setWalletPending(false)); +} + export function* unlockKeystore(action: UnlockKeystoreAction): SagaIterator { const { file, password } = action.payload; let wallet: null | IWallet = null; - + let spinnerTask: null | Task = null; try { - wallet = getKeystoreWallet(file, password); + if (determineKeystoreType(file) === KeystoreTypes.utc) { + spinnerTask = yield fork(startLoadingSpinner); + wallet = signWrapper(yield call(getUtcWallet, file, password)); + } else { + wallet = getKeystoreWallet(file, password); + } } catch (e) { - yield put(showNotification('danger', translate('ERROR_6'))); + yield call(stopLoadingSpinner, spinnerTask); + if ( + password === '' && + e.message === 'Private key does not satisfy the curve requirements (ie. it is invalid)' + ) { + yield put(setPasswordPrompt()); + } else { + yield put(showNotification('danger', translate('ERROR_6'))); + } return; } // TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above + yield call(stopLoadingSpinner, spinnerTask); yield put(setWallet(wallet)); } diff --git a/common/typescript/worker-loader.d.ts b/common/typescript/worker-loader.d.ts new file mode 100644 index 00000000..a25733f2 --- /dev/null +++ b/common/typescript/worker-loader.d.ts @@ -0,0 +1,6 @@ +declare module 'worker-loader!*' { + class WebpackWorker extends Worker { + constructor(); + } + export = WebpackWorker; +} diff --git a/jest_config/__mocks__/workerMock.js b/jest_config/__mocks__/workerMock.js new file mode 100644 index 00000000..7462fae2 --- /dev/null +++ b/jest_config/__mocks__/workerMock.js @@ -0,0 +1 @@ +module.exports = Object.create(null); \ No newline at end of file diff --git a/jest_config/jest.config.json b/jest_config/jest.config.json index 7833cd97..b93875a1 100644 --- a/jest_config/jest.config.json +++ b/jest_config/jest.config.json @@ -5,11 +5,12 @@ }, "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", "moduleDirectories": ["node_modules", "common"], - "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"], + "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "worker.ts"], "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/jest_config/__mocks__/fileMock.ts", - "\\.(css|scss|less)$": "/jest_config/__mocks__/styleMock.ts" + "\\.(css|scss|less)$": "/jest_config/__mocks__/styleMock.ts", + "\\.worker.ts":"/jest_config/__mocks__/workerMock.js" }, "testPathIgnorePatterns": ["/common/config"], "setupFiles": [ diff --git a/package.json b/package.json index 0e746389..c4c1efa8 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,8 @@ "webpack": "3.10.0", "webpack-dev-middleware": "2.0.4", "webpack-hot-middleware": "2.21.0", - "webpack-sources": "1.0.1" + "webpack-sources": "1.0.1", + "worker-loader": "1.1.0" }, "scripts": { "freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js", diff --git a/spec/sagas/wallet.spec.tsx b/spec/sagas/wallet.spec.tsx index 8f233226..e9e741d1 100644 --- a/spec/sagas/wallet.spec.tsx +++ b/spec/sagas/wallet.spec.tsx @@ -25,14 +25,17 @@ import { unlockKeystore, unlockMnemonic, unlockWeb3, - getTokenBalances + getTokenBalances, + startLoadingSpinner, + stopLoadingSpinner } from 'sagas/wallet'; -import { PrivKeyWallet } from 'libs/wallet/non-deterministic'; +import { getUtcWallet, PrivKeyWallet } from 'libs/wallet'; import { TypeKeys as ConfigTypeKeys } from 'actions/config/constants'; import Web3Node from 'libs/nodes/web3'; -import { cloneableGenerator } from 'redux-saga/utils'; +import { cloneableGenerator, createMockTask } from 'redux-saga/utils'; import { showNotification } from 'actions/notifications'; import translate from 'translations'; +import { IFullWallet, fromV3 } from 'ethereumjs-wallet'; // init module configuredStore.getState(); @@ -206,6 +209,24 @@ describe('unlockKeystore*', () => { password: 'testtesttest' }); const gen = unlockKeystore(action); + const mockTask = createMockTask(); + const spinnerFork = fork(startLoadingSpinner); + + it('should fork startLoadingSpinner', () => { + expect(gen.next().value).toEqual(spinnerFork); + }); + + it('should call getUtcWallet', () => { + expect(gen.next(mockTask).value).toEqual( + call(getUtcWallet, action.payload.file, action.payload.password) + ); + }); + + //keystore in this case decrypts quickly, so use fromV3 in ethjs-wallet to avoid testing with promises + it('should call stopLoadingSpinner', () => { + const mockWallet: IFullWallet = fromV3(action.payload.file, action.payload.password, true); + expect(gen.next(mockWallet).value).toEqual(call(stopLoadingSpinner, mockTask)); + }); it('should match put setWallet snapshot', () => { expect(gen.next().value).toMatchSnapshot(); diff --git a/webpack_config/webpack.base.js b/webpack_config/webpack.base.js index 3fb56ad1..798248a0 100644 --- a/webpack_config/webpack.base.js +++ b/webpack_config/webpack.base.js @@ -35,6 +35,10 @@ const webpackConfig = { .map(dir => path.resolve(__dirname, `../common/${dir}`)) .concat([path.resolve(__dirname, '../node_modules')]) }, + { + test: /\.worker\.js$/, + loader: 'worker-loader' + }, { include: [ path.resolve(__dirname, '../common/assets'), From 7d2c3e19901799ab83852192f8829e5d05331ac8 Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Thu, 11 Jan 2018 01:47:48 -0500 Subject: [PATCH 09/35] Unit tests for token & contract JSON (#768) --- common/config/contracts/etc.json | 1 - common/config/contracts/eth.json | 1 - common/config/contracts/index.ts | 16 +++++--- common/config/contracts/ropsten.json | 1 - common/config/data.ts | 2 +- common/config/tokens/index.ts | 19 +++++++++ .../components/InteractForm/index.tsx | 24 ++++++----- spec/config/contracts.spec.ts | 32 +++++++++++++++ spec/config/tokens.spec.ts | 40 +++++++++++++++++++ 9 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 common/config/tokens/index.ts create mode 100644 spec/config/contracts.spec.ts create mode 100644 spec/config/tokens.spec.ts diff --git a/common/config/contracts/etc.json b/common/config/contracts/etc.json index 59a14651..ccfb3343 100644 --- a/common/config/contracts/etc.json +++ b/common/config/contracts/etc.json @@ -11,7 +11,6 @@ }, { "name": "Mist's Multisig Contract", - "address": "0x0000000000000000000000000000000000000000", "abi": "[{\"constant\":false,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"removeOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_addr\",\"type\":\"address\"}],\"name\":\"isOwner\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_numOwners\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_lastDay\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"resetSpentToday\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_spentToday\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"addOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_required\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_h\",\"type\":\"bytes32\"}],\"name\":\"confirm\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newLimit\",\"type\":\"uint256\"}],\"name\":\"setDailyLimit\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"},{\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"execute\",\"outputs\":[{\"name\":\"_r\",\"type\":\"bytes32\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_operation\",\"type\":\"bytes32\"}],\"name\":\"revoke\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newRequired\",\"type\":\"uint256\"}],\"name\":\"changeRequirement\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_operation\",\"type\":\"bytes32\"},{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"hasConfirmed\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"kill\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"changeOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_dailyLimit\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"inputs\":[{\"name\":\"_owners\",\"type\":\"address[]\"},{\"name\":\"_required\",\"type\":\"uint256\"},{\"name\":\"_daylimit\",\"type\":\"uint256\"}],\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"}],\"name\":\"Confirmation\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"}],\"name\":\"Revoke\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"oldOwner\",\"type\":\"address\"}],\"name\":\"OwnerRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"newRequirement\",\"type\":\"uint256\"}],\"name\":\"RequirementChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"SingleTransact\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"MultiTransact\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"ConfirmationNeeded\",\"type\":\"event\"}]" } ] diff --git a/common/config/contracts/eth.json b/common/config/contracts/eth.json index fe4f0e35..9789ab1a 100644 --- a/common/config/contracts/eth.json +++ b/common/config/contracts/eth.json @@ -66,7 +66,6 @@ }, { "name": "Mist's Multisig Contract", - "address": "0x0101010101010101010101010101010101010101", "abi": "[{\"constant\":false,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"removeOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_addr\",\"type\":\"address\"}],\"name\":\"isOwner\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_numOwners\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_lastDay\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"resetSpentToday\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_spentToday\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"addOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_required\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_h\",\"type\":\"bytes32\"}],\"name\":\"confirm\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newLimit\",\"type\":\"uint256\"}],\"name\":\"setDailyLimit\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"},{\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"execute\",\"outputs\":[{\"name\":\"_r\",\"type\":\"bytes32\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_operation\",\"type\":\"bytes32\"}],\"name\":\"revoke\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newRequired\",\"type\":\"uint256\"}],\"name\":\"changeRequirement\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_operation\",\"type\":\"bytes32\"},{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"hasConfirmed\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"kill\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"changeOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_dailyLimit\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"inputs\":[{\"name\":\"_owners\",\"type\":\"address[]\"},{\"name\":\"_required\",\"type\":\"uint256\"},{\"name\":\"_daylimit\",\"type\":\"uint256\"}],\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"}],\"name\":\"Confirmation\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"}],\"name\":\"Revoke\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"oldOwner\",\"type\":\"address\"}],\"name\":\"OwnerRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"newRequirement\",\"type\":\"uint256\"}],\"name\":\"RequirementChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"SingleTransact\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"MultiTransact\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"ConfirmationNeeded\",\"type\":\"event\"}]" }, { diff --git a/common/config/contracts/index.ts b/common/config/contracts/index.ts index 5ebcdedc..95054404 100644 --- a/common/config/contracts/index.ts +++ b/common/config/contracts/index.ts @@ -1,11 +1,17 @@ -import ETC from './ETC.json'; -import ETH from './ETH.json'; -import Rinkeby from './Rinkeby.json'; -import Ropsten from './Ropsten.json'; +import ETC from './etc.json'; +import ETH from './eth.json'; +import EXP from './exp.json'; +import Rinkeby from './rinkeby.json'; +import Ropsten from './ropsten.json'; +import RSK from './rsk.json'; +import UBQ from './ubq.json'; export default { ETC, ETH, + EXP, Rinkeby, - Ropsten + Ropsten, + RSK, + UBQ }; diff --git a/common/config/contracts/ropsten.json b/common/config/contracts/ropsten.json index 7b8f6da5..798eb1fd 100644 --- a/common/config/contracts/ropsten.json +++ b/common/config/contracts/ropsten.json @@ -21,7 +21,6 @@ }, { "name": "Mist's Multisig Contract", - "address": "0x0000000000000000000000000000000000000000", "abi": "[{\"constant\":false,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"removeOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_addr\",\"type\":\"address\"}],\"name\":\"isOwner\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_numOwners\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_lastDay\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"resetSpentToday\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_spentToday\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"addOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_required\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_h\",\"type\":\"bytes32\"}],\"name\":\"confirm\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newLimit\",\"type\":\"uint256\"}],\"name\":\"setDailyLimit\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"},{\"name\":\"_data\",\"type\":\"bytes\"}],\"name\":\"execute\",\"outputs\":[{\"name\":\"_r\",\"type\":\"bytes32\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_operation\",\"type\":\"bytes32\"}],\"name\":\"revoke\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newRequired\",\"type\":\"uint256\"}],\"name\":\"changeRequirement\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_operation\",\"type\":\"bytes32\"},{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"hasConfirmed\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"kill\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"changeOwner\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"m_dailyLimit\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"inputs\":[{\"name\":\"_owners\",\"type\":\"address[]\"},{\"name\":\"_required\",\"type\":\"uint256\"},{\"name\":\"_daylimit\",\"type\":\"uint256\"}],\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"}],\"name\":\"Confirmation\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"}],\"name\":\"Revoke\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"oldOwner\",\"type\":\"address\"}],\"name\":\"OwnerRemoved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"newRequirement\",\"type\":\"uint256\"}],\"name\":\"RequirementChanged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"from\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Deposit\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"SingleTransact\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"MultiTransact\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"operation\",\"type\":\"bytes32\"},{\"indexed\":false,\"name\":\"initiator\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"ConfirmationNeeded\",\"type\":\"event\"}]" } ] diff --git a/common/config/data.ts b/common/config/data.ts index 774bf6c6..56c8a7c9 100644 --- a/common/config/data.ts +++ b/common/config/data.ts @@ -67,7 +67,7 @@ export interface Token { export interface NetworkContract { name: string; - address: string; + address?: string; abi: string; } diff --git a/common/config/tokens/index.ts b/common/config/tokens/index.ts new file mode 100644 index 00000000..27c8a518 --- /dev/null +++ b/common/config/tokens/index.ts @@ -0,0 +1,19 @@ +import ETC from './etc.json'; +import ETH from './eth.json'; +import EXP from './exp.json'; +import Kovan from './kovan.json'; +import Rinkeby from './rinkeby.json'; +import Ropsten from './ropsten.json'; +import RSK from './rsk.json'; +import UBQ from './ubq.json'; + +export default { + ETC, + ETH, + EXP, + Kovan, + Rinkeby, + Ropsten, + RSK, + UBQ +}; diff --git a/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx b/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx index 5d621f5f..65723922 100644 --- a/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx +++ b/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx @@ -46,9 +46,10 @@ e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]'; contractOptions = contractOptions.concat( contracts.map(contract => { + const addr = contract.address ? `(${contract.address.substr(0, 10)}...)` : ''; return { - name: `${contract.name} (${contract.address.substr(0, 10)}...)`, - value: contract.address + name: `${contract.name} ${addr}`, + value: this.makeContractValue(contract) }; }) ); @@ -122,23 +123,26 @@ e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]'; ); } - private handleInput = name => (ev: any) => { + private handleInput = name => (ev: React.FormEvent) => { this.props.resetState(); - this.setState({ [name]: ev.target.value }); + this.setState({ [name]: ev.currentTarget.value }); }; - private handleSelectContract = (ev: any) => { + private handleSelectContract = (ev: React.FormEvent) => { this.props.resetState(); - const addr = ev.target.value; - const contract = this.props.contracts.reduce((prev, currContract) => { - return currContract.address === addr ? currContract : prev; + const contract = this.props.contracts.find(currContract => { + return this.makeContractValue(currContract) === ev.currentTarget.value; }); this.setState({ - address: contract.address, - abiJson: contract.abi + address: contract && contract.address ? contract.address : '', + abiJson: contract && contract.abi ? contract.abi : '' }); }; + + private makeContractValue(contract: NetworkContract) { + return `${contract.name}:${contract.address}`; + } } const mapStateToProps = (state: AppState) => ({ diff --git a/spec/config/contracts.spec.ts b/spec/config/contracts.spec.ts new file mode 100644 index 00000000..6cd5eae4 --- /dev/null +++ b/spec/config/contracts.spec.ts @@ -0,0 +1,32 @@ +import CONTRACTS from 'config/contracts'; +import { isValidETHAddress } from 'libs/validators'; + +describe('Contracts JSON', () => { + Object.keys(CONTRACTS).forEach(network => { + it(`${network} contracts array properly formatted`, () => { + const contracts = CONTRACTS[network]; + const addressCollisionMap = {}; + + contracts.forEach(contract => { + if (contract.address && !isValidETHAddress(contract.address)) { + throw Error(`Contract '${contract.name}' has invalid address '${contract.address}'`); + } + if (addressCollisionMap[contract.address]) { + throw Error( + `Contract '${contract.name}' has the same address as ${ + addressCollisionMap[contract.address] + }` + ); + } + + try { + JSON.stringify(contract.abi); + } catch (err) { + throw Error(`Contract '${contract.name}' has invalid JSON ABI`); + } + + addressCollisionMap[contract.address] = contract.name; + }); + }); + }); +}); diff --git a/spec/config/tokens.spec.ts b/spec/config/tokens.spec.ts new file mode 100644 index 00000000..f9cc9628 --- /dev/null +++ b/spec/config/tokens.spec.ts @@ -0,0 +1,40 @@ +import TOKENS from 'config/tokens'; +import { isValidETHAddress } from 'libs/validators'; + +describe('Tokens JSON', () => { + Object.keys(TOKENS).forEach(network => { + it(`${network} tokens array properly formatted`, () => { + const tokens = TOKENS[network]; + const addressCollisionMap = {}; + const symbolCollisionMap = {}; + + tokens.forEach(token => { + if (!isValidETHAddress(token.address)) { + throw Error(`Token ${token.symbol} has invalid contract address '${token.address}'`); + } + if (addressCollisionMap[token.address]) { + throw Error( + `Token ${token.symbol} has the same address as ${addressCollisionMap[token.address]}` + ); + } + if (symbolCollisionMap[token.symbol]) { + throw Error( + `Symbol ${token.symbol} is repeated between tokens at ${token.address} and ${ + symbolCollisionMap[token.symbol] + }` + ); + } + if ( + token.decimal < 0 || + token.decimal > 18 || + token.decimal === null || + token.decimal === undefined + ) { + throw Error(`Token ${token.symbol} has invalid decimal '${token.decimal}'`); + } + addressCollisionMap[token.address] = token.symbol; + symbolCollisionMap[token.symbol] = token.address; + }); + }); + }); +}); From 418b186642042e27add5af015ae7e8c75a625daa Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Thu, 11 Jan 2018 01:50:31 -0500 Subject: [PATCH 10/35] Resolve custom token conflicts (#767) * Remove custom token if it conflicts with symbol or address. * Refactor deduping to utils function. Add unit tests for said function. * Fix tscheck --- common/store.ts | 11 +++++++-- common/utils/tokens.ts | 18 +++++++++++++++ spec/utils/tokens.spec.ts | 47 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 common/utils/tokens.ts create mode 100644 spec/utils/tokens.spec.ts diff --git a/common/store.ts b/common/store.ts index bbc1a92b..6bc9a7e9 100644 --- a/common/store.ts +++ b/common/store.ts @@ -19,6 +19,7 @@ import RootReducer from './reducers'; import promiseMiddleware from 'redux-promise-middleware'; import { getNodeConfigFromId } from 'utils/node'; import { getNetworkConfigFromId } from 'utils/network'; +import { dedupeCustomTokens } from 'utils/tokens'; import sagas from './sagas'; import { gasPricetoBase } from 'libs/units'; @@ -59,7 +60,6 @@ const configureStore = () => { } : { ...swapInitialState }; - const localCustomTokens = loadStatePropertyOrEmptyObject('customTokens'); const savedTransactionState = loadStatePropertyOrEmptyObject('transaction'); const savedConfigState = loadStatePropertyOrEmptyObject('config'); @@ -82,6 +82,13 @@ const configureStore = () => { } } + // Dedupe custom tokens initially + const savedCustomTokensState = + loadStatePropertyOrEmptyObject('customTokens') || customTokensInitialState; + const initialNetwork = + (savedConfigState && savedConfigState.network) || configInitialState.network; + const customTokens = dedupeCustomTokens(initialNetwork.tokens, savedCustomTokensState); + const persistedInitialState = { config: { ...configInitialState, @@ -100,7 +107,7 @@ const configureStore = () => { : transactionInitialState.fields.gasPrice } }, - customTokens: localCustomTokens || customTokensInitialState, + customTokens, // ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3 swap: swapState }; diff --git a/common/utils/tokens.ts b/common/utils/tokens.ts new file mode 100644 index 00000000..23d44baf --- /dev/null +++ b/common/utils/tokens.ts @@ -0,0 +1,18 @@ +import { Token } from 'config/data'; + +export function dedupeCustomTokens(networkTokens: Token[], customTokens: Token[]): Token[] { + if (!customTokens.length) { + return []; + } + + // If any tokens have the same symbol or contract address, remove them + const tokenCollisionMap = networkTokens.reduce((prev, token) => { + prev[token.symbol] = true; + prev[token.address] = true; + return prev; + }, {}); + + return customTokens.filter(token => { + return !tokenCollisionMap[token.address] && !tokenCollisionMap[token.symbol]; + }); +} diff --git a/spec/utils/tokens.spec.ts b/spec/utils/tokens.spec.ts new file mode 100644 index 00000000..dcf96822 --- /dev/null +++ b/spec/utils/tokens.spec.ts @@ -0,0 +1,47 @@ +import { dedupeCustomTokens } from 'utils/tokens'; + +describe('dedupeCustomTokens', () => { + const networkTokens = [ + { + address: '0x48c80F1f4D53D5951e5D5438B54Cba84f29F32a5', + symbol: 'REP', + decimal: 18 + }, + { + address: '0xa74476443119A942dE498590Fe1f2454d7D4aC0d', + symbol: 'GNT', + decimal: 18 + } + ]; + + const DUPLICATE_ADDRESS = { + address: networkTokens[0].address, + symbol: 'REP2', + decimal: 18 + }; + const DUPLICATE_SYMBOL = { + address: '0x0', + symbol: networkTokens[1].symbol, + decimal: 18 + }; + const NONDUPLICATE_CUSTOM = { + address: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8', + symbol: 'MEW', + decimal: 0 + }; + + const customTokens = [DUPLICATE_ADDRESS, DUPLICATE_SYMBOL, NONDUPLICATE_CUSTOM]; + const dedupedTokens = dedupeCustomTokens(networkTokens, customTokens); + + it('Should remove duplicate address custom tokens', () => { + expect(dedupedTokens.includes(DUPLICATE_ADDRESS)).toBeFalsy(); + }); + + it('Should remove duplicate symbol custom tokens', () => { + expect(dedupedTokens.includes(DUPLICATE_SYMBOL)).toBeFalsy(); + }); + + it('Should not remove custom tokens that aren’t duplicates', () => { + expect(dedupedTokens.includes(NONDUPLICATE_CUSTOM)).toBeTruthy(); + }); +}); From db4dc516e217ff128aa0d570a3ca79a352d4771a Mon Sep 17 00:00:00 2001 From: Eddie Wang Date: Thu, 11 Jan 2018 02:07:39 -0500 Subject: [PATCH 11/35] Clear transaction data on transaction resign (#788) --- common/reducers/transaction/sign/sign.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/reducers/transaction/sign/sign.ts b/common/reducers/transaction/sign/sign.ts index 8a5f565b..fb53b233 100644 --- a/common/reducers/transaction/sign/sign.ts +++ b/common/reducers/transaction/sign/sign.ts @@ -14,8 +14,8 @@ const INITIAL_STATE: State = { pending: false }; -const signLocalTransactionRequested = (state: State): State => ({ - ...state, +const signLocalTransactionRequested = (): State => ({ + ...INITIAL_STATE, pending: true }); @@ -48,7 +48,7 @@ const reset = () => INITIAL_STATE; export const sign = (state: State = INITIAL_STATE, action: SignAction | ResetAction) => { switch (action.type) { case TK.SIGN_LOCAL_TRANSACTION_REQUESTED: - return signLocalTransactionRequested(state); + return signLocalTransactionRequested(); case TK.SIGN_LOCAL_TRANSACTION_SUCCEEDED: return signLocalTransactionSucceeded(state, action); case TK.SIGN_WEB3_TRANSACTION_SUCCEEDED: From f4b8364abcd41ad2ebb15cac19bf581b57fbba6b Mon Sep 17 00:00:00 2001 From: Eddie Wang Date: Thu, 11 Jan 2018 02:08:36 -0500 Subject: [PATCH 12/35] Stop Timer when Swap Order is Received (#791) --- common/sagas/swap/orders.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/sagas/swap/orders.ts b/common/sagas/swap/orders.ts index 38786d7c..3a4d4843 100644 --- a/common/sagas/swap/orders.ts +++ b/common/sagas/swap/orders.ts @@ -301,6 +301,9 @@ export function* shapeshiftOrderTimeRemaining(): SagaIterator { yield put(showNotification('danger', ORDER_TIMEOUT_MESSAGE, Infinity)); } break; + case 'received': + yield put(stopOrderTimerSwap()); + break; case 'complete': yield put(stopPollShapeshiftOrderStatus()); yield put(stopLoadShapeshiftRatesSwap()); From fe86f2f79f583d94ea48599cb0d9b6eb07536a67 Mon Sep 17 00:00:00 2001 From: Daniel Ternyak Date: Thu, 11 Jan 2018 01:35:22 -0600 Subject: [PATCH 13/35] chore(package): update tslint to version 5.9.1 (#795) Closes #792 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4c1efa8..c91bd247 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "thread-loader": "1.1.2", "ts-jest": "22.0.1", "ts-loader": "3.2.0", - "tslint": "5.8.0", + "tslint": "5.9.1", "tslint-config-prettier": "1.6.0", "tslint-microsoft-contrib": "5.0.1", "tslint-react": "3.3.3", From 2236bb173ff1ad178a3c6c61c082737949fd8554 Mon Sep 17 00:00:00 2001 From: Daniel Ternyak Date: Thu, 11 Jan 2018 01:57:36 -0600 Subject: [PATCH 14/35] chore(package): update prettier to version 1.10.2 (#797) Closes #787 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c91bd247..bbca58af 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "node-sass": "4.7.2", "nodemon": "1.14.10", "null-loader": "0.1.1", - "prettier": "1.9.2", + "prettier": "1.10.2", "progress": "2.0.0", "react-hot-loader": "3.1.3", "react-test-renderer": "16.2.0", From 6df4013d4d56dbedc19667e22d06d9bc7121d02c Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 11 Jan 2018 10:32:57 -0600 Subject: [PATCH 15/35] chore(package): update nodemon to version 1.14.11 (#799) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bbca58af..1012d5fc 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "lint-staged": "6.0.0", "minimist": "1.2.0", "node-sass": "4.7.2", - "nodemon": "1.14.10", + "nodemon": "1.14.11", "null-loader": "0.1.1", "prettier": "1.10.2", "progress": "2.0.0", From 3a7a0822e27e8b9432b8052e3ea3e363b1d4d2e0 Mon Sep 17 00:00:00 2001 From: Eddie Wang Date: Thu, 11 Jan 2018 12:27:00 -0500 Subject: [PATCH 16/35] Hide Equivalent Values on Testnet (#763) * Hide eqv values when not on testnet * use isTestnet and variabalize rateExistsOrAll --- .../BalanceSidebar/EquivalentValues.tsx | 16 ++++++++++++++-- common/components/BalanceSidebar/index.tsx | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index afd88972..050cb846 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -5,6 +5,7 @@ import { State } from 'reducers/rates'; import { rateSymbols, TFetchCCRates } from 'actions/rates'; import { TokenBalance } from 'selectors/wallet'; import { Balance } from 'libs/wallet'; +import { NetworkConfig } from 'config/data'; import { ETH_DECIMAL, convertTokenBase } from 'libs/units'; import Spinner from 'components/ui/Spinner'; import UnitDisplay from 'components/ui/UnitDisplay'; @@ -18,6 +19,7 @@ interface Props { rates: State['rates']; ratesError?: State['ratesError']; fetchCCRates: TFetchCCRates; + network: NetworkConfig; } interface CmpState { @@ -50,16 +52,18 @@ export default class EquivalentValues extends React.Component { } public render() { - const { balance, tokenBalances, rates, ratesError } = this.props; + const { balance, tokenBalances, rates, ratesError, network } = this.props; const { currency } = this.state; // There are a bunch of reasons why the incorrect balances might be rendered // while we have incomplete data that's being fetched. const isFetching = !balance || balance.isPending || !tokenBalances || Object.keys(rates).length === 0; + // Currency exists in rates or the all option is selected + const rateExistsOrAll = rates[currency] || currency === ALL_OPTION; let valuesEl; - if (!isFetching && (rates[currency] || currency === ALL_OPTION)) { + if (!isFetching && rateExistsOrAll && !network.isTestnet) { const values = this.getEquivalentValues(currency); valuesEl = rateSymbols.map(key => { if (!values[key] || key === currency) { @@ -80,6 +84,14 @@ export default class EquivalentValues extends React.Component { ); }); + } else if (network.isTestnet) { + valuesEl = ( +
+
+ On test network, equivalent values will not be displayed. +
+
+ ); } else if (ratesError) { valuesEl =
{ratesError}
; } else if (tokenBalances && tokenBalances.length === 0) { diff --git a/common/components/BalanceSidebar/index.tsx b/common/components/BalanceSidebar/index.tsx index acba0611..2b6f773a 100644 --- a/common/components/BalanceSidebar/index.tsx +++ b/common/components/BalanceSidebar/index.tsx @@ -53,6 +53,7 @@ export class BalanceSidebar extends React.Component { name: 'Equivalent Values', content: ( Date: Thu, 11 Jan 2018 11:27:14 -0600 Subject: [PATCH 17/35] chore(package): update jest to version 22.0.6 (#798) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1012d5fc..c63c1e18 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "html-webpack-plugin": "2.30.1", "husky": "0.14.3", "image-webpack-loader": "3.4.2", - "jest": "22.0.5", + "jest": "22.0.6", "less": "2.7.3", "less-loader": "4.0.5", "lint-staged": "6.0.0", From 4f6e83acf461de92a99c129009ce04ba8904971d Mon Sep 17 00:00:00 2001 From: William O'Beirne Date: Thu, 11 Jan 2018 13:04:11 -0500 Subject: [PATCH 18/35] Better Offline UX (#785) * Check offline status immediately. * If they start the page offline, show a less severe error message. * Get rid of offline aware header. Disable wallet options when offline. * Add online indicator to the header. * Prevent some components from render, some requests from firing when offline. * Allow for array of elements with typing. * Dont show dollars in fee summary when offline. * Fix up saga tests. * Fix sidebar component offline styles. * Remove force offline. * Dont request rates if offline. * Nonce in advanced, show even of online. * Show invalid advanced props. * Fix up offline poll tests. --- common/Root.tsx | 5 + common/actions/config/actionCreators.ts | 7 -- common/actions/config/actionTypes.ts | 6 -- common/actions/config/constants.ts | 1 - .../BalanceSidebar/EquivalentValues.scss | 5 + .../BalanceSidebar/EquivalentValues.tsx | 23 +++-- .../BalanceSidebar/TokenBalances/index.scss | 5 + .../BalanceSidebar/TokenBalances/index.tsx | 15 ++- common/components/BalanceSidebar/index.tsx | 7 +- common/components/GasSlider/GasSlider.tsx | 16 ++- .../GasSlider/components/AdvancedGas.tsx | 37 +++++-- .../GasSlider/components/FeeSummary.tsx | 8 +- .../Header/components/OnlineStatus.scss | 36 +++++++ .../Header/components/OnlineStatus.tsx | 15 +++ common/components/Header/index.scss | 4 + common/components/Header/index.tsx | 7 ++ common/components/NonceField/NonceInput.tsx | 4 +- .../components/OfflineAwareUnlockHeader.tsx | 36 ------- .../WalletDecrypt/WalletDecrypt.tsx | 13 +-- common/components/WalletDecrypt/disables.json | 3 +- common/components/index.ts | 1 - common/components/ui/UnlockHeader.tsx | 2 +- common/containers/TabSection/OfflineTab.scss | 26 +++++ common/containers/TabSection/OfflineTab.tsx | 16 +++ common/containers/TabSection/index.tsx | 14 ++- common/containers/Tabs/BroadcastTx/index.tsx | 2 +- common/containers/Tabs/Contracts/index.tsx | 2 +- common/containers/Tabs/ENS/components/ENS.tsx | 2 +- .../components/Fields/Fields.tsx | 6 -- .../containers/Tabs/SendTransaction/index.tsx | 7 +- common/containers/Tabs/Swap/index.tsx | 22 ++++- common/reducers/config.ts | 11 --- common/sagas/config.ts | 52 +++++----- common/sagas/transaction/network/gas.ts | 12 ++- common/sagas/transaction/network/nonce.ts | 8 +- common/sagas/wallet/wallet.ts | 28 +++++- common/selectors/config.ts | 6 -- common/selectors/derived.ts | 7 +- package.json | 6 +- spec/pages/SendTransaction.spec.tsx | 4 +- spec/pages/Swap.spec.tsx | 5 +- spec/pages/__snapshots__/Swap.spec.tsx.snap | 1 + spec/reducers/config.spec.ts | 22 ----- spec/sagas/__snapshots__/config.spec.ts.snap | 38 ++++--- spec/sagas/config.spec.ts | 72 +++----------- spec/sagas/transaction/network/gas.spec.ts | 16 ++- spec/sagas/transaction/network/nonce.spec.ts | 21 ++-- spec/sagas/wallet.spec.tsx | 98 +++++++++++-------- 48 files changed, 441 insertions(+), 319 deletions(-) create mode 100644 common/components/Header/components/OnlineStatus.scss create mode 100644 common/components/Header/components/OnlineStatus.tsx delete mode 100644 common/components/OfflineAwareUnlockHeader.tsx create mode 100644 common/containers/TabSection/OfflineTab.scss create mode 100644 common/containers/TabSection/OfflineTab.tsx diff --git a/common/Root.tsx b/common/Root.tsx index e572c223..612523b9 100644 --- a/common/Root.tsx +++ b/common/Root.tsx @@ -15,6 +15,7 @@ import PageNotFound from 'components/PageNotFound'; import LogOutPrompt from 'components/LogOutPrompt'; import { Aux } from 'components/ui'; import { Store } from 'redux'; +import { pollOfflineStatus } from 'actions/config'; import { AppState } from 'reducers'; interface Props { @@ -30,6 +31,10 @@ export default class Root extends Component { error: null }; + public componentDidMount() { + this.props.store.dispatch(pollOfflineStatus()); + } + public componentDidCatch(error: Error) { this.setState({ error }); } diff --git a/common/actions/config/actionCreators.ts b/common/actions/config/actionCreators.ts index adb7b467..2e80892c 100644 --- a/common/actions/config/actionCreators.ts +++ b/common/actions/config/actionCreators.ts @@ -2,13 +2,6 @@ import * as interfaces from './actionTypes'; import { TypeKeys } from './constants'; import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config/data'; -export type TForceOfflineConfig = typeof forceOfflineConfig; -export function forceOfflineConfig(): interfaces.ForceOfflineAction { - return { - type: TypeKeys.CONFIG_FORCE_OFFLINE - }; -} - export type TToggleOfflineConfig = typeof toggleOfflineConfig; export function toggleOfflineConfig(): interfaces.ToggleOfflineAction { return { diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index 4157ca52..3dc3a336 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -6,11 +6,6 @@ export interface ToggleOfflineAction { type: TypeKeys.CONFIG_TOGGLE_OFFLINE; } -/*** Force Offline ***/ -export interface ForceOfflineAction { - type: TypeKeys.CONFIG_FORCE_OFFLINE; -} - /*** Change Language ***/ export interface ChangeLanguageAction { type: TypeKeys.CONFIG_LANGUAGE_CHANGE; @@ -80,7 +75,6 @@ export type ConfigAction = | ChangeLanguageAction | ToggleOfflineAction | PollOfflineStatus - | ForceOfflineAction | ChangeNodeIntentAction | AddCustomNodeAction | RemoveCustomNodeAction diff --git a/common/actions/config/constants.ts b/common/actions/config/constants.ts index dff08434..0e8981a4 100644 --- a/common/actions/config/constants.ts +++ b/common/actions/config/constants.ts @@ -3,7 +3,6 @@ export enum TypeKeys { CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE', CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT', CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE', - CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE', CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS', CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE', CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE', diff --git a/common/components/BalanceSidebar/EquivalentValues.scss b/common/components/BalanceSidebar/EquivalentValues.scss index 095651cf..9e4256fb 100644 --- a/common/components/BalanceSidebar/EquivalentValues.scss +++ b/common/components/BalanceSidebar/EquivalentValues.scss @@ -40,4 +40,9 @@ text-align: center; } } + + &-offline { + margin-bottom: 0; + text-align: center; + } } diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index 050cb846..ed885a91 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -20,6 +20,7 @@ interface Props { ratesError?: State['ratesError']; fetchCCRates: TFetchCCRates; network: NetworkConfig; + isOffline: boolean; } interface CmpState { @@ -44,15 +45,19 @@ export default class EquivalentValues extends React.Component { } public componentWillReceiveProps(nextProps: Props) { - const { balance, tokenBalances } = this.props; - if (nextProps.balance !== balance || nextProps.tokenBalances !== tokenBalances) { + const { balance, tokenBalances, isOffline } = this.props; + if ( + nextProps.balance !== balance || + nextProps.tokenBalances !== tokenBalances || + nextProps.isOffline !== isOffline + ) { this.makeBalanceLookup(nextProps); this.fetchRates(nextProps); } } public render() { - const { balance, tokenBalances, rates, ratesError, network } = this.props; + const { balance, tokenBalances, rates, ratesError, isOffline, network } = this.props; const { currency } = this.state; // There are a bunch of reasons why the incorrect balances might be rendered @@ -130,7 +135,13 @@ export default class EquivalentValues extends React.Component { -
    {valuesEl}
+ {isOffline ? ( +
+ Equivalent values are unavailable offline +
+ ) : ( +
    {valuesEl}
+ )}
); } @@ -154,8 +165,8 @@ export default class EquivalentValues extends React.Component { } private fetchRates(props: Props) { - // Duck out if we haven't gotten balances yet - if (!props.balance || !props.tokenBalances) { + // Duck out if we haven't gotten balances yet, or we're not going to + if (!props.balance || !props.tokenBalances || props.isOffline) { return; } diff --git a/common/components/BalanceSidebar/TokenBalances/index.scss b/common/components/BalanceSidebar/TokenBalances/index.scss index 0405b778..435e384f 100644 --- a/common/components/BalanceSidebar/TokenBalances/index.scss +++ b/common/components/BalanceSidebar/TokenBalances/index.scss @@ -41,4 +41,9 @@ color: $gray; } } + + &-offline { + margin-bottom: 0; + text-align: center; + } } diff --git a/common/components/BalanceSidebar/TokenBalances/index.tsx b/common/components/BalanceSidebar/TokenBalances/index.tsx index fa2618de..735498f4 100644 --- a/common/components/BalanceSidebar/TokenBalances/index.tsx +++ b/common/components/BalanceSidebar/TokenBalances/index.tsx @@ -29,6 +29,7 @@ interface StateProps { tokensError: AppState['wallet']['tokensError']; isTokensLoading: AppState['wallet']['isTokensLoading']; hasSavedWalletTokens: AppState['wallet']['hasSavedWalletTokens']; + isOffline: AppState['config']['offline']; } interface ActionProps { addCustomToken: TAddCustomToken; @@ -46,13 +47,20 @@ class TokenBalances extends React.Component { tokenBalances, hasSavedWalletTokens, isTokensLoading, - tokensError + tokensError, + isOffline } = this.props; const walletTokens = walletConfig ? walletConfig.tokens : []; let content; - if (tokensError) { + if (isOffline) { + content = ( +
+ Token balances are unavailable offline +
+ ); + } else if (tokensError) { content =
{tokensError}
; } else if (isTokensLoading) { content = ( @@ -109,7 +117,8 @@ function mapStateToProps(state: AppState): StateProps { tokenBalances: getTokenBalances(state), tokensError: state.wallet.tokensError, isTokensLoading: state.wallet.isTokensLoading, - hasSavedWalletTokens: state.wallet.hasSavedWalletTokens + hasSavedWalletTokens: state.wallet.hasSavedWalletTokens, + isOffline: state.config.offline }; } diff --git a/common/components/BalanceSidebar/index.tsx b/common/components/BalanceSidebar/index.tsx index 2b6f773a..ade5e0f9 100644 --- a/common/components/BalanceSidebar/index.tsx +++ b/common/components/BalanceSidebar/index.tsx @@ -19,6 +19,7 @@ interface Props { rates: AppState['rates']['rates']; ratesError: AppState['rates']['ratesError']; fetchCCRates: TFetchCCRates; + isOffline: AppState['config']['offline']; } interface Block { @@ -29,7 +30,7 @@ interface Block { export class BalanceSidebar extends React.Component { public render() { - const { wallet, balance, network, tokenBalances, rates, ratesError } = this.props; + const { wallet, balance, network, tokenBalances, rates, ratesError, isOffline } = this.props; if (!wallet) { return null; @@ -59,6 +60,7 @@ export class BalanceSidebar extends React.Component { rates={rates} ratesError={ratesError} fetchCCRates={this.props.fetchCCRates} + isOffline={isOffline} /> ) } @@ -83,7 +85,8 @@ function mapStateToProps(state: AppState) { tokenBalances: getShownTokenBalances(state, true), network: getNetworkConfig(state), rates: state.rates.rates, - ratesError: state.rates.ratesError + ratesError: state.rates.ratesError, + isOffline: state.config.offline }; } diff --git a/common/components/GasSlider/GasSlider.tsx b/common/components/GasSlider/GasSlider.tsx index 789d85a5..86bbd4ea 100644 --- a/common/components/GasSlider/GasSlider.tsx +++ b/common/components/GasSlider/GasSlider.tsx @@ -22,6 +22,7 @@ interface Props { // Data gasPrice: AppState['transaction']['fields']['gasPrice']; gasLimit: AppState['transaction']['fields']['gasLimit']; + nonce: AppState['transaction']['fields']['nonce']; offline: AppState['config']['offline']; network: AppState['config']['network']; // Actions @@ -41,11 +42,19 @@ class GasSlider extends React.Component { }; public componentDidMount() { - this.props.fetchCCRates([this.props.network.unit]); + if (!this.props.offline) { + this.props.fetchCCRates([this.props.network.unit]); + } + } + + public componentWillReceiveProps(nextProps: Props) { + if (this.props.offline && !nextProps.offline) { + this.props.fetchCCRates([this.props.network.unit]); + } } public render() { - const { gasPrice, gasLimit, offline, disableAdvanced } = this.props; + const { gasPrice, gasLimit, nonce, offline, disableAdvanced } = this.props; const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced; return ( @@ -54,8 +63,10 @@ class GasSlider extends React.Component { ) : ( @@ -86,6 +97,7 @@ function mapStateToProps(state: AppState) { return { gasPrice: state.transaction.fields.gasPrice, gasLimit: state.transaction.fields.gasLimit, + nonce: state.transaction.fields.nonce, offline: state.config.offline, network: getNetworkConfig(state) }; diff --git a/common/components/GasSlider/components/AdvancedGas.tsx b/common/components/GasSlider/components/AdvancedGas.tsx index bcc3d6c1..2af3438b 100644 --- a/common/components/GasSlider/components/AdvancedGas.tsx +++ b/common/components/GasSlider/components/AdvancedGas.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import classnames from 'classnames'; import translate from 'translations'; import { DataFieldFactory } from 'components/DataFieldFactory'; import FeeSummary from './FeeSummary'; @@ -7,35 +8,53 @@ import './AdvancedGas.scss'; interface Props { gasPrice: string; gasLimit: string; + nonce: string; changeGasPrice(gwei: string): void; changeGasLimit(wei: string): void; + changeNonce(nonce: string): void; } export default class AdvancedGas extends React.Component { public render() { + // Can't shadow var names for data & fee summary + const vals = this.props; + return (
-
+
-
+
-
+
+ + +
+ +
( @@ -69,4 +88,8 @@ export default class AdvancedGas extends React.Component { private handleGasLimitChange = (ev: React.FormEvent) => { this.props.changeGasLimit(ev.currentTarget.value); }; + + private handleNonceChange = (ev: React.FormEvent) => { + this.props.changeNonce(ev.currentTarget.value); + }; } diff --git a/common/components/GasSlider/components/FeeSummary.tsx b/common/components/GasSlider/components/FeeSummary.tsx index 7b597dc6..2afc4319 100644 --- a/common/components/GasSlider/components/FeeSummary.tsx +++ b/common/components/GasSlider/components/FeeSummary.tsx @@ -20,13 +20,14 @@ interface Props { gasLimit: AppState['transaction']['fields']['gasLimit']; rates: AppState['rates']['rates']; network: AppState['config']['network']; + isOffline: AppState['config']['offline']; // Component props render(data: RenderData): React.ReactElement | string; } class FeeSummary extends React.Component { public render() { - const { gasPrice, gasLimit, rates, network } = this.props; + const { gasPrice, gasLimit, rates, network, isOffline } = this.props; const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value); const fee = ( @@ -42,7 +43,7 @@ class FeeSummary extends React.Component { const usdBig = network.isTestnet ? new BN(0) : feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD); - const usd = ( + const usd = isOffline ? null : ( = ({ isOffline }) => ( +
+ {isOffline ? 'Offline' : 'Online'} +
+); + +export default OnlineStatus; diff --git a/common/components/Header/index.scss b/common/components/Header/index.scss index 233f540c..5d4c90dd 100644 --- a/common/components/Header/index.scss +++ b/common/components/Header/index.scss @@ -130,6 +130,10 @@ $small-size: 900px; margin-right: 10px; } + &-online { + margin-right: 6px; + } + &-dropdown { margin-left: 6px; diff --git a/common/components/Header/index.tsx b/common/components/Header/index.tsx index da87ea5d..480fd72e 100644 --- a/common/components/Header/index.tsx +++ b/common/components/Header/index.tsx @@ -24,6 +24,7 @@ import { import GasPriceDropdown from './components/GasPriceDropdown'; import Navigation from './components/Navigation'; import CustomNodeModal from './components/CustomNodeModal'; +import OnlineStatus from './components/OnlineStatus'; import { getKeyByValue } from 'utils/helpers'; import { makeCustomNodeId } from 'utils/node'; import { getNetworkConfigFromId } from 'utils/network'; @@ -35,6 +36,7 @@ interface Props { node: NodeConfig; nodeSelection: string; isChangingNode: boolean; + isOffline: boolean; gasPrice: AppState['transaction']['fields']['gasPrice']; customNodes: CustomNodeConfig[]; customNetworks: CustomNetworkConfig[]; @@ -62,6 +64,7 @@ export default class Header extends Component { node, nodeSelection, isChangingNode, + isOffline, customNodes, customNetworks } = this.props; @@ -127,6 +130,10 @@ export default class Header extends Component {
v{VERSION} +
+ +
+
{ } export const NonceInput = connect((state: AppState) => ({ - shouldDisplay: isAnyOffline(state) || nonceRequestFailed(state), + shouldDisplay: getOffline(state) || nonceRequestFailed(state), nonce: getNonce(state) }))(NonceInputClass); diff --git a/common/components/OfflineAwareUnlockHeader.tsx b/common/components/OfflineAwareUnlockHeader.tsx deleted file mode 100644 index 1790ae63..00000000 --- a/common/components/OfflineAwareUnlockHeader.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { UnlockHeader } from 'components/ui'; -import React, { Component } from 'react'; -import translate from 'translations'; -import { isAnyOffline } from 'selectors/config'; -import { connect } from 'react-redux'; -import { AppState } from 'reducers'; - -interface Props { - disabledWallets?: string[]; -} -export const OfflineAwareUnlockHeader: React.SFC = ({ disabledWallets }) => ( - } disabledWallets={disabledWallets} /> -); - -interface StateProps { - shouldDisplayOffline: boolean; -} - -class TitleClass extends Component { - public render() { - const { shouldDisplayOffline } = this.props; - const offlineTitle = shouldDisplayOffline ? ( - (Offline) - ) : null; - return ( -
- {translate('Account')} - {offlineTitle} -
- ); - } -} - -const Title = connect((state: AppState) => ({ - shouldDisplayOffline: isAnyOffline(state) -}))(TitleClass); diff --git a/common/components/WalletDecrypt/WalletDecrypt.tsx b/common/components/WalletDecrypt/WalletDecrypt.tsx index 8e0539e1..23eba26d 100644 --- a/common/components/WalletDecrypt/WalletDecrypt.tsx +++ b/common/components/WalletDecrypt/WalletDecrypt.tsx @@ -33,14 +33,15 @@ import { import { AppState } from 'reducers'; import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data'; import { IWallet } from 'libs/wallet'; +import DISABLES from './disables.json'; import { showNotification, TShowNotification } from 'actions/notifications'; + import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg'; import LedgerIcon from 'assets/images/wallets/ledger.svg'; import MetamaskIcon from 'assets/images/wallets/metamask.svg'; import MistIcon from 'assets/images/wallets/mist.svg'; import TrezorIcon from 'assets/images/wallets/trezor.svg'; import './WalletDecrypt.scss'; -type UnlockParams = {} | PrivateKeyValue; interface Props { resetTransactionState: TReset; @@ -59,6 +60,7 @@ interface Props { isPasswordPending: AppState['wallet']['isPasswordPending']; } +type UnlockParams = {} | PrivateKeyValue; interface State { selectedWalletKey: string | null; value: UnlockParams | null; @@ -227,11 +229,6 @@ export class WalletDecrypt extends Component { ); } - public isOnlineRequiredWalletAndOffline(selectedWalletKey) { - const onlineRequiredWallets = ['trezor', 'ledger-nano-s']; - return this.props.offline && onlineRequiredWallets.includes(selectedWalletKey); - } - public buildWalletOptions() { const viewOnly = this.WALLETS['view-only'] as InsecureWalletInfo; @@ -379,6 +376,10 @@ export class WalletDecrypt extends Component { }; private isWalletDisabled = (walletKey: string) => { + if (this.props.offline && DISABLES.ONLINE_ONLY.includes(walletKey)) { + return true; + } + if (!this.props.disabledWallets) { return false; } diff --git a/common/components/WalletDecrypt/disables.json b/common/components/WalletDecrypt/disables.json index 356d0fae..947cd2b2 100644 --- a/common/components/WalletDecrypt/disables.json +++ b/common/components/WalletDecrypt/disables.json @@ -1,4 +1,5 @@ { "READ_ONLY": ["view-only"], - "UNABLE_TO_SIGN": ["trezor", "view-only"] + "UNABLE_TO_SIGN": ["trezor", "view-only"], + "ONLINE_ONLY": ["web3", "trezor"] } diff --git a/common/components/index.ts b/common/components/index.ts index 68f4083d..6ad964b8 100644 --- a/common/components/index.ts +++ b/common/components/index.ts @@ -9,7 +9,6 @@ export * from './CurrentCustomMessage'; export * from './GenerateTransaction'; export * from './SendButton'; export * from './SigningStatus'; -export * from './OfflineAwareUnlockHeader'; export { default as Header } from './Header'; export { default as Footer } from './Footer'; export { default as BalanceSidebar } from './BalanceSidebar'; diff --git a/common/components/ui/UnlockHeader.tsx b/common/components/ui/UnlockHeader.tsx index 57cbcfa0..733c5c13 100644 --- a/common/components/ui/UnlockHeader.tsx +++ b/common/components/ui/UnlockHeader.tsx @@ -7,7 +7,7 @@ import { IWallet } from 'libs/wallet/IWallet'; import './UnlockHeader.scss'; interface Props { - title: React.ReactElement; + title: React.ReactElement | string; wallet: IWallet; disabledWallets?: string[]; } diff --git a/common/containers/TabSection/OfflineTab.scss b/common/containers/TabSection/OfflineTab.scss new file mode 100644 index 00000000..ce3587f0 --- /dev/null +++ b/common/containers/TabSection/OfflineTab.scss @@ -0,0 +1,26 @@ +@import 'common/sass/variables'; + +@keyframes ban-wifi { + 0% { + opacity: 0; + transform: scale(1.3); + } + 100% { + opacity: 1; + transform: scale(1.1); + } +} + +.OfflineTab { + text-align: center; + + &-icon { + opacity: 0.8; + + .fa-ban { + color: $brand-danger; + animation: ban-wifi 500ms ease 200ms 1; + animation-fill-mode: both; + } + } +} diff --git a/common/containers/TabSection/OfflineTab.tsx b/common/containers/TabSection/OfflineTab.tsx new file mode 100644 index 00000000..5bbb604d --- /dev/null +++ b/common/containers/TabSection/OfflineTab.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import './OfflineTab.scss'; + +const OfflineTab: React.SFC<{}> = () => ( +
+
+
+ + +
+

This feature is unavailable while offline

+
+
+); + +export default OfflineTab; diff --git a/common/containers/TabSection/index.tsx b/common/containers/TabSection/index.tsx index 1e54b071..c385d8e6 100644 --- a/common/containers/TabSection/index.tsx +++ b/common/containers/TabSection/index.tsx @@ -16,6 +16,7 @@ import { TSetGasPriceField, setGasPriceField as dSetGasPriceField } from 'action import { AlphaAgreement, Footer, Header } from 'components'; import { AppState } from 'reducers'; import Notifications from './Notifications'; +import OfflineTab from './OfflineTab'; import { getGasPrice } from 'selectors/transaction'; interface ReduxProps { @@ -23,6 +24,7 @@ interface ReduxProps { node: AppState['config']['node']; nodeSelection: AppState['config']['nodeSelection']; isChangingNode: AppState['config']['isChangingNode']; + isOffline: AppState['config']['offline']; customNodes: AppState['config']['customNodes']; customNetworks: AppState['config']['customNetworks']; latestBlock: AppState['config']['latestBlock']; @@ -39,19 +41,21 @@ interface ActionProps { } type Props = { - // FIXME - children: any; + isUnavailableOffline?: boolean; + children: string | React.ReactElement | React.ReactElement[]; } & ReduxProps & ActionProps; class TabSection extends Component { public render() { const { + isUnavailableOffline, children, // APP node, nodeSelection, isChangingNode, + isOffline, languageSelection, customNodes, customNetworks, @@ -70,6 +74,7 @@ class TabSection extends Component { node, nodeSelection, isChangingNode, + isOffline, gasPrice, customNodes, customNetworks, @@ -85,7 +90,9 @@ class TabSection extends Component {
-
{children}
+
+ {isUnavailableOffline && isOffline ? : children} +
@@ -100,6 +107,7 @@ function mapStateToProps(state: AppState): ReduxProps { node: state.config.node, nodeSelection: state.config.nodeSelection, isChangingNode: state.config.isChangingNode, + isOffline: state.config.offline, languageSelection: state.config.languageSelection, gasPrice: getGasPrice(state), customNodes: state.config.customNodes, diff --git a/common/containers/Tabs/BroadcastTx/index.tsx b/common/containers/Tabs/BroadcastTx/index.tsx index 090498a1..d97c045a 100644 --- a/common/containers/Tabs/BroadcastTx/index.tsx +++ b/common/containers/Tabs/BroadcastTx/index.tsx @@ -43,7 +43,7 @@ class BroadcastTx extends Component { }); return ( - +

Broadcast Signed Transaction

diff --git a/common/containers/Tabs/Contracts/index.tsx b/common/containers/Tabs/Contracts/index.tsx index b9d5a491..27e193e3 100644 --- a/common/containers/Tabs/Contracts/index.tsx +++ b/common/containers/Tabs/Contracts/index.tsx @@ -43,7 +43,7 @@ class Contracts extends Component { } return ( - +

diff --git a/common/containers/Tabs/ENS/components/ENS.tsx b/common/containers/Tabs/ENS/components/ENS.tsx index 9c04f9cd..aa5c17f2 100644 --- a/common/containers/Tabs/ENS/components/ENS.tsx +++ b/common/containers/Tabs/ENS/components/ENS.tsx @@ -9,7 +9,7 @@ interface ContainerTabPaneActiveProps { } const ContainerTabPaneActive = ({ children }: ContainerTabPaneActiveProps) => ( - +
diff --git a/common/containers/Tabs/SendTransaction/components/Fields/Fields.tsx b/common/containers/Tabs/SendTransaction/components/Fields/Fields.tsx index 80775339..7210301e 100644 --- a/common/containers/Tabs/SendTransaction/components/Fields/Fields.tsx +++ b/common/containers/Tabs/SendTransaction/components/Fields/Fields.tsx @@ -2,7 +2,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { isAnyOfflineWithWeb3 } from 'selectors/derived'; import { - NonceField, AddressField, AmountField, GasSlider, @@ -33,11 +32,6 @@ const content = (

-
-
- -
-
diff --git a/common/containers/Tabs/SendTransaction/index.tsx b/common/containers/Tabs/SendTransaction/index.tsx index b58366a3..d75d8e28 100644 --- a/common/containers/Tabs/SendTransaction/index.tsx +++ b/common/containers/Tabs/SendTransaction/index.tsx @@ -1,7 +1,8 @@ -import TabSection from 'containers/TabSection'; -import { OfflineAwareUnlockHeader } from 'components'; import React from 'react'; import { connect } from 'react-redux'; +import translate from 'translations'; +import TabSection from 'containers/TabSection'; +import { UnlockHeader } from 'components/ui'; import { SideBar } from './components/index'; import { IReadOnlyWallet, IFullWallet } from 'libs/wallet'; import { getWalletInst } from 'selectors/wallet'; @@ -52,7 +53,7 @@ class SendTransaction extends React.Component { return (
- + {wallet && }
diff --git a/common/containers/Tabs/Swap/index.tsx b/common/containers/Tabs/Swap/index.tsx index 7803e248..b72aeafb 100644 --- a/common/containers/Tabs/Swap/index.tsx +++ b/common/containers/Tabs/Swap/index.tsx @@ -73,6 +73,7 @@ interface ReduxStateProps { bityOrderStatus: string | null; shapeshiftOrderStatus: string | null; paymentAddress: string | null; + isOffline: boolean; } interface ReduxActionProps { @@ -98,8 +99,15 @@ interface ReduxActionProps { class Swap extends Component { public componentDidMount() { - this.props.loadBityRatesRequestedSwap(); - this.props.loadShapeshiftRatesRequestedSwap(); + if (!this.props.isOffline) { + this.loadRates(); + } + } + + public componentWillReceiveProps(nextProps: ReduxStateProps) { + if (this.props.isOffline && !nextProps.isOffline) { + this.loadRates(); + } } public componentWillUnmount() { @@ -107,6 +115,11 @@ class Swap extends Component { this.props.stopLoadShapeshiftRatesSwap(); } + public loadRates() { + this.props.loadBityRatesRequestedSwap(); + this.props.loadShapeshiftRatesRequestedSwap(); + } + public render() { const { // STATE @@ -222,7 +235,7 @@ class Swap extends Component { const CurrentRatesProps = { provider, bityRates, shapeshiftRates }; return ( - +
{step === 1 && } {step === 1 && } @@ -257,7 +270,8 @@ function mapStateToProps(state: AppState) { isPostingOrder: state.swap.isPostingOrder, bityOrderStatus: state.swap.bityOrderStatus, shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus, - paymentAddress: state.swap.paymentAddress + paymentAddress: state.swap.paymentAddress, + isOffline: state.config.offline }; } diff --git a/common/reducers/config.ts b/common/reducers/config.ts index e6cfb020..e218c2eb 100644 --- a/common/reducers/config.ts +++ b/common/reducers/config.ts @@ -28,7 +28,6 @@ export interface State { network: NetworkConfig; isChangingNode: boolean; offline: boolean; - forceOffline: boolean; customNodes: CustomNodeConfig[]; customNetworks: CustomNetworkConfig[]; latestBlock: string; @@ -42,7 +41,6 @@ export const INITIAL_STATE: State = { network: NETWORKS[NODES[defaultNode].network], isChangingNode: false, offline: false, - forceOffline: false, customNodes: [], customNetworks: [], latestBlock: '???' @@ -79,13 +77,6 @@ function toggleOffline(state: State): State { }; } -function forceOffline(state: State): State { - return { - ...state, - forceOffline: !state.forceOffline - }; -} - function addCustomNode(state: State, action: AddCustomNodeAction): State { const newId = makeCustomNodeId(action.payload); return { @@ -141,8 +132,6 @@ export function config(state: State = INITIAL_STATE, action: ConfigAction): Stat return changeNodeIntent(state); case TypeKeys.CONFIG_TOGGLE_OFFLINE: return toggleOffline(state); - case TypeKeys.CONFIG_FORCE_OFFLINE: - return forceOffline(state); case TypeKeys.CONFIG_ADD_CUSTOM_NODE: return addCustomNode(state, action); case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE: diff --git a/common/sagas/config.ts b/common/sagas/config.ts index 1d6f7774..08e927b5 100644 --- a/common/sagas/config.ts +++ b/common/sagas/config.ts @@ -22,8 +22,7 @@ import { getNodeConfig, getCustomNodeConfigs, getCustomNetworkConfigs, - getOffline, - getForceOffline + getOffline } from 'selectors/config'; import { AppState } from 'reducers'; import { TypeKeys } from 'actions/config/constants'; @@ -50,19 +49,11 @@ export function* pollOfflineStatus(): SagaIterator { while (true) { const node: NodeConfig = yield select(getNodeConfig); const isOffline: boolean = yield select(getOffline); - const isForcedOffline: boolean = yield select(getForceOffline); - - // If they're forcing themselves offline, exit the loop. It will be - // kicked off again if they toggle it in handleTogglePollOfflineStatus. - if (isForcedOffline) { - return; - } // If our offline state disagrees with the browser, run a check // Don't check if the user is in another tab or window const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline; if (shouldPing && !document.hidden) { - hasCheckedOnline = true; const { pingSucceeded } = yield race({ pingSucceeded: call(node.lib.ping.bind(node.lib)), timeout: call(delay, 5000) @@ -76,20 +67,33 @@ export function* pollOfflineStatus(): SagaIterator { yield put(toggleOfflineConfig()); } else if (!pingSucceeded && !isOffline) { // If we were unable to ping but redux says we're online, mark offline - yield put( - showNotification( - 'danger', - `You’ve lost your connection to the network, check your internet - connection or try changing networks from the dropdown at the - top right of the page.`, - Infinity - ) - ); + // If they had been online, show an error. + // If they hadn't been online, just inform them with a warning. + if (hasCheckedOnline) { + yield put( + showNotification( + 'danger', + `You’ve lost your connection to the network, check your internet + connection or try changing networks from the dropdown at the + top right of the page.`, + Infinity + ) + ); + } else { + yield put( + showNotification( + 'info', + 'You are currently offline. Some features will be unavailable.', + 5000 + ) + ); + } yield put(toggleOfflineConfig()); } else { // If neither case was true, try again in 5s yield call(delay, 5000); } + hasCheckedOnline = true; } else { yield call(delay, 1000); } @@ -103,15 +107,6 @@ export function* handlePollOfflineStatus(): SagaIterator { yield cancel(pollOfflineStatusTask); } -export function* handleTogglePollOfflineStatus(): SagaIterator { - const isForcedOffline: boolean = yield select(getForceOffline); - if (isForcedOffline) { - yield fork(handlePollOfflineStatus); - } else { - yield call(handlePollOfflineStatus); - } -} - // @HACK For now we reload the app when doing a language swap to force non-connected // data to reload. Also the use of timeout to avoid using additional actions for now. export function* reload(): SagaIterator { @@ -251,7 +246,6 @@ export const equivalentNodeOrDefault = (nodeConfig: NodeConfig) => { export default function* configSaga(): SagaIterator { yield takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus); - yield takeEvery(TypeKeys.CONFIG_FORCE_OFFLINE, handleTogglePollOfflineStatus); yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent); yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload); yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode); diff --git a/common/sagas/transaction/network/gas.ts b/common/sagas/transaction/network/gas.ts index 91963277..997b71d9 100644 --- a/common/sagas/transaction/network/gas.ts +++ b/common/sagas/transaction/network/gas.ts @@ -1,7 +1,7 @@ import { SagaIterator, buffers, delay } from 'redux-saga'; import { apply, put, select, take, actionChannel, call, fork } from 'redux-saga/effects'; import { INode } from 'libs/nodes/INode'; -import { getNodeLib } from 'selectors/config'; +import { getNodeLib, getOffline } from 'selectors/config'; import { getWalletInst } from 'selectors/wallet'; import { getTransaction, IGetTransaction } from 'selectors/transaction'; import { @@ -22,6 +22,11 @@ import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/ export function* shouldEstimateGas(): SagaIterator { while (true) { + const isOffline = yield select(getOffline); + if (isOffline) { + continue; + } + const action: | SetToFieldAction | SetDataFieldAction @@ -59,6 +64,11 @@ export function* estimateGas(): SagaIterator { const requestChan = yield actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1)); while (true) { + const isOffline = yield select(getOffline); + if (isOffline) { + continue; + } + const { payload }: EstimateGasRequestedAction = yield take(requestChan); // debounce 250 ms yield call(delay, 250); diff --git a/common/sagas/transaction/network/nonce.ts b/common/sagas/transaction/network/nonce.ts index 7e4a745e..071af61c 100644 --- a/common/sagas/transaction/network/nonce.ts +++ b/common/sagas/transaction/network/nonce.ts @@ -12,9 +12,13 @@ import { Nonce } from 'libs/units'; export function* handleNonceRequest(): SagaIterator { const nodeLib: INode = yield select(getNodeLib); const walletInst: AppState['wallet']['inst'] = yield select(getWalletInst); - const offline: boolean = yield select(getOffline); + const isOffline: boolean = yield select(getOffline); try { - if (!walletInst || offline) { + if (isOffline) { + return; + } + + if (!walletInst) { throw Error(); } const fromAddress: string = yield apply(walletInst, walletInst.getAddressString); diff --git a/common/sagas/wallet/wallet.ts b/common/sagas/wallet/wallet.ts index ef647cbf..44fbe114 100644 --- a/common/sagas/wallet/wallet.ts +++ b/common/sagas/wallet/wallet.ts @@ -39,7 +39,7 @@ import { import { NODES, initWeb3Node, Token } from 'config/data'; import { SagaIterator, delay, Task } from 'redux-saga'; import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects'; -import { getNodeLib, getAllTokens } from 'selectors/config'; +import { getNodeLib, getAllTokens, getOffline } from 'selectors/config'; import { getTokens, getWalletInst, @@ -58,6 +58,11 @@ export interface TokenBalanceLookup { export function* updateAccountBalance(): SagaIterator { try { + const isOffline = yield select(getOffline); + if (isOffline) { + return; + } + yield put(setBalancePending()); const wallet: null | IWallet = yield select(getWalletInst); if (!wallet) { @@ -75,6 +80,11 @@ export function* updateAccountBalance(): SagaIterator { export function* updateTokenBalances(): SagaIterator { try { + const isOffline = yield select(getOffline); + if (isOffline) { + return; + } + const wallet: null | IWallet = yield select(getWalletInst); const tokens: MergedToken[] = yield select(getWalletConfigTokens); if (!wallet || !tokens.length) { @@ -91,6 +101,11 @@ export function* updateTokenBalances(): SagaIterator { export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaIterator { try { + const isOffline = yield select(getOffline); + if (isOffline) { + return; + } + const wallet: null | IWallet = yield select(getWalletInst); const { tokenSymbol } = action.payload; const allTokens: Token[] = yield select(getAllTokens); @@ -115,6 +130,11 @@ export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaI export function* scanWalletForTokens(action: ScanWalletForTokensAction): SagaIterator { try { + const isOffline = yield select(getOffline); + if (isOffline) { + return; + } + const wallet = action.payload; const tokens: MergedToken[] = yield select(getTokens); yield put(setTokenBalancesPending()); @@ -288,7 +308,9 @@ export default function* walletSaga(): SagaIterator { takeEvery(TypeKeys.WALLET_SET, handleNewWallet), takeEvery(TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS, scanWalletForTokens), takeEvery(TypeKeys.WALLET_SET_WALLET_TOKENS, handleSetWalletTokens), - takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd), - takeEvery(TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, updateTokenBalance) + takeEvery(TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, updateTokenBalance), + // Foreign actions + takeEvery(ConfigTypeKeys.CONFIG_TOGGLE_OFFLINE, updateBalances), + takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd) ]; } diff --git a/common/selectors/config.ts b/common/selectors/config.ts index 9a8e5c40..a684d721 100644 --- a/common/selectors/config.ts +++ b/common/selectors/config.ts @@ -86,12 +86,6 @@ export function getOffline(state: AppState): boolean { return state.config.offline; } -export function getForceOffline(state: AppState): boolean { - return state.config.forceOffline; -} - -export const isAnyOffline = (state: AppState) => getOffline(state) || getForceOffline(state); - export function isSupportedUnit(state: AppState, unit: string) { const isToken: boolean = tokenExists(state, unit); const isEther: boolean = isEtherUnit(unit); diff --git a/common/selectors/derived.ts b/common/selectors/derived.ts index 450ca5dc..c76b4552 100644 --- a/common/selectors/derived.ts +++ b/common/selectors/derived.ts @@ -1,12 +1,9 @@ import { AppState } from 'reducers'; import { getWalletType } from 'selectors/wallet'; -import { getOffline, getForceOffline } from 'selectors/config'; +import { getOffline } from 'selectors/config'; export const isAnyOfflineWithWeb3 = (state: AppState): boolean => { const { isWeb3Wallet } = getWalletType(state); const offline = getOffline(state); - const forceOffline = getForceOffline(state); - const anyOffline = offline || forceOffline; - const anyOfflineAndWeb3 = anyOffline && isWeb3Wallet; - return anyOfflineAndWeb3; + return offline && isWeb3Wallet; }; diff --git a/package.json b/package.json index c63c1e18..a9dc55e9 100644 --- a/package.json +++ b/package.json @@ -143,10 +143,8 @@ "tscheck": "tsc --noEmit", "start": "npm run dev", "precommit": "lint-staged", - "formatAll": - "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override", - "prettier:diff": - "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"", + "formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override", + "prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"", "prepush": "npm run tslint && npm run tscheck" }, "lint-staged": { diff --git a/spec/pages/SendTransaction.spec.tsx b/spec/pages/SendTransaction.spec.tsx index e40e9187..4acd06b6 100644 --- a/spec/pages/SendTransaction.spec.tsx +++ b/spec/pages/SendTransaction.spec.tsx @@ -17,8 +17,7 @@ it('render snapshot', () => { nodeSelection: testNode, node: NODES[testNode], gasPriceGwei: 21, - offline: false, - forceOffline: false + offline: false }; const testState = { wallet: {}, @@ -31,7 +30,6 @@ it('render snapshot', () => { gasPrice: {}, transactions: {}, offline: {}, - forceOffline: {}, config: testStateConfig, customTokens: [] }; diff --git a/spec/pages/Swap.spec.tsx b/spec/pages/Swap.spec.tsx index 68344b65..740fa16a 100644 --- a/spec/pages/Swap.spec.tsx +++ b/spec/pages/Swap.spec.tsx @@ -4,12 +4,13 @@ import Adapter from 'enzyme-adapter-react-16'; import Swap from 'containers/Tabs/Swap'; import shallowWithStore from '../utils/shallowWithStore'; import { createMockStore } from 'redux-test-utils'; -import { INITIAL_STATE } from 'reducers/swap'; +import { INITIAL_STATE as swap } from 'reducers/swap'; +import { INITIAL_STATE as config } from 'reducers/config'; Enzyme.configure({ adapter: new Adapter() }); it('render snapshot', () => { - const store = createMockStore({ swap: INITIAL_STATE }); + const store = createMockStore({ swap, config }); const component = shallowWithStore(, store); expect(component).toMatchSnapshot(); diff --git a/spec/pages/__snapshots__/Swap.spec.tsx.snap b/spec/pages/__snapshots__/Swap.spec.tsx.snap index 4f972232..84781f27 100644 --- a/spec/pages/__snapshots__/Swap.spec.tsx.snap +++ b/spec/pages/__snapshots__/Swap.spec.tsx.snap @@ -22,6 +22,7 @@ exports[`render snapshot 1`] = ` destinationAddressSwap={[Function]} initSwap={[Function]} isFetchingRates={null} + isOffline={false} isPostingOrder={false} loadBityRatesRequestedSwap={[Function]} loadShapeshiftRatesRequestedSwap={[Function]} diff --git a/spec/reducers/config.spec.ts b/spec/reducers/config.spec.ts index 33b40017..0b6a6679 100644 --- a/spec/reducers/config.spec.ts +++ b/spec/reducers/config.spec.ts @@ -53,28 +53,6 @@ describe('config reducer', () => { }); }); - it('should handle CONFIG_FORCE_OFFLINE', () => { - const forceOfflineTrue = { - ...INITIAL_STATE, - forceOffline: true - }; - - const forceOfflineFalse = { - ...INITIAL_STATE, - forceOffline: false - }; - - expect(config(forceOfflineTrue, configActions.forceOfflineConfig())).toEqual({ - ...forceOfflineTrue, - forceOffline: false - }); - - expect(config(forceOfflineFalse, configActions.forceOfflineConfig())).toEqual({ - ...forceOfflineFalse, - forceOffline: true - }); - }); - it('should handle CONFIG_ADD_CUSTOM_NODE', () => { expect(config(undefined, configActions.addCustomNode(custNode))).toEqual({ ...INITIAL_STATE, diff --git a/spec/sagas/__snapshots__/config.spec.ts.snap b/spec/sagas/__snapshots__/config.spec.ts.snap index d67f1a3a..c0e9a2f6 100644 --- a/spec/sagas/__snapshots__/config.spec.ts.snap +++ b/spec/sagas/__snapshots__/config.spec.ts.snap @@ -52,26 +52,6 @@ Object { } `; -exports[`pollOfflineStatus* should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline 1`] = ` -Object { - "@@redux-saga/IO": true, - "PUT": Object { - "action": Object { - "payload": Object { - "duration": Infinity, - "id": 0.001, - "level": "danger", - "msg": "You’ve lost your connection to the network, check your internet - connection or try changing networks from the dropdown at the - top right of the page.", - }, - "type": "SHOW_NOTIFICATION", - }, - "channel": null, - }, -} -`; - exports[`pollOfflineStatus* should race pingSucceeded and timeout 1`] = ` Object { "@@redux-saga/IO": true, @@ -97,3 +77,21 @@ Object { }, } `; + +exports[`pollOfflineStatus* should toggle offline and show notification if navigator agrees with isOffline and ping fails 1`] = ` +Object { + "@@redux-saga/IO": true, + "PUT": Object { + "action": Object { + "payload": Object { + "duration": 5000, + "id": 0.001, + "level": "info", + "msg": "You are currently offline. Some features will be unavailable.", + }, + "type": "SHOW_NOTIFICATION", + }, + "channel": null, + }, +} +`; diff --git a/spec/sagas/config.spec.ts b/spec/sagas/config.spec.ts index fce3d408..7b03eb36 100644 --- a/spec/sagas/config.spec.ts +++ b/spec/sagas/config.spec.ts @@ -7,7 +7,6 @@ import { pollOfflineStatus, handlePollOfflineStatus, handleNodeChangeIntent, - handleTogglePollOfflineStatus, reload, unsetWeb3Node, unsetWeb3NodeOnWalletEvent, @@ -18,7 +17,6 @@ import { getNode, getNodeConfig, getOffline, - getForceOffline, getCustomNodeConfigs, getCustomNetworkConfigs } from 'selectors/config'; @@ -43,12 +41,13 @@ describe('pollOfflineStatus*', () => { } }; const isOffline = true; - const isForcedOffline = true; const raceSuccess = { - pingSucceeded: true + pingSucceeded: true, + timeout: false }; const raceFailure = { - pingSucceeded: false + pingSucceeded: false, + timeout: true }; let originalHidden; @@ -88,49 +87,32 @@ describe('pollOfflineStatus*', () => { expect(data.gen.next(node).value).toEqual(select(getOffline)); }); - it('should select getForceOffline', () => { - data.isOfflineClone = data.gen.clone(); - expect(data.gen.next(isOffline).value).toEqual(select(getForceOffline)); - }); - - it('should be done if isForcedOffline', () => { - data.clone1 = data.gen.clone(); - expect(data.clone1.next(isForcedOffline).done).toEqual(true); - }); - it('should call delay if document is hidden', () => { - data.clone2 = data.gen.clone(); + data.hiddenDoc = data.gen.clone(); doc.hidden = true; - - expect(data.clone2.next(!isForcedOffline).value).toEqual(call(delay, 1000)); + expect(data.hiddenDoc.next(!isOffline).value).toEqual(call(delay, 1000)); + doc.hidden = false; }); it('should race pingSucceeded and timeout', () => { - doc.hidden = false; - expect(data.gen.next(!isForcedOffline).value).toMatchSnapshot(); + data.isOfflineClone = data.gen.clone(); + data.shouldDelayClone = data.gen.clone(); + expect(data.gen.next(isOffline).value).toMatchSnapshot(); }); - it('should put showNotification and put toggleOfflineConfig if pingSucceeded && isOffline', () => { + it('should toggle offline and show notification if navigator disagrees with isOffline and ping succeeds', () => { expect(data.gen.next(raceSuccess).value).toEqual( put(showNotification('success', 'Your connection to the network has been restored!', 3000)) ); expect(data.gen.next().value).toEqual(put(toggleOfflineConfig())); }); - it('should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline', () => { - nav.onLine = !isOffline; - - data.isOfflineClone.next(!isOffline); - data.isOfflineClone.next(!isForcedOffline); - - data.clone3 = data.isOfflineClone.clone(); - + it('should toggle offline and show notification if navigator agrees with isOffline and ping fails', () => { + nav.onLine = isOffline; + expect(data.isOfflineClone.next(!isOffline)); expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot(); expect(data.isOfflineClone.next().value).toEqual(put(toggleOfflineConfig())); - }); - - it('should call delay when neither case is true', () => { - expect(data.clone3.next(raceSuccess).value).toEqual(call(delay, 5000)); + nav.onLine = !isOffline; }); }); @@ -152,30 +134,6 @@ describe('handlePollOfflineStatus*', () => { }); }); -describe('handleTogglePollOfflineStatus*', () => { - const data = {} as any; - data.gen = cloneableGenerator(handleTogglePollOfflineStatus)(); - const isForcedOffline = true; - - it('should select getForceOffline', () => { - expect(data.gen.next().value).toEqual(select(getForceOffline)); - }); - - it('should fork handlePollOfflineStatus when isForcedOffline', () => { - data.clone = data.gen.clone(); - expect(data.gen.next(isForcedOffline).value).toEqual(fork(handlePollOfflineStatus)); - }); - - it('should call handlePollOfflineStatus when !isForcedOffline', () => { - expect(data.clone.next(!isForcedOffline).value).toEqual(call(handlePollOfflineStatus)); - }); - - it('should be done', () => { - expect(data.gen.next().done).toEqual(true); - expect(data.clone.next().done).toEqual(true); - }); -}); - describe('handleNodeChangeIntent*', () => { let originalRandom; diff --git a/spec/sagas/transaction/network/gas.spec.ts b/spec/sagas/transaction/network/gas.spec.ts index 868369e5..0f6aaadf 100644 --- a/spec/sagas/transaction/network/gas.spec.ts +++ b/spec/sagas/transaction/network/gas.spec.ts @@ -1,6 +1,6 @@ import { buffers, delay } from 'redux-saga'; import { apply, put, select, take, actionChannel, call } from 'redux-saga/effects'; -import { getNodeLib } from 'selectors/config'; +import { getNodeLib, getOffline } from 'selectors/config'; import { getWalletInst } from 'selectors/wallet'; import { getTransaction } from 'selectors/transaction'; import { @@ -16,6 +16,7 @@ import { cloneableGenerator } from 'redux-saga/utils'; import { Wei } from 'libs/units'; describe('shouldEstimateGas*', () => { + const offline = false; const transaction: any = 'transaction'; const tx = { transaction }; const rest: any = { @@ -39,8 +40,12 @@ describe('shouldEstimateGas*', () => { const gen = shouldEstimateGas(); + it('should select getOffline', () => { + expect(gen.next().value).toEqual(select(getOffline)); + }); + it('should take expected types', () => { - expect(gen.next().value).toEqual( + expect(gen.next(offline).value).toEqual( take([ TypeKeys.TO_FIELD_SET, TypeKeys.DATA_FIELD_SET, @@ -65,6 +70,7 @@ describe('shouldEstimateGas*', () => { }); describe('estimateGas*', () => { + const offline = false; const requestChan = 'requestChan'; const payload: any = { mock1: 'mock1', @@ -102,8 +108,12 @@ describe('estimateGas*', () => { expect(expected).toEqual(result); }); + it('should select getOffline', () => { + expect(gens.gen.next(requestChan).value).toEqual(select(getOffline)); + }); + it('should take requestChan', () => { - expect(gens.gen.next(requestChan).value).toEqual(take(requestChan)); + expect(gens.gen.next(offline).value).toEqual(take(requestChan)); }); it('should call delay', () => { diff --git a/spec/sagas/transaction/network/nonce.spec.ts b/spec/sagas/transaction/network/nonce.spec.ts index 1c11287b..868ac7ed 100644 --- a/spec/sagas/transaction/network/nonce.spec.ts +++ b/spec/sagas/transaction/network/nonce.spec.ts @@ -40,18 +40,23 @@ describe('handleNonceRequest*', () => { expect(gens.gen.next(nodeLib).value).toEqual(select(getWalletInst)); }); + it('should handle being called without wallet inst correctly', () => { + gens.noWallet = gens.gen.clone(); + gens.noWallet.next(); + expect(gens.noWallet.next(offline).value).toEqual( + put(showNotification('warning', 'Your addresses nonce could not be fetched')) + ); + expect(gens.noWallet.next().value).toEqual(put(getNonceFailed())); + expect(gens.noWallet.next().done).toEqual(true); + }); + it('should select getOffline', () => { - gens.clone = gens.gen.clone(); expect(gens.gen.next(walletInst).value).toEqual(select(getOffline)); }); - it('should handle errors correctly', () => { - gens.clone.next(); - expect(gens.clone.next().value).toEqual( - put(showNotification('warning', 'Your addresses nonce could not be fetched')) - ); - expect(gens.clone.next().value).toEqual(put(getNonceFailed())); - expect(gens.clone.next().done).toEqual(true); + it('should exit if being called while offline', () => { + gens.offline = gens.gen.clone(); + expect(gens.offline.next(true).done).toEqual(true); }); it('should apply walletInst.getAddressString', () => { diff --git a/spec/sagas/wallet.spec.tsx b/spec/sagas/wallet.spec.tsx index e9e741d1..17fc1d4f 100644 --- a/spec/sagas/wallet.spec.tsx +++ b/spec/sagas/wallet.spec.tsx @@ -15,7 +15,7 @@ import { changeNodeIntent, web3UnsetNode } from 'actions/config'; import { INode } from 'libs/nodes/INode'; import { initWeb3Node, Token, N_FACTOR } from 'config/data'; import { apply, call, fork, put, select, take } from 'redux-saga/effects'; -import { getNodeLib } from 'selectors/config'; +import { getNodeLib, getOffline } from 'selectors/config'; import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet'; import { updateAccountBalance, @@ -39,7 +39,7 @@ import { IFullWallet, fromV3 } from 'ethereumjs-wallet'; // init module configuredStore.getState(); - +const offline = false; const pkey = '31e97f395cabc6faa37d8a9d6bb185187c35704e7b976c7a110e2f0eab37c344'; const wallet = PrivKeyWallet(Buffer.from(pkey, 'hex')); const address = '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854'; @@ -83,90 +83,108 @@ const utcKeystore = { // necessary so we can later inject a mocked web3 to the window describe('updateAccountBalance*', () => { - const gen1 = updateAccountBalance(); - const gen2 = updateAccountBalance(); + const gen = updateAccountBalance(); + + it('should select offline', () => { + expect(gen.next().value).toEqual(select(getOffline)); + }); it('should put setBalancePending', () => { - expect(gen1.next().value).toEqual(put(setBalancePending())); + expect(gen.next(false).value).toEqual(put(setBalancePending())); }); it('should select getWalletInst', () => { - expect(gen1.next().value).toEqual(select(getWalletInst)); - }); - - it('should return if wallet is falsey', () => { - gen2.next(); - gen2.next(); - gen2.next(null); - expect(gen2.next().done).toBe(true); + expect(gen.next(false).value).toEqual(select(getWalletInst)); }); it('should select getNodeLib', () => { - expect(gen1.next(wallet).value).toEqual(select(getNodeLib)); + expect(gen.next(wallet).value).toEqual(select(getNodeLib)); }); it('should apply wallet.getAddressString', () => { - expect(gen1.next(node).value).toEqual(apply(wallet, wallet.getAddressString)); + expect(gen.next(node).value).toEqual(apply(wallet, wallet.getAddressString)); }); it('should apply node.getBalance', () => { - expect(gen1.next(address).value).toEqual(apply(node, node.getBalance, [address])); + expect(gen.next(address).value).toEqual(apply(node, node.getBalance, [address])); }); it('should put setBalanceFulfilled', () => { - expect(gen1.next(balance).value).toEqual(put(setBalanceFullfilled(balance))); + expect(gen.next(balance).value).toEqual(put(setBalanceFullfilled(balance))); }); it('should be done', () => { - expect(gen1.next().done).toEqual(true); + expect(gen.next().done).toEqual(true); + }); + + it('should bail out if offline', () => { + const offlineGen = updateAccountBalance(); + offlineGen.next(); + expect(offlineGen.next(true).done).toBe(true); + }); + + it('should bail out if wallet inst is missing', () => { + const noWalletGen = updateAccountBalance(); + noWalletGen.next(); + noWalletGen.next(false); + noWalletGen.next(false); + expect(noWalletGen.next(null).done).toBe(true); }); }); describe('updateTokenBalances*', () => { - const gen1 = cloneableGenerator(updateTokenBalances)(); - const gen2 = updateTokenBalances(); - const gen3 = updateTokenBalances(); + const gen = cloneableGenerator(updateTokenBalances)(); - it('should select getWalletInst', () => { - expect(gen1.next().value).toEqual(select(getWalletInst)); + it('should bail out if offline', () => { + const offlineGen = gen.clone(); + expect(offlineGen.next()); + expect(offlineGen.next(true).done).toBe(true); }); - it('should select getWalletConfigTokens', () => { - expect(gen1.next(wallet).value).toEqual(select(getWalletConfigTokens)); + it('should select getOffline', () => { + expect(gen.next().value).toEqual(select(getOffline)); + }); + + it('should select getWalletInst', () => { + expect(gen.next(offline).value).toEqual(select(getWalletInst)); }); it('should return if wallet is falsey', () => { - gen2.next(); - gen2.next(null); - expect(gen2.next().done).toEqual(true); + const noWalletGen = gen.clone(); + noWalletGen.next(null); + expect(noWalletGen.next().done).toEqual(true); }); - it('should return if tokens are falsey', () => { - gen3.next(); - gen3.next(wallet); - expect(gen3.next({}).done).toEqual(true); + it('should select getWalletConfigTokens', () => { + expect(gen.next(wallet).value).toEqual(select(getWalletConfigTokens)); + }); + + it('should return if no tokens are requested', () => { + const noTokensGen = gen.clone(); + noTokensGen.next({}); + expect(noTokensGen.next().done).toEqual(true); }); it('should put setTokenBalancesPending', () => { - expect(gen1.next(tokens).value).toEqual(put(setTokenBalancesPending())); + expect(gen.next(tokens).value).toEqual(put(setTokenBalancesPending())); }); - it('should throw and put setTokenBalancesRejected', () => { - const gen4 = gen1.clone(); - if (gen4.throw) { - expect(gen4.throw().value).toEqual(put(setTokenBalancesRejected())); + it('should put setTokenBalancesRejected on throw', () => { + const throwGen = gen.clone(); + if (throwGen.throw) { + expect(throwGen.throw().value).toEqual(put(setTokenBalancesRejected())); } }); it('should call getTokenBalances', () => { - expect(gen1.next().value).toEqual(call(getTokenBalances, wallet, tokens)); + expect(gen.next().value).toEqual(call(getTokenBalances, wallet, tokens)); }); it('should put setTokenBalancesFufilled', () => { - expect(gen1.next({}).value).toEqual(put(setTokenBalancesFulfilled({}))); + expect(gen.next({}).value).toEqual(put(setTokenBalancesFulfilled({}))); }); it('should be done', () => { - expect(gen1.next().done).toEqual(true); + expect(gen.next().done).toEqual(true); }); }); From 303e44abb30074a993c7354b4956a7063b9d2f2c Mon Sep 17 00:00:00 2001 From: Olajide Ogundipe Jr Date: Thu, 11 Jan 2018 13:13:14 -0500 Subject: [PATCH 19/35] Onboarding Modal (#611) * [WIP] Start port of V3 Modal * allow lambda functions in React Components * lint code * add null case for modalRef * fix action test * reduce onboard slide boilerplate * delete images and componentize OnboardSlide * comment out info onboarding message * fix merge conflict * fix prettier error * revert tslint file * fix type in modal * add translations to onboard modal * add in images, fix stlyes --- .../actions/onboardStatus/actionCreators.ts | 31 +++ common/actions/onboardStatus/actionTypes.ts | 24 +++ common/actions/onboardStatus/constants.ts | 6 + common/actions/onboardStatus/index.ts | 2 + common/assets/images/onboarding_icon-01.svg | 1 + common/assets/images/onboarding_icon-02.svg | 1 + common/assets/images/onboarding_icon-03.svg | 1 + common/assets/images/onboarding_icon-04.svg | 1 + common/assets/images/onboarding_icon-05.svg | 1 + common/assets/images/onboarding_icon-06.svg | 1 + common/assets/images/onboarding_icon-07.svg | 1 + common/assets/images/onboarding_icon-08.svg | 1 + common/assets/images/onboarding_icon-09.svg | 1 + common/assets/images/onboarding_icon-10.svg | 1 + common/components/Footer/index.tsx | 2 + common/components/ui/Modal.tsx | 22 +- .../components/BlockchainSlide.tsx | 20 ++ .../OnboardModal/components/FinalSlide.tsx | 97 +++++++++ .../components/InterfaceSlide.tsx | 22 ++ .../OnboardModal/components/NotABankSlide.tsx | 22 ++ .../OnboardModal/components/OnboardSlide.scss | 42 ++++ .../OnboardModal/components/OnboardSlide.tsx | 26 +++ .../components/SecureSlideOne.tsx | 28 +++ .../components/SecureSlideThree.tsx | 32 +++ .../components/SecureSlideTwo.tsx | 25 +++ .../OnboardModal/components/WelcomeSlide.scss | 12 ++ .../OnboardModal/components/WelcomeSlide.tsx | 34 ++++ .../OnboardModal/components/WhyMewSlide.tsx | 21 ++ .../OnboardModal/components/WhySlide.tsx | 30 +++ .../OnboardModal/components/index.ts | 10 + common/containers/OnboardModal/index.scss | 18 ++ common/containers/OnboardModal/index.tsx | 192 ++++++++++++++++++ common/reducers/index.ts | 4 + common/reducers/onboardStatus.ts | 51 +++++ common/translations/lang/ar.json | 97 ++++++++- common/translations/lang/de.json | 97 ++++++++- common/translations/lang/el.json | 97 ++++++++- common/translations/lang/en.json | 95 ++++++++- common/translations/lang/es.json | 97 ++++++++- common/translations/lang/fi.json | 97 ++++++++- common/translations/lang/fr.json | 97 ++++++++- common/translations/lang/ht.json | 97 ++++++++- common/translations/lang/hu.json | 97 ++++++++- common/translations/lang/id.json | 97 ++++++++- common/translations/lang/it.json | 97 ++++++++- common/translations/lang/ja.json | 97 ++++++++- common/translations/lang/ko.json | 97 ++++++++- common/translations/lang/nl.json | 97 ++++++++- common/translations/lang/no.json | 97 ++++++++- common/translations/lang/pl.json | 97 ++++++++- common/translations/lang/pt.json | 97 ++++++++- common/translations/lang/ru.json | 97 ++++++++- common/translations/lang/sk.json | 97 ++++++++- common/translations/lang/sl.json | 97 ++++++++- common/translations/lang/sv.json | 97 ++++++++- common/translations/lang/tr.json | 97 ++++++++- common/translations/lang/vi.json | 97 ++++++++- common/translations/lang/zhcn.json | 97 ++++++++- common/translations/lang/zhtw.json | 97 ++++++++- package.json | 1 + spec/actions/onboardStatus.spec.ts | 32 +++ spec/reducers/onboardStatus.spec.ts | 11 + 62 files changed, 3193 insertions(+), 57 deletions(-) create mode 100644 common/actions/onboardStatus/actionCreators.ts create mode 100644 common/actions/onboardStatus/actionTypes.ts create mode 100644 common/actions/onboardStatus/constants.ts create mode 100644 common/actions/onboardStatus/index.ts create mode 100644 common/assets/images/onboarding_icon-01.svg create mode 100644 common/assets/images/onboarding_icon-02.svg create mode 100644 common/assets/images/onboarding_icon-03.svg create mode 100644 common/assets/images/onboarding_icon-04.svg create mode 100644 common/assets/images/onboarding_icon-05.svg create mode 100644 common/assets/images/onboarding_icon-06.svg create mode 100644 common/assets/images/onboarding_icon-07.svg create mode 100644 common/assets/images/onboarding_icon-08.svg create mode 100644 common/assets/images/onboarding_icon-09.svg create mode 100644 common/assets/images/onboarding_icon-10.svg create mode 100644 common/containers/OnboardModal/components/BlockchainSlide.tsx create mode 100644 common/containers/OnboardModal/components/FinalSlide.tsx create mode 100644 common/containers/OnboardModal/components/InterfaceSlide.tsx create mode 100644 common/containers/OnboardModal/components/NotABankSlide.tsx create mode 100644 common/containers/OnboardModal/components/OnboardSlide.scss create mode 100644 common/containers/OnboardModal/components/OnboardSlide.tsx create mode 100644 common/containers/OnboardModal/components/SecureSlideOne.tsx create mode 100644 common/containers/OnboardModal/components/SecureSlideThree.tsx create mode 100644 common/containers/OnboardModal/components/SecureSlideTwo.tsx create mode 100644 common/containers/OnboardModal/components/WelcomeSlide.scss create mode 100644 common/containers/OnboardModal/components/WelcomeSlide.tsx create mode 100644 common/containers/OnboardModal/components/WhyMewSlide.tsx create mode 100644 common/containers/OnboardModal/components/WhySlide.tsx create mode 100644 common/containers/OnboardModal/components/index.ts create mode 100644 common/containers/OnboardModal/index.scss create mode 100644 common/containers/OnboardModal/index.tsx create mode 100644 common/reducers/onboardStatus.ts create mode 100644 spec/actions/onboardStatus.spec.ts create mode 100644 spec/reducers/onboardStatus.spec.ts diff --git a/common/actions/onboardStatus/actionCreators.ts b/common/actions/onboardStatus/actionCreators.ts new file mode 100644 index 00000000..fc0a40a9 --- /dev/null +++ b/common/actions/onboardStatus/actionCreators.ts @@ -0,0 +1,31 @@ +import * as interfaces from './actionTypes'; +import { TypeKeys } from './constants'; + +export type TStartOnboardSession = typeof startOnboardSession; +export function startOnboardSession(): interfaces.StartOnboardSessionAction { + return { + type: TypeKeys.START_ONBOARD_SESSION + }; +} + +export type TResumeSlide = typeof resumeSlide; +export function resumeSlide(slideNumber: number): interfaces.ResumeSlideAction { + return { + type: TypeKeys.RESUME_SLIDE, + slideNumber + }; +} + +export type TDecrementSlide = typeof decrementSlide; +export function decrementSlide(): interfaces.DecrementSlideAction { + return { + type: TypeKeys.DECREMENT_SLIDE + }; +} + +export type TIncrementSlide = typeof incrementSlide; +export function incrementSlide(): interfaces.IncrementSlideAction { + return { + type: TypeKeys.INCREMENT_SLIDE + }; +} diff --git a/common/actions/onboardStatus/actionTypes.ts b/common/actions/onboardStatus/actionTypes.ts new file mode 100644 index 00000000..a2eae24b --- /dev/null +++ b/common/actions/onboardStatus/actionTypes.ts @@ -0,0 +1,24 @@ +import { TypeKeys } from './constants'; + +export interface StartOnboardSessionAction { + type: TypeKeys.START_ONBOARD_SESSION; +} + +export interface ResumeSlideAction { + type: TypeKeys.RESUME_SLIDE; + slideNumber: number; +} + +export interface DecrementSlideAction { + type: TypeKeys.DECREMENT_SLIDE; +} + +export interface IncrementSlideAction { + type: TypeKeys.INCREMENT_SLIDE; +} + +export type OnboardStatusAction = + | StartOnboardSessionAction + | ResumeSlideAction + | DecrementSlideAction + | IncrementSlideAction; diff --git a/common/actions/onboardStatus/constants.ts b/common/actions/onboardStatus/constants.ts new file mode 100644 index 00000000..859e539a --- /dev/null +++ b/common/actions/onboardStatus/constants.ts @@ -0,0 +1,6 @@ +export enum TypeKeys { + START_ONBOARD_SESSION = 'START_ONBOARD_SESSION', + RESUME_SLIDE = 'RESUME_SLIDE', + DECREMENT_SLIDE = 'DECREMENT_SLIDE', + INCREMENT_SLIDE = 'INCREMENT_SLIDE' +} diff --git a/common/actions/onboardStatus/index.ts b/common/actions/onboardStatus/index.ts new file mode 100644 index 00000000..2608a18e --- /dev/null +++ b/common/actions/onboardStatus/index.ts @@ -0,0 +1,2 @@ +export * from './actionTypes'; +export * from './actionCreators'; diff --git a/common/assets/images/onboarding_icon-01.svg b/common/assets/images/onboarding_icon-01.svg new file mode 100644 index 00000000..3a26533b --- /dev/null +++ b/common/assets/images/onboarding_icon-01.svg @@ -0,0 +1 @@ +onboarding_icon-01 \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-02.svg b/common/assets/images/onboarding_icon-02.svg new file mode 100644 index 00000000..8836f414 --- /dev/null +++ b/common/assets/images/onboarding_icon-02.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-03.svg b/common/assets/images/onboarding_icon-03.svg new file mode 100644 index 00000000..2966e1d3 --- /dev/null +++ b/common/assets/images/onboarding_icon-03.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-04.svg b/common/assets/images/onboarding_icon-04.svg new file mode 100644 index 00000000..6aaad3d8 --- /dev/null +++ b/common/assets/images/onboarding_icon-04.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-05.svg b/common/assets/images/onboarding_icon-05.svg new file mode 100644 index 00000000..2192d668 --- /dev/null +++ b/common/assets/images/onboarding_icon-05.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-06.svg b/common/assets/images/onboarding_icon-06.svg new file mode 100644 index 00000000..781c83d5 --- /dev/null +++ b/common/assets/images/onboarding_icon-06.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-07.svg b/common/assets/images/onboarding_icon-07.svg new file mode 100644 index 00000000..3505afb4 --- /dev/null +++ b/common/assets/images/onboarding_icon-07.svg @@ -0,0 +1 @@ +onboarding_icon-07 \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-08.svg b/common/assets/images/onboarding_icon-08.svg new file mode 100644 index 00000000..3e27f52a --- /dev/null +++ b/common/assets/images/onboarding_icon-08.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-09.svg b/common/assets/images/onboarding_icon-09.svg new file mode 100644 index 00000000..566a9337 --- /dev/null +++ b/common/assets/images/onboarding_icon-09.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/assets/images/onboarding_icon-10.svg b/common/assets/images/onboarding_icon-10.svg new file mode 100644 index 00000000..11a47ca3 --- /dev/null +++ b/common/assets/images/onboarding_icon-10.svg @@ -0,0 +1 @@ +onboarding_icons \ No newline at end of file diff --git a/common/components/Footer/index.tsx b/common/components/Footer/index.tsx index 2bd98259..26c054b3 100644 --- a/common/components/Footer/index.tsx +++ b/common/components/Footer/index.tsx @@ -14,6 +14,7 @@ import './index.scss'; import PreFooter from './PreFooter'; import Modal, { IButton } from 'components/ui/Modal'; import { NewTabLink } from 'components/ui'; +import OnboardModal from 'containers/OnboardModal'; const AffiliateTag = ({ link, text }: Link) => { return ( @@ -125,6 +126,7 @@ export default class Footer extends React.Component { const buttons: IButton[] = [{ text: 'Okay', type: 'default', onClick: this.closeModal }]; return (
+