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 {
-
+ {isOffline ? (
+
+ Equivalent values are unavailable offline
+
+ ) : (
+
+ )}
);
}
@@ -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 (
-
+
{translate('OFFLINE_Step2_Label_3')} (gwei)
-
+
{translate('OFFLINE_Step2_Label_4')}
-
+
+ {translate('OFFLINE_Step2_Label_5')}
+
+
+
+
{translate('OFFLINE_Step2_Label_6')}
(
@@ -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 (
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 (