diff --git a/common/actions/config/actionCreators.ts b/common/actions/config/actionCreators.ts index 1fd60c9b..0c298680 100644 --- a/common/actions/config/actionCreators.ts +++ b/common/actions/config/actionCreators.ts @@ -1,9 +1,8 @@ import * as interfaces from './actionTypes'; import { TypeKeys } from './constants'; -import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config'; -export type TToggleOfflineConfig = typeof toggleOfflineConfig; -export function toggleOfflineConfig(): interfaces.ToggleOfflineAction { +export type TToggleOffline = typeof toggleOffline; +export function toggleOffline(): interfaces.ToggleOfflineAction { return { type: TypeKeys.CONFIG_TOGGLE_OFFLINE }; @@ -26,13 +25,11 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction { export type TChangeNode = typeof changeNode; export function changeNode( - nodeSelection: string, - node: NodeConfig, - network: NetworkConfig + payload: interfaces.ChangeNodeAction['payload'] ): interfaces.ChangeNodeAction { return { type: TypeKeys.CONFIG_NODE_CHANGE, - payload: { nodeSelection, node, network } + payload }; } @@ -52,7 +49,9 @@ export function changeNodeIntent(payload: string): interfaces.ChangeNodeIntentAc } export type TAddCustomNode = typeof addCustomNode; -export function addCustomNode(payload: CustomNodeConfig): interfaces.AddCustomNodeAction { +export function addCustomNode( + payload: interfaces.AddCustomNodeAction['payload'] +): interfaces.AddCustomNodeAction { return { type: TypeKeys.CONFIG_ADD_CUSTOM_NODE, payload @@ -60,7 +59,9 @@ export function addCustomNode(payload: CustomNodeConfig): interfaces.AddCustomNo } export type TRemoveCustomNode = typeof removeCustomNode; -export function removeCustomNode(payload: CustomNodeConfig): interfaces.RemoveCustomNodeAction { +export function removeCustomNode( + payload: interfaces.RemoveCustomNodeAction['payload'] +): interfaces.RemoveCustomNodeAction { return { type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, payload @@ -68,7 +69,9 @@ export function removeCustomNode(payload: CustomNodeConfig): interfaces.RemoveCu } export type TAddCustomNetwork = typeof addCustomNetwork; -export function addCustomNetwork(payload: CustomNetworkConfig): interfaces.AddCustomNetworkAction { +export function addCustomNetwork( + payload: interfaces.AddCustomNetworkAction['payload'] +): interfaces.AddCustomNetworkAction { return { type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK, payload @@ -77,7 +80,7 @@ export function addCustomNetwork(payload: CustomNetworkConfig): interfaces.AddCu export type TRemoveCustomNetwork = typeof removeCustomNetwork; export function removeCustomNetwork( - payload: CustomNetworkConfig + payload: interfaces.RemoveCustomNetworkAction['payload'] ): interfaces.RemoveCustomNetworkAction { return { type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK, @@ -93,6 +96,15 @@ export function setLatestBlock(payload: string): interfaces.SetLatestBlockAction }; } +export function web3SetNode( + payload: interfaces.Web3setNodeAction['payload'] +): interfaces.Web3setNodeAction { + return { + type: TypeKeys.CONFIG_NODE_WEB3_SET, + payload + }; +} + export type TWeb3UnsetNode = typeof web3UnsetNode; export function web3UnsetNode(): interfaces.Web3UnsetNodeAction { return { diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index 5552661e..8758af67 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -1,5 +1,6 @@ import { TypeKeys } from './constants'; -import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config'; +import { CustomNodeConfig, Web3NodeConfig } from 'types/node'; +import { CustomNetworkConfig } from 'types/network'; /*** Toggle Offline ***/ export interface ToggleOfflineAction { @@ -19,11 +20,9 @@ export interface ChangeLanguageAction { /*** Change Node ***/ export interface ChangeNodeAction { type: TypeKeys.CONFIG_NODE_CHANGE; - // FIXME $keyof? payload: { - nodeSelection: string; - node: NodeConfig; - network: NetworkConfig; + nodeId: string; + networkId: string; }; } @@ -41,25 +40,25 @@ export interface ChangeNodeIntentAction { /*** Add Custom Node ***/ export interface AddCustomNodeAction { type: TypeKeys.CONFIG_ADD_CUSTOM_NODE; - payload: CustomNodeConfig; + payload: { id: string; config: CustomNodeConfig }; } /*** Remove Custom Node ***/ export interface RemoveCustomNodeAction { type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE; - payload: CustomNodeConfig; + payload: { id: string }; } /*** Add Custom Network ***/ export interface AddCustomNetworkAction { type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK; - payload: CustomNetworkConfig; + payload: { id: string; config: CustomNetworkConfig }; } /*** Remove Custom Network ***/ export interface RemoveCustomNetworkAction { type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK; - payload: CustomNetworkConfig; + payload: { id: string }; } /*** Set Latest Block ***/ @@ -73,17 +72,28 @@ export interface Web3UnsetNodeAction { type: TypeKeys.CONFIG_NODE_WEB3_UNSET; } -/*** Union Type ***/ -export type ConfigAction = +/*** Set Web3 as a Node ***/ +export interface Web3setNodeAction { + type: TypeKeys.CONFIG_NODE_WEB3_SET; + payload: { id: 'web3'; config: Web3NodeConfig }; +} + +export type CustomNetworkAction = AddCustomNetworkAction | RemoveCustomNetworkAction; + +export type CustomNodeAction = AddCustomNodeAction | RemoveCustomNodeAction; + +export type NodeAction = | ChangeNodeAction + | ChangeNodeIntentAction + | Web3UnsetNodeAction + | Web3setNodeAction; + +export type MetaAction = | ChangeLanguageAction | ToggleOfflineAction | ToggleAutoGasLimitAction | PollOfflineStatus - | ChangeNodeIntentAction - | AddCustomNodeAction - | RemoveCustomNodeAction - | AddCustomNetworkAction - | RemoveCustomNetworkAction - | SetLatestBlockAction - | Web3UnsetNodeAction; + | SetLatestBlockAction; + +/*** Union Type ***/ +export type ConfigAction = CustomNetworkAction | CustomNodeAction | NodeAction | MetaAction; diff --git a/common/actions/config/constants.ts b/common/actions/config/constants.ts index 58fa8e15..a3b58ac1 100644 --- a/common/actions/config/constants.ts +++ b/common/actions/config/constants.ts @@ -1,14 +1,19 @@ export enum TypeKeys { CONFIG_LANGUAGE_CHANGE = 'CONFIG_LANGUAGE_CHANGE', - CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE', - CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT', + CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE', CONFIG_TOGGLE_AUTO_GAS_LIMIT = 'CONFIG_TOGGLE_AUTO_GAS_LIMIT', CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS', + CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK', + + CONFIG_NODE_WEB3_SET = 'CONFIG_NODE_WEB3_SET', + CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET', + CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE', + CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT', + CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE', CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE', + CONFIG_ADD_CUSTOM_NETWORK = 'CONFIG_ADD_CUSTOM_NETWORK', - CONFIG_REMOVE_CUSTOM_NETWORK = 'CONFIG_REMOVE_CUSTOM_NETWORK', - CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK', - CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET' + CONFIG_REMOVE_CUSTOM_NETWORK = 'CONFIG_REMOVE_CUSTOM_NETWORK' } diff --git a/common/actions/customTokens/actionCreators.ts b/common/actions/customTokens/actionCreators.ts index a95ed461..72d5246d 100644 --- a/common/actions/customTokens/actionCreators.ts +++ b/common/actions/customTokens/actionCreators.ts @@ -1,6 +1,6 @@ -import { Token } from 'config'; import * as interfaces from './actionTypes'; import { TypeKeys } from './constants'; +import { Token } from 'types/network'; export type TAddCustomToken = typeof addCustomToken; export function addCustomToken(payload: Token): interfaces.AddCustomTokenAction { diff --git a/common/actions/customTokens/actionTypes.ts b/common/actions/customTokens/actionTypes.ts index 7c4f0445..b1368005 100644 --- a/common/actions/customTokens/actionTypes.ts +++ b/common/actions/customTokens/actionTypes.ts @@ -1,5 +1,6 @@ -import { Token } from 'config'; import { TypeKeys } from './constants'; +import { Token } from 'types/network'; + /*** Add custom token ***/ export interface AddCustomTokenAction { type: TypeKeys.CUSTOM_TOKEN_ADD; diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index 370156eb..c5f791ec 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -1,5 +1,4 @@ import { Identicon, UnitDisplay } from 'components/ui'; -import { NetworkConfig } from 'config'; import { IWallet, Balance, TrezorWallet, LedgerWallet } from 'libs/wallet'; import React from 'react'; import translate from 'translations'; @@ -8,6 +7,7 @@ import Spinner from 'components/ui/Spinner'; import { getNetworkConfig, getOffline } from 'selectors/config'; import { AppState } from 'reducers'; import { connect } from 'react-redux'; +import { NetworkConfig } from 'types/network'; import { TSetAccountBalance, setAccountBalance } from 'actions/wallet'; interface OwnProps { @@ -72,7 +72,14 @@ class AccountInfo extends React.Component { public render() { const { network, balance, isOffline } = this.props; const { address, showLongBalance, confirmAddr } = this.state; - const { blockExplorer, tokenExplorer } = network; + let blockExplorer; + let tokenExplorer; + if (!network.isCustom) { + // this is kind of ugly but its the result of typeguards, maybe we can find a cleaner solution later on such as just dedicating it to a selector + blockExplorer = network.blockExplorer; + tokenExplorer = network.tokenExplorer; + } + const wallet = this.props.wallet as LedgerWallet | TrezorWallet; return (
diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index a1cf1d38..5af1589f 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -7,12 +7,12 @@ import { rateSymbols } from 'api/rates'; import { chain, flatMap } from 'lodash'; import { TokenBalance, getShownTokenBalances } from 'selectors/wallet'; import { Balance } from 'libs/wallet'; -import { NetworkConfig } from 'config'; import './EquivalentValues.scss'; import { Wei } from 'libs/units'; import { AppState } from 'reducers'; -import { getNetworkConfig } from 'selectors/config'; +import { getNetworkConfig, getOffline } from 'selectors/config'; import { connect } from 'react-redux'; +import { NetworkConfig } from 'types/network'; interface AllValue { symbol: string; @@ -37,10 +37,11 @@ interface State { interface StateProps { balance: Balance; network: NetworkConfig; + tokenBalances: TokenBalance[]; rates: AppState['rates']['rates']; ratesError: AppState['rates']['ratesError']; - isOffline: AppState['config']['offline']; + isOffline: AppState['config']['meta']['offline']; } interface DispatchProps { @@ -68,7 +69,7 @@ class EquivalentValues extends React.Component { public defaultOption( balance: Balance, tokenBalances: TokenBalance[], - network: NetworkConfig + network: StateProps['network'] ): DefaultOption { return { label: 'All', @@ -257,7 +258,6 @@ class EquivalentValues extends React.Component { this.requestedCurrencies = currencies; } } - function mapStateToProps(state: AppState): StateProps { return { balance: state.wallet.balance, @@ -265,7 +265,7 @@ function mapStateToProps(state: AppState): StateProps { network: getNetworkConfig(state), rates: state.rates.rates, ratesError: state.rates.ratesError, - isOffline: state.config.offline + isOffline: getOffline(state) }; } diff --git a/common/components/BalanceSidebar/TokenBalances/AddCustomTokenForm.tsx b/common/components/BalanceSidebar/TokenBalances/AddCustomTokenForm.tsx index 6a452908..efc52718 100644 --- a/common/components/BalanceSidebar/TokenBalances/AddCustomTokenForm.tsx +++ b/common/components/BalanceSidebar/TokenBalances/AddCustomTokenForm.tsx @@ -1,10 +1,11 @@ import React from 'react'; import classnames from 'classnames'; -import { Token, HELP_ARTICLE } from 'config'; +import { HELP_ARTICLE } from 'config'; import { isPositiveIntegerOrZero, isValidETHAddress } from 'libs/validators'; import translate from 'translations'; import { HelpLink } from 'components/ui'; import './AddCustomTokenForm.scss'; +import { Token } from 'types/network'; interface Props { allTokens: Token[]; diff --git a/common/components/BalanceSidebar/TokenBalances/Balances.tsx b/common/components/BalanceSidebar/TokenBalances/Balances.tsx index 27cc9ef7..4f3fe566 100644 --- a/common/components/BalanceSidebar/TokenBalances/Balances.tsx +++ b/common/components/BalanceSidebar/TokenBalances/Balances.tsx @@ -1,9 +1,9 @@ import React from 'react'; import translate from 'translations'; -import { Token } from 'config'; import { TokenBalance } from 'selectors/wallet'; import AddCustomTokenForm from './AddCustomTokenForm'; import TokenRow from './TokenRow'; +import { Token } from 'types/network'; interface Props { allTokens: Token[]; diff --git a/common/components/BalanceSidebar/TokenBalances/index.tsx b/common/components/BalanceSidebar/TokenBalances/index.tsx index 7fca475d..39ce3d42 100644 --- a/common/components/BalanceSidebar/TokenBalances/index.tsx +++ b/common/components/BalanceSidebar/TokenBalances/index.tsx @@ -13,12 +13,12 @@ import { setWalletTokens, TSetWalletTokens } from 'actions/wallet'; -import { getAllTokens } from 'selectors/config'; +import { getAllTokens, getOffline } from 'selectors/config'; import { getTokenBalances, getWalletInst, getWalletConfig, TokenBalance } from 'selectors/wallet'; -import { Token } from 'config'; import translate from 'translations'; import Balances from './Balances'; import Spinner from 'components/ui/Spinner'; +import { Token } from 'types/network'; import './index.scss'; interface StateProps { @@ -29,7 +29,7 @@ interface StateProps { tokensError: AppState['wallet']['tokensError']; isTokensLoading: AppState['wallet']['isTokensLoading']; hasSavedWalletTokens: AppState['wallet']['hasSavedWalletTokens']; - isOffline: AppState['config']['offline']; + isOffline: AppState['config']['meta']['offline']; } interface ActionProps { addCustomToken: TAddCustomToken; @@ -118,7 +118,7 @@ function mapStateToProps(state: AppState): StateProps { tokensError: state.wallet.tokensError, isTokensLoading: state.wallet.isTokensLoading, hasSavedWalletTokens: state.wallet.hasSavedWalletTokens, - isOffline: state.config.offline + isOffline: getOffline(state) }; } diff --git a/common/components/ConfirmationModal/components/Body/Body.tsx b/common/components/ConfirmationModal/components/Body/Body.tsx index ff25034a..7a8e44e0 100644 --- a/common/components/ConfirmationModal/components/Body/Body.tsx +++ b/common/components/ConfirmationModal/components/Body/Body.tsx @@ -6,13 +6,14 @@ import { connect } from 'react-redux'; import { AppState } from 'reducers'; import './Body.scss'; import { getNetworkConfig } from 'selectors/config'; +import { NetworkConfig } from 'types/network'; interface State { showDetails: boolean; } interface StateProps { - network: AppState['config']['network']; + network: NetworkConfig; } class BodyClass extends React.Component { diff --git a/common/components/ConfirmationModal/components/Body/components/Amounts.tsx b/common/components/ConfirmationModal/components/Body/components/Amounts.tsx index a5f5969d..e05823c4 100644 --- a/common/components/ConfirmationModal/components/Body/components/Amounts.tsx +++ b/common/components/ConfirmationModal/components/Body/components/Amounts.tsx @@ -6,7 +6,7 @@ import { getAllUSDValuesFromSerializedTx, AllUSDValues } from 'selectors/rates'; import { SerializedTxParams, getParamsFromSerializedTx } from 'selectors/transaction'; import { connect } from 'react-redux'; import { getNetworkConfig } from 'selectors/config'; -import { NetworkConfig } from 'config'; +import { NetworkConfig } from 'types/network'; interface StateProps extends SerializedTxParams, AllUSDValues { network: NetworkConfig; diff --git a/common/components/ConfirmationModal/components/Body/components/Details.tsx b/common/components/ConfirmationModal/components/Body/components/Details.tsx index 2fcfa5fb..5f6a20d8 100644 --- a/common/components/ConfirmationModal/components/Body/components/Details.tsx +++ b/common/components/ConfirmationModal/components/Body/components/Details.tsx @@ -4,9 +4,9 @@ import './Details.scss'; import { SerializedTransaction } from 'components/renderCbs'; import { AppState } from 'reducers'; import { getNodeConfig } from 'selectors/config'; -import { NodeConfig } from 'config'; import { connect } from 'react-redux'; import { TokenValue } from 'libs/units'; +import { NodeConfig } from 'types/node'; interface StateProps { node: NodeConfig; diff --git a/common/components/ConfirmationModal/components/Body/components/Node.tsx b/common/components/ConfirmationModal/components/Body/components/Node.tsx index 2d29f2a5..f8941f1e 100644 --- a/common/components/ConfirmationModal/components/Body/components/Node.tsx +++ b/common/components/ConfirmationModal/components/Body/components/Node.tsx @@ -1,11 +1,11 @@ -import { NodeConfig } from 'config'; import React, { Component } from 'react'; import { AppState } from 'reducers'; import { connect } from 'react-redux'; import { getNodeConfig } from 'selectors/config'; +import { StaticNodeConfig } from 'types/node'; interface StateProps { - node: NodeConfig; + node: StaticNodeConfig; } class NodeClass extends Component { diff --git a/common/components/CurrentCustomMessage.tsx b/common/components/CurrentCustomMessage.tsx index 4b3f734e..0dee62c3 100644 --- a/common/components/CurrentCustomMessage.tsx +++ b/common/components/CurrentCustomMessage.tsx @@ -4,7 +4,8 @@ import { AppState } from 'reducers'; import { getCurrentTo, ICurrentTo } from 'selectors/transaction'; import { getAllTokens } from 'selectors/config'; import { getWalletInst } from 'selectors/wallet'; -import { getAddressMessage, Token } from 'config'; +import { getAddressMessage } from 'config'; +import { Token } from 'types/network'; interface ReduxProps { currentTo: ICurrentTo; diff --git a/common/components/ExtendedNotifications/TransactionSucceeded.tsx b/common/components/ExtendedNotifications/TransactionSucceeded.tsx index 95770c1d..ea0cb9b0 100644 --- a/common/components/ExtendedNotifications/TransactionSucceeded.tsx +++ b/common/components/ExtendedNotifications/TransactionSucceeded.tsx @@ -1,6 +1,6 @@ -import { BlockExplorerConfig } from 'config'; import React from 'react'; import { translateRaw } from 'translations'; +import { BlockExplorerConfig } from 'types/network'; export interface TransactionSucceededProps { txHash: string; diff --git a/common/components/Header/components/CustomNodeModal.tsx b/common/components/Header/components/CustomNodeModal.tsx index 122ff9c3..2df65db1 100644 --- a/common/components/Header/components/CustomNodeModal.tsx +++ b/common/components/Header/components/CustomNodeModal.tsx @@ -2,11 +2,18 @@ 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,20 +22,27 @@ 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; port: string; network: string; - customNetworkName: string; + customNetworkId: string; customNetworkUnit: string; customNetworkChainId: string; hasAuth: boolean; @@ -36,13 +50,15 @@ interface State { password: string; } -export default class CustomNodeModal extends React.PureComponent { +type Props = OwnProps & StateProps & DispatchProps; + +class CustomNodeModal extends React.Component { public state: State = { name: '', url: '', port: '', - network: NETWORK_KEYS[0], - customNetworkName: '', + network: Object.keys(this.props.staticNetworks)[0], + customNetworkId: '', customNetworkUnit: '', customNetworkChainId: '', hasAuth: false, @@ -51,7 +67,7 @@ export default class CustomNodeModal extends React.PureComponent { }; 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,19 +125,16 @@ export default class CustomNodeModal extends React.PureComponent { value={network} onChange={this.handleChange} > - {NETWORK_KEYS.map(net => ( + {Object.keys(staticNetworks).map(net => ( ))} - {customNetworks.map(net => { - const id = makeCustomNetworkId(net); - return ( - - ); - })} + {Object.entries(customNetworks).map(([id, net]) => ( + + ))}
@@ -133,7 +146,7 @@ export default class CustomNodeModal extends React.PureComponent { {this.renderInput( { - name: 'customNetworkName', + name: 'customNetworkId', placeholder: 'My Custom Network' }, invalids @@ -248,7 +261,7 @@ export default class CustomNodeModal extends React.PureComponent { username, password, network, - customNetworkName, + customNetworkId, customNetworkUnit, customNetworkChainId } = this.state; @@ -285,8 +298,8 @@ export default class CustomNodeModal extends React.PureComponent { // If they have a custom network, make sure info is provided if (network === CUSTOM) { - if (!customNetworkName) { - invalids.customNetworkName = true; + if (!customNetworkId) { + invalids.customNetworkId = true; } if (!customNetworkUnit) { invalids.customNetworkUnit = true; @@ -303,13 +316,14 @@ export default class CustomNodeModal extends React.PureComponent { } 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 { - name: this.state.customNetworkName, + isCustom: true, + name: this.state.customNetworkId, unit: this.state.customNetworkUnit, chainId: this.state.customNetworkChainId ? parseInt(this.state.customNetworkChainId, 10) : 0, dPathFormats @@ -318,29 +332,42 @@ export default class CustomNodeModal extends React.PureComponent { private makeCustomNodeConfigFromState(): CustomNodeConfig { const { network } = this.state; - const node: CustomNodeConfig = { + + const networkId = + network === CUSTOM + ? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState()) + : network; + + const port = parseInt(this.state.port, 10); + const url = this.state.url.trim(); + const node: Omit = { + 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, + ...(this.state.hasAuth + ? { + auth: { + username: this.state.username, + password: this.state.password + } + } + : {}) }; - if (this.state.hasAuth) { - node.auth = { - username: this.state.username, - password: this.state.password - }; - } + const lib = new CustomNode(node); - 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) => { @@ -359,9 +386,25 @@ export default class CustomNodeModal extends React.PureComponent { 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 }); }; + + private makeCustomNetworkId(config: CustomNetworkConfig): string { + return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`; + } } + +const mapStateToProps = (state: AppState): StateProps => ({ + customNetworks: getCustomNetworkConfigs(state), + customNodes: getCustomNodeConfigs(state), + staticNetworks: getStaticNetworkConfigs(state) +}); + +const mapDispatchToProps: DispatchProps = { + addCustomNetwork +}; + +export default connect(mapStateToProps, mapDispatchToProps)(CustomNodeModal); diff --git a/common/components/Header/components/Navigation.tsx b/common/components/Header/components/Navigation.tsx index 22cb8c9e..2695082f 100644 --- a/common/components/Header/components/Navigation.tsx +++ b/common/components/Header/components/Navigation.tsx @@ -46,7 +46,7 @@ const tabs: TabLink[] = [ ]; interface Props { - color?: string; + color?: string | false; } interface State { diff --git a/common/components/Header/index.tsx b/common/components/Header/index.tsx index 2c528c62..23ce24b7 100644 --- a/common/components/Header/index.tsx +++ b/common/components/Header/index.tsx @@ -3,39 +3,43 @@ import { TChangeNodeIntent, TAddCustomNode, TRemoveCustomNode, - TAddCustomNetwork + TAddCustomNetwork, + AddCustomNodeAction, + changeLanguage, + changeNodeIntent, + addCustomNode, + removeCustomNode, + addCustomNetwork } from 'actions/config'; import logo from 'assets/images/logo-mycrypto.svg'; import { Dropdown, ColorDropdown } from 'components/ui'; -import React, { PureComponent } from 'react'; +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 { 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, + getNodeId, + 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; @@ -44,11 +48,42 @@ interface Props { addCustomNetwork: TAddCustomNetwork; } +interface StateProps { + network: NetworkConfig; + languageSelection: AppState['config']['meta']['languageSelection']; + node: NodeConfig; + nodeSelection: AppState['config']['nodes']['selectedNode']['nodeId']; + 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: getNodeId(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 PureComponent { +type Props = StateProps & DispatchProps; + +class Header extends Component { public state = { isAddingCustomNode: false }; @@ -56,50 +91,40 @@ export default class Header extends PureComponent { 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; - - 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: { networkId, nodeId }, isCustom, id, ...rest } = n; return { - value: key, + ...rest, name: ( - {network && network.name} ({n.service}) + {networkId} - {nodeId} (custom) ), - 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: ( - - {network && network.name} - {cn.name} (custom) - - ), - color: network && network.color, - hidden: false, - onRemove: () => this.props.removeCustomNode(cn) - }; - }) - ); + } else { + const { name: { networkId, service }, isCustom, ...rest } = n; + return { + ...rest, + name: ( + + {networkId} ({service}) + + ) + }; + } + }); return (
@@ -154,15 +179,15 @@ export default class Header extends PureComponent { change node. current node is on the ${node.network} network provided by ${node.service} `} - options={nodeOptions} - value={nodeSelection} + options={options} + value={nodeSelection || ''} extra={
  • Add Custom Node
  • } disabled={nodeSelection === 'web3'} - onChange={changeNodeIntent} + onChange={this.props.changeNodeIntent} size="smr" color="white" menuAlign="right" @@ -172,14 +197,11 @@ export default class Header extends PureComponent { - + {isAddingCustomNode && ( )} @@ -202,8 +224,10 @@ export default class Header extends PureComponent { 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); diff --git a/common/components/SendButtonFactory/OfflineBroadcast.tsx b/common/components/SendButtonFactory/OfflineBroadcast.tsx index 128e6246..90787e29 100644 --- a/common/components/SendButtonFactory/OfflineBroadcast.tsx +++ b/common/components/SendButtonFactory/OfflineBroadcast.tsx @@ -6,7 +6,7 @@ import { getOffline } from 'selectors/config'; import { NewTabLink } from 'components/ui'; interface StateProps { - offline: AppState['config']['offline']; + offline: AppState['config']['meta']['offline']; } class OfflineBroadcastClass extends Component { public render() { diff --git a/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx b/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx index 2efc5a00..22d56dfa 100644 --- a/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx +++ b/common/components/TXMetaDataPanel/TXMetaDataPanel.tsx @@ -20,13 +20,14 @@ import SimpleGas from './components/SimpleGas'; import AdvancedGas, { AdvancedOptions } from './components/AdvancedGas'; import './TXMetaDataPanel.scss'; import { getGasPrice } from 'selectors/transaction'; +import { NetworkConfig } from 'types/network'; type SliderStates = 'simple' | 'advanced'; interface StateProps { gasPrice: AppState['transaction']['fields']['gasPrice']; - offline: AppState['config']['offline']; - network: AppState['config']['network']; + offline: AppState['config']['meta']['offline']; + network: NetworkConfig; } interface DispatchProps { diff --git a/common/components/TXMetaDataPanel/components/AdvancedGas.tsx b/common/components/TXMetaDataPanel/components/AdvancedGas.tsx index e34acabb..4377b2c8 100644 --- a/common/components/TXMetaDataPanel/components/AdvancedGas.tsx +++ b/common/components/TXMetaDataPanel/components/AdvancedGas.tsx @@ -27,7 +27,7 @@ interface OwnProps { } interface StateProps { - autoGasLimitEnabled: AppState['config']['autoGasLimit']; + autoGasLimitEnabled: AppState['config']['meta']['autoGasLimit']; validGasPrice: boolean; } diff --git a/common/components/TXMetaDataPanel/components/FeeSummary.tsx b/common/components/TXMetaDataPanel/components/FeeSummary.tsx index 4c68f504..0dcd532e 100644 --- a/common/components/TXMetaDataPanel/components/FeeSummary.tsx +++ b/common/components/TXMetaDataPanel/components/FeeSummary.tsx @@ -2,8 +2,9 @@ import React from 'react'; import BN from 'bn.js'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; -import { getNetworkConfig } from 'selectors/config'; +import { getNetworkConfig, getOffline } from 'selectors/config'; import { UnitDisplay } from 'components/ui'; +import { NetworkConfig } from 'types/network'; import './FeeSummary.scss'; interface RenderData { @@ -17,8 +18,8 @@ interface RenderData { interface ReduxStateProps { gasLimit: AppState['transaction']['fields']['gasLimit']; rates: AppState['rates']['rates']; - network: AppState['config']['network']; - isOffline: AppState['config']['offline']; + network: NetworkConfig; + isOffline: AppState['config']['meta']['offline']; } interface OwnProps { @@ -75,7 +76,7 @@ function mapStateToProps(state: AppState): ReduxStateProps { gasLimit: state.transaction.fields.gasLimit, rates: state.rates.rates, network: getNetworkConfig(state), - isOffline: state.config.offline + isOffline: getOffline(state) }; } diff --git a/common/components/UnitDropDown/UnitDropDown.tsx b/common/components/UnitDropDown/UnitDropDown.tsx index 389f18d4..4119f357 100644 --- a/common/components/UnitDropDown/UnitDropDown.tsx +++ b/common/components/UnitDropDown/UnitDropDown.tsx @@ -8,6 +8,7 @@ import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { getUnit } from 'selectors/transaction'; import { getNetworkConfig } from 'selectors/config'; +import { NetworkConfig } from 'types/network'; interface DispatchProps { setUnitMeta: TSetUnitMeta; @@ -18,7 +19,7 @@ interface StateProps { tokens: TokenBalance[]; allTokens: MergedToken[]; showAllTokens?: boolean; - network: AppState['config']['network']; + network: NetworkConfig; } const StringDropdown = Dropdown as new () => Dropdown; diff --git a/common/components/WalletDecrypt/WalletDecrypt.tsx b/common/components/WalletDecrypt/WalletDecrypt.tsx index 619ffd62..c0e2d85b 100644 --- a/common/components/WalletDecrypt/WalletDecrypt.tsx +++ b/common/components/WalletDecrypt/WalletDecrypt.tsx @@ -40,11 +40,10 @@ import { InsecureWalletName, MiscWalletName, WalletName, - isWeb3NodeAvailable, knowledgeBaseURL, donationAddressMap } from 'config'; - +import { isWeb3NodeAvailable } from 'libs/nodes/web3'; import LedgerIcon from 'assets/images/wallets/ledger.svg'; import MetamaskIcon from 'assets/images/wallets/metamask.svg'; import MistIcon from 'assets/images/wallets/mist.svg'; diff --git a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx index 6a1107aa..4edba224 100644 --- a/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx +++ b/common/components/WalletDecrypt/components/DeterministicWalletsModal.tsx @@ -8,7 +8,6 @@ import { } from 'actions/deterministicWallets'; import Modal, { IButton } from 'components/ui/Modal'; import { AppState } from 'reducers'; -import { NetworkConfig } from 'config'; import { isValidPath } from 'libs/validators'; import React from 'react'; import { connect } from 'react-redux'; @@ -16,7 +15,7 @@ import { getNetworkConfig } from 'selectors/config'; import { getTokens, MergedToken } from 'selectors/wallet'; import { UnitDisplay } from 'components/ui'; import './DeterministicWalletsModal.scss'; -import { DPath } from 'config/dpaths'; +import { StaticNetworkConfig } from 'types/network'; import Select from 'react-select'; const WALLETS_PER_PAGE = 5; @@ -34,7 +33,7 @@ interface Props { // Redux state wallets: AppState['deterministicWallets']['wallets']; desiredToken: AppState['deterministicWallets']['desiredToken']; - network: NetworkConfig; + network: StaticNetworkConfig; tokens: MergedToken[]; // Redux actions diff --git a/common/components/WalletDecrypt/components/LedgerNano.tsx b/common/components/WalletDecrypt/components/LedgerNano.tsx index c57a4494..fd7da02c 100644 --- a/common/components/WalletDecrypt/components/LedgerNano.tsx +++ b/common/components/WalletDecrypt/components/LedgerNano.tsx @@ -7,10 +7,8 @@ import ledger from 'ledgerco'; import { Spinner, NewTabLink } from 'components/ui'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; -import { getNetworkConfig } from 'selectors/config'; import { SecureWalletName, ledgerReferralURL } from 'config'; -import { DPath } from 'config/dpaths'; -import { getPaths, getSingleDPath } from 'utils/network'; +import { getPaths, getSingleDPath } from 'selectors/config/wallet'; interface OwnProps { onUnlock(param: any): void; @@ -18,6 +16,7 @@ interface OwnProps { interface StateProps { dPath: DPath; + dPaths: DPath[]; } interface State { @@ -103,7 +102,7 @@ class LedgerNanoSDecryptClass extends PureComponent { publicKey={publicKey} chainCode={chainCode} dPath={dPath} - dPaths={getPaths(SecureWalletName.LEDGER_NANO_S)} + dPaths={this.props.dPaths} onCancel={this.handleCancel} onConfirmAddress={this.handleUnlock} onPathChange={this.handlePathChange} @@ -169,9 +168,9 @@ class LedgerNanoSDecryptClass extends PureComponent { } function mapStateToProps(state: AppState): StateProps { - const network = getNetworkConfig(state); return { - dPath: getSingleDPath(SecureWalletName.LEDGER_NANO_S, network) + dPath: getSingleDPath(state, SecureWalletName.LEDGER_NANO_S), + dPaths: getPaths(state, SecureWalletName.LEDGER_NANO_S) }; } diff --git a/common/components/WalletDecrypt/components/Mnemonic.tsx b/common/components/WalletDecrypt/components/Mnemonic.tsx index 5a76f814..c24fb25c 100644 --- a/common/components/WalletDecrypt/components/Mnemonic.tsx +++ b/common/components/WalletDecrypt/components/Mnemonic.tsx @@ -5,10 +5,8 @@ import DeterministicWalletsModal from './DeterministicWalletsModal'; import { formatMnemonic } from 'utils/formatters'; import { InsecureWalletName } from 'config'; import { AppState } from 'reducers'; -import { getNetworkConfig } from 'selectors/config'; import { connect } from 'react-redux'; -import { DPath } from 'config/dpaths'; -import { getPaths, getSingleDPath } from 'utils/network'; +import { getSingleDPath, getPaths } from 'selectors/config/wallet'; import { TogglablePassword } from 'components'; interface Props { @@ -17,6 +15,7 @@ interface Props { interface StateProps { dPath: DPath; + dPaths: DPath[]; } interface State { @@ -80,7 +79,7 @@ class MnemonicDecryptClass extends PureComponent { isOpen={!!seed} seed={seed} dPath={dPath} - dPaths={getPaths(InsecureWalletName.MNEMONIC_PHRASE)} + dPaths={this.props.dPaths} onCancel={this.handleCancel} onConfirmAddress={this.handleUnlock} onPathChange={this.handlePathChange} @@ -147,9 +146,9 @@ class MnemonicDecryptClass extends PureComponent { } function mapStateToProps(state: AppState): StateProps { - const network = getNetworkConfig(state); return { - dPath: getSingleDPath(InsecureWalletName.MNEMONIC_PHRASE, network) + dPath: getSingleDPath(state, InsecureWalletName.MNEMONIC_PHRASE), + dPaths: getPaths(state, InsecureWalletName.MNEMONIC_PHRASE) }; } diff --git a/common/components/WalletDecrypt/components/Trezor.tsx b/common/components/WalletDecrypt/components/Trezor.tsx index 6eaa7d48..9be4d5a8 100644 --- a/common/components/WalletDecrypt/components/Trezor.tsx +++ b/common/components/WalletDecrypt/components/Trezor.tsx @@ -5,12 +5,10 @@ import TrezorConnect from 'vendor/trezor-connect'; import DeterministicWalletsModal from './DeterministicWalletsModal'; import './Trezor.scss'; import { Spinner, NewTabLink } from 'components/ui'; -import { getNetworkConfig } from 'selectors/config'; import { AppState } from 'reducers'; import { connect } from 'react-redux'; import { SecureWalletName, trezorReferralURL } from 'config'; -import { DPath } from 'config/dpaths'; -import { getPaths, getSingleDPath } from 'utils/network'; +import { getSingleDPath, getPaths } from 'selectors/config/wallet'; //todo: conflicts with comment in walletDecrypt -> onUnlock method interface OwnProps { @@ -19,6 +17,7 @@ interface OwnProps { interface StateProps { dPath: DPath; + dPaths: DPath[]; } // todo: nearly duplicates ledger component props @@ -80,7 +79,7 @@ class TrezorDecryptClass extends PureComponent { publicKey={publicKey} chainCode={chainCode} dPath={dPath} - dPaths={getPaths(SecureWalletName.TREZOR)} + dPaths={this.props.dPaths} onCancel={this.handleCancel} onConfirmAddress={this.handleUnlock} onPathChange={this.handlePathChange} @@ -143,9 +142,9 @@ class TrezorDecryptClass extends PureComponent { } function mapStateToProps(state: AppState): StateProps { - const network = getNetworkConfig(state); return { - dPath: getSingleDPath(SecureWalletName.TREZOR, network) + dPath: getSingleDPath(state, SecureWalletName.TREZOR), + dPaths: getPaths(state, SecureWalletName.TREZOR) }; } diff --git a/common/components/ui/ColorDropdown.tsx b/common/components/ui/ColorDropdown.tsx index 32da87cc..7459f0be 100644 --- a/common/components/ui/ColorDropdown.tsx +++ b/common/components/ui/ColorDropdown.tsx @@ -8,7 +8,7 @@ interface Option { name: any; value: T; color?: string; - hidden: boolean | undefined; + hidden?: boolean | undefined; onRemove?(): void; } diff --git a/common/components/ui/UnitDisplay.tsx b/common/components/ui/UnitDisplay.tsx index 3b55e167..da9d840e 100644 --- a/common/components/ui/UnitDisplay.tsx +++ b/common/components/ui/UnitDisplay.tsx @@ -85,7 +85,7 @@ export default UnitDisplay; * Circumvents typescript issue with union props on connected components. */ interface OfflineProps { - offline: AppState['config']['offline']; + offline: AppState['config']['meta']['offline']; children: React.ReactElement; } diff --git a/common/config/dpaths.ts b/common/config/dpaths.ts index c162025b..9b2c0a4c 100644 --- a/common/config/dpaths.ts +++ b/common/config/dpaths.ts @@ -1,8 +1,3 @@ -export interface DPath { - label: string; - value: string; // TODO determine method for more precise typing for path -} - export const ETH_DEFAULT: DPath = { label: 'Default (ETH)', value: "m/44'/60'/0'/0" diff --git a/common/config/index.ts b/common/config/index.ts index c66161be..871c7f8d 100644 --- a/common/config/index.ts +++ b/common/config/index.ts @@ -1,4 +1,3 @@ -export * from './networks'; export * from './data'; export * from './bity'; export * from './addressMessages'; diff --git a/common/config/networks.ts b/common/config/networks.ts deleted file mode 100644 index ccdb8891..00000000 --- a/common/config/networks.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from './data'; -import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes'; -import { networkIdToName } from 'libs/values'; -import { - ETH_DEFAULT, - ETH_TREZOR, - ETH_LEDGER, - ETC_LEDGER, - ETC_TREZOR, - ETH_TESTNET, - EXP_DEFAULT, - UBQ_DEFAULT, - DPath -} from 'config/dpaths'; - -export interface BlockExplorerConfig { - origin: string; - txUrl(txHash: string): string; - addressUrl(address: string): string; -} - -export interface Token { - address: string; - symbol: string; - decimal: number; - error?: string | null; -} - -export interface NetworkContract { - name: NetworkKeys; - address?: string; - abi: string; -} - -export interface DPathFormats { - trezor: DPath; - ledgerNanoS: DPath; - mnemonicPhrase: DPath; -} - -export interface NetworkConfig { - // TODO really try not to allow strings due to custom networks - name: NetworkKeys; - unit: string; - color?: string; - blockExplorer?: BlockExplorerConfig; - tokenExplorer?: { - name: string; - address(address: string): string; - }; - chainId: number; - tokens: Token[]; - contracts: NetworkContract[] | null; - dPathFormats: DPathFormats; - isTestnet?: boolean; -} - -export interface CustomNetworkConfig { - name: string; - unit: string; - chainId: number; - dPathFormats: DPathFormats | null; -} - -export interface NodeConfig { - network: NetworkKeys; - lib: RPCNode | Web3Node; - service: string; - estimateGas?: boolean; - hidden?: boolean; -} - -export interface CustomNodeConfig { - name: string; - url: string; - port: number; - network: string; - auth?: { - username: string; - password: string; - }; -} - -// Must be a website that follows the ethplorer convention of /tx/[hash] and -// address/[address] to generate the correct functions. -function makeExplorer(origin: string): BlockExplorerConfig { - return { - origin, - txUrl: hash => `${origin}/tx/${hash}`, - addressUrl: address => `${origin}/address/${address}` - }; -} - -const ETH: NetworkConfig = { - name: 'ETH', - unit: 'ETH', - chainId: 1, - color: '#0e97c0', - blockExplorer: makeExplorer('https://etherscan.io'), - tokenExplorer: { - name: ethPlorer, - address: ETHTokenExplorer - }, - tokens: require('./tokens/eth.json'), - contracts: require('./contracts/eth.json'), - dPathFormats: { - [SecureWalletName.TREZOR]: ETH_TREZOR, - [SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER, - [InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT - } -}; - -const Ropsten: NetworkConfig = { - name: 'Ropsten', - unit: 'ETH', - chainId: 3, - color: '#adc101', - blockExplorer: makeExplorer('https://ropsten.etherscan.io'), - tokens: require('./tokens/ropsten.json'), - contracts: require('./contracts/ropsten.json'), - isTestnet: true, - dPathFormats: { - [SecureWalletName.TREZOR]: ETH_TESTNET, - [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, - [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET - } -}; - -const Kovan: NetworkConfig = { - name: 'Kovan', - unit: 'ETH', - chainId: 42, - color: '#adc101', - blockExplorer: makeExplorer('https://kovan.etherscan.io'), - tokens: require('./tokens/ropsten.json'), - contracts: require('./contracts/ropsten.json'), - isTestnet: true, - dPathFormats: { - [SecureWalletName.TREZOR]: ETH_TESTNET, - [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, - [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET - } -}; - -const Rinkeby: NetworkConfig = { - name: 'Rinkeby', - unit: 'ETH', - chainId: 4, - color: '#adc101', - blockExplorer: makeExplorer('https://rinkeby.etherscan.io'), - tokens: require('./tokens/rinkeby.json'), - contracts: require('./contracts/rinkeby.json'), - isTestnet: true, - dPathFormats: { - [SecureWalletName.TREZOR]: ETH_TESTNET, - [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, - [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET - } -}; - -const ETC: NetworkConfig = { - name: 'ETC', - unit: 'ETC', - chainId: 61, - color: '#669073', - blockExplorer: makeExplorer('https://gastracker.io'), - tokens: require('./tokens/etc.json'), - contracts: require('./contracts/etc.json'), - dPathFormats: { - [SecureWalletName.TREZOR]: ETC_TREZOR, - [SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER, - [InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR - } -}; - -const UBQ: NetworkConfig = { - name: 'UBQ', - unit: 'UBQ', - chainId: 8, - color: '#b37aff', - blockExplorer: makeExplorer('https://ubiqscan.io/en'), - tokens: require('./tokens/ubq.json'), - contracts: require('./contracts/ubq.json'), - dPathFormats: { - [SecureWalletName.TREZOR]: UBQ_DEFAULT, - [SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT, - [InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT - } -}; - -const EXP: NetworkConfig = { - name: 'EXP', - unit: 'EXP', - chainId: 2, - color: '#673ab7', - // tslint:disable:no-http-string - Unavailable behind HTTPS right now - blockExplorer: makeExplorer('http://www.gander.tech'), - // tslint:enable:no-http-string - tokens: require('./tokens/exp.json'), - contracts: require('./contracts/exp.json'), - dPathFormats: { - [SecureWalletName.TREZOR]: EXP_DEFAULT, - [SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT, - [InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT - } -}; - -export const NETWORKS = { - ETH, - Ropsten, - Kovan, - Rinkeby, - ETC, - UBQ, - EXP -}; - -export type NetworkKeys = keyof typeof NETWORKS; - -enum NodeName { - ETH_MEW = 'eth_mew', - ETH_MYCRYPTO = 'eth_mycrypto', - ETH_ETHSCAN = 'eth_ethscan', - ETH_INFURA = 'eth_infura', - ROP_MEW = 'rop_mew', - ROP_INFURA = 'rop_infura', - KOV_ETHSCAN = 'kov_ethscan', - RIN_ETHSCAN = 'rin_ethscan', - RIN_INFURA = 'rin_infura', - ETC_EPOOL = 'etc_epool', - UBQ = 'ubq', - EXP_TECH = 'exp_tech' -} - -type NonWeb3NodeConfigs = { [key in NodeName]: NodeConfig }; - -interface Web3NodeConfig { - web3?: NodeConfig; -} - -type NodeConfigs = NonWeb3NodeConfigs & Web3NodeConfig; - -export const NODES: NodeConfigs = { - eth_mew: { - network: 'ETH', - lib: new RPCNode('https://api.myetherapi.com/eth'), - service: 'MyEtherWallet', - estimateGas: true - }, - eth_mycrypto: { - network: 'ETH', - lib: new RPCNode('https://api.mycryptoapi.com/eth'), - service: 'MyCrypto', - estimateGas: true - }, - eth_ethscan: { - network: 'ETH', - service: 'Etherscan.io', - lib: new EtherscanNode('https://api.etherscan.io/api'), - estimateGas: false - }, - eth_infura: { - network: 'ETH', - service: 'infura.io', - lib: new InfuraNode('https://mainnet.infura.io/mew'), - estimateGas: false - }, - rop_mew: { - network: 'Ropsten', - service: 'MyEtherWallet', - lib: new RPCNode('https://api.myetherapi.com/rop'), - estimateGas: false - }, - rop_infura: { - network: 'Ropsten', - service: 'infura.io', - lib: new InfuraNode('https://ropsten.infura.io/mew'), - estimateGas: false - }, - kov_ethscan: { - network: 'Kovan', - service: 'Etherscan.io', - lib: new EtherscanNode('https://kovan.etherscan.io/api'), - estimateGas: false - }, - rin_ethscan: { - network: 'Rinkeby', - service: 'Etherscan.io', - lib: new EtherscanNode('https://rinkeby.etherscan.io/api'), - estimateGas: false - }, - rin_infura: { - network: 'Rinkeby', - service: 'infura.io', - lib: new InfuraNode('https://rinkeby.infura.io/mew'), - estimateGas: false - }, - etc_epool: { - network: 'ETC', - service: 'Epool.io', - lib: new RPCNode('https://mewapi.epool.io'), - estimateGas: false - }, - ubq: { - network: 'UBQ', - service: 'ubiqscan.io', - lib: new RPCNode('https://pyrus2.ubiqscan.io'), - estimateGas: true - }, - exp_tech: { - network: 'EXP', - service: 'Expanse.tech', - lib: new RPCNode('https://node.expanse.tech/'), - estimateGas: true - } -}; - -interface Web3NodeInfo { - networkId: string; - lib: Web3Node; -} - -export async function setupWeb3Node(): Promise { - const { web3 } = window as any; - - if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) { - throw new Error( - 'Web3 not found. Please check that MetaMask is installed, or that MyCrypto is open in Mist.' - ); - } - - const lib = new Web3Node(); - const networkId = await lib.getNetVersion(); - const accounts = await lib.getAccounts(); - - if (!accounts.length) { - throw new Error('No accounts found in MetaMask / Mist.'); - } - - if (networkId === 'loading') { - throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.'); - } - - return { networkId, lib }; -} - -export async function isWeb3NodeAvailable(): Promise { - try { - await setupWeb3Node(); - return true; - } catch (e) { - return false; - } -} - -export const Web3Service = 'MetaMask / Mist'; - -export interface NodeConfigOverride extends NodeConfig { - network: any; -} - -export async function initWeb3Node(): Promise { - const { networkId, lib } = await setupWeb3Node(); - const web3: NodeConfigOverride = { - network: networkIdToName(networkId), - service: Web3Service, - lib, - estimateGas: false, - hidden: true - }; - - NODES.web3 = web3; -} diff --git a/common/containers/TabSection/index.tsx b/common/containers/TabSection/index.tsx index 7c58fbd2..7d86f79d 100644 --- a/common/containers/TabSection/index.tsx +++ b/common/containers/TabSection/index.tsx @@ -1,91 +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, getLatestBlock } from 'selectors/config'; -interface ReduxProps { - languageSelection: AppState['config']['languageSelection']; - 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']; +interface StateProps { + isOffline: AppState['config']['meta']['offline']; + latestBlock: AppState['config']['meta']['latestBlock']; } -interface ActionProps { - changeLanguage: TChangeLanguage; - changeNodeIntent: TChangeNodeIntent; - addCustomNode: TAddCustomNode; - removeCustomNode: TRemoveCustomNode; - addCustomNetwork: TAddCustomNetwork; - setGasPriceField: TSetGasPriceField; -} - -type Props = { +interface OwnProps { isUnavailableOffline?: boolean; children: string | React.ReactElement | React.ReactElement[]; -} & ReduxProps & - ActionProps; +} + +type Props = OwnProps & StateProps; class TabSection extends Component { public render() { - const { - isUnavailableOffline, - children, - // APP - node, - nodeSelection, - isChangingNode, - isOffline, - languageSelection, - customNodes, - customNetworks, - latestBlock, - setGasPriceField, - changeLanguage, - changeNodeIntent, - addCustomNode, - removeCustomNode, - addCustomNetwork - } = this.props; - - const headerProps = { - languageSelection, - node, - nodeSelection, - isChangingNode, - isOffline, - customNodes, - customNetworks, - changeLanguage, - changeNodeIntent, - setGasPriceField, - addCustomNode, - removeCustomNode, - addCustomNetwork - }; + const { isUnavailableOffline, children, isOffline, latestBlock } = this.props; return (
    -
    +
    {isUnavailableOffline && isOffline ? : children}
    @@ -98,24 +38,11 @@ class TabSection extends Component { } } -function mapStateToProps(state: AppState): ReduxProps { +function mapStateToProps(state: AppState): StateProps { return { - node: state.config.node, - nodeSelection: state.config.nodeSelection, - isChangingNode: state.config.isChangingNode, - isOffline: state.config.offline, - languageSelection: state.config.languageSelection, - customNodes: state.config.customNodes, - customNetworks: state.config.customNetworks, - latestBlock: state.config.latestBlock + isOffline: getOffline(state), + latestBlock: getLatestBlock(state) }; } -export default connect(mapStateToProps, { - setGasPriceField: dSetGasPriceField, - changeLanguage: dChangeLanguage, - changeNodeIntent: dChangeNodeIntent, - addCustomNode: dAddCustomNode, - removeCustomNode: dRemoveCustomNode, - addCustomNetwork: dAddCustomNetwork -})(TabSection); +export default connect(mapStateToProps, {})(TabSection); diff --git a/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx b/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx index 15cf7981..8e6f05c6 100644 --- a/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx +++ b/common/containers/Tabs/Contracts/components/Interact/components/InteractExplorer/index.tsx @@ -4,7 +4,6 @@ import './InteractExplorer.scss'; import { TShowNotification, showNotification } from 'actions/notifications'; import { getNodeLib } from 'selectors/config'; import { getTo, getDataExists } from 'selectors/transaction'; -import { INode } from 'libs/nodes/INode'; import { GenerateTransaction } from 'components/GenerateTransaction'; import { AppState } from 'reducers'; import { connect } from 'react-redux'; @@ -12,9 +11,11 @@ import { Fields } from './components'; import { setDataField, TSetDataField } from 'actions/transaction'; import { Data } from 'libs/units'; import Select from 'react-select'; +import { Web3Node } from 'libs/nodes'; +import RpcNode from 'libs/nodes/rpc'; interface StateProps { - nodeLib: INode; + nodeLib: RpcNode | Web3Node; to: AppState['transaction']['fields']['to']; dataExists: boolean; } 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 41e13ec3..a56791d7 100644 --- a/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx +++ b/common/containers/Tabs/Contracts/components/Interact/components/InteractForm/index.tsx @@ -1,12 +1,13 @@ import React, { Component } from 'react'; import translate from 'translations'; -import { NetworkContract, donationAddressMap } from 'config'; import { getNetworkContracts } from 'selectors/config'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { isValidETHAddress, isValidAbiJson } from 'libs/validators'; import classnames from 'classnames'; import Select from 'react-select'; +import { NetworkContract } from 'types/network'; +import { donationAddressMap } from 'config'; interface ContractOption { name: string; diff --git a/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx b/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx index 8c54567f..3579a5f8 100644 --- a/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx +++ b/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx @@ -12,7 +12,6 @@ import { ICurrentValue } from 'selectors/transaction/current'; import BN from 'bn.js'; -import { NetworkConfig } from 'config'; import { validNumber, validDecimal } from 'libs/validators'; import { getGasLimit } from 'selectors/transaction'; import { AddressField, AmountField, TXMetaDataPanel } from 'components'; @@ -21,6 +20,7 @@ import { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values'; import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config'; import './RequestPayment.scss'; import { reset, TReset, setCurrentTo, TSetCurrentTo } from 'actions/transaction'; +import { NetworkConfig } from 'types/network'; interface OwnProps { wallet: AppState['wallet']['inst']; @@ -31,7 +31,7 @@ interface StateProps { currentTo: ICurrentTo; currentValue: ICurrentValue; gasLimit: SetGasLimitFieldAction['payload']; - networkConfig: NetworkConfig | undefined; + networkConfig: NetworkConfig; decimal: number; tokenContractAddress: string; } diff --git a/common/containers/Tabs/SendTransaction/index.tsx b/common/containers/Tabs/SendTransaction/index.tsx index c19a4022..03d395eb 100644 --- a/common/containers/Tabs/SendTransaction/index.tsx +++ b/common/containers/Tabs/SendTransaction/index.tsx @@ -15,9 +15,8 @@ import { UnavailableWallets } from 'containers/Tabs/SendTransaction/components'; import SubTabs, { Tab } from 'components/SubTabs'; -import { getNetworkConfig } from 'selectors/config'; -import { isNetworkUnit } from 'utils/network'; import { RouteNotFound } from 'components/RouteNotFound'; +import { isNetworkUnit } from 'selectors/config/wallet'; const Send = () => ( @@ -28,7 +27,7 @@ const Send = () => ( interface StateProps { wallet: AppState['wallet']['inst']; - network: AppState['config']['network']; + requestDisabled: boolean; } type Props = StateProps & RouteComponentProps<{}>; @@ -46,7 +45,7 @@ class SendTransaction extends React.Component { { path: 'request', name: translate('Request Payment'), - disabled: !isNetworkUnit(this.props.network, 'ETH') + disabled: this.props.requestDisabled }, { path: 'info', @@ -100,5 +99,5 @@ class SendTransaction extends React.Component { export default connect((state: AppState) => ({ wallet: getWalletInst(state), - network: getNetworkConfig(state) + requestDisabled: !isNetworkUnit(state, 'ETH') }))(SendTransaction); diff --git a/common/containers/Tabs/Swap/components/LiteSend/LiteSend.tsx b/common/containers/Tabs/Swap/components/LiteSend/LiteSend.tsx index d3058a8e..589fd14a 100644 --- a/common/containers/Tabs/Swap/components/LiteSend/LiteSend.tsx +++ b/common/containers/Tabs/Swap/components/LiteSend/LiteSend.tsx @@ -8,7 +8,7 @@ import { configureLiteSend, TConfigureLiteSend } from 'actions/swap'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { shouldDisplayLiteSend } from 'selectors/swap'; -import { NetworkConfig } from 'config'; +import { NetworkConfig } from 'types/network'; interface DispatchProps { configureLiteSend: TConfigureLiteSend; diff --git a/common/containers/Tabs/Swap/index.tsx b/common/containers/Tabs/Swap/index.tsx index 564b7f6d..729a83c5 100644 --- a/common/containers/Tabs/Swap/index.tsx +++ b/common/containers/Tabs/Swap/index.tsx @@ -56,6 +56,7 @@ import TabSection from 'containers/TabSection'; import { merge } from 'lodash'; import { RouteNotFound } from 'components/RouteNotFound'; import { Switch, Route, RouteComponentProps } from 'react-router'; +import { getOffline } from 'selectors/config'; interface ReduxStateProps { step: number; @@ -284,7 +285,7 @@ function mapStateToProps(state: AppState) { bityOrderStatus: state.swap.bityOrderStatus, shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus, paymentAddress: state.swap.paymentAddress, - isOffline: state.config.offline + isOffline: getOffline(state) }; } diff --git a/common/libs/nodes/INode.ts b/common/libs/nodes/INode.ts index 2d29b87d..a2064e71 100644 --- a/common/libs/nodes/INode.ts +++ b/common/libs/nodes/INode.ts @@ -1,6 +1,6 @@ -import { Token } from 'config'; import { Wei, TokenValue } from 'libs/units'; import { IHexStrTransaction } from 'libs/transaction'; +import { Token } from 'types/network'; export interface TxObj { to: string; diff --git a/common/libs/nodes/custom/index.ts b/common/libs/nodes/custom/index.ts index 91f9d9ca..79254e74 100644 --- a/common/libs/nodes/custom/index.ts +++ b/common/libs/nodes/custom/index.ts @@ -1,11 +1,11 @@ import RPCNode from '../rpc'; import RPCClient from '../rpc/client'; -import { CustomNodeConfig } from 'config'; +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) { + 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); } } diff --git a/common/libs/nodes/etherscan/requests.ts b/common/libs/nodes/etherscan/requests.ts index 96e0a1d0..6fda0377 100644 --- a/common/libs/nodes/etherscan/requests.ts +++ b/common/libs/nodes/etherscan/requests.ts @@ -1,4 +1,3 @@ -import { Token } from 'config'; import ERC20 from 'libs/erc20'; import RPCRequests from '../rpc/requests'; import { @@ -10,6 +9,7 @@ import { SendRawTxRequest, GetCurrentBlockRequest } from './types'; +import { Token } from 'types/network'; export default class EtherscanRequests extends RPCRequests { public sendRawTx(signedTx: string): SendRawTxRequest { diff --git a/common/libs/nodes/rpc/index.ts b/common/libs/nodes/rpc/index.ts index 8bb52f68..3b6a1674 100644 --- a/common/libs/nodes/rpc/index.ts +++ b/common/libs/nodes/rpc/index.ts @@ -1,5 +1,4 @@ import BN from 'bn.js'; -import { Token } from 'config'; import { IHexStrTransaction } from 'libs/transaction'; import { Wei, TokenValue } from 'libs/units'; import { stripHexPrefix } from 'libs/values'; @@ -15,6 +14,7 @@ import { isValidCurrentBlock, isValidRawTxApi } from '../../validators'; +import { Token } from 'types/network'; export default class RpcNode implements INode { public client: RPCClient; diff --git a/common/libs/nodes/rpc/requests.ts b/common/libs/nodes/rpc/requests.ts index 3db0bb17..1d1c8d4f 100644 --- a/common/libs/nodes/rpc/requests.ts +++ b/common/libs/nodes/rpc/requests.ts @@ -1,4 +1,3 @@ -import { Token } from 'config'; import ERC20 from 'libs/erc20'; import { CallRequest, @@ -11,6 +10,8 @@ import { } from './types'; import { hexEncodeData } from './utils'; import { TxObj } from '../INode'; +import { Token } from 'types/network'; + export default class RPCRequests { public getNetVersion() { return { method: 'net_version' }; diff --git a/common/libs/nodes/web3/index.ts b/common/libs/nodes/web3/index.ts index 6540eeaf..06733e13 100644 --- a/common/libs/nodes/web3/index.ts +++ b/common/libs/nodes/web3/index.ts @@ -53,3 +53,38 @@ export default class Web3Node extends RPCNode { export function isWeb3Node(nodeLib: INode | Web3Node): nodeLib is Web3Node { return nodeLib instanceof Web3Node; } + +export const Web3Service = 'MetaMask / Mist'; + +export async function setupWeb3Node() { + const { web3 } = window as any; + + if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) { + throw new Error( + 'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.' + ); + } + + const lib = new Web3Node(); + const networkId = await lib.getNetVersion(); + const accounts = await lib.getAccounts(); + + if (!accounts.length) { + throw new Error('No accounts found in MetaMask / Mist.'); + } + + if (networkId === 'loading') { + throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.'); + } + + return { networkId, lib }; +} + +export async function isWeb3NodeAvailable(): Promise { + try { + await setupWeb3Node(); + return true; + } catch (e) { + return false; + } +} diff --git a/common/libs/values.ts b/common/libs/values.ts index a7673753..544c631c 100644 --- a/common/libs/values.ts +++ b/common/libs/values.ts @@ -1,7 +1,6 @@ import { Wei, toTokenBase } from 'libs/units'; import { addHexPrefix } from 'ethereumjs-util'; import BN from 'bn.js'; -import { NetworkKeys } from 'config'; export function stripHexPrefix(value: string) { return value.replace('0x', ''); @@ -24,21 +23,6 @@ export function sanitizeHex(hex: string) { return hex !== '' ? `0x${padLeftEven(hexStr)}` : ''; } -export function networkIdToName(networkId: string | number): NetworkKeys { - switch (networkId.toString()) { - case '1': - return 'ETH'; - case '3': - return 'Ropsten'; - case '4': - return 'Rinkeby'; - case '42': - return 'Kovan'; - default: - throw new Error(`Network ${networkId} is unsupported.`); - } -} - export const buildEIP681EtherRequest = ( recipientAddr: string, chainId: number, diff --git a/common/libs/wallet/non-deterministic/web3.ts b/common/libs/wallet/non-deterministic/web3.ts index ba794838..60929823 100644 --- a/common/libs/wallet/non-deterministic/web3.ts +++ b/common/libs/wallet/non-deterministic/web3.ts @@ -1,9 +1,8 @@ import { getTransactionFields, makeTransaction } from 'libs/transaction'; import { IFullWallet } from '../IWallet'; -import { networkIdToName } from 'libs/values'; import { bufferToHex } from 'ethereumjs-util'; import { configuredStore } from 'store'; -import { getNodeLib } from 'selectors/config'; +import { getNodeLib, getNetworkNameByChainId } from 'selectors/config'; import Web3Node, { isWeb3Node } from 'libs/nodes/web3'; import { INode } from 'libs/nodes/INode'; @@ -29,6 +28,9 @@ export default class Web3Wallet implements IFullWallet { const state = configuredStore.getState(); const nodeLib: Web3Node | INode = getNodeLib(state); + if (!nodeLib) { + throw new Error(''); + } if (!isWeb3Node(nodeLib)) { throw new Error('Web3 wallets can only be used with a Web3 node.'); } @@ -55,7 +57,7 @@ export default class Web3Wallet implements IFullWallet { }; const state = configuredStore.getState(); - const nodeLib: Web3Node | INode = getNodeLib(state); + const nodeLib: Web3Node | INode | undefined = getNodeLib(state); if (!isWeb3Node(nodeLib)) { throw new Error('Web3 wallets can only be used with a Web3 node.'); @@ -67,7 +69,7 @@ export default class Web3Wallet implements IFullWallet { private async networkCheck(lib: Web3Node) { const netId = await lib.getNetVersion(); - const netName = networkIdToName(netId); + const netName = getNetworkNameByChainId(configuredStore.getState(), netId); if (this.network !== netName) { throw new Error( `Expected MetaMask / Mist network to be ${ diff --git a/common/reducers/config.ts b/common/reducers/config.ts deleted file mode 100644 index bf73c880..00000000 --- a/common/reducers/config.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - ChangeLanguageAction, - ChangeNodeAction, - AddCustomNodeAction, - RemoveCustomNodeAction, - AddCustomNetworkAction, - RemoveCustomNetworkAction, - SetLatestBlockAction, - ConfigAction -} from 'actions/config'; -import { TypeKeys } from 'actions/config/constants'; -import { - NODES, - NETWORKS, - NodeConfig, - CustomNodeConfig, - NetworkConfig, - CustomNetworkConfig -} from 'config'; -import { makeCustomNodeId } from 'utils/node'; -import { makeCustomNetworkId } from 'utils/network'; - -export interface State { - // FIXME - languageSelection: string; - nodeSelection: string; - node: NodeConfig; - network: NetworkConfig; - isChangingNode: boolean; - offline: boolean; - autoGasLimit: boolean; - customNodes: CustomNodeConfig[]; - customNetworks: CustomNetworkConfig[]; - latestBlock: string; -} - -const defaultNode = 'eth_mew'; -export const INITIAL_STATE: State = { - languageSelection: 'en', - nodeSelection: defaultNode, - node: NODES[defaultNode], - network: NETWORKS[NODES[defaultNode].network], - isChangingNode: false, - offline: false, - autoGasLimit: true, - customNodes: [], - customNetworks: [], - latestBlock: '???' -}; - -function changeLanguage(state: State, action: ChangeLanguageAction): State { - return { - ...state, - languageSelection: action.payload - }; -} - -function changeNode(state: State, action: ChangeNodeAction): State { - return { - ...state, - nodeSelection: action.payload.nodeSelection, - node: action.payload.node, - network: action.payload.network, - isChangingNode: false - }; -} - -function changeNodeIntent(state: State): State { - return { - ...state, - isChangingNode: true - }; -} - -function toggleOffline(state: State): State { - return { - ...state, - offline: !state.offline - }; -} - -function toggleAutoGasLimitEstimation(state: State): State { - return { - ...state, - autoGasLimit: !state.autoGasLimit - }; -} - -function addCustomNode(state: State, action: AddCustomNodeAction): State { - const newId = makeCustomNodeId(action.payload); - return { - ...state, - customNodes: [ - ...state.customNodes.filter(node => makeCustomNodeId(node) !== newId), - action.payload - ] - }; -} - -function removeCustomNode(state: State, action: RemoveCustomNodeAction): State { - const id = makeCustomNodeId(action.payload); - return { - ...state, - customNodes: state.customNodes.filter(cn => cn !== action.payload), - nodeSelection: id === state.nodeSelection ? defaultNode : state.nodeSelection - }; -} - -function addCustomNetwork(state: State, action: AddCustomNetworkAction): State { - const newId = makeCustomNetworkId(action.payload); - return { - ...state, - customNetworks: [ - ...state.customNetworks.filter(node => makeCustomNetworkId(node) !== newId), - action.payload - ] - }; -} - -function removeCustomNetwork(state: State, action: RemoveCustomNetworkAction): State { - return { - ...state, - customNetworks: state.customNetworks.filter(cn => cn !== action.payload) - }; -} - -function setLatestBlock(state: State, action: SetLatestBlockAction): State { - return { - ...state, - latestBlock: action.payload - }; -} - -export function config(state: State = INITIAL_STATE, action: ConfigAction): State { - switch (action.type) { - case TypeKeys.CONFIG_LANGUAGE_CHANGE: - return changeLanguage(state, action); - case TypeKeys.CONFIG_NODE_CHANGE: - return changeNode(state, action); - case TypeKeys.CONFIG_NODE_CHANGE_INTENT: - return changeNodeIntent(state); - case TypeKeys.CONFIG_TOGGLE_OFFLINE: - return toggleOffline(state); - case TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT: - return toggleAutoGasLimitEstimation(state); - case TypeKeys.CONFIG_ADD_CUSTOM_NODE: - return addCustomNode(state, action); - case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE: - return removeCustomNode(state, action); - case TypeKeys.CONFIG_ADD_CUSTOM_NETWORK: - return addCustomNetwork(state, action); - case TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK: - return removeCustomNetwork(state, action); - case TypeKeys.CONFIG_SET_LATEST_BLOCK: - return setLatestBlock(state, action); - default: - return state; - } -} diff --git a/common/reducers/config/index.ts b/common/reducers/config/index.ts new file mode 100644 index 00000000..8389d994 --- /dev/null +++ b/common/reducers/config/index.ts @@ -0,0 +1,12 @@ +import { meta, State as MetaState } from './meta'; +import { networks, State as NetworksState } from './networks'; +import { nodes, State as NodesState } from './nodes'; +import { combineReducers } from 'redux'; + +export interface State { + meta: MetaState; + networks: NetworksState; + nodes: NodesState; +} + +export const config = combineReducers({ meta, networks, nodes }); diff --git a/common/reducers/config/meta/index.ts b/common/reducers/config/meta/index.ts new file mode 100644 index 00000000..c9942f46 --- /dev/null +++ b/common/reducers/config/meta/index.ts @@ -0,0 +1 @@ +export * from './meta'; diff --git a/common/reducers/config/meta/meta.ts b/common/reducers/config/meta/meta.ts new file mode 100644 index 00000000..7cdbb23b --- /dev/null +++ b/common/reducers/config/meta/meta.ts @@ -0,0 +1,61 @@ +import { ChangeLanguageAction, SetLatestBlockAction, MetaAction } from 'actions/config'; +import { TypeKeys } from 'actions/config/constants'; + +export interface State { + languageSelection: string; + offline: boolean; + autoGasLimit: boolean; + latestBlock: string; +} + +const INITIAL_STATE: State = { + languageSelection: 'en', + offline: false, + autoGasLimit: true, + latestBlock: '???' +}; + +function changeLanguage(state: State, action: ChangeLanguageAction): State { + return { + ...state, + languageSelection: action.payload + }; +} + +function toggleOffline(state: State): State { + return { + ...state, + offline: !state.offline + }; +} + +function toggleAutoGasLimitEstimation(state: State): State { + return { + ...state, + autoGasLimit: !state.autoGasLimit + }; +} + +function setLatestBlock(state: State, action: SetLatestBlockAction): State { + return { + ...state, + latestBlock: action.payload + }; +} + +export function meta(state: State = INITIAL_STATE, action: MetaAction): State { + switch (action.type) { + case TypeKeys.CONFIG_LANGUAGE_CHANGE: + return changeLanguage(state, action); + + case TypeKeys.CONFIG_TOGGLE_OFFLINE: + return toggleOffline(state); + case TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT: + return toggleAutoGasLimitEstimation(state); + + case TypeKeys.CONFIG_SET_LATEST_BLOCK: + return setLatestBlock(state, action); + default: + return state; + } +} diff --git a/common/reducers/config/networks/customNetworks.ts b/common/reducers/config/networks/customNetworks.ts new file mode 100644 index 00000000..fb49972c --- /dev/null +++ b/common/reducers/config/networks/customNetworks.ts @@ -0,0 +1,34 @@ +import { + AddCustomNetworkAction, + RemoveCustomNetworkAction, + CustomNetworkAction, + TypeKeys +} from 'actions/config'; +import { CustomNetworkConfig } from 'types/network'; + +// TODO: this doesn't accurately represent state, as +export interface State { + [customNetworkId: string]: CustomNetworkConfig; +} + +const addCustomNetwork = (state: State, { payload }: AddCustomNetworkAction): State => ({ + ...state, + [payload.id]: payload.config +}); + +function removeCustomNetwork(state: State, { payload }: RemoveCustomNetworkAction): State { + const stateCopy = { ...state }; + Reflect.deleteProperty(stateCopy, payload.id); + return stateCopy; +} + +export const customNetworks = (state: State = {}, action: CustomNetworkAction) => { + switch (action.type) { + case TypeKeys.CONFIG_ADD_CUSTOM_NETWORK: + return addCustomNetwork(state, action); + case TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK: + return removeCustomNetwork(state, action); + default: + return state; + } +}; diff --git a/common/reducers/config/networks/index.ts b/common/reducers/config/networks/index.ts new file mode 100644 index 00000000..9c0a2760 --- /dev/null +++ b/common/reducers/config/networks/index.ts @@ -0,0 +1,15 @@ +import { customNetworks, State as CustomNetworksState } from './customNetworks'; +import { staticNetworks, State as StaticNetworksState } from './staticNetworks'; +import { combineReducers } from 'redux'; + +interface State { + customNetworks: CustomNetworksState; + staticNetworks: StaticNetworksState; +} + +const networks = combineReducers({ + customNetworks, + staticNetworks +}); + +export { State, networks, StaticNetworksState, CustomNetworksState }; diff --git a/common/reducers/config/networks/staticNetworks.ts b/common/reducers/config/networks/staticNetworks.ts new file mode 100644 index 00000000..cc1e4bc2 --- /dev/null +++ b/common/reducers/config/networks/staticNetworks.ts @@ -0,0 +1,148 @@ +import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from 'config/data'; +import { + ETH_DEFAULT, + ETH_TREZOR, + ETH_LEDGER, + ETC_LEDGER, + ETC_TREZOR, + ETH_TESTNET, + EXP_DEFAULT, + UBQ_DEFAULT +} from 'config/dpaths'; +import { ConfigAction } from 'actions/config'; +import { StaticNetworkIds, StaticNetworkConfig, BlockExplorerConfig } from 'types/network'; + +export type State = { [key in StaticNetworkIds]: StaticNetworkConfig }; + +// Must be a website that follows the ethplorer convention of /tx/[hash] and +// address/[address] to generate the correct functions. +// TODO: put this in utils / libs +export function makeExplorer(origin: string): BlockExplorerConfig { + return { + origin, + txUrl: hash => `${origin}/tx/${hash}`, + addressUrl: address => `${origin}/address/${address}` + }; +} + +const INITIAL_STATE: State = { + ETH: { + name: 'ETH', + unit: 'ETH', + chainId: 1, + isCustom: false, + color: '#0e97c0', + blockExplorer: makeExplorer('https://etherscan.io'), + tokenExplorer: { + name: ethPlorer, + address: ETHTokenExplorer + }, + tokens: require('config/tokens/eth.json'), + contracts: require('config/contracts/eth.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TREZOR, + [SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT + } + }, + Ropsten: { + name: 'Ropsten', + unit: 'ETH', + chainId: 3, + isCustom: false, + color: '#adc101', + blockExplorer: makeExplorer('https://ropsten.etherscan.io'), + tokens: require('config/tokens/ropsten.json'), + contracts: require('config/contracts/ropsten.json'), + isTestnet: true, + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TESTNET, + [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET + } + }, + Kovan: { + name: 'Kovan', + unit: 'ETH', + chainId: 42, + isCustom: false, + color: '#adc101', + blockExplorer: makeExplorer('https://kovan.etherscan.io'), + tokens: require('config/tokens/ropsten.json'), + contracts: require('config/contracts/ropsten.json'), + isTestnet: true, + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TESTNET, + [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET + } + }, + Rinkeby: { + name: 'Rinkeby', + unit: 'ETH', + chainId: 4, + isCustom: false, + color: '#adc101', + blockExplorer: makeExplorer('https://rinkeby.etherscan.io'), + tokens: require('config/tokens/rinkeby.json'), + contracts: require('config/contracts/rinkeby.json'), + isTestnet: true, + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TESTNET, + [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET + } + }, + ETC: { + name: 'ETC', + unit: 'ETC', + chainId: 61, + isCustom: false, + color: '#669073', + blockExplorer: makeExplorer('https://gastracker.io'), + tokens: require('config/tokens/etc.json'), + contracts: require('config/contracts/etc.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: ETC_TREZOR, + [SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER, + [InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR + } + }, + UBQ: { + name: 'UBQ', + unit: 'UBQ', + chainId: 8, + isCustom: false, + color: '#b37aff', + blockExplorer: makeExplorer('https://ubiqscan.io/en'), + tokens: require('config/tokens/ubq.json'), + contracts: require('config/contracts/ubq.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: UBQ_DEFAULT, + [SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT, + [InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT + } + }, + EXP: { + name: 'EXP', + unit: 'EXP', + chainId: 2, + isCustom: false, + color: '#673ab7', + blockExplorer: makeExplorer('https://www.gander.tech'), + tokens: require('config/tokens/exp.json'), + contracts: require('config/contracts/exp.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: EXP_DEFAULT, + [SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT, + [InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT + } + } +}; + +export const staticNetworks = (state: State = INITIAL_STATE, action: ConfigAction) => { + switch (action.type) { + default: + return state; + } +}; diff --git a/common/reducers/config/nodes/customNodes.ts b/common/reducers/config/nodes/customNodes.ts new file mode 100644 index 00000000..f424af1b --- /dev/null +++ b/common/reducers/config/nodes/customNodes.ts @@ -0,0 +1,33 @@ +import { + TypeKeys, + CustomNodeAction, + AddCustomNodeAction, + RemoveCustomNodeAction +} from 'actions/config'; +import { CustomNodeConfig } from 'types/node'; + +export interface State { + [customNodeId: string]: CustomNodeConfig; +} + +const addCustomNode = (state: State, { payload }: AddCustomNodeAction): State => ({ + ...state, + [payload.id]: payload.config +}); + +function removeCustomNode(state: State, { payload }: RemoveCustomNodeAction): State { + const stateCopy = { ...state }; + Reflect.deleteProperty(stateCopy, payload.id); + return stateCopy; +} + +export const customNodes = (state: State = {}, action: CustomNodeAction): State => { + switch (action.type) { + case TypeKeys.CONFIG_ADD_CUSTOM_NODE: + return addCustomNode(state, action); + case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE: + return removeCustomNode(state, action); + default: + return state; + } +}; diff --git a/common/reducers/config/nodes/index.ts b/common/reducers/config/nodes/index.ts new file mode 100644 index 00000000..07fdebea --- /dev/null +++ b/common/reducers/config/nodes/index.ts @@ -0,0 +1,14 @@ +import { customNodes, State as CustomNodesState } from './customNodes'; +import { staticNodes, State as StaticNodesState } from './staticNodes'; +import { selectedNode, State as SelectedNodeState } from './selectedNode'; +import { combineReducers } from 'redux'; + +interface State { + customNodes: CustomNodesState; + staticNodes: StaticNodesState; + selectedNode: SelectedNodeState; +} + +const nodes = combineReducers({ customNodes, staticNodes, selectedNode }); + +export { State, nodes, CustomNodesState, StaticNodesState, SelectedNodeState }; diff --git a/common/reducers/config/nodes/selectedNode.ts b/common/reducers/config/nodes/selectedNode.ts new file mode 100644 index 00000000..6d69e6e7 --- /dev/null +++ b/common/reducers/config/nodes/selectedNode.ts @@ -0,0 +1,53 @@ +import { + ChangeNodeAction, + ChangeNodeIntentAction, + NodeAction, + TypeKeys, + RemoveCustomNodeAction, + CustomNodeAction +} from 'actions/config'; + +interface NodeLoaded { + pending: false; + nodeId: string; +} + +interface NodeChangePending { + pending: true; + nodeId: string; +} + +export type State = NodeLoaded | NodeChangePending; + +export const INITIAL_STATE: NodeLoaded = { + nodeId: 'eth_mycrypto', + pending: false +}; + +const changeNode = (_: State, { payload }: ChangeNodeAction): State => ({ + nodeId: payload.nodeId, + pending: false +}); + +const changeNodeIntent = (state: State, _: ChangeNodeIntentAction): State => ({ + ...state, + pending: true +}); + +const handleRemoveCustomNode = (_: State, _1: RemoveCustomNodeAction): State => INITIAL_STATE; + +export const selectedNode = ( + state: State = INITIAL_STATE, + action: NodeAction | CustomNodeAction +) => { + switch (action.type) { + case TypeKeys.CONFIG_NODE_CHANGE: + return changeNode(state, action); + case TypeKeys.CONFIG_NODE_CHANGE_INTENT: + return changeNodeIntent(state, action); + case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE: + return handleRemoveCustomNode(state, action); + default: + return state; + } +}; diff --git a/common/reducers/config/nodes/staticNodes.ts b/common/reducers/config/nodes/staticNodes.ts new file mode 100644 index 00000000..b5b9e029 --- /dev/null +++ b/common/reducers/config/nodes/staticNodes.ts @@ -0,0 +1,91 @@ +import { EtherscanNode, InfuraNode, RPCNode } from 'libs/nodes'; +import { TypeKeys, NodeAction } from 'actions/config'; +import { NonWeb3NodeConfigs, Web3NodeConfigs } from 'types/node'; + +export type State = NonWeb3NodeConfigs & Web3NodeConfigs; + +export const INITIAL_STATE: State = { + 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_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 + } +}; + +export const staticNodes = (state: State = INITIAL_STATE, action: NodeAction) => { + switch (action.type) { + case TypeKeys.CONFIG_NODE_WEB3_SET: + return { ...state, [action.payload.id]: action.payload.config }; + case TypeKeys.CONFIG_NODE_WEB3_UNSET: + const stateCopy = { ...state }; + Reflect.deleteProperty(stateCopy, 'web3'); + return stateCopy; + default: + return state; + } +}; diff --git a/common/reducers/customTokens.ts b/common/reducers/customTokens.ts index 3ebca005..23780834 100644 --- a/common/reducers/customTokens.ts +++ b/common/reducers/customTokens.ts @@ -4,7 +4,7 @@ import { RemoveCustomTokenAction } from 'actions/customTokens'; import { TypeKeys } from 'actions/customTokens/constants'; -import { Token } from 'config'; +import { Token } from 'types/network'; export type State = Token[]; diff --git a/common/reducers/index.ts b/common/reducers/index.ts index 73122d55..e29992df 100644 --- a/common/reducers/index.ts +++ b/common/reducers/index.ts @@ -28,7 +28,7 @@ export interface AppState { transaction: TransactionState; } -export default combineReducers({ +export default combineReducers({ config, swap, notifications, diff --git a/common/sagas/config.ts b/common/sagas/config.ts deleted file mode 100644 index db184fce..00000000 --- a/common/sagas/config.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { delay, SagaIterator } from 'redux-saga'; -import { - call, - cancel, - fork, - put, - take, - takeLatest, - takeEvery, - select, - race -} from 'redux-saga/effects'; -import { - NODES, - NETWORKS, - NodeConfig, - CustomNodeConfig, - CustomNetworkConfig, - Web3Service -} from 'config'; -import { - makeCustomNodeId, - getCustomNodeConfigFromId, - makeNodeConfigFromCustomConfig -} from 'utils/node'; -import { makeCustomNetworkId, getNetworkConfigFromId } from 'utils/network'; -import { - getNode, - getNodeConfig, - getCustomNodeConfigs, - getCustomNetworkConfigs, - getOffline -} from 'selectors/config'; -import { AppState } from 'reducers'; -import { TypeKeys } from 'actions/config/constants'; -import { - toggleOfflineConfig, - changeNode, - changeNodeIntent, - setLatestBlock, - removeCustomNetwork, - AddCustomNodeAction, - ChangeNodeIntentAction -} from 'actions/config'; -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'; - -export const getConfig = (state: AppState): ConfigState => state.config; - -let hasCheckedOnline = false; -export function* pollOfflineStatus(): SagaIterator { - while (true) { - const node: NodeConfig = yield select(getNodeConfig); - const isOffline: boolean = yield select(getOffline); - - // 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) { - const { pingSucceeded } = yield race({ - pingSucceeded: call(node.lib.ping.bind(node.lib)), - timeout: call(delay, 5000) - }); - - if (pingSucceeded && isOffline) { - // If we were able to ping but redux says we're offline, mark online - yield put( - showNotification('success', 'Your connection to the network has been restored!', 3000) - ); - yield put(toggleOfflineConfig()); - } else if (!pingSucceeded && !isOffline) { - // If we were unable to ping but redux says we're online, mark offline - // 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); - } - } -} - -// Fork our recurring API call, watch for the need to cancel. -export function* handlePollOfflineStatus(): SagaIterator { - const pollOfflineStatusTask = yield fork(pollOfflineStatus); - yield take('CONFIG_STOP_POLL_OFFLINE_STATE'); - yield cancel(pollOfflineStatusTask); -} - -// @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 { - setTimeout(() => location.reload(), 1150); -} - -export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIterator { - 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: CustomNodeConfig[] = yield select(getCustomNodeConfigs); - const config = getCustomNodeConfigFromId(action.payload, customConfigs); - if (config) { - actionConfig = makeNodeConfigFromCustomConfig(config); - } - } - - 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 - // Give it 5 seconds before we call it offline - let latestBlock; - let timeout; - try { - const { lb, to } = yield race({ - lb: call(actionConfig.lib.getCurrentBlock.bind(actionConfig.lib)), - to: call(delay, 5000) - }); - latestBlock = lb; - timeout = to; - } catch (err) { - // Whether it times out or errors, same message - timeout = true; - } - - if (timeout) { - 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, actionNetwork)); - - // 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 - - // 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 && currentConfig.network !== actionConfig.network) { - - const isNewNetwork = currentConfig.network !== actionConfig.network; - const newIsWeb3 = actionConfig.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); - } -} - -export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator { - const nodeId = makeCustomNodeId(action.payload); - yield put(changeNodeIntent(nodeId)); -} - -// If there are any orphaned custom networks, purge them -export function* cleanCustomNetworks(): SagaIterator { - const customNodes = yield select(getCustomNodeConfigs); - const customNetworks = yield select(getCustomNetworkConfigs); - const networksInUse = customNodes.reduce((prev, conf) => { - prev[conf.network] = true; - return prev; - }, {}); - - for (const net of customNetworks) { - if (!networksInUse[makeCustomNetworkId(net)]) { - yield put(removeCustomNetwork(net)); - } - } -} - -// unset web3 as the selected node if a non-web3 wallet has been selected -export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator { - const node = yield select(getNode); - const nodeConfig = yield select(getNodeConfig); - const newWallet = action.payload; - const isWeb3Wallet = newWallet instanceof Web3Wallet; - - if (node !== 'web3' || isWeb3Wallet) { - return; - } - - // switch back to a node with the same network as MetaMask/Mist - yield put(changeNodeIntent(equivalentNodeOrDefault(nodeConfig))); -} - -export function* unsetWeb3Node(): SagaIterator { - const node = yield select(getNode); - - if (node !== 'web3') { - return; - } - - const nodeConfig: NodeConfig = yield select(getNodeConfig); - const newNode = equivalentNodeOrDefault(nodeConfig); - - yield put(changeNodeIntent(newNode)); -} - -export const equivalentNodeOrDefault = (nodeConfig: NodeConfig) => { - 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); - yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload); - yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode); - yield takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, cleanCustomNetworks); - yield takeEvery(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node); - yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent); - yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3NodeOnWalletEvent); -} diff --git a/common/sagas/config/index.ts b/common/sagas/config/index.ts new file mode 100644 index 00000000..2f9a93d5 --- /dev/null +++ b/common/sagas/config/index.ts @@ -0,0 +1,9 @@ +import { network } from './network'; +import { node } from './node'; +import { web3 } from './web3'; +import { all } from 'redux-saga/effects'; +import { SagaIterator } from 'redux-saga'; + +export default function*(): SagaIterator { + yield all([...network, ...node, ...web3]); +} diff --git a/common/sagas/config/network.ts b/common/sagas/config/network.ts new file mode 100644 index 00000000..7f75e9c4 --- /dev/null +++ b/common/sagas/config/network.ts @@ -0,0 +1,30 @@ +import { select, takeEvery, put } from 'redux-saga/effects'; +import { getCustomNodeConfigs, getCustomNetworkConfigs } from 'selectors/config'; +import { removeCustomNetwork, TypeKeys } from 'actions/config'; +import { SagaIterator } from 'redux-saga'; +import { AppState } from 'reducers'; + +// If there are any orphaned custom networks, prune them +export function* pruneCustomNetworks(): SagaIterator { + const customNodes: AppState['config']['nodes']['customNodes'] = yield select( + getCustomNodeConfigs + ); + const customNetworks: AppState['config']['networks']['customNetworks'] = yield select( + getCustomNetworkConfigs + ); + + //construct lookup table of networks + + const linkedNetworks = Object.values(customNodes).reduce( + (networkMap, currentNode) => ({ ...networkMap, [currentNode.network]: true }), + {} + ); + + for (const currNetwork of Object.keys(customNetworks)) { + if (!linkedNetworks[currNetwork]) { + yield put(removeCustomNetwork({ id: currNetwork })); + } + } +} + +export const network = [takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, pruneCustomNetworks)]; diff --git a/common/sagas/config/node.ts b/common/sagas/config/node.ts new file mode 100644 index 00000000..23029509 --- /dev/null +++ b/common/sagas/config/node.ts @@ -0,0 +1,195 @@ +import { delay, SagaIterator } from 'redux-saga'; +import { + call, + cancel, + fork, + put, + take, + takeEvery, + select, + race, + apply, + takeLatest +} from 'redux-saga/effects'; +import { + getNodeId, + getNodeConfig, + getOffline, + isStaticNodeId, + getCustomNodeFromId, + getStaticNodeFromId, + getNetworkConfigById +} from 'selectors/config'; +import { TypeKeys } from 'actions/config/constants'; +import { + toggleOffline, + changeNode, + changeNodeIntent, + setLatestBlock, + AddCustomNodeAction, + ChangeNodeIntentAction +} from 'actions/config'; +import { showNotification } from 'actions/notifications'; +import { translateRaw } from 'translations'; +import { StaticNodeConfig, CustomNodeConfig, NodeConfig } from 'types/node'; +import { CustomNetworkConfig, StaticNetworkConfig } from 'types/network'; +import { Web3Service } from 'libs/nodes/web3'; + +let hasCheckedOnline = false; +export function* pollOfflineStatus(): SagaIterator { + while (true) { + const nodeConfig: StaticNodeConfig = yield select(getNodeConfig); + const isOffline: boolean = yield select(getOffline); + + // 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) { + const { pingSucceeded } = yield race({ + pingSucceeded: call(nodeConfig.lib.ping.bind(nodeConfig.lib)), + timeout: call(delay, 5000) + }); + + if (pingSucceeded && isOffline) { + // If we were able to ping but redux says we're offline, mark online + yield put( + showNotification('success', 'Your connection to the network has been restored!', 3000) + ); + yield put(toggleOffline()); + } else if (!pingSucceeded && !isOffline) { + // If we were unable to ping but redux says we're online, mark offline + // 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(toggleOffline()); + } else { + // If neither case was true, try again in 5s + yield call(delay, 5000); + } + hasCheckedOnline = true; + } else { + yield call(delay, 1000); + } + } +} + +// Fork our recurring API call, watch for the need to cancel. +export function* handlePollOfflineStatus(): SagaIterator { + const pollOfflineStatusTask = yield fork(pollOfflineStatus); + yield take('CONFIG_STOP_POLL_OFFLINE_STATE'); + yield cancel(pollOfflineStatusTask); +} + +// @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 { + setTimeout(() => location.reload(), 1150); +} + +export function* handleNodeChangeIntent({ + payload: nodeIdToSwitchTo +}: ChangeNodeIntentAction): SagaIterator { + const isStaticNode: boolean = yield select(isStaticNodeId, nodeIdToSwitchTo); + const currentConfig: NodeConfig = yield select(getNodeConfig); + + function* bailOut(message: string) { + const currentNodeId: string = yield select(getNodeId); + yield put(showNotification('danger', message, 5000)); + yield put(changeNode({ networkId: currentConfig.network, nodeId: currentNodeId })); + } + + let nextNodeConfig: CustomNodeConfig | StaticNodeConfig; + + if (!isStaticNode) { + const config: CustomNodeConfig | undefined = yield select( + getCustomNodeFromId, + nodeIdToSwitchTo + ); + + if (config) { + nextNodeConfig = config; + } else { + return yield* bailOut(`Attempted to switch to unknown node '${nodeIdToSwitchTo}'`); + } + } else { + nextNodeConfig = yield select(getStaticNodeFromId, nodeIdToSwitchTo); + } + + // Grab current block from the node, before switching, to confirm it's online + // Give it 5 seconds before we call it offline + let currentBlock; + let timeout; + try { + const { lb, to } = yield race({ + lb: apply(nextNodeConfig.lib, nextNodeConfig.lib.getCurrentBlock), + to: call(delay, 5000) + }); + currentBlock = lb; + timeout = to; + } catch (err) { + console.error(err); + // Whether it times out or errors, same message + timeout = true; + } + + if (timeout) { + return yield* bailOut(translateRaw('ERROR_32')); + } + + const nextNetwork: StaticNetworkConfig | CustomNetworkConfig = yield select( + getNetworkConfigById, + nextNodeConfig.network + ); + + if (!nextNetwork) { + return yield* bailOut( + `Unknown custom network for your node '${nodeIdToSwitchTo}', try re-adding it` + ); + } + + yield put(setLatestBlock(currentBlock)); + yield put(changeNode({ networkId: nextNodeConfig.network, nodeId: 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 + + // 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 && currentConfig.network !== actionConfig.network) { + + 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); + } +} + +export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator { + yield put(changeNodeIntent(action.payload.id)); +} + +export const node = [ + takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent), + takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus), + takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload), + takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode) +]; diff --git a/common/sagas/config/web3.ts b/common/sagas/config/web3.ts new file mode 100644 index 00000000..7853c412 --- /dev/null +++ b/common/sagas/config/web3.ts @@ -0,0 +1,57 @@ +import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants'; +import { Web3Wallet } from 'libs/wallet'; +import { SagaIterator } from 'redux-saga'; +import { select, put, takeEvery, call } from 'redux-saga/effects'; +import { changeNodeIntent, TypeKeys, web3SetNode } from 'actions/config'; +import { getNodeId, getStaticAltNodeIdToWeb3, getNetworkNameByChainId } from 'selectors/config'; +import { setupWeb3Node, Web3Service } from 'libs/nodes/web3'; +import { Web3NodeConfig } from 'types/node'; + +export function* initWeb3Node(): SagaIterator { + const { networkId, lib } = yield call(setupWeb3Node); + const network = yield select(getNetworkNameByChainId, networkId); + + const config: Web3NodeConfig = { + isCustom: false, + network, + service: Web3Service, + lib, + estimateGas: false, + hidden: true + }; + + yield put(web3SetNode({ id: 'web3', config })); +} + +// unset web3 as the selected node if a non-web3 wallet has been selected +export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator { + const node = yield select(getNodeId); + const newWallet = action.payload; + const isWeb3Wallet = newWallet instanceof Web3Wallet; + + if (node !== 'web3' || isWeb3Wallet) { + return; + } + + const altNode = yield select(getStaticAltNodeIdToWeb3); + // switch back to a node with the same network as MetaMask/Mist + yield put(changeNodeIntent(altNode)); +} + +export function* unsetWeb3Node(): SagaIterator { + const node = yield select(getNodeId); + + if (node !== 'web3') { + return; + } + + const altNode = yield select(getStaticAltNodeIdToWeb3); + // switch back to a node with the same network as MetaMask/Mist + yield put(changeNodeIntent(altNode)); +} + +export const web3 = [ + takeEvery(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node), + takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent), + takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3NodeOnWalletEvent) +]; diff --git a/common/sagas/deterministicWallets.ts b/common/sagas/deterministicWallets.ts index a6818c6f..d4e7164b 100644 --- a/common/sagas/deterministicWallets.ts +++ b/common/sagas/deterministicWallets.ts @@ -5,7 +5,6 @@ import { updateDeterministicWallet } from 'actions/deterministicWallets'; import { showNotification } from 'actions/notifications'; -import { Token } from 'config'; import { publicToAddress, toChecksumAddress } from 'ethereumjs-util'; import HDKey from 'hdkey'; import { INode } from 'libs/nodes/INode'; @@ -16,6 +15,7 @@ import { getDesiredToken, getWallets } from 'selectors/deterministicWallets'; import { getTokens } from 'selectors/wallet'; import translate from 'translations'; import { TokenValue } from 'libs/units'; +import { Token } from 'types/network'; export function* getDeterministicWallets(action: GetDeterministicWalletsAction): SagaIterator { const { seed, dPath, publicKey, chainCode, limit, offset } = action.payload; diff --git a/common/sagas/transaction/signing/helpers.ts b/common/sagas/transaction/signing/helpers.ts index 307f54c6..8281be0d 100644 --- a/common/sagas/transaction/signing/helpers.ts +++ b/common/sagas/transaction/signing/helpers.ts @@ -11,9 +11,9 @@ import { SignTransactionRequestedAction } from 'actions/transaction'; import Tx from 'ethereumjs-tx'; -import { NetworkConfig } from 'config'; import { SagaIterator } from 'redux-saga'; import { showNotification } from 'actions/notifications'; +import { StaticNetworkConfig } from 'types/network'; interface IFullWalletAndTransaction { wallet: IFullWallet; @@ -46,7 +46,7 @@ function* getWalletAndTransaction(partialTx: SignTransactionRequestedAction['pay throw Error('Could not get wallet instance to sign transaction'); } // get the chainId - const { chainId }: NetworkConfig = yield select(getNetworkConfig); + const { chainId }: StaticNetworkConfig = yield select(getNetworkConfig); // get the rest of the transaction parameters partialTx._chainId = chainId; diff --git a/common/sagas/wallet/helpers.ts b/common/sagas/wallet/helpers.ts index 65044417..6c2041c6 100644 --- a/common/sagas/wallet/helpers.ts +++ b/common/sagas/wallet/helpers.ts @@ -1,6 +1,5 @@ import { apply, select, call } from 'redux-saga/effects'; import { AppState } from 'reducers'; -import { Token } from 'config'; import { INode } from 'libs/nodes/INode'; import { IWallet, WalletConfig } from 'libs/wallet'; import { TokenBalance } from 'selectors/wallet'; @@ -8,6 +7,7 @@ import { getCustomTokens } from 'selectors/customTokens'; import { getNodeLib } from 'selectors/config'; import { loadWalletConfig } from 'utils/localStorage'; import { TokenBalanceLookup } from './wallet'; +import { Token } from 'types/network'; export function* getTokenBalances(wallet: IWallet, tokens: Token[]) { const node: INode = yield select(getNodeLib); diff --git a/common/sagas/wallet/wallet.ts b/common/sagas/wallet/wallet.ts index 78e4eb69..02b94d58 100644 --- a/common/sagas/wallet/wallet.ts +++ b/common/sagas/wallet/wallet.ts @@ -36,10 +36,9 @@ import { Web3Wallet, WalletConfig } from 'libs/wallet'; -import { NODES, initWeb3Node, Token } from 'config'; 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 +50,9 @@ import translate from 'translations'; 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'; +import { initWeb3Node } from 'sagas/config/web3'; export interface TokenBalanceLookup { [symbol: string]: TokenBalance; @@ -262,14 +264,15 @@ export function* unlockWeb3(): SagaIterator { yield put(changeNodeIntent('web3')); yield take( action => - action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeSelection === 'web3' + action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeId === '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.'); @@ -284,6 +287,7 @@ export function* unlockWeb3(): SagaIterator { const wallet = new Web3Wallet(address, network); yield put(setWallet(wallet)); } catch (err) { + console.error(err); // unset web3 node so node dropdown isn't disabled yield put(web3UnsetNode()); yield put(showNotification('danger', translate(err.message))); diff --git a/common/selectors/config.ts b/common/selectors/config.ts deleted file mode 100644 index e9fd8f4e..00000000 --- a/common/selectors/config.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - CustomNetworkConfig, - CustomNodeConfig, - NetworkConfig, - NetworkContract, - NodeConfig, - Token -} from 'config'; -import { INode } from 'libs/nodes/INode'; -import { AppState } from 'reducers'; -import { getUnit } from 'selectors/transaction/meta'; -import { isEtherUnit } from 'libs/units'; -import { SHAPESHIFT_TOKEN_WHITELIST } from 'api/shapeshift'; - -export function getNode(state: AppState): string { - return state.config.nodeSelection; -} - -export function getIsWeb3Node(state: AppState): boolean { - return getNode(state) === 'web3'; -} - -export function getNodeConfig(state: AppState): NodeConfig { - return state.config.node; -} - -export function getNodeLib(state: AppState): INode { - return getNodeConfig(state).lib; -} - -export function getNetworkConfig(state: AppState): NetworkConfig { - return state.config.network; -} - -export function getNetworkContracts(state: AppState): NetworkContract[] | null { - const network = getNetworkConfig(state); - return network ? network.contracts : []; -} - -export function getNetworkTokens(state: AppState): Token[] { - const network = getNetworkConfig(state); - return network ? network.tokens : []; -} - -export function getAllTokens(state: AppState): Token[] { - const networkTokens = getNetworkTokens(state); - return networkTokens.concat(state.customTokens); -} - -export function getSelectedTokenContractAddress(state: AppState): string { - const allTokens = getAllTokens(state); - const currentUnit = getUnit(state); - - if (currentUnit === 'ether') { - return ''; - } - - return allTokens.reduce((tokenAddr, tokenInfo) => { - if (tokenAddr && tokenAddr.length) { - return tokenAddr; - } - - if (tokenInfo.symbol === currentUnit) { - return tokenInfo.address; - } - - return tokenAddr; - }, ''); -} - -export function tokenExists(state: AppState, token: string): boolean { - const existInWhitelist = SHAPESHIFT_TOKEN_WHITELIST.includes(token); - const existsInNetwork = !!getAllTokens(state).find(t => t.symbol === token); - return existsInNetwork || existInWhitelist; -} - -export function getLanguageSelection(state: AppState): string { - return state.config.languageSelection; -} - -export function getCustomNodeConfigs(state: AppState): CustomNodeConfig[] { - return state.config.customNodes; -} - -export function getCustomNetworkConfigs(state: AppState): CustomNetworkConfig[] { - return state.config.customNetworks; -} - -export function getOffline(state: AppState): boolean { - return state.config.offline; -} - -export function getAutoGasLimitEnabled(state: AppState): boolean { - return state.config.autoGasLimit; -} - -export function isSupportedUnit(state: AppState, unit: string) { - const isToken: boolean = tokenExists(state, unit); - const isEther: boolean = isEtherUnit(unit); - if (!isToken && !isEther) { - return false; - } - return true; -} diff --git a/common/selectors/config/index.ts b/common/selectors/config/index.ts new file mode 100644 index 00000000..bc685c38 --- /dev/null +++ b/common/selectors/config/index.ts @@ -0,0 +1,4 @@ +export * from './meta'; +export * from './networks'; +export * from './nodes'; +export * from './tokens'; diff --git a/common/selectors/config/meta.ts b/common/selectors/config/meta.ts new file mode 100644 index 00000000..17f54908 --- /dev/null +++ b/common/selectors/config/meta.ts @@ -0,0 +1,21 @@ +import { AppState } from 'reducers'; +const getConfig = (state: AppState) => state.config; + +export const getMeta = (state: AppState) => getConfig(state).meta; + +export function getOffline(state: AppState): boolean { + return getMeta(state).offline; +} + +export function getAutoGasLimitEnabled(state: AppState): boolean { + const meta = getMeta(state); + return meta.autoGasLimit; +} + +export function getLanguageSelection(state: AppState): string { + return getMeta(state).languageSelection; +} + +export function getLatestBlock(state: AppState) { + return getMeta(state).latestBlock; +} diff --git a/common/selectors/config/networks.ts b/common/selectors/config/networks.ts new file mode 100644 index 00000000..d9d355f6 --- /dev/null +++ b/common/selectors/config/networks.ts @@ -0,0 +1,76 @@ +import { AppState } from 'reducers'; +import { + CustomNetworkConfig, + StaticNetworkConfig, + StaticNetworkIds, + NetworkContract +} from 'types/network'; +import { getNodeConfig } from 'selectors/config'; +const getConfig = (state: AppState) => state.config; + +export const getNetworks = (state: AppState) => getConfig(state).networks; + +export const getNetworkConfigById = (state: AppState, networkId: string) => + isStaticNetworkId(state, networkId) + ? getStaticNetworkConfigs(state)[networkId] + : getCustomNetworkConfigs(state)[networkId]; + +export const getNetworkNameByChainId = (state: AppState, chainId: number | string) => { + const network = + Object.values(getStaticNetworkConfigs(state)).find(n => +n.chainId === +chainId) || + Object.values(getCustomNetworkConfigs(state)).find(n => +n.chainId === +chainId); + if (!network) { + return null; + } + return network.name; +}; + +export const getStaticNetworkIds = (state: AppState): StaticNetworkIds[] => + Object.keys(getNetworks(state).staticNetworks) as StaticNetworkIds[]; + +export const isStaticNetworkId = ( + state: AppState, + networkId: string +): networkId is StaticNetworkIds => Object.keys(getStaticNetworkConfigs(state)).includes(networkId); + +export const getStaticNetworkConfig = (state: AppState): StaticNetworkConfig | undefined => { + const selectedNetwork = getSelectedNetwork(state); + + const { staticNetworks } = getNetworks(state); + + const defaultNetwork = isStaticNetworkId(state, selectedNetwork) + ? staticNetworks[selectedNetwork] + : undefined; + return defaultNetwork; +}; + +export const getSelectedNetwork = (state: AppState) => getNodeConfig(state).network; + +export const getCustomNetworkConfig = (state: AppState): CustomNetworkConfig | undefined => { + const selectedNetwork = getSelectedNetwork(state); + const { customNetworks } = getNetworks(state); + const customNetwork = customNetworks[selectedNetwork]; + return customNetwork; +}; + +export const getNetworkConfig = (state: AppState): StaticNetworkConfig | CustomNetworkConfig => { + const config = getStaticNetworkConfig(state) || getCustomNetworkConfig(state); + + if (!config) { + const selectedNetwork = getSelectedNetwork(state); + + throw Error( + `No network config found for ${selectedNetwork} in either static or custom networks` + ); + } + return config; +}; + +export const getNetworkContracts = (state: AppState): NetworkContract[] | null => { + const network = getStaticNetworkConfig(state); + return network ? network.contracts : []; +}; + +export const getCustomNetworkConfigs = (state: AppState) => getNetworks(state).customNetworks; + +export const getStaticNetworkConfigs = (state: AppState) => getNetworks(state).staticNetworks; diff --git a/common/selectors/config/nodes.ts b/common/selectors/config/nodes.ts new file mode 100644 index 00000000..f6e6366b --- /dev/null +++ b/common/selectors/config/nodes.ts @@ -0,0 +1,176 @@ +import { AppState } from 'reducers'; +import { + getStaticNetworkConfigs, + getCustomNetworkConfigs, + isStaticNetworkId +} from 'selectors/config'; +import { + CustomNodeConfig, + StaticNodeConfig, + StaticNodeId, + Web3NodeConfig, + StaticNodeWithWeb3Id +} from 'types/node'; + +const getConfig = (state: AppState) => state.config; + +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, nodeId: string): CustomNodeConfig | undefined { + return getCustomNodeConfigs(state)[nodeId]; +} + +export const getCustomNodeFromId = ( + state: AppState, + nodeId: string +): CustomNodeConfig | undefined => getCustomNodeConfigs(state)[nodeId]; + +export const getStaticAltNodeIdToWeb3 = (state: AppState) => { + const { web3, ...configs } = getStaticNodeConfigs(state); + if (!web3) { + return SELECTED_NODE_INITIAL_STATE.nodeId; + } + const res = Object.entries(configs).find( + ([_, config]: [StaticNodeId, StaticNodeConfig]) => web3.network === config.network + ); + if (res) { + return res[0]; + } + return SELECTED_NODE_INITIAL_STATE.nodeId; +}; + +export const getStaticNodeFromId = (state: AppState, nodeId: StaticNodeId) => + getStaticNodeConfigs(state)[nodeId]; + +export const isStaticNodeId = (state: AppState, nodeId: string): nodeId is StaticNodeWithWeb3Id => + Object.keys(getStaticNodeConfigs(state)).includes(nodeId); + +const getStaticNodeConfigs = (state: AppState) => getNodes(state).staticNodes; + +export const getStaticNodeConfig = (state: AppState) => { + const { staticNodes, selectedNode: { nodeId } } = getNodes(state); + + const defaultNetwork = isStaticNodeId(state, nodeId) ? staticNodes[nodeId] : undefined; + return defaultNetwork; +}; + +export const getWeb3Node = (state: AppState): Web3NodeConfig | null => { + const isWeb3Node = (nodeId: string, _: StaticNodeConfig | Web3NodeConfig): _ is Web3NodeConfig => + nodeId === 'web3'; + const currNode = getStaticNodeConfig(state); + const currNodeId = getNodeId(state); + if (currNode && currNodeId && isWeb3Node(currNodeId, currNode)) { + return currNode; + } + return null; +}; + +export const getCustomNodeConfig = (state: AppState): CustomNodeConfig | undefined => { + const { customNodes, selectedNode: { nodeId } } = getNodes(state); + + const customNode = customNodes[nodeId]; + return customNode; +}; + +export function getCustomNodeConfigs(state: AppState) { + return getNodes(state).customNodes; +} + +export function getStaticNodes(state: AppState) { + return getNodes(state).staticNodes; +} + +export function getSelectedNode(state: AppState) { + return getNodes(state).selectedNode; +} + +export function isNodeChanging(state): boolean { + return getSelectedNode(state).pending; +} + +export function getNodeId(state: AppState): string { + return getSelectedNode(state).nodeId; +} + +export function getIsWeb3Node(state: AppState): boolean { + return getNodeId(state) === 'web3'; +} + +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.nodeId} in either static or custom nodes`); + } + return config; +} + +export function getNodeLib(state: AppState) { + const config = getNodeConfig(state); + if (!config) { + throw Error('No node lib found when trying to select from state'); + } + return config.lib; +} + +export interface NodeOption { + isCustom: false; + value: string; + name: { networkId?: string; service: string }; + color?: string; + hidden?: boolean; +} + +export function getStaticNodeOptions(state: AppState): NodeOption[] { + const staticNetworkConfigs = getStaticNetworkConfigs(state); + return Object.entries(getStaticNodes(state)).map(([nodeId, node]: [string, StaticNodeConfig]) => { + const networkId = node.network; + const associatedNetwork = staticNetworkConfigs[networkId]; + const opt: NodeOption = { + isCustom: node.isCustom, + value: nodeId, + name: { networkId, service: node.service }, + color: associatedNetwork.color, + hidden: node.hidden + }; + return opt; + }); +} + +export interface CustomNodeOption { + isCustom: true; + id: string; + value: string; + name: { networkId?: string; nodeId: 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( + ([nodeId, node]: [string, CustomNodeConfig]) => { + const networkId = node.network; + const associatedNetwork = isStaticNetworkId(state, networkId) + ? staticNetworkConfigs[networkId] + : customNetworkConfigs[networkId]; + const opt: CustomNodeOption = { + isCustom: node.isCustom, + value: node.id, + name: { networkId, nodeId }, + color: associatedNetwork.isCustom ? undefined : associatedNetwork.color, + hidden: false, + id: node.id + }; + return opt; + } + ); +} + +export function getNodeOptions(state: AppState) { + return [...getStaticNodeOptions(state), ...getCustomNodeOptions(state)]; +} diff --git a/common/selectors/config/tokens.ts b/common/selectors/config/tokens.ts new file mode 100644 index 00000000..a26fd2eb --- /dev/null +++ b/common/selectors/config/tokens.ts @@ -0,0 +1,52 @@ +import { AppState } from 'reducers'; +import { getUnit } from 'selectors/transaction/meta'; +import { isEtherUnit } from 'libs/units'; +import { SHAPESHIFT_TOKEN_WHITELIST } from 'api/shapeshift'; +import { getStaticNetworkConfig } from 'selectors/config'; +import { Token } from 'types/network'; + +export function getNetworkTokens(state: AppState): Token[] { + const network = getStaticNetworkConfig(state); + return network ? network.tokens : []; +} + +export function getAllTokens(state: AppState): Token[] { + const networkTokens = getNetworkTokens(state); + return networkTokens.concat(state.customTokens); +} + +export function getSelectedTokenContractAddress(state: AppState): string { + const allTokens = getAllTokens(state); + const currentUnit = getUnit(state); + + if (isEtherUnit(currentUnit)) { + return ''; + } + + return allTokens.reduce((tokenAddr, tokenInfo) => { + if (tokenAddr && tokenAddr.length) { + return tokenAddr; + } + + if (tokenInfo.symbol === currentUnit) { + return tokenInfo.address; + } + + return tokenAddr; + }, ''); +} + +export function tokenExists(state: AppState, token: string): boolean { + const existInWhitelist = SHAPESHIFT_TOKEN_WHITELIST.includes(token); + const existsInNetwork = !!getAllTokens(state).find(t => t.symbol === token); + return existsInNetwork || existInWhitelist; +} + +export function isSupportedUnit(state: AppState, unit: string) { + const isToken: boolean = tokenExists(state, unit); + const isEther: boolean = isEtherUnit(unit); + if (!isToken && !isEther) { + return false; + } + return true; +} diff --git a/common/selectors/config/wallet.ts b/common/selectors/config/wallet.ts new file mode 100644 index 00000000..2afc86bd --- /dev/null +++ b/common/selectors/config/wallet.ts @@ -0,0 +1,82 @@ +import { InsecureWalletName, SecureWalletName, WalletName, walletNames } from 'config'; +import { EXTRA_PATHS } from 'config/dpaths'; +import sortedUniq from 'lodash/sortedUniq'; +import difference from 'lodash/difference'; +import { StaticNetworkConfig, DPathFormats } from 'types/network'; +import { AppState } from 'reducers'; +import { getStaticNetworkConfigs, getStaticNetworkConfig } from 'selectors/config'; + +type PathType = keyof DPathFormats; + +type DPathFormat = + | SecureWalletName.TREZOR + | SecureWalletName.LEDGER_NANO_S + | InsecureWalletName.MNEMONIC_PHRASE; + +export function getPaths(state: AppState, pathType: PathType): DPath[] { + const paths = Object.values(getStaticNetworkConfigs(state)) + .reduce( + (networkPaths: DPath[], { dPathFormats }) => + dPathFormats ? [...networkPaths, dPathFormats[pathType]] : networkPaths, + + [] + ) + .concat(EXTRA_PATHS); + return sortedUniq(paths); +} + +export function getSingleDPath(state: AppState, format: DPathFormat): DPath { + const network = getStaticNetworkConfig(state); + if (!network) { + throw Error('No static network config loaded'); + } + const dPathFormats = network.dPathFormats; + return dPathFormats[format]; +} + +export function isNetworkUnit(state: AppState, unit: string) { + const currentNetwork = getStaticNetworkConfig(state); + //TODO: logic check + if (!currentNetwork) { + return false; + } + const networks = getStaticNetworkConfigs(state); + const validNetworks = Object.values(networks).filter((n: StaticNetworkConfig) => n.unit === unit); + return validNetworks.includes(currentNetwork); +} + +export function isWalletFormatSupportedOnNetwork(state: AppState, format: WalletName): boolean { + const network = getStaticNetworkConfig(state); + + const CHECK_FORMATS: DPathFormat[] = [ + SecureWalletName.LEDGER_NANO_S, + SecureWalletName.TREZOR, + InsecureWalletName.MNEMONIC_PHRASE + ]; + + const isHDFormat = (f: string): f is DPathFormat => CHECK_FORMATS.includes(f as DPathFormat); + + // Ensure DPath's are found + if (isHDFormat(format)) { + if (!network) { + return false; + } + const dPath = network.dPathFormats && network.dPathFormats[format]; + return !!dPath; + } + + // Ensure Web3 is only enabled on ETH or ETH Testnets (MetaMask does not support other networks) + if (format === SecureWalletName.WEB3) { + return isNetworkUnit(state, 'ETH'); + } + + // All other wallet formats are supported + return true; +} + +export function unSupportedWalletFormatsOnNetwork(state: AppState): WalletName[] { + const supportedFormats = walletNames.filter(walletName => + isWalletFormatSupportedOnNetwork(state, walletName) + ); + return difference(walletNames, supportedFormats); +} diff --git a/common/selectors/wallet.ts b/common/selectors/wallet.ts index 6156f81e..4caae15f 100644 --- a/common/selectors/wallet.ts +++ b/common/selectors/wallet.ts @@ -1,11 +1,12 @@ import { TokenValue, Wei } from 'libs/units'; -import { Token, SecureWalletName, WalletName } from 'config'; +import { SecureWalletName, WalletName } from 'config'; import { AppState } from 'reducers'; -import { getNetworkConfig, getOffline } from 'selectors/config'; +import { getNetworkConfig, getOffline, getStaticNetworkConfig } from 'selectors/config'; import { IWallet, Web3Wallet, LedgerWallet, TrezorWallet, WalletConfig } from 'libs/wallet'; import { isEtherTransaction, getUnit } from './transaction'; -import { unSupportedWalletFormatsOnNetwork } from 'utils/network'; import { DisabledWallets } from 'components/WalletDecrypt'; +import { Token } from 'types/network'; +import { unSupportedWalletFormatsOnNetwork } from 'selectors/config/wallet'; export function getWalletInst(state: AppState): IWallet | null | undefined { return state.wallet.inst; @@ -32,7 +33,7 @@ export type MergedToken = Token & { }; export function getTokens(state: AppState): MergedToken[] { - const network = getNetworkConfig(state); + const network = getStaticNetworkConfig(state); const tokens: Token[] = network ? network.tokens : []; return tokens.concat( state.customTokens.map((token: Token) => { @@ -164,7 +165,7 @@ export function getDisabledWallets(state: AppState): DisabledWallets { // Some wallets don't support some networks addReason( - unSupportedWalletFormatsOnNetwork(network), + unSupportedWalletFormatsOnNetwork(state), `${network.name} does not support this wallet` ); diff --git a/common/store.ts b/common/store.ts deleted file mode 100644 index 603a7d90..00000000 --- a/common/store.ts +++ /dev/null @@ -1,128 +0,0 @@ -import throttle from 'lodash/throttle'; -import { routerMiddleware } from 'react-router-redux'; -import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config'; -import { - State as CustomTokenState, - INITIAL_STATE as customTokensInitialState -} from 'reducers/customTokens'; -import { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap'; -import { applyMiddleware, createStore } from 'redux'; -import { composeWithDevTools } from 'redux-devtools-extension'; -import { createLogger } from 'redux-logger'; -import createSagaMiddleware from 'redux-saga'; -import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage'; -import RootReducer, { AppState } from './reducers'; -import { getNodeConfigFromId } from 'utils/node'; -import { getNetworkConfigFromId } from 'utils/network'; -import { dedupeCustomTokens } from 'utils/tokens'; -import sagas from './sagas'; - -const configureStore = () => { - const logger = createLogger({ - collapsed: true - }); - const sagaMiddleware = createSagaMiddleware(); - let middleware; - let store; - - if (process.env.NODE_ENV !== 'production') { - middleware = composeWithDevTools( - applyMiddleware(sagaMiddleware, logger, routerMiddleware(history as any)) - ); - } else { - middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history as any)); - } - - const localSwapState = loadStatePropertyOrEmptyObject('swap'); - const swapState = - localSwapState && localSwapState.step === 3 - ? { - ...swapInitialState, - ...localSwapState - } - : { ...swapInitialState }; - - const savedConfigState = loadStatePropertyOrEmptyObject('config'); - - // 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) { - const savedNode = getNodeConfigFromId( - savedConfigState.nodeSelection, - savedConfigState.customNodes - ); - // 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; - } - } - - // 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, - ...savedConfigState - }, - customTokens, - // ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3 - swap: swapState - }; - // if 'web3' has persisted as node selection, reset to app default - // necessary because web3 is only initialized as a node upon MetaMask / Mist unlock - if (persistedInitialState.config.nodeSelection === 'web3') { - persistedInitialState.config.nodeSelection = configInitialState.nodeSelection; - } - - store = createStore(RootReducer, persistedInitialState, middleware); - - // Add all of the sagas to the middleware - Object.keys(sagas).forEach(saga => { - sagaMiddleware.run(sagas[saga]); - }); - - store.subscribe( - throttle(() => { - const state: AppState = store.getState(); - saveState({ - config: { - nodeSelection: state.config.nodeSelection, - languageSelection: state.config.languageSelection, - customNodes: state.config.customNodes, - customNetworks: state.config.customNetworks - }, - swap: { - ...state.swap, - options: { - byId: {}, - allIds: [] - }, - bityRates: { - byId: {}, - allIds: [] - }, - shapeshiftRates: { - byId: {}, - allIds: [] - } - }, - customTokens: state.customTokens - }); - }, 50) - ); - - return store; -}; - -export const configuredStore = configureStore(); diff --git a/common/store/configAndTokens.ts b/common/store/configAndTokens.ts new file mode 100644 index 00000000..7dd7ae0d --- /dev/null +++ b/common/store/configAndTokens.ts @@ -0,0 +1,162 @@ +import { State as ConfigState, config } from 'reducers/config'; +import { dedupeCustomTokens } from 'utils/tokens'; +import { + State as CustomTokenState, + INITIAL_STATE as customTokensInitialState +} from 'reducers/customTokens'; +import { loadStatePropertyOrEmptyObject } from 'utils/localStorage'; +import { + isStaticNodeId, + isStaticNetworkId, + getLanguageSelection, + getCustomNodeConfigs, + getSelectedNode, + getCustomNetworkConfigs +} from 'selectors/config'; +import RootReducer, { AppState } from 'reducers'; +import CustomNode from 'libs/nodes/custom'; +import { CustomNodeConfig } from 'types/node'; +const appInitialState = RootReducer(undefined as any, { type: 'inital_state' }); + +type DeepPartial = { [P in keyof T]?: DeepPartial }; +export function getConfigAndCustomTokensStateToSubscribe( + state: AppState +): Pick, 'config' | 'customTokens'> { + const subscribedConfig: DeepPartial = { + meta: { languageSelection: getLanguageSelection(state) }, + nodes: { customNodes: getCustomNodeConfigs(state), selectedNode: getSelectedNode(state) }, + networks: { + customNetworks: getCustomNetworkConfigs(state) + } + }; + + const subscribedTokens = state.customTokens; + + return { config: subscribedConfig, customTokens: subscribedTokens }; +} + +export function rehydrateConfigAndCustomTokenState() { + const configInitialState = config(undefined as any, { type: 'inital_state' }); + const savedConfigState = loadStatePropertyOrEmptyObject('config'); + const nextConfigState = { ...configInitialState }; + + // 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) { + // we assign networks first so that when we re-hydrate custom nodes, we can check that the network exists + nextConfigState.networks = rehydrateNetworks( + configInitialState.networks, + savedConfigState.networks + ); + nextConfigState.nodes = rehydrateNodes( + configInitialState.nodes, + savedConfigState.nodes, + nextConfigState.networks + ); + nextConfigState.meta = { ...nextConfigState.meta, ...savedConfigState.meta }; + } + + const { customNodes, selectedNode: { nodeId }, staticNodes } = nextConfigState.nodes; + const selectedNode = isStaticNodeId(appInitialState, nodeId) + ? staticNodes[nodeId] + : customNodes[nodeId]; + + if (!selectedNode) { + return { config: configInitialState, customTokens: customTokensInitialState }; + } + + const nextCustomTokenState = rehydrateCustomTokens( + nextConfigState.networks, + selectedNode.network + ); + + return { config: nextConfigState, customTokens: nextCustomTokenState }; +} + +function rehydrateCustomTokens(networkState: ConfigState['networks'], selectedNetwork: string) { + // Dedupe custom tokens initially + const savedCustomTokensState = + loadStatePropertyOrEmptyObject('customTokens') || customTokensInitialState; + + const { customNetworks, staticNetworks } = networkState; + const network = isStaticNetworkId(appInitialState, selectedNetwork) + ? staticNetworks[selectedNetwork] + : customNetworks[selectedNetwork]; + + return network.isCustom + ? savedCustomTokensState + : dedupeCustomTokens(network.tokens, savedCustomTokensState); +} + +function rehydrateNetworks( + initialState: ConfigState['networks'], + savedState: ConfigState['networks'] +): ConfigState['networks'] { + const nextNetworkState = { ...initialState }; + nextNetworkState.customNetworks = savedState.customNetworks; + return nextNetworkState; +} + +function rehydrateNodes( + initalState: ConfigState['nodes'], + savedState: ConfigState['nodes'], + networkState: ConfigState['networks'] +): ConfigState['nodes'] { + const nextNodeState = { ...initalState }; + + // re-assign the hydrated nodes + nextNodeState.customNodes = rehydrateCustomNodes(savedState.customNodes, networkState); + const { customNodes, staticNodes } = nextNodeState; + nextNodeState.selectedNode = getSavedSelectedNode( + nextNodeState.selectedNode, + savedState.selectedNode, + customNodes, + staticNodes + ); + return nextNodeState; +} + +function getSavedSelectedNode( + initialState: ConfigState['nodes']['selectedNode'], + savedState: ConfigState['nodes']['selectedNode'], + customNodes: ConfigState['nodes']['customNodes'], + staticNodes: ConfigState['nodes']['staticNodes'] +): ConfigState['nodes']['selectedNode'] { + const { nodeId: savedNodeId } = savedState; + + // if 'web3' has persisted as node selection, reset to app default + // necessary because web3 is only initialized as a node upon MetaMask / Mist unlock + + if (savedNodeId === 'web3') { + return { nodeId: initialState.nodeId, pending: false }; + } + + const nodeConfigExists = isStaticNodeId(appInitialState, savedNodeId) + ? staticNodes[savedNodeId] + : customNodes[savedNodeId]; + + return { nodeId: nodeConfigExists ? savedNodeId : initialState.nodeId, pending: false }; +} + +function rehydrateCustomNodes( + state: ConfigState['nodes']['customNodes'], + networkState: ConfigState['networks'] +) { + const networkExists = (networkId: string) => + Object.keys(networkState.customNetworks).includes(networkId) || + Object.keys(networkState.staticNetworks).includes(networkId); + + const rehydratedCustomNodes = Object.entries(state).reduce( + (hydratedNodes, [customNodeId, configToHydrate]) => { + if (!networkExists(configToHydrate.network)) { + return hydratedNodes; + } + + const lib = new CustomNode(configToHydrate); + const hydratedNode: CustomNodeConfig = { ...configToHydrate, lib }; + return { ...hydratedNodes, [customNodeId]: hydratedNode }; + }, + {} as ConfigState['nodes']['customNodes'] + ); + return rehydratedCustomNodes; +} diff --git a/common/store/index.ts b/common/store/index.ts new file mode 100644 index 00000000..d4068169 --- /dev/null +++ b/common/store/index.ts @@ -0,0 +1 @@ +export * from './store'; diff --git a/common/store/store.ts b/common/store/store.ts new file mode 100644 index 00000000..30661a76 --- /dev/null +++ b/common/store/store.ts @@ -0,0 +1,107 @@ +import throttle from 'lodash/throttle'; +import { routerMiddleware } from 'react-router-redux'; +import { + INITIAL_STATE as transactionInitialState, + State as TransactionState +} from 'reducers/transaction'; +import { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap'; +import { applyMiddleware, createStore } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import { createLogger } from 'redux-logger'; +import createSagaMiddleware from 'redux-saga'; +import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage'; +import RootReducer, { AppState } from 'reducers'; +import sagas from 'sagas'; +import { gasPricetoBase } from 'libs/units'; +import { + rehydrateConfigAndCustomTokenState, + getConfigAndCustomTokensStateToSubscribe +} from './configAndTokens'; + +const configureStore = () => { + const logger = createLogger({ + collapsed: true + }); + const sagaMiddleware = createSagaMiddleware(); + let middleware; + let store; + + if (process.env.NODE_ENV !== 'production') { + middleware = composeWithDevTools( + applyMiddleware(sagaMiddleware, logger, routerMiddleware(history as any)) + ); + } else { + middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history as any)); + } + + const localSwapState = loadStatePropertyOrEmptyObject('swap'); + const swapState = + localSwapState && localSwapState.step === 3 + ? { + ...swapInitialState, + ...localSwapState + } + : { ...swapInitialState }; + + const savedTransactionState = loadStatePropertyOrEmptyObject('transaction'); + + const persistedInitialState = { + transaction: { + ...transactionInitialState, + fields: { + ...transactionInitialState.fields, + gasPrice: + savedTransactionState && savedTransactionState.fields.gasPrice + ? { + raw: savedTransactionState.fields.gasPrice.raw, + value: gasPricetoBase(+savedTransactionState.fields.gasPrice.raw) + } + : transactionInitialState.fields.gasPrice + } + }, + + // ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3 + swap: swapState, + ...rehydrateConfigAndCustomTokenState() + }; + + store = createStore(RootReducer, persistedInitialState, middleware); + + // Add all of the sagas to the middleware + Object.keys(sagas).forEach(saga => { + sagaMiddleware.run(sagas[saga]); + }); + + store.subscribe( + throttle(() => { + const state: AppState = store.getState(); + saveState({ + transaction: { + fields: { + gasPrice: state.transaction.fields.gasPrice + } + }, + swap: { + ...state.swap, + options: { + byId: {}, + allIds: [] + }, + bityRates: { + byId: {}, + allIds: [] + }, + shapeshiftRates: { + byId: {}, + allIds: [] + } + }, + ...getConfigAndCustomTokensStateToSubscribe(state) + }); + }, 50) + ); + + return store; +}; + +export const configuredStore = configureStore(); diff --git a/common/utils/network.ts b/common/utils/network.ts deleted file mode 100644 index 82e8291f..00000000 --- a/common/utils/network.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - CustomNetworkConfig, - DPathFormats, - InsecureWalletName, - NetworkConfig, - NETWORKS, - SecureWalletName, - WalletName, - walletNames -} from 'config'; -import { DPath, EXTRA_PATHS } from 'config/dpaths'; -import sortedUniq from 'lodash/sortedUniq'; -import difference from 'lodash/difference'; - -export function makeCustomNetworkId(config: CustomNetworkConfig): string { - return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`; -} - -export function makeNetworkConfigFromCustomConfig(config: CustomNetworkConfig): NetworkConfig { - // TODO - re-enable this block and classify customConfig after user-inputted dPaths are implemented - // ------------------------------------------------- - // this still provides the type safety we want - // as we know config coming in is CustomNetworkConfig - // meaning name will be a string - // then we cast it as any to keep it as a network key - // interface Override extends NetworkConfig { - // name: any; - // } - // ------------------------------------------------- - - // TODO - allow for user-inputted dPaths so we don't need to use any below and can use supplied dPaths - // In the meantime, networks with an unknown chainId will have HD wallets disabled - const customConfig: any = { - ...config, - color: '#000', - tokens: [], - contracts: [] - }; - - return customConfig; -} - -export function getNetworkConfigFromId( - id: string, - configs: CustomNetworkConfig[] -): NetworkConfig | 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 = - | SecureWalletName.TREZOR - | SecureWalletName.LEDGER_NANO_S - | InsecureWalletName.MNEMONIC_PHRASE; - -export function getPaths(pathType: PathType): DPath[] { - const networkPaths: DPath[] = []; - Object.values(NETWORKS).forEach(networkConfig => { - const path = networkConfig.dPathFormats ? networkConfig.dPathFormats[pathType] : []; - if (path) { - networkPaths.push(path as DPath); - } - }); - const paths = networkPaths.concat(EXTRA_PATHS); - return sortedUniq(paths); -} - -export function getSingleDPath(format: DPathFormat, network: NetworkConfig): DPath { - const dPathFormats = network.dPathFormats; - return dPathFormats[format]; -} - -export function isNetworkUnit(network: NetworkConfig, unit: string) { - const validNetworks = Object.values(NETWORKS).filter((n: NetworkConfig) => n.unit === unit); - return validNetworks.includes(network); -} - -export function isWalletFormatSupportedOnNetwork( - format: WalletName, - network: NetworkConfig -): boolean { - const CHECK_FORMATS: DPathFormat[] = [ - SecureWalletName.LEDGER_NANO_S, - SecureWalletName.TREZOR, - InsecureWalletName.MNEMONIC_PHRASE - ]; - - const isHDFormat = (f: string): f is DPathFormat => CHECK_FORMATS.includes(f as DPathFormat); - - // Ensure DPath's are found - if (isHDFormat(format)) { - const dPath = network.dPathFormats && network.dPathFormats[format]; - return !!dPath; - } - - // Ensure Web3 is only enabled on ETH or ETH Testnets (MetaMask does not support other networks) - if (format === SecureWalletName.WEB3) { - return isNetworkUnit(network, 'ETH'); - } - - // All other wallet formats are supported - return true; -} - -export function allWalletFormatsSupportedOnNetwork(network: NetworkConfig): WalletName[] { - return walletNames.filter(walletName => isWalletFormatSupportedOnNetwork(walletName, network)); -} - -export function unSupportedWalletFormatsOnNetwork(network: NetworkConfig): WalletName[] { - const supportedFormats = allWalletFormatsSupportedOnNetwork(network); - return difference(walletNames, supportedFormats); -} diff --git a/common/utils/node.ts b/common/utils/node.ts deleted file mode 100644 index 948f934f..00000000 --- a/common/utils/node.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { CustomNode } from 'libs/nodes'; -import { NODES, NodeConfig, CustomNodeConfig } from 'config'; - -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[] -): NodeConfig | undefined { - if (NODES[id]) { - return NODES[id]; - } - - const config = getCustomNodeConfigFromId(id, configs); - if (config) { - return makeNodeConfigFromCustomConfig(config); - } -} - -export function makeNodeConfigFromCustomConfig(config: CustomNodeConfig): NodeConfig { - interface Override extends NodeConfig { - network: any; - } - - const customConfig: Override = { - network: config.network, - lib: new CustomNode(config), - service: 'your custom node', - estimateGas: true - }; - - return customConfig; -} diff --git a/common/utils/tokens.ts b/common/utils/tokens.ts index f39a482e..f092624f 100644 --- a/common/utils/tokens.ts +++ b/common/utils/tokens.ts @@ -1,4 +1,4 @@ -import { Token } from 'config'; +import { Token } from 'types/network'; export function dedupeCustomTokens(networkTokens: Token[], customTokens: Token[]): Token[] { if (!customTokens.length) { diff --git a/electron-app/main/menu.ts b/electron-app/main/menu.ts index 740342b0..d2655135 100644 --- a/electron-app/main/menu.ts +++ b/electron-app/main/menu.ts @@ -38,7 +38,7 @@ const HELP_MENU = { { label: 'Help / FAQ', click() { - shell.openExternal('https://support.mycrypto.com/'); + shell.openExternal('https://support.mycrypto.com/'); } }, { diff --git a/shared/types/hardware-wallets.d.ts b/shared/types/hardware-wallets.d.ts new file mode 100644 index 00000000..6f568370 --- /dev/null +++ b/shared/types/hardware-wallets.d.ts @@ -0,0 +1,4 @@ +interface DPath { + label: string; + value: string; // TODO determine method for more precise typing for path +} diff --git a/shared/types/network.d.ts b/shared/types/network.d.ts new file mode 100644 index 00000000..7280268c --- /dev/null +++ b/shared/types/network.d.ts @@ -0,0 +1,56 @@ +import { StaticNetworksState, CustomNetworksState } from 'reducers/config/networks'; + +type StaticNetworkIds = 'ETH' | 'Ropsten' | 'Kovan' | 'Rinkeby' | 'ETC' | 'UBQ' | 'EXP'; + +interface BlockExplorerConfig { + origin: string; + txUrl(txHash: string): string; + addressUrl(address: string): string; +} + +interface Token { + address: string; + symbol: string; + decimal: number; + error?: string | null; +} + +interface NetworkContract { + name: StaticNetworkIds; + address?: string; + abi: string; +} + +interface DPathFormats { + trezor: DPath; + ledgerNanoS: DPath; + mnemonicPhrase: DPath; +} + +interface StaticNetworkConfig { + isCustom: false; // used for type guards + name: StaticNetworkIds; + unit: string; + color?: string; + blockExplorer?: BlockExplorerConfig; + tokenExplorer?: { + name: string; + address(address: string): string; + }; + chainId: number; + tokens: Token[]; + contracts: NetworkContract[] | null; + dPathFormats: DPathFormats; + isTestnet?: boolean; +} + +interface CustomNetworkConfig { + isCustom: true; // used for type guards + isTestnet?: boolean; + name: string; + unit: string; + chainId: number; + dPathFormats: DPathFormats | null; +} + +type NetworkConfig = StaticNetworksState[StaticNetworkIds] | CustomNetworksState[string]; diff --git a/shared/types/node.d.ts b/shared/types/node.d.ts new file mode 100644 index 00000000..dc30d0bd --- /dev/null +++ b/shared/types/node.d.ts @@ -0,0 +1,55 @@ +import { RPCNode, Web3Node } from 'libs/nodes'; +import { StaticNetworkIds } 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; + auth?: { + username: string; + password: string; + }; +} + +interface StaticNodeConfig { + isCustom: false; + network: StaticNetworkIds; + lib: RPCNode | Web3Node; + service: string; + estimateGas?: boolean; + hidden?: boolean; +} + +interface Web3NodeConfig extends StaticNodeConfig { + lib: Web3Node; +} + +declare enum StaticNodeId { + ETH_MYCRYPTO = 'eth_mycrypto', + ETH_ETHSCAN = 'eth_ethscan', + ETH_INFURA = 'eth_infura', + ROP_INFURA = 'rop_infura', + KOV_ETHSCAN = 'kov_ethscan', + RIN_ETHSCAN = 'rin_ethscan', + RIN_INFURA = 'rin_infura', + ETC_EPOOL = 'etc_epool', + UBQ = 'ubq', + EXP_TECH = 'exp_tech' +} + +type StaticNodeWithWeb3Id = StaticNodeId | 'web3'; + +type NonWeb3NodeConfigs = { [key in StaticNodeId]: StaticNodeConfig }; + +interface Web3NodeConfigs { + web3?: Web3NodeConfig; +} + +type NodeConfig = StaticNodesState[StaticNodeId] | CustomNodesState[string]; diff --git a/spec/config/networks.spec.ts b/spec/config/networks.spec.ts deleted file mode 100644 index 5227ce5a..00000000 --- a/spec/config/networks.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NETWORKS, NetworkConfig } from 'config'; - -describe('Networks', () => { - Object.keys(NETWORKS).forEach(networkId => { - it(`${networkId} contains non-null dPathFormats`, () => { - const network: NetworkConfig = NETWORKS[networkId]; - Object.values(network.dPathFormats).forEach(dPathFormat => { - expect(dPathFormat).toBeTruthy(); - }); - }); - }); - - it(`contain unique chainIds`, () => { - const networkValues = Object.values(NETWORKS); - const chainIds = networkValues.map(a => a.chainId); - const chainIdsSet = new Set(chainIds); - expect(Array.from(chainIdsSet).length).toEqual(chainIds.length); - }); -}); diff --git a/spec/integration/RpcNodeTestConfig.js b/spec/integration/RpcNodeTestConfig.js index 998645c8..f9928c31 100644 --- a/spec/integration/RpcNodeTestConfig.js +++ b/spec/integration/RpcNodeTestConfig.js @@ -1,5 +1,5 @@ module.exports = { - RpcNodes: ['eth_mew', 'etc_epool', 'etc_epool', 'rop_mew'], + RpcNodes: ['eth_mycrypto', 'etc_epool', 'etc_epool', 'rop_mew'], EtherscanNodes: ['eth_ethscan', 'kov_ethscan', 'rin_ethscan'], InfuraNodes: ['eth_infura', 'rop_infura', 'rin_infura'] }; diff --git a/spec/integration/data.int.ts b/spec/integration/data.int.ts index 8cd56153..3a7db3f6 100644 --- a/spec/integration/data.int.ts +++ b/spec/integration/data.int.ts @@ -1,4 +1,3 @@ -import { NODES, NodeConfig } from 'config'; import { RPCNode } from '../../common/libs/nodes'; import { Validator } from 'jsonschema'; import { schema } from '../../common/libs/validators'; @@ -6,6 +5,8 @@ import 'url-search-params-polyfill'; import EtherscanNode from 'libs/nodes/etherscan'; import InfuraNode from 'libs/nodes/infura'; import RpcNodeTestConfig from './RpcNodeTestConfig'; +import { StaticNodeConfig } from 'types/node'; +import { staticNodesExpectedState } from '../reducers/config/nodes/staticNodes.spec'; const v = new Validator(); @@ -63,7 +64,7 @@ function testRpcRequests(node: RPCNode, service: string) { }); } -const mapNodeEndpoints = (nodes: { [key: string]: NodeConfig }) => { +const mapNodeEndpoints = (nodes: { [key: string]: StaticNodeConfig }) => { const { RpcNodes, EtherscanNodes, InfuraNodes } = RpcNodeTestConfig; RpcNodes.forEach(n => { @@ -79,4 +80,6 @@ const mapNodeEndpoints = (nodes: { [key: string]: NodeConfig }) => { }); }; -mapNodeEndpoints(NODES); +mapNodeEndpoints((staticNodesExpectedState.initialState as any) as { + [key: string]: StaticNodeConfig; +}); diff --git a/spec/pages/SendTransaction.spec.tsx b/spec/pages/SendTransaction.spec.tsx index cd48a794..e5503d77 100644 --- a/spec/pages/SendTransaction.spec.tsx +++ b/spec/pages/SendTransaction.spec.tsx @@ -4,21 +4,14 @@ import Adapter from 'enzyme-adapter-react-16'; import SendTransaction from 'containers/Tabs/SendTransaction'; import shallowWithStore from '../utils/shallowWithStore'; import { createMockStore } from 'redux-test-utils'; -import { NODES } from 'config'; import { RouteComponentProps } from 'react-router'; import { createMockRouteComponentProps } from '../utils/mockRouteComponentProps'; +import { config } from 'reducers/config'; Enzyme.configure({ adapter: new Adapter() }); it('render snapshot', () => { - const testNode = 'rop_mew'; - const testStateConfig = { - languageSelection: 'en', - nodeSelection: testNode, - node: NODES[testNode], - gasPriceGwei: 21, - offline: false - }; + const testStateConfig = config(undefined as any, {} as any); const testState = { wallet: {}, balance: {}, diff --git a/spec/pages/Swap.spec.tsx b/spec/pages/Swap.spec.tsx index 3928b607..a738eac2 100644 --- a/spec/pages/Swap.spec.tsx +++ b/spec/pages/Swap.spec.tsx @@ -5,7 +5,7 @@ import Swap from 'containers/Tabs/Swap'; import shallowWithStore from '../utils/shallowWithStore'; import { createMockStore } from 'redux-test-utils'; import { INITIAL_STATE as swap } from 'reducers/swap'; -import { INITIAL_STATE as config } from 'reducers/config'; +import { config } from 'reducers/config'; import { RouteComponentProps } from 'react-router'; import { createMockRouteComponentProps } from '../utils/mockRouteComponentProps'; @@ -22,7 +22,7 @@ const routeProps: RouteComponentProps = createMockRouteComponentProps({ }); it('render snapshot', () => { - const store = createMockStore({ swap, config }); + const store = createMockStore({ swap, config: config(undefined as any, {} as any) }); const component = shallowWithStore(, store); expect(component).toMatchSnapshot(); diff --git a/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap b/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap index 9d66e2cd..b79b9289 100644 --- a/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap +++ b/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap @@ -41,5 +41,6 @@ exports[`render snapshot 1`] = ` "url": "/account", } } + requestDisabled={false} /> `; diff --git a/spec/reducers/config.spec.ts b/spec/reducers/config.spec.ts deleted file mode 100644 index f4926f95..00000000 --- a/spec/reducers/config.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { config, INITIAL_STATE } from 'reducers/config'; -import * as configActions from 'actions/config'; -import { NODES, NETWORKS } from 'config'; -import { makeCustomNodeId, makeNodeConfigFromCustomConfig } from 'utils/node'; - -const custNode = { - name: 'Test Config', - url: 'https://somecustomconfig.org/', - port: 443, - network: 'ETH' -}; - -describe('config reducer', () => { - it('should handle CONFIG_LANGUAGE_CHANGE', () => { - const language = 'en'; - expect(config(undefined, configActions.changeLanguage(language))).toEqual({ - ...INITIAL_STATE, - languageSelection: language - }); - }); - - 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, node, network))).toEqual({ - ...INITIAL_STATE, - node: NODES[key], - nodeSelection: key - }); - }); - - it('should handle CONFIG_TOGGLE_OFFLINE', () => { - const offlineState = { - ...INITIAL_STATE, - offline: true - }; - - const onlineState = { - ...INITIAL_STATE, - offline: false - }; - - expect(config(offlineState, configActions.toggleOfflineConfig())).toEqual({ - ...offlineState, - offline: false - }); - - expect(config(onlineState, configActions.toggleOfflineConfig())).toEqual({ - ...onlineState, - offline: true - }); - }); - - it('should handle CONFIG_ADD_CUSTOM_NODE', () => { - expect(config(undefined, configActions.addCustomNode(custNode))).toEqual({ - ...INITIAL_STATE, - customNodes: [custNode] - }); - }); - - describe('should handle CONFIG_REMOVE_CUSTOM_NODE', () => { - const customNodeId = makeCustomNodeId(custNode); - const addedState = config(undefined, configActions.addCustomNode(custNode)); - const addedAndActiveState = config( - addedState, - configActions.changeNode( - customNodeId, - makeNodeConfigFromCustomConfig(custNode), - NETWORKS[custNode.network] - ) - ); - const removedState = config(addedAndActiveState, configActions.removeCustomNode(custNode)); - - it('should remove the custom node from `customNodes`', () => { - expect(removedState.customNodes.length).toBe(0); - }); - - it('should change the active node, if the custom one was active', () => { - expect(removedState.nodeSelection === customNodeId).toBeFalsy(); - }); - }); - - it('should handle CONFIG_SET_LATEST_BLOCK', () => { - expect(config(undefined, configActions.setLatestBlock('12345'))).toEqual({ - ...INITIAL_STATE, - latestBlock: '12345' - }); - }); -}); diff --git a/spec/sagas/__snapshots__/config.spec.ts.snap b/spec/reducers/config/__snapshots__/config.spec.ts.snap similarity index 79% rename from spec/sagas/__snapshots__/config.spec.ts.snap rename to spec/reducers/config/__snapshots__/config.spec.ts.snap index c0e9a2f6..fb70e64f 100644 --- a/spec/sagas/__snapshots__/config.spec.ts.snap +++ b/spec/reducers/config/__snapshots__/config.spec.ts.snap @@ -8,7 +8,17 @@ Object { "@@redux-saga/IO": true, "CALL": Object { "args": Array [], - "context": null, + "context": RpcNode { + "client": RPCClient { + "batch": [Function], + "call": [Function], + "createHeaders": [Function], + "decorateRequest": [Function], + "endpoint": "https://node.expanse.tech/", + "headers": Object {}, + }, + "requests": RPCRequests {}, + }, "fn": [Function], }, }, @@ -29,25 +39,9 @@ Object { exports[`handleNodeChangeIntent* should select getCustomNodeConfig and match race snapshot 1`] = ` Object { "@@redux-saga/IO": true, - "RACE": Object { - "lb": Object { - "@@redux-saga/IO": true, - "CALL": Object { - "args": Array [], - "context": null, - "fn": [Function], - }, - }, - "to": Object { - "@@redux-saga/IO": true, - "CALL": Object { - "args": Array [ - 5000, - ], - "context": null, - "fn": [Function], - }, - }, + "SELECT": Object { + "args": Array [], + "selector": [Function], }, } `; diff --git a/spec/sagas/config.spec.ts b/spec/reducers/config/config.spec.ts similarity index 60% rename from spec/sagas/config.spec.ts rename to spec/reducers/config/config.spec.ts index 1ce5bed8..c5a03304 100644 --- a/spec/sagas/config.spec.ts +++ b/spec/reducers/config/config.spec.ts @@ -2,33 +2,38 @@ import { configuredStore } from 'store'; import { delay } from 'redux-saga'; import { call, cancel, fork, put, take, select } from 'redux-saga/effects'; import { cloneableGenerator, createMockTask } from 'redux-saga/utils'; -import { toggleOfflineConfig, changeNode, changeNodeIntent, setLatestBlock } from 'actions/config'; +import { toggleOffline, changeNode, changeNodeIntent, setLatestBlock } from 'actions/config'; import { - pollOfflineStatus, - handlePollOfflineStatus, handleNodeChangeIntent, - unsetWeb3Node, - unsetWeb3NodeOnWalletEvent, - equivalentNodeOrDefault, + handlePollOfflineStatus, + pollOfflineStatus, reload -} from 'sagas/config'; -import { NODES, NodeConfig, NETWORKS } from 'config'; +} from 'sagas/config/node'; import { - getNode, + getNodeId, getNodeConfig, getOffline, - getCustomNodeConfigs, - getCustomNetworkConfigs + isStaticNodeId, + getStaticNodeFromId, + getNetworkConfigById, + getCustomNodeFromId, + getStaticAltNodeIdToWeb3 } from 'selectors/config'; -import { INITIAL_STATE as configInitialState } from 'reducers/config'; import { Web3Wallet } from 'libs/wallet'; -import { RPCNode } from 'libs/nodes'; import { showNotification } from 'actions/notifications'; import { translateRaw } from 'translations'; +import { StaticNodeConfig } from 'types/node'; +import { staticNodesExpectedState } from './nodes/staticNodes.spec'; +import { metaExpectedState } from './meta/meta.spec'; +import { selectedNodeExpectedState } from './nodes/selectedNode.spec'; +import { customNodesExpectedState, firstCustomNodeId } from './nodes/customNodes.spec'; +import { unsetWeb3Node, unsetWeb3NodeOnWalletEvent } from 'sagas/config/web3'; + // init module configuredStore.getState(); describe('pollOfflineStatus*', () => { + const { togglingToOffline, togglingToOnline } = metaExpectedState; const nav = navigator as any; const doc = document as any; const data = {} as any; @@ -38,7 +43,6 @@ describe('pollOfflineStatus*', () => { ping: jest.fn() } }; - const isOffline = true; const raceSuccess = { pingSucceeded: true, timeout: false @@ -88,29 +92,29 @@ describe('pollOfflineStatus*', () => { it('should call delay if document is hidden', () => { data.hiddenDoc = data.gen.clone(); doc.hidden = true; - expect(data.hiddenDoc.next(!isOffline).value).toEqual(call(delay, 1000)); + expect(data.hiddenDoc.next(togglingToOnline.offline).value).toEqual(call(delay, 1000)); doc.hidden = false; }); it('should race pingSucceeded and timeout', () => { data.isOfflineClone = data.gen.clone(); data.shouldDelayClone = data.gen.clone(); - expect(data.gen.next(isOffline).value).toMatchSnapshot(); + expect(data.gen.next(togglingToOffline.offline).value).toMatchSnapshot(); }); 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())); + expect(data.gen.next().value).toEqual(put(toggleOffline())); }); it('should toggle offline and show notification if navigator agrees with isOffline and ping fails', () => { - nav.onLine = isOffline; - expect(data.isOfflineClone.next(!isOffline)); + nav.onLine = togglingToOffline.offline; + expect(data.isOfflineClone.next(togglingToOnline.offline)); expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot(); - expect(data.isOfflineClone.next().value).toEqual(put(toggleOfflineConfig())); - nav.onLine = !isOffline; + expect(data.isOfflineClone.next().value).toEqual(put(toggleOffline())); + nav.onLine = togglingToOnline.offline; }); }); @@ -136,17 +140,15 @@ describe('handleNodeChangeIntent*', () => { let originalRandom; // 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[cur].network !== defaultNodeConfig.network ? cur : acc) + const defaultNodeId = selectedNodeExpectedState.initialState.nodeId; + const defaultNodeConfig: StaticNodeConfig = staticNodesExpectedState.initialState[defaultNodeId]; + const newNodeId = Object.keys(staticNodesExpectedState.initialState).reduce( + (acc, cur) => + staticNodesExpectedState.initialState[cur].network !== defaultNodeConfig.network ? cur : acc ); - const newNodeConfig = NODES[newNode]; - const newNodeNetwork = NETWORKS[newNodeConfig.network]; + const newNodeConfig: StaticNodeConfig = staticNodesExpectedState.initialState[newNodeId]; - const changeNodeIntentAction = changeNodeIntent(newNode); + const changeNodeIntentAction = changeNodeIntent(newNodeId); const latestBlock = '0xa'; const raceSuccess = { lb: latestBlock @@ -159,9 +161,10 @@ describe('handleNodeChangeIntent*', () => { data.gen = cloneableGenerator(handleNodeChangeIntent)(changeNodeIntentAction); function shouldBailOut(gen, nextVal, errMsg) { - expect(gen.next(nextVal).value).toEqual(put(showNotification('danger', errMsg, 5000))); + expect(gen.next(nextVal).value).toEqual(select(getNodeId)); + expect(gen.next(defaultNodeId).value).toEqual(put(showNotification('danger', errMsg, 5000))); expect(gen.next().value).toEqual( - put(changeNode(defaultNode, defaultNodeConfig, defaultNodeNetwork)) + put(changeNode({ networkId: defaultNodeConfig.network, nodeId: defaultNodeId })) ); expect(gen.next().done).toEqual(true); } @@ -175,34 +178,38 @@ describe('handleNodeChangeIntent*', () => { Math.random = originalRandom; }); - it('should select getNode', () => { - expect(data.gen.next().value).toEqual(select(getNode)); + it('should select is static node', () => { + expect(data.gen.next().value).toEqual(select(isStaticNodeId, newNodeId)); }); it('should select nodeConfig', () => { - expect(data.gen.next(defaultNode).value).toEqual(select(getNodeConfig)); + expect(data.gen.next(defaultNodeId).value).toEqual(select(getNodeConfig)); }); - it('should select getCustomNetworkConfigs', () => { - expect(data.gen.next(defaultNodeConfig).value).toEqual(select(getCustomNetworkConfigs)); + it('should select getStaticNodeFromId', () => { + expect(data.gen.next(defaultNodeConfig).value).toEqual(select(getStaticNodeFromId, newNodeId)); }); it('should race getCurrentBlock and delay', () => { - expect(data.gen.next(customNetworkConfigs).value).toMatchSnapshot(); + expect(data.gen.next(newNodeConfig).value).toMatchSnapshot(); }); it('should show error and revert to previous node if check times out', () => { data.clone1 = data.gen.clone(); shouldBailOut(data.clone1, raceFailure, translateRaw('ERROR_32')); }); - + it('should getNetworkConfigById', () => { + expect(data.gen.next(raceSuccess).value).toEqual( + select(getNetworkConfigById, newNodeConfig.network) + ); + }); it('should put setLatestBlock', () => { expect(data.gen.next(raceSuccess).value).toEqual(put(setLatestBlock(latestBlock))); }); it('should put changeNode', () => { expect(data.gen.next().value).toEqual( - put(changeNode(changeNodeIntentAction.payload, newNodeConfig, newNodeNetwork)) + put(changeNode({ networkId: newNodeConfig.network, nodeId: newNodeId })) ); }); @@ -216,41 +223,40 @@ describe('handleNodeChangeIntent*', () => { }); // custom node variables - const customNodeConfigs = [ - { - name: 'name', - url: 'url', - port: 443, - network: 'network' - } - ]; - const customNodeIdFound = 'url:443'; - const customNodeIdNotFound = 'notFound'; - const customNodeAction = changeNodeIntent(customNodeIdFound); - const customNodeNotFoundAction = changeNodeIntent(customNodeIdNotFound); + const customNodeConfigs = customNodesExpectedState.addFirstCustomNode; + const customNodeAction = changeNodeIntent(firstCustomNodeId); data.customNode = handleNodeChangeIntent(customNodeAction); - data.customNodeNotFound = handleNodeChangeIntent(customNodeNotFoundAction); // test custom node it('should select getCustomNodeConfig and match race snapshot', () => { data.customNode.next(); - data.customNode.next(defaultNode); - data.customNode.next(defaultNodeConfig); - expect(data.customNode.next(customNetworkConfigs).value).toEqual(select(getCustomNodeConfigs)); + data.customNode.next(false); + expect(data.customNode.next(defaultNodeConfig).value).toEqual( + select(getCustomNodeFromId, firstCustomNodeId) + ); expect(data.customNode.next(customNodeConfigs).value).toMatchSnapshot(); }); + const customNodeIdNotFound = firstCustomNodeId + 'notFound'; + const customNodeNotFoundAction = changeNodeIntent(customNodeIdNotFound); + data.customNodeNotFound = handleNodeChangeIntent(customNodeNotFoundAction); + // test custom node not found it('should handle unknown / missing custom node', () => { data.customNodeNotFound.next(); - data.customNodeNotFound.next(defaultNode); - data.customNodeNotFound.next(defaultNodeConfig); - expect(data.customNodeNotFound.next(customNetworkConfigs).value).toEqual( - select(getCustomNodeConfigs) + data.customNodeNotFound.next(false); + }); + + it('should blah', () => { + expect(data.customNodeNotFound.next(defaultNodeConfig).value).toEqual( + select(getCustomNodeFromId, customNodeIdNotFound) ); + }); + + it('should blahah', () => { shouldBailOut( data.customNodeNotFound, - customNodeConfigs, + null, `Attempted to switch to unknown node '${customNodeNotFoundAction.payload}'` ); }); @@ -258,20 +264,19 @@ describe('handleNodeChangeIntent*', () => { describe('unsetWeb3Node*', () => { const node = 'web3'; - const mockNodeConfig = { network: 'ETH' } as any; - const newNode = equivalentNodeOrDefault(mockNodeConfig); + const alternativeNodeId = 'eth_mycrypto'; const gen = unsetWeb3Node(); it('should select getNode', () => { - expect(gen.next().value).toEqual(select(getNode)); + expect(gen.next().value).toEqual(select(getNodeId)); }); - it('should select getNodeConfig', () => { - expect(gen.next(node).value).toEqual(select(getNodeConfig)); + it('should select an alternative node to web3', () => { + expect(gen.next(node).value).toEqual(select(getStaticAltNodeIdToWeb3)); }); it('should put changeNodeIntent', () => { - expect(gen.next(mockNodeConfig).value).toEqual(put(changeNodeIntent(newNode))); + expect(gen.next(alternativeNodeId).value).toEqual(put(changeNodeIntent(alternativeNodeId))); }); it('should be done', () => { @@ -288,22 +293,20 @@ describe('unsetWeb3Node*', () => { describe('unsetWeb3NodeOnWalletEvent*', () => { const fakeAction = {}; - const mockNode = 'web3'; - const mockNodeConfig: Partial = { network: 'ETH' }; + const mockNodeId = 'web3'; + const alternativeNodeId = 'eth_mycrypto'; const gen = unsetWeb3NodeOnWalletEvent(fakeAction); it('should select getNode', () => { - expect(gen.next().value).toEqual(select(getNode)); + expect(gen.next().value).toEqual(select(getNodeId)); }); - it('should select getNodeConfig', () => { - expect(gen.next(mockNode).value).toEqual(select(getNodeConfig)); + it('should select an alternative node to web3', () => { + expect(gen.next(mockNodeId).value).toEqual(select(getStaticAltNodeIdToWeb3)); }); it('should put changeNodeIntent', () => { - expect(gen.next(mockNodeConfig).value).toEqual( - put(changeNodeIntent(equivalentNodeOrDefault(mockNodeConfig as any))) - ); + expect(gen.next(alternativeNodeId).value).toEqual(put(changeNodeIntent(alternativeNodeId))); }); it('should be done', () => { @@ -327,51 +330,3 @@ describe('unsetWeb3NodeOnWalletEvent*', () => { expect(gen2.next().done).toEqual(true); }); }); - -describe('equivalentNodeOrDefault', () => { - const originalNodeList = Object.keys(NODES); - const appDefaultNode = configInitialState.nodeSelection; - const mockNodeConfig = { - network: 'ETH', - service: 'fakeService', - lib: new RPCNode('fakeEndpoint'), - estimateGas: false - }; - - afterEach(() => { - Object.keys(NODES).forEach(node => { - if (originalNodeList.indexOf(node) === -1) { - delete NODES[node]; - } - }); - }); - - it('should return node with equivalent network', () => { - const node = equivalentNodeOrDefault({ - ...mockNodeConfig, - network: 'Kovan' - }); - expect(NODES[node].network).toEqual('Kovan'); - }); - - it('should return app default if no eqivalent is found', () => { - const node = equivalentNodeOrDefault({ - ...mockNodeConfig, - network: 'noEqivalentExists' - } as any); - expect(node).toEqual(appDefaultNode); - }); - - it('should ignore web3 from node list', () => { - NODES.web3 = { - ...mockNodeConfig, - network: 'uniqueToWeb3' - } as any; - - const node = equivalentNodeOrDefault({ - ...mockNodeConfig, - network: 'uniqueToWeb3' - } as any); - expect(node).toEqual(appDefaultNode); - }); -}); diff --git a/spec/reducers/config/meta/meta.spec.ts b/spec/reducers/config/meta/meta.spec.ts new file mode 100644 index 00000000..03126fb4 --- /dev/null +++ b/spec/reducers/config/meta/meta.spec.ts @@ -0,0 +1,76 @@ +import { meta } from 'reducers/config/meta'; +import { changeLanguage, toggleOffline, toggleAutoGasLimit, setLatestBlock } from 'actions/config'; + +const expectedInitialState = { + languageSelection: 'en', + offline: false, + autoGasLimit: true, + latestBlock: '???' +}; + +const expectedState = { + initialState: expectedInitialState, + changingLanguage: { + ...expectedInitialState, + languageSelection: 'langaugeToChange' + }, + togglingToOffline: { + ...expectedInitialState, + offline: true + }, + togglingToOnline: { + ...expectedInitialState, + offline: false + }, + togglingToManualGasLimit: { + ...expectedInitialState, + autoGasLimit: false + }, + togglingToAutoGasLimit: { + ...expectedInitialState, + autoGasLimit: true + }, + settingLatestBlock: { + ...expectedInitialState, + latestBlock: '12345' + } +}; + +const actions = { + changeLangauge: changeLanguage('langaugeToChange'), + toggleOffline: toggleOffline(), + toggleAutoGasLimit: toggleAutoGasLimit(), + setLatestBlock: setLatestBlock('12345') +}; + +describe('meta reducer', () => { + it('should return the inital state', () => + expect(meta(undefined, {} as any)).toEqual(expectedState.initialState)); + + it('should handle toggling to offline', () => + expect(meta(expectedState.initialState, actions.toggleOffline)).toEqual( + expectedState.togglingToOffline + )); + + it('should handle toggling back to online', () => + expect(meta(expectedState.togglingToOffline, actions.toggleOffline)).toEqual( + expectedState.togglingToOnline + )); + + it('should handle toggling to manual gas limit', () => + expect(meta(expectedState.initialState, actions.toggleAutoGasLimit)).toEqual( + expectedState.togglingToManualGasLimit + )); + + it('should handle toggling back to auto gas limit', () => + expect(meta(expectedState.togglingToManualGasLimit, actions.toggleAutoGasLimit)).toEqual( + expectedState.togglingToAutoGasLimit + )); + + it('should handle setting the latest block', () => + expect(meta(expectedState.initialState, actions.setLatestBlock)).toEqual( + expectedState.settingLatestBlock + )); +}); + +export { actions as metaActions, expectedState as metaExpectedState }; diff --git a/spec/reducers/config/networks/customNetworks.spec.ts b/spec/reducers/config/networks/customNetworks.spec.ts new file mode 100644 index 00000000..ce3f0f3f --- /dev/null +++ b/spec/reducers/config/networks/customNetworks.spec.ts @@ -0,0 +1,62 @@ +import { CustomNetworkConfig } from 'types/network'; +import { addCustomNetwork, removeCustomNetwork } from 'actions/config'; +import { customNetworks } from 'reducers/config/networks/customNetworks'; + +const firstCustomNetworkId = 'firstCustomNetwork'; +const firstCustomNetworkConfig: CustomNetworkConfig = { + isCustom: true, + chainId: 1, + name: firstCustomNetworkId, + unit: 'customNetworkUnit', + dPathFormats: null +}; + +const secondCustomNetworkId = 'secondCustomNetwork'; +const secondCustomNetworkConfig: CustomNetworkConfig = { + ...firstCustomNetworkConfig, + name: secondCustomNetworkId +}; + +const expectedState = { + initialState: {}, + addFirstCustomNetwork: { [firstCustomNetworkId]: firstCustomNetworkConfig }, + addSecondCustomNetwork: { + [firstCustomNetworkId]: firstCustomNetworkConfig, + [secondCustomNetworkId]: secondCustomNetworkConfig + }, + removeFirstCustomNetwork: { [secondCustomNetworkId]: secondCustomNetworkConfig } +}; + +const actions = { + addFirstCustomNetwork: addCustomNetwork({ + id: firstCustomNetworkId, + config: firstCustomNetworkConfig + }), + addSecondCustomNetwork: addCustomNetwork({ + config: secondCustomNetworkConfig, + id: secondCustomNetworkId + }), + removeFirstCustomNetwork: removeCustomNetwork({ id: firstCustomNetworkId }) +}; + +describe('custom networks reducer', () => { + it('should return the intial state', () => + expect(customNetworks(undefined, {} as any)).toEqual(expectedState.initialState)); + + it('should handle adding the first custom network', () => + expect(customNetworks(expectedState.initialState, actions.addFirstCustomNetwork)).toEqual( + expectedState.addFirstCustomNetwork + )); + + it('should handle adding the second custom network', () => + expect( + customNetworks(expectedState.addFirstCustomNetwork, actions.addSecondCustomNetwork) + ).toEqual(expectedState.addSecondCustomNetwork)); + + it('should handle removing the first custom network', () => + expect( + customNetworks(expectedState.addSecondCustomNetwork, actions.removeFirstCustomNetwork) + ).toEqual(expectedState.removeFirstCustomNetwork)); +}); + +export { actions as customNetworksActions, expectedState as customNetworksExpectedState }; diff --git a/spec/reducers/config/networks/staticNetworks.spec.ts b/spec/reducers/config/networks/staticNetworks.spec.ts new file mode 100644 index 00000000..b9fdb977 --- /dev/null +++ b/spec/reducers/config/networks/staticNetworks.spec.ts @@ -0,0 +1,149 @@ +import { staticNetworks, makeExplorer } from 'reducers/config/networks/staticNetworks'; +import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from 'config/data'; +import { + ETH_DEFAULT, + ETH_TREZOR, + ETH_LEDGER, + ETC_LEDGER, + ETC_TREZOR, + ETH_TESTNET, + EXP_DEFAULT, + UBQ_DEFAULT +} from 'config/dpaths'; + +const expectedInitialState = { + ETH: { + name: 'ETH', + unit: 'ETH', + chainId: 1, + isCustom: false, + color: '#0e97c0', + blockExplorer: makeExplorer('https://etherscan.io'), + tokenExplorer: { + name: ethPlorer, + address: ETHTokenExplorer + }, + tokens: require('config/tokens/eth.json'), + contracts: require('config/contracts/eth.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TREZOR, + [SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT + } + }, + Ropsten: { + name: 'Ropsten', + unit: 'ETH', + chainId: 3, + isCustom: false, + color: '#adc101', + blockExplorer: makeExplorer('https://ropsten.etherscan.io'), + tokens: require('config/tokens/ropsten.json'), + contracts: require('config/contracts/ropsten.json'), + isTestnet: true, + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TESTNET, + [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET + } + }, + Kovan: { + name: 'Kovan', + unit: 'ETH', + chainId: 42, + isCustom: false, + color: '#adc101', + blockExplorer: makeExplorer('https://kovan.etherscan.io'), + tokens: require('config/tokens/ropsten.json'), + contracts: require('config/contracts/ropsten.json'), + isTestnet: true, + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TESTNET, + [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET + } + }, + Rinkeby: { + name: 'Rinkeby', + unit: 'ETH', + chainId: 4, + isCustom: false, + color: '#adc101', + blockExplorer: makeExplorer('https://rinkeby.etherscan.io'), + tokens: require('config/tokens/rinkeby.json'), + contracts: require('config/contracts/rinkeby.json'), + isTestnet: true, + dPathFormats: { + [SecureWalletName.TREZOR]: ETH_TESTNET, + [SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET, + [InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET + } + }, + ETC: { + name: 'ETC', + unit: 'ETC', + chainId: 61, + isCustom: false, + color: '#669073', + blockExplorer: makeExplorer('https://gastracker.io'), + tokens: require('config/tokens/etc.json'), + contracts: require('config/contracts/etc.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: ETC_TREZOR, + [SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER, + [InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR + } + }, + UBQ: { + name: 'UBQ', + unit: 'UBQ', + chainId: 8, + isCustom: false, + color: '#b37aff', + blockExplorer: makeExplorer('https://ubiqscan.io/en'), + tokens: require('config/tokens/ubq.json'), + contracts: require('config/contracts/ubq.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: UBQ_DEFAULT, + [SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT, + [InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT + } + }, + EXP: { + name: 'EXP', + unit: 'EXP', + chainId: 2, + isCustom: false, + color: '#673ab7', + blockExplorer: makeExplorer('https://www.gander.tech'), + tokens: require('config/tokens/exp.json'), + contracts: require('config/contracts/exp.json'), + dPathFormats: { + [SecureWalletName.TREZOR]: EXP_DEFAULT, + [SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT, + [InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT + } + } +}; + +const expectedState = { + initialState: expectedInitialState +}; + +describe('Testing contained data', () => { + it(`contain unique chainIds`, () => { + const networkValues = Object.values(expectedInitialState); + const chainIds = networkValues.map(a => a.chainId); + const chainIdsSet = new Set(chainIds); + expect(Array.from(chainIdsSet).length).toEqual(chainIds.length); + }); +}); + +describe('static networks reducer', () => { + it('should return the initial state', () => + expect(JSON.stringify(staticNetworks(undefined, {} as any))).toEqual( + JSON.stringify(expectedState.initialState) + )); +}); + +export { expectedState as staticNetworksExpectedState }; diff --git a/spec/reducers/config/nodes/customNodes.spec.ts b/spec/reducers/config/nodes/customNodes.spec.ts new file mode 100644 index 00000000..b18f7be1 --- /dev/null +++ b/spec/reducers/config/nodes/customNodes.spec.ts @@ -0,0 +1,57 @@ +import { addCustomNode, removeCustomNode } from 'actions/config'; +import { CustomNodeConfig } from 'types/node'; +import { customNodes } from 'reducers/config/nodes/customNodes'; + +export const firstCustomNodeId = 'customNode1'; +const firstCustomNode: CustomNodeConfig = { + isCustom: true, + id: firstCustomNodeId, + lib: jest.fn() as any, + name: 'My cool custom node', + network: 'CustomNetworkId', + port: 8080, + service: 'your custom node', + url: '127.0.0.1' +}; + +const secondCustomNodeId = 'customNode2'; +const secondCustomNode: CustomNodeConfig = { + ...firstCustomNode, + id: secondCustomNodeId +}; + +const expectedState = { + initialState: {}, + addFirstCustomNode: { [firstCustomNodeId]: firstCustomNode }, + addSecondCustomNode: { + [firstCustomNodeId]: firstCustomNode, + [secondCustomNodeId]: secondCustomNode + }, + removeFirstCustomNode: { [secondCustomNodeId]: secondCustomNode } +}; + +const actions = { + addFirstCustomNode: addCustomNode({ id: firstCustomNodeId, config: firstCustomNode }), + addSecondCustomNode: addCustomNode({ id: secondCustomNodeId, config: secondCustomNode }), + removeFirstCustomNode: removeCustomNode({ id: firstCustomNodeId }) +}; + +describe('custom nodes reducer', () => { + it('should return the initial state', () => + expect(customNodes(undefined, {} as any)).toEqual({})); + + it('should handle adding the first custom node', () => + expect(customNodes(expectedState.initialState, actions.addFirstCustomNode)).toEqual( + expectedState.addFirstCustomNode + )); + it('should handle adding a second custom node', () => + expect(customNodes(expectedState.addFirstCustomNode, actions.addSecondCustomNode)).toEqual( + expectedState.addSecondCustomNode + )); + it('should handle removing the first custom node', () => + expect(customNodes(expectedState.addSecondCustomNode, actions.removeFirstCustomNode)).toEqual( + expectedState.removeFirstCustomNode + )); +}); + +export { actions as customNodesActions, expectedState as customNodesExpectedState }; diff --git a/spec/reducers/config/nodes/selectedNode.spec.ts b/spec/reducers/config/nodes/selectedNode.spec.ts new file mode 100644 index 00000000..691c5604 --- /dev/null +++ b/spec/reducers/config/nodes/selectedNode.spec.ts @@ -0,0 +1,28 @@ +import { changeNodeIntent, changeNode } from 'actions/config'; +import { State, selectedNode } from 'reducers/config/nodes/selectedNode'; + +export const expectedState = { + initialState: { nodeId: 'eth_mycrypto', pending: false }, + nodeChange: { nodeId: 'nodeToChangeTo', pending: false }, + nodeChangeIntent: { nodeId: 'eth_mycrypto', pending: true } +}; + +export const actions = { + changeNode: changeNode({ nodeId: 'nodeToChangeTo', networkId: 'networkToChangeTo' }), + changeNodeIntent: changeNodeIntent('eth_mycrypto') +}; + +describe('selected node reducer', () => { + it(' should return the initial state', () => + expect(selectedNode(undefined, {} as any)).toEqual(expectedState.initialState)); + + it('should handle a node change', () => + expect(selectedNode(undefined, actions.changeNode)).toEqual(expectedState.nodeChange)); + + it('should handle the intent to change a node', () => + expect(selectedNode(expectedState.initialState as State, actions.changeNodeIntent)).toEqual( + expectedState.nodeChangeIntent + )); +}); + +export { actions as selectedNodeActions, expectedState as selectedNodeExpectedState }; diff --git a/spec/reducers/config/nodes/staticNodes.spec.ts b/spec/reducers/config/nodes/staticNodes.spec.ts new file mode 100644 index 00000000..3454b4f1 --- /dev/null +++ b/spec/reducers/config/nodes/staticNodes.spec.ts @@ -0,0 +1,116 @@ +import { web3SetNode, web3UnsetNode } from 'actions/config'; +import { staticNodes, INITIAL_STATE } from 'reducers/config/nodes/staticNodes'; +import { EtherscanNode, InfuraNode, RPCNode } from 'libs/nodes'; +import { Web3NodeConfig } from 'types/node'; +import { Web3Service } from 'libs/nodes/web3'; + +const expectedInitialState = { + 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_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 + } +}; + +const web3Id = 'web3'; +const web3Node: Web3NodeConfig = { + isCustom: false, + network: 'ETH', + service: Web3Service, + lib: jest.fn() as any, + estimateGas: false, + hidden: true +}; + +const expectedState = { + initialState: expectedInitialState, + setWeb3: { ...INITIAL_STATE, [web3Id]: web3Node }, + unsetWeb3: { ...INITIAL_STATE } +}; + +const actions = { + web3SetNode: web3SetNode({ id: web3Id, config: web3Node }), + web3UnsetNode: web3UnsetNode() +}; + +describe('static nodes reducer', () => { + it('should return the inital state', () => + // turn the JSON into a string because we're storing function in the state + expect(JSON.stringify(staticNodes(undefined, {} as any))).toEqual( + JSON.stringify(expectedState.initialState) + )); + it('should handle setting the web3 node', () => + expect(staticNodes(INITIAL_STATE, actions.web3SetNode)).toEqual(expectedState.setWeb3)); + + it('should handle unsetting the web3 node', () => + expect(staticNodes(expectedState.setWeb3, actions.web3UnsetNode)).toEqual( + expectedState.unsetWeb3 + )); +}); + +export { actions as staticNodesActions, expectedState as staticNodesExpectedState }; diff --git a/spec/reducers/customTokens.spec.ts b/spec/reducers/customTokens.spec.ts index a52df732..8b28c5af 100644 --- a/spec/reducers/customTokens.spec.ts +++ b/spec/reducers/customTokens.spec.ts @@ -1,6 +1,6 @@ import { customTokens } from 'reducers/customTokens'; -import { Token } from 'config'; import * as customTokensActions from 'actions/customTokens'; +import { Token } from 'types/network'; describe('customTokens reducer', () => { const token1: Token = { diff --git a/spec/sagas/__snapshots__/wallet.spec.tsx.snap b/spec/sagas/__snapshots__/wallet.spec.tsx.snap index bcdea4eb..1cebb6d4 100644 --- a/spec/sagas/__snapshots__/wallet.spec.tsx.snap +++ b/spec/sagas/__snapshots__/wallet.spec.tsx.snap @@ -172,7 +172,7 @@ Object { "action": Object { "payload": Web3Wallet { "address": "0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854", - "network": "ETH", + "network": undefined, }, "type": "WALLET_SET", }, diff --git a/spec/sagas/deterministicWallets.spec.ts b/spec/sagas/deterministicWallets.spec.ts index 630815fe..f05cf444 100644 --- a/spec/sagas/deterministicWallets.spec.ts +++ b/spec/sagas/deterministicWallets.spec.ts @@ -7,13 +7,13 @@ import { getDesiredToken, getWallets } from 'selectors/deterministicWallets'; import { getTokens } from 'selectors/wallet'; import { getNodeLib } from 'selectors/config'; import * as dWalletActions from 'actions/deterministicWallets'; -import { Token } from 'config'; import { getDeterministicWallets, updateWalletValues, updateWalletTokenValues } from 'sagas/deterministicWallets'; import { TokenValue, Wei } from 'libs/units'; +import { Token } from 'types/network'; // init module configuredStore.getState(); diff --git a/spec/sagas/wallet.spec.tsx b/spec/sagas/wallet.spec.tsx index fc97297e..db8ecd44 100644 --- a/spec/sagas/wallet.spec.tsx +++ b/spec/sagas/wallet.spec.tsx @@ -14,9 +14,8 @@ import { import { Wei } from 'libs/units'; import { changeNodeIntent, web3UnsetNode } from 'actions/config'; import { INode } from 'libs/nodes/INode'; -import { initWeb3Node, Token } from 'config'; import { apply, call, fork, put, select, take, cancel } from 'redux-saga/effects'; -import { getNodeLib, getOffline } from 'selectors/config'; +import { getNodeLib, getOffline, getWeb3Node } from 'selectors/config'; import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet'; import { updateAccountBalance, @@ -37,6 +36,8 @@ import { cloneableGenerator, createMockTask } from 'redux-saga/utils'; import { showNotification } from 'actions/notifications'; import translate from 'translations'; import { IFullWallet, IV3Wallet, fromV3 } from 'ethereumjs-wallet'; +import { Token } from 'types/network'; +import { initWeb3Node } from 'sagas/config/web3'; // init module configuredStore.getState(); @@ -339,13 +340,13 @@ describe('unlockWeb3*', () => { expect(JSON.stringify(expected)).toEqual(JSON.stringify(result)); }); - it('should select getNodeLib', () => { - expect(data.gen.next().value).toEqual(select(getNodeLib)); + it('should select getWeb3Node', () => { + expect(data.gen.next().value).toEqual(select(getWeb3Node)); }); it('should throw & catch if node is not web3 node', () => { data.clone = data.gen.clone(); - expect(data.clone.next().value).toEqual(put(web3UnsetNode())); + expect(data.clone.next(nodeLib).value).toEqual(put(web3UnsetNode())); expect(data.clone.next().value).toEqual( put(showNotification('danger', translate('Cannot use Web3 wallet without a Web3 node.'))) ); @@ -353,7 +354,7 @@ describe('unlockWeb3*', () => { }); it('should apply nodeLib.getAccounts', () => { - expect(data.gen.next(nodeLib).value).toEqual(apply(nodeLib, nodeLib.getAccounts)); + expect(data.gen.next({ lib: nodeLib }).value).toEqual(apply(nodeLib, nodeLib.getAccounts)); }); it('should throw & catch if no accounts found', () => { diff --git a/spec/utils/node.spec.ts b/spec/utils/node.spec.ts deleted file mode 100644 index 48dbe0b7..00000000 --- a/spec/utils/node.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - makeCustomNodeId, - getCustomNodeConfigFromId, - getNodeConfigFromId, - makeNodeConfigFromCustomConfig -} from 'utils/node'; - -const custNode = { - name: 'Test Config', - url: 'https://somecustomconfig.org/', - port: 443, - network: 'ETH' -}; -const custNodeId = 'https://somecustomconfig.org/:443'; - -describe('makeCustomNodeId', () => { - it('should construct an ID from url:port', () => { - expect(makeCustomNodeId(custNode) === custNodeId).toBeTruthy(); - }); -}); - -describe('getCustomNodeConfigFromId', () => { - it('should fetch the correct config, given its ID', () => { - expect(getCustomNodeConfigFromId(custNodeId, [custNode])).toBeTruthy(); - }); -}); - -describe('getNodeConfigFromId', () => { - it('should fetch the correct config, given its ID', () => { - expect(getNodeConfigFromId(custNodeId, [custNode])).toBeTruthy(); - }); -}); - -describe('makeNodeConfigFromCustomConfig', () => { - it('Should create a node config from a custom config', () => { - expect(makeNodeConfigFromCustomConfig(custNode)).toBeTruthy(); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 0547c07f..1af567ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,12 +9,10 @@ "allowJs": true, "baseUrl": "./common/", "paths": { - "shared*": ["../shared*"] + "shared*": ["../shared*"], + "types*": ["../shared/types*"] }, - "lib": [ - "es2017", - "dom" - ], + "lib": ["es2017", "dom"], "allowSyntheticDefaultImports": true, "moduleResolution": "node", "noEmitOnError": false, @@ -24,6 +22,7 @@ "include": [ "./common/", "./electron-app/", + "./shared/", "spec", "./node_modules/types-rlp/index.d.ts" ],