Fix up components to use selectors, work on fixing sagas

This commit is contained in:
HenryNguyen5 2018-01-29 22:52:19 -05:00
parent 139cf405e7
commit 7fbe1966de
18 changed files with 374 additions and 290 deletions

View File

@ -24,10 +24,12 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
}
export type TChangeNode = typeof changeNode;
export function changeNode(networkName: string, nodeName: string): interfaces.ChangeNodeAction {
export function changeNode(
payload: interfaces.ChangeNodeAction['payload']
): interfaces.ChangeNodeAction {
return {
type: TypeKeys.CONFIG_NODE_CHANGE,
payload: { networkName, nodeName }
payload
};
}

View File

@ -2,11 +2,19 @@ import React from 'react';
import classnames from 'classnames';
import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations';
import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config';
import { makeCustomNodeId } from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';
import { CustomNetworkConfig } from 'types/network';
import { CustomNodeConfig } from 'types/node';
import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config';
import { connect, Omit } from 'react-redux';
import { AppState } from 'reducers';
import {
getCustomNetworkConfigs,
getCustomNodeConfigs,
getStaticNetworkConfigs
} from 'selectors/config';
import { CustomNode } from 'libs/nodes';
const NETWORK_KEYS = Object.keys(NETWORKS);
const CUSTOM = 'custom';
interface Input {
@ -15,14 +23,21 @@ interface Input {
type?: string;
}
interface Props {
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
handleAddCustomNode(node: CustomNodeConfig): void;
handleAddCustomNetwork(node: CustomNetworkConfig): void;
interface OwnProps {
addCustomNode(payload: AddCustomNodeAction['payload']): void;
handleClose(): void;
}
interface DispatchProps {
addCustomNetwork: TAddCustomNetwork;
}
interface StateProps {
customNodes: AppState['config']['nodes']['customNodes'];
customNetworks: AppState['config']['networks']['customNetworks'];
staticNetworks: AppState['config']['networks']['staticNetworks'];
}
interface State {
name: string;
url: string;
@ -36,12 +51,14 @@ interface State {
password: string;
}
export default class CustomNodeModal extends React.Component<Props, State> {
type Props = OwnProps & StateProps & DispatchProps;
class CustomNodeModal extends React.Component<Props, State> {
public state: State = {
name: '',
url: '',
port: '',
network: NETWORK_KEYS[0],
network: Object.keys(this.props.staticNetworks)[0],
customNetworkName: '',
customNetworkUnit: '',
customNetworkChainId: '',
@ -51,7 +68,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
};
public render() {
const { customNetworks, handleClose } = this.props;
const { customNetworks, handleClose, staticNetworks } = this.props;
const { network } = this.state;
const isHttps = window.location.protocol.includes('https');
const invalids = this.getInvalids();
@ -109,12 +126,12 @@ export default class CustomNodeModal extends React.Component<Props, State> {
value={network}
onChange={this.handleChange}
>
{NETWORK_KEYS.map(net => (
{Object.keys(staticNetworks).map(net => (
<option key={net} value={net}>
{net}
</option>
))}
{customNetworks.map(net => {
{Object.values(customNetworks).map(net => {
const id = makeCustomNetworkId(net);
return (
<option key={id} value={id}>
@ -173,7 +190,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
placeholder: 'http://127.0.0.1/'
},
invalids
)}
)}node
</div>
<div className="col-sm-3">
@ -303,12 +320,13 @@ export default class CustomNodeModal extends React.Component<Props, State> {
}
private makeCustomNetworkConfigFromState(): CustomNetworkConfig {
const similarNetworkConfig = Object.values(NETWORKS).find(
const similarNetworkConfig = Object.values(this.props.staticNetworks).find(
n => n.chainId === +this.state.customNetworkChainId
);
const dPathFormats = similarNetworkConfig ? similarNetworkConfig.dPathFormats : null;
return {
isCustom: true,
name: this.state.customNetworkName,
unit: this.state.customNetworkUnit,
chainId: this.state.customNetworkChainId ? parseInt(this.state.customNetworkChainId, 10) : 0,
@ -318,14 +336,22 @@ export default class CustomNodeModal extends React.Component<Props, State> {
private makeCustomNodeConfigFromState(): CustomNodeConfig {
const { network } = this.state;
const node: CustomNodeConfig = {
const networkId =
network === CUSTOM ? makeCustomNetworkId(this.makeCustomNetworkConfigFromState()) : network;
const port = parseInt(this.state.port, 10);
const url = this.state.url.trim();
const node: Omit<CustomNodeConfig, 'lib'> = {
isCustom: true,
service: 'your custom node',
id: `${url}:${port}`,
name: this.state.name.trim(),
url: this.state.url.trim(),
port: parseInt(this.state.port, 10),
network:
network === CUSTOM ? makeCustomNetworkId(this.makeCustomNetworkConfigFromState()) : network
url,
port,
network: networkId
};
const lib = new CustomNode(node);
if (this.state.hasAuth) {
node.auth = {
username: this.state.username,
@ -333,14 +359,14 @@ export default class CustomNodeModal extends React.Component<Props, State> {
};
}
return node;
return { ...node, lib };
}
private getConflictedNode(): CustomNodeConfig | undefined {
const { customNodes } = this.props;
const config = this.makeCustomNodeConfigFromState();
const thisId = makeCustomNodeId(config);
return customNodes.find(conf => makeCustomNodeId(conf) === thisId);
return customNodes[config.id];
}
private handleChange = (ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>) => {
@ -359,9 +385,21 @@ export default class CustomNodeModal extends React.Component<Props, State> {
if (this.state.network === CUSTOM) {
const network = this.makeCustomNetworkConfigFromState();
this.props.handleAddCustomNetwork(network);
this.props.addCustomNetwork({ config: network, id: node.network });
}
this.props.handleAddCustomNode(node);
this.props.addCustomNode({ config: node, id: node.id });
};
}
const mapStateToProps = (state: AppState): StateProps => ({
customNetworks: getCustomNetworkConfigs(state),
customNodes: getCustomNodeConfigs(state),
staticNetworks: getStaticNetworkConfigs(state)
});
const mapDispatchToProps: DispatchProps = {
addCustomNetwork
};
export default connect(mapStateToProps, mapDispatchToProps)(CustomNodeModal);

View File

@ -46,7 +46,7 @@ const tabs: TabLink[] = [
];
interface Props {
color?: string;
color?: string | false;
}
interface State {

View File

@ -3,40 +3,44 @@ import {
TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode,
TAddCustomNetwork
TAddCustomNetwork,
AddCustomNodeAction,
changeLanguage,
changeNodeIntent,
addCustomNode,
removeCustomNode,
addCustomNetwork
} from 'actions/config';
import logo from 'assets/images/logo-myetherwallet.svg';
import { Dropdown, ColorDropdown } from 'components/ui';
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { TSetGasPriceField } from 'actions/transaction';
import {
ANNOUNCEMENT_MESSAGE,
ANNOUNCEMENT_TYPE,
languages,
NODES,
NodeConfig,
CustomNodeConfig,
CustomNetworkConfig
} from 'config';
import { TSetGasPriceField, setGasPriceField } from 'actions/transaction';
import { ANNOUNCEMENT_MESSAGE, ANNOUNCEMENT_TYPE, languages } from 'config';
import Navigation from './components/Navigation';
import CustomNodeModal from './components/CustomNodeModal';
import OnlineStatus from './components/OnlineStatus';
import Version from './components/Version';
import { getKeyByValue } from 'utils/helpers';
import { makeCustomNodeId } from 'utils/node';
import { getNetworkConfigFromId } from 'utils/network';
import { NodeConfig } from 'types/node';
import './index.scss';
import { AppState } from 'reducers';
import {
getOffline,
isNodeChanging,
getLanguageSelection,
getNodeName,
getNodeConfig,
CustomNodeOption,
NodeOption,
getNodeOptions,
getNetworkConfig
} from 'selectors/config';
import { NetworkConfig } from 'types/network';
import { connect } from 'react-redux';
interface Props {
languageSelection: string;
node: NodeConfig;
nodeSelection: string;
isChangingNode: boolean;
isOffline: boolean;
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
interface DispatchProps {
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
setGasPriceField: TSetGasPriceField;
@ -45,11 +49,42 @@ interface Props {
addCustomNetwork: TAddCustomNetwork;
}
interface StateProps {
network: NetworkConfig;
languageSelection: AppState['config']['meta']['languageSelection'];
node: NodeConfig;
nodeSelection: AppState['config']['nodes']['selectedNode']['nodeName'];
isChangingNode: AppState['config']['nodes']['selectedNode']['pending'];
isOffline: AppState['config']['meta']['offline'];
nodeOptions: (CustomNodeOption | NodeOption)[];
}
const mapStateToProps = (state: AppState): StateProps => ({
isOffline: getOffline(state),
isChangingNode: isNodeChanging(state),
languageSelection: getLanguageSelection(state),
nodeSelection: getNodeName(state),
node: getNodeConfig(state),
nodeOptions: getNodeOptions(state),
network: getNetworkConfig(state)
});
const mapDispatchToProps: DispatchProps = {
setGasPriceField,
changeLanguage,
changeNodeIntent,
addCustomNode,
removeCustomNode,
addCustomNetwork
};
interface State {
isAddingCustomNode: boolean;
}
export default class Header extends Component<Props, State> {
type Props = StateProps & DispatchProps;
class Header extends Component<Props, State> {
public state = {
isAddingCustomNode: false
};
@ -57,50 +92,40 @@ export default class Header extends Component<Props, State> {
public render() {
const {
languageSelection,
changeNodeIntent,
node,
nodeSelection,
isChangingNode,
isOffline,
customNodes,
customNetworks
nodeOptions,
network
} = this.props;
const { isAddingCustomNode } = this.state;
const selectedLanguage = languageSelection;
const selectedNetwork = getNetworkConfigFromId(node.network, customNetworks);
const LanguageDropDown = Dropdown as new () => Dropdown<typeof selectedLanguage>;
const nodeOptions = Object.keys(NODES)
.map(key => {
const n = NODES[key];
const network = getNetworkConfigFromId(n.network, customNetworks);
const options = nodeOptions.map(n => {
if (n.isCustom) {
const { name: { networkName, nodeName }, isCustom, id, ...rest } = n;
return {
value: key,
...rest,
name: (
<span>
{network && network.name} <small>({n.service})</small>
{networkName} - {nodeName} <small>(custom)</small>
</span>
),
color: network && network.color,
hidden: n.hidden
onRemove: () => this.props.removeCustomNode({ id })
};
})
.concat(
customNodes.map(cn => {
const network = getNetworkConfigFromId(cn.network, customNetworks);
return {
value: makeCustomNodeId(cn),
name: (
<span>
{network && network.name} - {cn.name} <small>(custom)</small>
</span>
),
color: network && network.color,
hidden: false,
onRemove: () => this.props.removeCustomNode(cn)
};
})
);
} else {
const { name: { networkName, service }, isCustom, ...rest } = n;
return {
...rest,
name: (
<span>
{networkName} <small>({service})</small>
</span>
)
};
}
});
return (
<div className="Header">
@ -162,8 +187,8 @@ export default class Header extends Component<Props, State> {
change node. current node is on the ${node.network} network
provided by ${node.service}
`}
options={nodeOptions}
value={nodeSelection}
options={options}
value={nodeSelection || ''}
extra={
<li>
<a onClick={this.openCustomNodeModal}>Add Custom Node</a>
@ -180,14 +205,11 @@ export default class Header extends Component<Props, State> {
</section>
</section>
<Navigation color={selectedNetwork && selectedNetwork.color} />
<Navigation color={!network.isCustom && network.color} />
{isAddingCustomNode && (
<CustomNodeModal
customNodes={customNodes}
customNetworks={customNetworks}
handleAddCustomNode={this.addCustomNode}
handleAddCustomNetwork={this.props.addCustomNetwork}
addCustomNode={this.addCustomNode}
handleClose={this.closeCustomNodeModal}
/>
)}
@ -210,8 +232,10 @@ export default class Header extends Component<Props, State> {
this.setState({ isAddingCustomNode: false });
};
private addCustomNode = (node: CustomNodeConfig) => {
private addCustomNode = (payload: AddCustomNodeAction['payload']) => {
this.setState({ isAddingCustomNode: false });
this.props.addCustomNode(node);
this.props.addCustomNode(payload);
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);

View File

@ -8,7 +8,7 @@ interface Option<T> {
name: any;
value: T;
color?: string;
hidden: boolean | undefined;
hidden?: boolean | undefined;
onRemove?(): void;
}

View File

@ -1,51 +1,31 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
changeLanguage as dChangeLanguage,
changeNodeIntent as dChangeNodeIntent,
addCustomNode as dAddCustomNode,
removeCustomNode as dRemoveCustomNode,
addCustomNetwork as dAddCustomNetwork,
TChangeLanguage,
TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode,
TAddCustomNetwork
} from 'actions/config';
import { TSetGasPriceField, setGasPriceField as dSetGasPriceField } from 'actions/transaction';
import { AlphaAgreement, Footer, Header } from 'components';
import { AppState } from 'reducers';
import Notifications from './Notifications';
import OfflineTab from './OfflineTab';
import {
getOffline,
getLanguageSelection,
getCustomNodeConfigs,
getCustomNetworkConfigs,
getLatestBlock,
isNodeChanging
} from 'selectors/config';
import { NodeConfig, CustomNodeConfig } from 'types/node';
import { CustomNetworkConfig } from 'types/network';
import { getOffline, getLatestBlock } from 'selectors/config';
interface Props {
interface StateProps {
isOffline: AppState['config']['meta']['offline'];
latestBlock: AppState['config']['meta']['latestBlock'];
}
interface OwnProps {
isUnavailableOffline?: boolean;
children: string | React.ReactElement<string> | React.ReactElement<string>[];
}
type Props = OwnProps & StateProps;
class TabSection extends Component<Props, {}> {
public render() {
const {
isUnavailableOffline,
children,
// APP
isOffline
} = this.props;
const { isUnavailableOffline, children, isOffline, latestBlock } = this.props;
return (
<div className="page-layout">
<main>
<Header {...headerProps} />
<Header />
<div className="Tab container">
{isUnavailableOffline && isOffline ? <OfflineTab /> : children}
</div>
@ -58,15 +38,9 @@ class TabSection extends Component<Props, {}> {
}
}
function mapStateToProps(state: AppState): ReduxProps {
function mapStateToProps(state: AppState): StateProps {
return {
node: state.config.node,
nodeSelection: state.config.nodeSelection,
isChangingNode: isNodeChanging(state),
isOffline: getOffline(state),
languageSelection: getLanguageSelection(state),
customNodes: getCustomNodeConfigs(state),
customNetworks: getCustomNetworkConfigs(state),
latestBlock: getLatestBlock(state)
};
}

View File

@ -1,11 +1,11 @@
import RPCNode from '../rpc';
import RPCClient from '../rpc/client';
import { CustomNodeConfig } from 'types/node';
import { Omit } from 'react-router';
export default class CustomNode extends RPCNode {
constructor(config: CustomNodeConfig) {
const endpoint = `${config.url}:${config.port}`;
super(endpoint);
constructor(config: Omit<CustomNodeConfig, 'lib'>) {
super(config.id);
const headers: { [key: string]: string } = {};
if (config.auth) {
@ -13,6 +13,6 @@ export default class CustomNode extends RPCNode {
headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;
}
this.client = new RPCClient(endpoint, headers);
this.client = new RPCClient(config.id, headers);
}
}

View File

@ -7,7 +7,7 @@ interface NodeLoaded {
interface NodeChangePending {
pending: true;
nodeName: undefined;
nodeName: string;
}
export type State = NodeLoaded | NodeChangePending;
@ -22,8 +22,8 @@ const changeNode = (_: State, { payload }: ChangeNodeAction): State => ({
pending: false
});
const changeNodeIntent = (_: State, _2: ChangeNodeIntentAction): State => ({
nodeName: undefined,
const changeNodeIntent = (state: State, _: ChangeNodeIntentAction): State => ({
...state,
pending: true
});

View File

@ -7,72 +7,84 @@ export type State = NonWeb3NodeConfigs & Web3NodeConfig;
export const INITIAL_STATE: State = {
eth_mew: {
network: 'ETH',
isCustom: false,
lib: new RPCNode('https://api.myetherapi.com/eth'),
service: 'MyEtherWallet',
estimateGas: true
},
eth_mycrypto: {
network: 'ETH',
isCustom: false,
lib: new RPCNode('https://api.mycryptoapi.com/eth'),
service: 'MyCrypto',
estimateGas: true
},
eth_ethscan: {
network: 'ETH',
isCustom: false,
service: 'Etherscan.io',
lib: new EtherscanNode('https://api.etherscan.io/api'),
estimateGas: false
},
eth_infura: {
network: 'ETH',
isCustom: false,
service: 'infura.io',
lib: new InfuraNode('https://mainnet.infura.io/mew'),
estimateGas: false
},
rop_mew: {
network: 'Ropsten',
isCustom: false,
service: 'MyEtherWallet',
lib: new RPCNode('https://api.myetherapi.com/rop'),
estimateGas: false
},
rop_infura: {
network: 'Ropsten',
isCustom: false,
service: 'infura.io',
lib: new InfuraNode('https://ropsten.infura.io/mew'),
estimateGas: false
},
kov_ethscan: {
network: 'Kovan',
isCustom: false,
service: 'Etherscan.io',
lib: new EtherscanNode('https://kovan.etherscan.io/api'),
estimateGas: false
},
rin_ethscan: {
network: 'Rinkeby',
isCustom: false,
service: 'Etherscan.io',
lib: new EtherscanNode('https://rinkeby.etherscan.io/api'),
estimateGas: false
},
rin_infura: {
network: 'Rinkeby',
isCustom: false,
service: 'infura.io',
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
estimateGas: false
},
etc_epool: {
network: 'ETC',
isCustom: false,
service: 'Epool.io',
lib: new RPCNode('https://mewapi.epool.io'),
estimateGas: false
},
ubq: {
network: 'UBQ',
isCustom: false,
service: 'ubiqscan.io',
lib: new RPCNode('https://pyrus2.ubiqscan.io'),
estimateGas: true
},
exp_tech: {
network: 'EXP',
isCustom: false,
service: 'Expanse.tech',
lib: new RPCNode('https://node.expanse.tech/'),
estimateGas: true

View File

@ -8,20 +8,22 @@ import {
takeLatest,
takeEvery,
select,
race
race,
apply
} from 'redux-saga/effects';
import {
makeCustomNodeId,
getCustomNodeConfigFromId,
makeNodeConfigFromCustomConfig
} from 'utils/node';
import { makeCustomNetworkId, getNetworkConfigFromId } from 'utils/network';
import { makeCustomNetworkId } from 'utils/network';
import {
getNodeName,
getNodeConfig,
getCustomNodeConfigs,
getCustomNetworkConfigs,
getOffline
getOffline,
getNetworkConfig,
isStaticNodeName,
getCustomNodeFromId,
getStaticNodeFromId,
getNetworkConfigById,
getStaticAltNodeToWeb3
} from 'selectors/config';
import { AppState } from 'reducers';
import { TypeKeys } from 'actions/config/constants';
@ -38,9 +40,10 @@ import { showNotification } from 'actions/notifications';
import { translateRaw } from 'translations';
import { Web3Wallet } from 'libs/wallet';
import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants';
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
import { StaticNodeConfig, CustomNodeConfig } from 'types/node';
import { CustomNetworkConfig } from 'types/network';
import { State as ConfigState } from 'reducers/config';
import { StaticNodeConfig, CustomNodeConfig, NodeConfig } from 'types/node';
import { CustomNetworkConfig, StaticNetworkConfig } from 'types/network';
import { Web3Service } from 'reducers/config/nodes/typings';
export const getConfig = (state: AppState): ConfigState => state.config;
@ -113,41 +116,45 @@ export function* reload(): SagaIterator {
setTimeout(() => location.reload(), 1150);
}
export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIterator {
const currentNode: string = yield select(getNodeName);
const currentConfig: StaticNodeConfig = yield select(getNodeConfig);
const customNets: CustomNetworkConfig[] = yield select(getCustomNetworkConfigs);
const currentNetwork =
getNetworkConfigFromId(currentConfig.network, customNets) || NETWORKS[currentConfig.network];
export function* handleNodeChangeIntent({
payload: nodeIdToSwitchTo
}: ChangeNodeIntentAction): SagaIterator {
const isStaticNode: boolean = yield select(isStaticNodeName, nodeIdToSwitchTo);
const currentConfig: NodeConfig = yield select(getNodeConfig);
function* bailOut(message: string) {
const currentNodeName: string = yield select(getNodeName);
yield put(showNotification('danger', message, 5000));
yield put(changeNode(currentNode, currentConfig, currentNetwork));
yield put(changeNode({ networkName: currentConfig.network, nodeName: currentNodeName }));
}
let actionConfig = NODES[action.payload];
if (!actionConfig) {
const customConfigs: CustomNodeConfig[] = yield select(getCustomNodeConfigs);
const config = getCustomNodeConfigFromId(action.payload, customConfigs);
let nextNodeConfig: CustomNodeConfig | StaticNodeConfig;
if (!isStaticNode) {
const config: CustomNodeConfig | undefined = yield select(
getCustomNodeFromId,
nodeIdToSwitchTo
);
if (config) {
actionConfig = makeNodeConfigFromCustomConfig(config);
nextNodeConfig = config;
} else {
return yield* bailOut(`Attempted to switch to unknown node '${nodeIdToSwitchTo}'`);
}
} else {
nextNodeConfig = yield select(getStaticNodeFromId, nodeIdToSwitchTo);
}
if (!actionConfig) {
return yield* bailOut(`Attempted to switch to unknown node '${action.payload}'`);
}
// Grab latest block from the node, before switching, to confirm it's online
// Grab current block from the node, before switching, to confirm it's online
// Give it 5 seconds before we call it offline
let latestBlock;
let currentBlock;
let timeout;
try {
const { lb, to } = yield race({
lb: call(actionConfig.lib.getCurrentBlock.bind(actionConfig.lib)),
lb: apply(nextNodeConfig, nextNodeConfig.lib.getCurrentBlock),
to: call(delay, 5000)
});
latestBlock = lb;
currentBlock = lb;
timeout = to;
} catch (err) {
// Whether it times out or errors, same message
@ -158,16 +165,19 @@ export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIte
return yield* bailOut(translateRaw('ERROR_32'));
}
const actionNetwork = getNetworkConfigFromId(actionConfig.network, customNets);
const nextNetwork: StaticNetworkConfig | CustomNetworkConfig = yield select(
getNetworkConfigById,
nextNodeConfig.network
);
if (!actionNetwork) {
if (!nextNetwork) {
return yield* bailOut(
`Unknown custom network for your node '${action.payload}', try re-adding it`
`Unknown custom network for your node '${nodeIdToSwitchTo}', try re-adding it`
);
}
yield put(setLatestBlock(latestBlock));
yield put(changeNode(action.payload, actionConfig, actionNetwork));
yield put(setLatestBlock(currentBlock));
yield put(changeNode({ networkName: nextNodeConfig.network, nodeName: nodeIdToSwitchTo }));
// TODO - re-enable once DeterministicWallet state is fixed to flush properly.
// DeterministicWallet keeps path related state we need to flush before we can stop reloading
@ -176,8 +186,8 @@ export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIte
// if there's no wallet, do not reload as there's no component state to resync
// if (currentWallet && currentConfig.network !== actionConfig.network) {
const isNewNetwork = currentConfig.network !== actionConfig.network;
const newIsWeb3 = actionConfig.service === Web3Service;
const isNewNetwork = currentConfig.network !== nextNodeConfig.network;
const newIsWeb3 = nextNodeConfig.service === Web3Service;
// don't reload when web3 is selected; node will automatically re-set and state is not an issue here
if (isNewNetwork && !newIsWeb3) {
yield call(reload);
@ -185,8 +195,7 @@ export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIte
}
export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator {
const nodeId = makeCustomNodeId(action.payload);
yield put(changeNodeIntent(nodeId));
yield put(changeNodeIntent(action.payload.id));
}
// If there are any orphaned custom networks, purge them
@ -208,7 +217,6 @@ export function* cleanCustomNetworks(): SagaIterator {
// unset web3 as the selected node if a non-web3 wallet has been selected
export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator {
const node = yield select(getNodeName);
const nodeConfig = yield select(getNodeConfig);
const newWallet = action.payload;
const isWeb3Wallet = newWallet instanceof Web3Wallet;
@ -216,8 +224,9 @@ export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator {
return;
}
const altNode = yield select(getStaticAltNodeToWeb3);
// switch back to a node with the same network as MetaMask/Mist
yield put(changeNodeIntent(equivalentNodeOrDefault(nodeConfig)));
yield put(changeNodeIntent(altNode));
}
export function* unsetWeb3Node(): SagaIterator {
@ -227,30 +236,11 @@ export function* unsetWeb3Node(): SagaIterator {
return;
}
const nodeConfig: StaticNodeConfig = yield select(getNodeConfig);
const newNode = equivalentNodeOrDefault(nodeConfig);
yield put(changeNodeIntent(newNode));
const altNode = yield select(getStaticAltNodeToWeb3);
// switch back to a node with the same network as MetaMask/Mist
yield put(changeNodeIntent(altNode));
}
export const equivalentNodeOrDefault = (nodeConfig: StaticNodeConfig) => {
const node = Object.keys(NODES)
.filter(key => key !== 'web3')
.reduce((found, key) => {
const config = NODES[key];
if (found.length) {
return found;
}
if (nodeConfig.network === config.network) {
return (found = key);
}
return found;
}, '');
// if no equivalent node was found, use the app default
return node.length ? node : configInitialState.nodeSelection;
};
export default function* configSaga(): SagaIterator {
yield takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus);
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);

View File

@ -38,7 +38,7 @@ import {
} from 'libs/wallet';
import { SagaIterator, delay, Task } from 'redux-saga';
import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects';
import { getNodeLib, getAllTokens, getOffline } from 'selectors/config';
import { getNodeLib, getAllTokens, getOffline, getWeb3Node } from 'selectors/config';
import {
getTokens,
getWalletInst,
@ -51,6 +51,7 @@ import Web3Node, { isWeb3Node } from 'libs/nodes/web3';
import { loadWalletConfig, saveWalletConfig } from 'utils/localStorage';
import { getTokenBalances, filterScannedTokenBalances } from './helpers';
import { Token } from 'types/network';
import { Web3NodeConfig } from '../../../shared/types/node';
export interface TokenBalanceLookup {
[symbol: string]: TokenBalance;
@ -265,11 +266,12 @@ export function* unlockWeb3(): SagaIterator {
action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeSelection === 'web3'
);
if (!NODES.web3) {
const web3Node: Web3NodeConfig | null = yield select(getWeb3Node);
if (!web3Node) {
throw Error('Web3 node config not found!');
}
const network = NODES.web3.network;
const nodeLib: INode | Web3Node = yield select(getNodeLib);
const network = web3Node.network;
const nodeLib: Web3Node = web3Node.lib;
if (!isWeb3Node(nodeLib)) {
throw new Error('Cannot use Web3 wallet without a Web3 node.');

View File

@ -9,14 +9,24 @@ import {
export const getNetworks = (state: AppState) => getConfig(state).networks;
export const getNetworkConfigById = (state: AppState, networkId: string) =>
isStaticNetworkName(state, networkId)
? getStaticNetworkConfigs(state)[networkId]
: getCustomNetworkConfigs(state)[networkId];
export const getStaticNetworkNames = (state: AppState): StaticNetworkNames[] =>
Object.keys(getNetworks(state).staticNetworks) as StaticNetworkNames[];
export const isStaticNetworkName = (
state: AppState,
networkName: string
): networkName is StaticNetworkNames =>
Object.keys(getStaticNetworkConfigs(state)).includes(networkName);
export const getStaticNetworkConfig = (state: AppState): StaticNetworkConfig | undefined => {
const { staticNetworks, selectedNetwork } = getNetworks(state);
const isDefaultNetworkName = (networkName: string): networkName is StaticNetworkNames =>
Object.keys(staticNetworks).includes(networkName);
const defaultNetwork = isDefaultNetworkName(selectedNetwork)
const defaultNetwork = isStaticNetworkName(state, selectedNetwork)
? staticNetworks[selectedNetwork]
: undefined;
return defaultNetwork;

View File

@ -1,29 +1,70 @@
import { AppState } from 'reducers';
import { getConfig, getStaticNetworkConfigs } from 'selectors/config';
import { CustomNodeConfig, StaticNodeConfig, StaticNodeName } from 'types/node';
import {
getConfig,
getStaticNetworkConfigs,
getCustomNetworkConfigs,
isStaticNetworkName
} from 'selectors/config';
import { CustomNodeConfig, StaticNodeConfig, StaticNodeName, Web3NodeConfig } from 'types/node';
import { INITIAL_STATE as SELECTED_NODE_INITIAL_STATE } from 'reducers/config/nodes/selectedNode';
export const getNodes = (state: AppState) => getConfig(state).nodes;
export function isNodeCustom(state: AppState, nodeName: string): CustomNodeConfig | undefined {
return getCustomNodeConfigs(state)[nodeName];
}
export const getCustomNodeFromId = (
state: AppState,
nodeName: string
): CustomNodeConfig | undefined => getCustomNodeConfigs(state)[nodeName];
export const getStaticAltNodeToWeb3 = (state: AppState) => {
const { web3, ...configs } = getStaticNodeConfigs(state);
if (!web3) {
return SELECTED_NODE_INITIAL_STATE.nodeName;
}
const res = Object.entries(configs).find(
([_, config]: [StaticNodeName, StaticNodeConfig]) => web3.network === config.network
);
if (res) {
return res[0];
}
return SELECTED_NODE_INITIAL_STATE.nodeName;
};
export const getStaticNodeFromId = (state: AppState, nodeName: StaticNodeName) =>
getStaticNodeConfigs(state)[nodeName];
export const isStaticNodeName = (state: AppState, nodeName: string): nodeName is StaticNodeName =>
Object.keys(getStaticNodeConfigs(state)).includes(nodeName);
const getStaticNodeConfigs = (state: AppState) => getNodes(state).staticNodes;
export const getStaticNodeConfig = (state: AppState): StaticNodeConfig | undefined => {
const { staticNodes, selectedNode: { nodeName } } = getNodes(state);
if (nodeName === undefined) {
return nodeName;
}
const isStaticNodeName = (networkName: string): networkName is StaticNodeName =>
Object.keys(staticNodes).includes(networkName);
const defaultNetwork = isStaticNodeName(nodeName) ? staticNodes[nodeName] : undefined;
const defaultNetwork = isStaticNodeName(state, nodeName) ? staticNodes[nodeName] : undefined;
return defaultNetwork;
};
export const getWeb3Node = (state: AppState): Web3NodeConfig | null => {
const currNode = getStaticNodeConfig(state);
const currNodeName = getNodeName(state);
if (
currNode &&
currNodeName &&
isStaticNodeName(state, currNodeName) &&
currNodeName === 'web3'
) {
return currNode;
}
return null;
};
export const getCustomNodeConfig = (state: AppState): CustomNodeConfig | undefined => {
const { customNodes, selectedNode: { nodeName } } = getNodes(state);
if (nodeName === undefined) {
return nodeName;
}
const customNode = customNodes[nodeName];
return customNode;
};
@ -40,7 +81,7 @@ export function isNodeChanging(state): boolean {
return getNodes(state).selectedNode.pending;
}
export function getNodeName(state: AppState): string | undefined {
export function getNodeName(state: AppState): string {
return getNodes(state).selectedNode.nodeName;
}
@ -48,15 +89,15 @@ export function getIsWeb3Node(state: AppState): boolean {
return getNodeName(state) === 'web3';
}
export function getNodeConfig(state: AppState): StaticNodeConfig | CustomNodeConfig | undefined {
export function getNodeConfig(state: AppState): StaticNodeConfig | CustomNodeConfig {
const config = getStaticNodeConfig(state) || getCustomNodeConfig(state);
/*
if (!config) {
const { selectedNode } = getNodes(state);
throw Error(
`No node config found for ${selectedNode.nodeName} in either static or custom nodes`
);
}*/
}
return config;
}
@ -68,25 +109,63 @@ export function getNodeLib(state: AppState) {
return config.lib;
}
interface NodeOption {
export interface NodeOption {
isCustom: false;
value: string;
name: { networkName?: string; service: string };
color?: string;
hidden?: boolean;
}
export function getStaticNodeOptions(state: AppState) {
export function getStaticNodeOptions(state: AppState): NodeOption[] {
const staticNetworkConfigs = getStaticNetworkConfigs(state);
Object.entries(getStaticNodes(state)).map(
([nodeName, nodeConfig]: [string, StaticNodeConfig]) => {
const networkName = nodeConfig.network;
return Object.entries(getStaticNodes(state)).map(
([nodeName, node]: [string, StaticNodeConfig]) => {
const networkName = node.network;
const associatedNetwork = staticNetworkConfigs[networkName];
return {
const opt: NodeOption = {
isCustom: node.isCustom,
value: nodeName,
name: { networkName, service: nodeConfig.service },
name: { networkName, service: node.service },
color: associatedNetwork.color,
hidden: nodeConfig.hidden
hidden: node.hidden
};
return opt;
}
);
}
export interface CustomNodeOption {
isCustom: true;
id: string;
value: string;
name: { networkName?: string; nodeName: string };
color?: string;
hidden?: boolean;
}
export function getCustomNodeOptions(state: AppState): CustomNodeOption[] {
const staticNetworkConfigs = getStaticNetworkConfigs(state);
const customNetworkConfigs = getCustomNetworkConfigs(state);
return Object.entries(getCustomNodeConfigs(state)).map(
([nodeName, node]: [string, CustomNodeConfig]) => {
const networkName = node.network;
const associatedNetwork = isStaticNetworkName(state, networkName)
? staticNetworkConfigs[networkName]
: customNetworkConfigs[networkName];
const opt: CustomNodeOption = {
isCustom: node.isCustom,
value: node.id,
name: { networkName, nodeName },
color: associatedNetwork.isCustom ? undefined : associatedNetwork.color,
hidden: false,
id: node.id
};
return opt;
}
);
}
export function getNodeOptions(state: AppState) {
return [...getStaticNodeOptions(state), ...getCustomNodeOptions(state)];
}

View File

@ -65,7 +65,7 @@ const configureStore = () => {
// If they have a saved node, make sure we assign that too. The node selected
// isn't serializable, so we have to assign it here.
if (savedConfigState && savedConfigState.nodeSelection) {
if (savedConfigState && savedConfigState.nodes.selectedNode.nodeName) {
const savedNode = getNodeConfigFromId(
savedConfigState.nodeSelection,
savedConfigState.customNodes

View File

@ -39,20 +39,6 @@ export function makeNetworkConfigFromCustomConfig(
return customConfig;
}
export function getNetworkConfigFromId(
id: string,
configs: CustomNetworkConfig[]
): StaticNetworkConfig | undefined {
if (NETWORKS[id]) {
return NETWORKS[id];
}
const customConfig = configs.find(conf => makeCustomNetworkId(conf) === id);
if (customConfig) {
return makeNetworkConfigFromCustomConfig(customConfig);
}
}
type PathType = keyof DPathFormats;
type DPathFormat =

View File

@ -1,42 +0,0 @@
import { CustomNode } from 'libs/nodes';
import { CustomNodeConfig, StaticNodeConfig } from 'types/node';
export function makeCustomNodeId(config: CustomNodeConfig): string {
return `${config.url}:${config.port}`;
}
export function getCustomNodeConfigFromId(
id: string,
configs: CustomNodeConfig[]
): CustomNodeConfig | undefined {
return configs.find(node => makeCustomNodeId(node) === id);
}
export function getNodeConfigFromId(
id: string,
configs: CustomNodeConfig[]
): StaticNodeConfig | undefined {
if (NODES[id]) {
return NODES[id];
}
const config = getCustomNodeConfigFromId(id, configs);
if (config) {
return makeNodeConfigFromCustomConfig(config);
}
}
export function makeNodeConfigFromCustomConfig(config: CustomNodeConfig): StaticNodeConfig {
interface Override extends StaticNodeConfig {
network: any;
}
const customConfig: Override = {
network: config.network,
lib: new CustomNode(config),
service: 'your custom node',
estimateGas: true
};
return customConfig;
}

View File

@ -28,7 +28,6 @@ interface DPathFormats {
}
interface StaticNetworkConfig {
// TODO really try not to allow strings due to custom networks
isCustom: false; // used for type guards
name: StaticNetworkNames;
unit: string;

View File

@ -1,9 +1,14 @@
import { RPCNode, Web3Node } from 'libs/nodes';
import { StaticNetworkNames } from './network';
import { StaticNodesState, CustomNodesState } from 'reducers/config/nodes';
import CustomNode from 'libs/nodes/custom';
interface CustomNodeConfig {
id: string;
isCustom: true;
name: string;
lib: CustomNode;
service: 'your custom node';
url: string;
port: number;
network: string;
@ -14,6 +19,7 @@ interface CustomNodeConfig {
}
interface StaticNodeConfig {
isCustom: false;
network: StaticNetworkNames;
lib: RPCNode | Web3Node;
service: string;
@ -21,6 +27,10 @@ interface StaticNodeConfig {
hidden?: boolean;
}
interface Web3NodeConfig extends StaticNodeConfig {
lib: Web3Node;
}
declare enum StaticNodeName {
ETH_MEW = 'eth_mew',
ETH_MYCRYPTO = 'eth_mycrypto',
@ -39,7 +49,7 @@ declare enum StaticNodeName {
type NonWeb3NodeConfigs = { [key in StaticNodeName]: StaticNodeConfig };
interface Web3NodeConfig {
web3?: StaticNodeConfig;
web3?: Web3NodeConfig;
}
type NodeConfig = StaticNodesState[StaticNodeName] | CustomNodesState[string];