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.
This commit is contained in:
William O'Beirne 2018-01-09 15:13:14 -05:00 committed by Daniel Ternyak
parent c54ba441fa
commit 6e2b74c79a
10 changed files with 109 additions and 70 deletions

View File

@ -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 }
};
}

View File

@ -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;
};
}

View File

@ -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<StateProps> {
@ -20,14 +22,16 @@ class AmountClass extends Component<StateProps> {
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 (
<UnitDisplay
decimal={decimal}
value={
unit === 'ether' ? Wei(value) : TokenValue(ERC20.transfer.decodeInput(data)._value)
}
symbol={unit}
value={handledValue}
symbol={isToken ? unit : network.unit}
checkOffline={false}
/>
);
@ -39,5 +43,6 @@ class AmountClass extends Component<StateProps> {
export const Amount = connect((state: AppState) => ({
decimal: getDecimal(state),
unit: getUnit(state)
unit: getUnit(state),
network: getNetworkConfig(state)
}))(AmountClass);

View File

@ -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<string>;
@ -24,7 +26,7 @@ const ConditionalStringDropDown = withConditional(StringDropdown);
class UnitDropdownClass extends Component<DispatchProps & StateProps> {
public render() {
const { tokens, allTokens, showAllTokens, unit } = this.props;
const { tokens, allTokens, showAllTokens, unit, network } = this.props;
const focusedTokens = showAllTokens ? allTokens : tokens;
return (
<div className="input-group-btn">
@ -32,8 +34,8 @@ class UnitDropdownClass extends Component<DispatchProps & StateProps> {
params={['readOnly']}
withQuery={({ readOnly }) => (
<ConditionalStringDropDown
options={['ether', ...getTokenSymbols(focusedTokens)]}
value={unit}
options={[network.unit, ...getTokenSymbols(focusedTokens)]}
value={unit === 'ether' ? network.unit : unit}
condition={!readOnly}
conditionalProps={{
onChange: this.handleOnChange
@ -55,7 +57,8 @@ function mapStateToProps(state: AppState) {
return {
tokens: getShownTokenBalances(state, true),
allTokens: getTokens(state),
unit: getUnit(state)
unit: getUnit(state),
network: getNetworkConfig(state)
};
}

View File

@ -60,6 +60,7 @@ function changeNode(state: State, action: ChangeNodeAction): State {
...state,
nodeSelection: action.payload.nodeSelection,
node: action.payload.node,
network: action.payload.network,
isChangingNode: false
};
}

View File

@ -10,13 +10,13 @@ import {
select,
race
} from 'redux-saga/effects';
import { NODES, NodeConfig } from 'config/data';
import { NODES, NETWORKS, NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
import {
makeCustomNodeId,
getCustomNodeConfigFromId,
makeNodeConfigFromCustomConfig
} from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';
import { makeCustomNetworkId, getNetworkConfigFromId } from 'utils/network';
import {
getNode,
getNodeConfig,
@ -37,8 +37,8 @@ import {
ChangeNodeIntentAction
} from 'actions/config';
import { showNotification } from 'actions/notifications';
import translate from 'translations';
import { Web3Wallet } from 'libs/wallet';
import { translateRaw } from 'translations';
import { IWallet, Web3Wallet } from 'libs/wallet';
import { getWalletInst } from 'selectors/wallet';
import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants';
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
@ -48,9 +48,9 @@ export const getConfig = (state: AppState): ConfigState => 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);
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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));

View File

@ -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);
});
});