diff --git a/common/actions/config/actionCreators.ts b/common/actions/config/actionCreators.ts index 5c18d5b9..db77a6ca 100644 --- a/common/actions/config/actionCreators.ts +++ b/common/actions/config/actionCreators.ts @@ -28,12 +28,12 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction { }; } -export type TChangeNode = typeof changeNode; -export function changeNode( - payload: interfaces.ChangeNodeAction['payload'] -): interfaces.ChangeNodeAction { +export type TChangeNodeSucceded = typeof changeNodeSucceeded; +export function changeNodeSucceeded( + payload: interfaces.ChangeNodeSucceededAction['payload'] +): interfaces.ChangeNodeSucceededAction { return { - type: TypeKeys.CONFIG_NODE_CHANGE, + type: TypeKeys.CONFIG_CHANGE_NODE_SUCCEEDED, payload }; } @@ -45,18 +45,20 @@ export function pollOfflineStatus(): interfaces.PollOfflineStatus { }; } -export type TChangeNodeIntent = typeof changeNodeIntent; -export function changeNodeIntent(payload: string): interfaces.ChangeNodeIntentAction { +export type TChangeNodeRequested = typeof changeNodeRequested; +export function changeNodeRequested(payload: string): interfaces.ChangeNodeRequestedAction { return { - type: TypeKeys.CONFIG_NODE_CHANGE_INTENT, + type: TypeKeys.CONFIG_CHANGE_NODE_REQUESTED, payload }; } -export type TChangeNodeIntentOneTime = typeof changeNodeIntentOneTime; -export function changeNodeIntentOneTime(payload: string): interfaces.ChangeNodeIntentOneTimeAction { +export type TChangeNodeRequestedOneTime = typeof changeNodeRequestedOneTime; +export function changeNodeRequestedOneTime( + payload: string +): interfaces.ChangeNodeRequestedOneTimeAction { return { - type: TypeKeys.CONFIG_NODE_CHANGE_INTENT_ONETIME, + type: TypeKeys.CONFIG_CHANGE_NODE_REQUESTED_ONETIME, payload }; } @@ -64,7 +66,17 @@ export function changeNodeIntentOneTime(payload: string): interfaces.ChangeNodeI export type TChangeNodeForce = typeof changeNodeForce; export function changeNodeForce(payload: string): interfaces.ChangeNodeForceAction { return { - type: TypeKeys.CONFIG_NODE_CHANGE_FORCE, + type: TypeKeys.CONFIG_CHANGE_NODE_FORCE, + payload + }; +} + +export type TChangeNetworkRequested = typeof changeNetworkRequested; +export function changeNetworkRequested( + payload: interfaces.ChangeNetworkRequestedAction['payload'] +): interfaces.ChangeNetworkRequestedAction { + return { + type: TypeKeys.CONFIG_CHANGE_NETWORK_REQUESTED, payload }; } diff --git a/common/actions/config/actionTypes.ts b/common/actions/config/actionTypes.ts index 0b8a57ba..785f87e4 100644 --- a/common/actions/config/actionTypes.ts +++ b/common/actions/config/actionTypes.ts @@ -20,60 +20,66 @@ export interface ChangeLanguageAction { payload: string; } -/*** Change Node ***/ -export interface ChangeNodeAction { - type: TypeKeys.CONFIG_NODE_CHANGE; +/*** Poll offline status ***/ +export interface PollOfflineStatus { + type: TypeKeys.CONFIG_POLL_OFFLINE_STATUS; +} + +/*** Change Node Requested ***/ +export interface ChangeNodeRequestedAction { + type: TypeKeys.CONFIG_CHANGE_NODE_REQUESTED; + payload: string; +} + +/*** Change Node Succeeded ***/ +export interface ChangeNodeSucceededAction { + type: TypeKeys.CONFIG_CHANGE_NODE_SUCCEEDED; payload: { nodeId: string; networkId: string; }; } -/*** Poll offline status ***/ -export interface PollOfflineStatus { - type: TypeKeys.CONFIG_POLL_OFFLINE_STATUS; -} - -/*** Change Node ***/ -export interface ChangeNodeIntentAction { - type: TypeKeys.CONFIG_NODE_CHANGE_INTENT; - payload: string; -} - /*** Change Node Onetime ***/ -export interface ChangeNodeIntentOneTimeAction { - type: TypeKeys.CONFIG_NODE_CHANGE_INTENT_ONETIME; +export interface ChangeNodeRequestedOneTimeAction { + type: TypeKeys.CONFIG_CHANGE_NODE_REQUESTED_ONETIME; payload: string; } /*** Force Change Node ***/ export interface ChangeNodeForceAction { - type: TypeKeys.CONFIG_NODE_CHANGE_FORCE; + type: TypeKeys.CONFIG_CHANGE_NODE_FORCE; + payload: string; +} + +/*** Change Network Intent ***/ +export interface ChangeNetworkRequestedAction { + type: TypeKeys.CONFIG_CHANGE_NETWORK_REQUESTED; payload: string; } /*** Add Custom Node ***/ export interface AddCustomNodeAction { type: TypeKeys.CONFIG_ADD_CUSTOM_NODE; - payload: { id: string; config: CustomNodeConfig }; + payload: CustomNodeConfig; } /*** Remove Custom Node ***/ export interface RemoveCustomNodeAction { type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE; - payload: { id: string }; + payload: string; } /*** Add Custom Network ***/ export interface AddCustomNetworkAction { type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK; - payload: { id: string; config: CustomNetworkConfig }; + payload: CustomNetworkConfig; } /*** Remove Custom Network ***/ export interface RemoveCustomNetworkAction { type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK; - payload: { id: string }; + payload: string; } /*** Set Latest Block ***/ @@ -98,8 +104,8 @@ export type CustomNetworkAction = AddCustomNetworkAction | RemoveCustomNetworkAc export type CustomNodeAction = AddCustomNodeAction | RemoveCustomNodeAction; export type NodeAction = - | ChangeNodeAction - | ChangeNodeIntentAction + | ChangeNodeSucceededAction + | ChangeNodeRequestedAction | Web3UnsetNodeAction | Web3setNodeAction; diff --git a/common/actions/config/constants.ts b/common/actions/config/constants.ts index b2c2d421..e64d1cf8 100644 --- a/common/actions/config/constants.ts +++ b/common/actions/config/constants.ts @@ -11,10 +11,11 @@ export enum TypeKeys { 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_NODE_CHANGE_INTENT_ONETIME = 'CONFIG_NODE_CHANGE_INTENT_ONETIME', - CONFIG_NODE_CHANGE_FORCE = 'CONFIG_NODE_CHANGE_FORCE', + CONFIG_CHANGE_NODE_REQUESTED = 'CONFIG_CHANGE_NODE_REQUESTED', + CONFIG_CHANGE_NODE_SUCCEEDED = 'CONFIG_CHANGE_NODE_SUCCEEDED', + CONFIG_CHANGE_NODE_REQUESTED_ONETIME = 'CONFIG_CHANGE_NODE_REQUESTED_ONETIME', + CONFIG_CHANGE_NODE_FORCE = 'CONFIG_CHANGE_NODE_FORCE', + CONFIG_CHANGE_NETWORK_REQUESTED = 'CONFIG_CHANGE_NETWORK_REQUESTED', CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE', CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE', diff --git a/common/components/BalanceSidebar/AccountInfo.tsx b/common/components/BalanceSidebar/AccountInfo.tsx index b61ea52a..b1169619 100644 --- a/common/components/BalanceSidebar/AccountInfo.tsx +++ b/common/components/BalanceSidebar/AccountInfo.tsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { toChecksumAddress } from 'ethereumjs-util'; import { UnitDisplay, NewTabLink } from 'components/ui'; import { IWallet, TrezorWallet, LedgerWallet, Balance } from 'libs/wallet'; -import translate from 'translations'; +import translate, { translateRaw } from 'translations'; import Spinner from 'components/ui/Spinner'; import { getNetworkConfig, getOffline } from 'selectors/config'; import { AppState } from 'reducers'; @@ -163,7 +163,7 @@ class AccountInfo extends React.Component { )} - {network.name === 'ETH' && ( + {network.id === 'ETH' && (
  • {`${network.name} (${etherChainExplorerInst.origin})`} @@ -186,7 +186,7 @@ class AccountInfo extends React.Component { private setSymbol(network: NetworkConfig) { if (network.isTestnet) { - return network.unit + ' (' + network.name + ')'; + return `${network.unit} (${translateRaw('TESTNET')})`; } return network.unit; } diff --git a/common/components/CustomNodeModal/CustomNodeModal.tsx b/common/components/CustomNodeModal/CustomNodeModal.tsx index fbfe600a..883f8679 100644 --- a/common/components/CustomNodeModal/CustomNodeModal.tsx +++ b/common/components/CustomNodeModal/CustomNodeModal.tsx @@ -4,7 +4,7 @@ import translate, { translateRaw } from 'translations'; import { CustomNetworkConfig } from 'types/network'; import { CustomNodeConfig } from 'types/node'; import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config'; -import { connect, Omit } from 'react-redux'; +import { connect } from 'react-redux'; import { AppState } from 'reducers'; import { getCustomNetworkConfigs, @@ -13,7 +13,6 @@ import { } from 'selectors/config'; import { Input, Dropdown } from 'components/ui'; import './CustomNodeModal.scss'; -import { shepherdProvider } from 'libs/nodes'; const CUSTOM = { label: 'Custom', value: 'custom' }; @@ -70,7 +69,7 @@ class CustomNodeModal extends React.Component { public render() { const { customNetworks, handleClose, staticNetworks, isOpen } = this.props; - const { network } = this.state; + const { network, customNetworkChainId } = this.state; const isHttps = window.location.protocol.includes('https'); const invalids = this.getInvalids(); @@ -88,7 +87,10 @@ class CustomNodeModal extends React.Component { } ]; - const conflictedNode = this.getConflictedNode(); + const nameConflictNode = this.getNameConflictNode(); + const chainidConflictNetwork = + network === CUSTOM.value && this.getChainIdCollisionNetwork(customNetworkChainId); + const staticNetwrks = Object.keys(staticNetworks).map(net => { return { label: net, value: net }; }); @@ -106,9 +108,9 @@ class CustomNodeModal extends React.Component { > {isHttps &&
    {translate('NODE_WARNING')}
    } - {conflictedNode && ( + {nameConflictNode && (
    - {translate('CUSTOM_NODE_CONFLICT', { conflictedNode: conflictedNode.name })} + {translate('CUSTOM_NODE_NAME_CONFLICT', { $node: nameConflictNode.name })}
    )} @@ -171,6 +173,11 @@ class CustomNodeModal extends React.Component { )} + {chainidConflictNetwork && ( +
    + {translate('CUSTOM_NODE_CHAINID_CONFLICT', { $network: chainidConflictNetwork.name })} +
    + )}
  • - } - disabled={nodeSelection === 'web3'} - onChange={this.props.changeNodeIntent} - size="smr" - color="white" - menuAlign="right" - /> + @@ -242,7 +177,7 @@ class Header extends Component { private attemptSetNodeFromQueryParameter() { const { shouldSetNodeFromQS, networkParam } = this.props; if (shouldSetNodeFromQS) { - this.props.changeNodeIntentOneTime(networkParam!); + this.props.changeNodeRequestedOneTime(networkParam!); } } } diff --git a/common/components/NetworkSelector/NetworkOption.scss b/common/components/NetworkSelector/NetworkOption.scss new file mode 100644 index 00000000..a20b59a2 --- /dev/null +++ b/common/components/NetworkSelector/NetworkOption.scss @@ -0,0 +1,106 @@ +@import 'common/sass/variables'; +@import 'common/sass/mixins'; + +$radio-size: 12px; +$label-padding: 0.3rem 0.4rem; +$left-border-size: 2px; + +@if ($is-electron) { + $radio-size: 14px; + $label-padding: 12px 8px; + $left-border-size: 4px; +} + +.NetworkOption { + border-left: $left-border-size solid; + border-bottom: 1px solid $gray-lighter; + + &-label { + display: flex; + align-items: center; + padding: $label-padding; + cursor: pointer; + + &-name { + flex: 1; + @include ellipsis; + + @if ($is-electron) { + &.is-long-name { + font-size: 14px; + } + } + + &:before { + content: ""; + position: relative; + top: 1px; + display: inline-block; + border: 1px solid rgba($gray-light, 0.4); + border-radius: 100%; + height: $radio-size; + width: $radio-size; + margin-right: $space-xs; + box-shadow: 0 0 0 1px #FFF inset; + } + + &:hover { + &:before { + border-color: rgba($gray-light, 0.7); + } + } + + &.is-selected { + &:before { + background: $brand-primary; + border-color: $brand-primary; + } + } + + &.is-specific-node { + &:before { + background: linear-gradient(135deg, #FFF, #FFF 45%, $brand-primary 45%, $brand-primary 100%); + } + + &:hover:before { + background: rgba($brand-primary, 0.6); + } + } + + &-badge { + display: inline-block; + opacity: 0.5; + margin-left: 0.2rem; + font-size: 11px; + } + } + + &-expand { + @include reset-button; + width: 20px; + height: 20px; + font-size: 10px; + color: $gray-light; + border-radius: 100%; + transition-property: color, background-color, transform; + transition-duration: 75ms; + transition-timing-function: ease; + + &:hover { + color: $gray-dark; + background: rgba(#000, 0.1); + } + + &.is-expanded { + color: $gray-dark; + transform: rotate(180deg); + } + } + } + + &-nodes { + padding: 0.2rem 0.5rem; + background: $gray-lightest; + border-top: 1px solid $gray-lighter; + } +} diff --git a/common/components/NetworkSelector/NetworkOption.tsx b/common/components/NetworkSelector/NetworkOption.tsx new file mode 100644 index 00000000..7fdef9ef --- /dev/null +++ b/common/components/NetworkSelector/NetworkOption.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import translate, { translateRaw } from 'translations'; +import classnames from 'classnames'; +import { isAutoNode, isAutoNodeConfig } from 'libs/nodes'; +import { NodeConfig } from 'types/node'; +import { NetworkConfig } from 'types/network'; +import NodeOption from './NodeOption'; +import './NetworkOption.scss'; + +interface Props { + nodes: NodeConfig[]; + network: NetworkConfig; + nodeSelection: string; + isNetworkSelected: boolean; + isExpanded: boolean; + selectNode(node: NodeConfig): void; + selectNetwork(network: NetworkConfig): void; + toggleExpand(network: NetworkConfig): void; +} + +export default class NetworkOption extends React.PureComponent { + public render() { + const { nodes, network, nodeSelection, isExpanded, isNetworkSelected } = this.props; + const borderLeftColor = network.isCustom ? '#CCC' : network.color; + const singleNodes = nodes.filter(node => !isAutoNodeConfig(node)); + const isAutoSelected = isNetworkSelected && isAutoNode(nodeSelection); + const isLongName = network.name.length > 14; + + return ( +
    +
    +
    1, + 'is-long-name': isLongName + })} + title={translateRaw('NETWORKS_SWITCH', { $network: network.name })} + onClick={this.handleSelect} + > + {network.name} + {network.isTestnet && ( + ({translate('TESTNET')}) + )} +
    + +
    + {isExpanded && ( +
    + {singleNodes.map(node => ( + + ))} +
    + )} +
    + ); + } + + private handleSelect = () => { + this.props.selectNetwork(this.props.network); + }; + + private handleToggleExpand = () => { + this.props.toggleExpand(this.props.network); + }; +} diff --git a/common/components/NetworkSelector/NetworkSelector.scss b/common/components/NetworkSelector/NetworkSelector.scss new file mode 100644 index 00000000..51f127bc --- /dev/null +++ b/common/components/NetworkSelector/NetworkSelector.scss @@ -0,0 +1,51 @@ +@import 'common/sass/variables'; +@import 'common/sass/mixins'; + +$button-padding: $space-xs $space-md; +$button-font-size: $font-size-small; + +@if ($is-electron) { + $button-padding: 12px 0px; + $button-font-size: 13px; +} + + +.NetworkSelector { + width: 100%; + text-align: left; + color: $text-color; + font-size: $font-size-base; + + &-add, + &-alts { + @include reset-button; + display: block; + width: 100%; + padding: $button-padding; + text-align: center; + color: $text-color; + font-size: $button-font-size; + + &:hover { + color: $link-hover-color; + } + + .fa { + font-size: 12px; + width: 11px; + text-align: center; + opacity: 0.8; + margin-right: $space-xs; + } + } + + &-alts { + border-bottom: 1px solid $gray-lighter; + } + + @if ($is-electron) { + &-add { + border-bottom: 1px solid $gray-lighter; + } + } +} diff --git a/common/components/NetworkSelector/NetworkSelector.tsx b/common/components/NetworkSelector/NetworkSelector.tsx new file mode 100644 index 00000000..419790ec --- /dev/null +++ b/common/components/NetworkSelector/NetworkSelector.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import translate, { translateRaw } from 'translations'; +import NetworkOption from './NetworkOption'; +import { + TChangeNodeRequested, + changeNodeRequested, + TChangeNetworkRequested, + changeNetworkRequested +} from 'actions/config'; +import { + getNodeConfig, + getNetworkConfig, + getAllNodes, + getAllNetworkConfigs +} from 'selectors/config'; +import { NodeConfig } from 'types/node'; +import { NetworkConfig } from 'types/network'; +import { AppState } from 'reducers'; +import './NetworkSelector.scss'; + +const CORE_NETWORKS = ['ETH', 'ETC', 'Ropsten', 'Kovan', 'Rinkeby']; + +interface OwnProps { + openCustomNodeModal(): void; + onSelectNetwork?(network: NetworkConfig): void; + onSelectNode?(node: NodeConfig): void; +} + +interface StateProps { + node: NodeConfig; + network: NetworkConfig; + allNodes: { [key: string]: NodeConfig }; + allNetworks: { [key: string]: NetworkConfig }; +} + +interface DispatchProps { + changeNodeRequested: TChangeNodeRequested; + changeNetworkRequested: TChangeNetworkRequested; +} + +interface State { + isShowingAltNetworks: boolean; + expandedNetwork: null | NetworkConfig; +} + +type Props = OwnProps & StateProps & DispatchProps; + +class NetworkSelector extends React.Component { + public state: State = { + isShowingAltNetworks: false, + expandedNetwork: null + }; + + public componentDidMount() { + const { node } = this.props; + const newState = { ...this.state }; + // Expand alt networks by default if they're on one + if (!CORE_NETWORKS.includes(node.network)) { + newState.isShowingAltNetworks = true; + } + // Expand the network they're on if they selected a specific node + if (node.isCustom || !node.isAuto) { + newState.expandedNetwork = this.props.allNetworks[node.network]; + } + this.setState(newState); + } + + public render() { + const { allNodes, allNetworks, node } = this.props; + const { expandedNetwork, isShowingAltNetworks } = this.state; + + const nodesByNetwork = {} as { + [network: string]: NodeConfig[]; + }; + Object.values(allNodes).forEach((n: NodeConfig) => { + if (!nodesByNetwork[n.network]) { + nodesByNetwork[n.network] = []; + } + nodesByNetwork[n.network].push(n); + }, {}); + + const options = { + core: [] as React.ReactElement[], + alt: [] as React.ReactElement[] + }; + Object.keys(nodesByNetwork) + .sort((a, b) => { + // Sort by CORE_NETWORKS first, custom networks last + const idxA = CORE_NETWORKS.includes(a) ? CORE_NETWORKS.indexOf(a) : 999; + const idxB = CORE_NETWORKS.includes(b) ? CORE_NETWORKS.indexOf(b) : 999; + return idxA - idxB; + }) + .forEach(netKey => { + const network = allNetworks[netKey]; + const nodeType = CORE_NETWORKS.includes(netKey) || network.isCustom ? 'core' : 'alt'; + options[nodeType].push( + + ); + }); + + return ( +
    + {options.core} + + {isShowingAltNetworks && options.alt} + +
    + ); + } + + private selectNetwork = (net: NetworkConfig) => { + const { node } = this.props; + if (net.id === node.network && node.isAuto) { + return; + } + + this.props.changeNetworkRequested(net.id); + if (this.props.onSelectNetwork) { + this.props.onSelectNetwork(net); + } + }; + + private selectNode = (node: NodeConfig) => { + if (node.id === this.props.node.id) { + return; + } + + this.props.changeNodeRequested(node.id); + if (this.props.onSelectNode) { + this.props.onSelectNode(node); + } + }; + + private toggleNetworkExpand = (network: NetworkConfig) => { + this.setState({ + expandedNetwork: network === this.state.expandedNetwork ? null : network + }); + }; + + private toggleShowAltNetworks = () => { + this.setState({ isShowingAltNetworks: !this.state.isShowingAltNetworks }); + }; +} + +export default connect( + (state: AppState): StateProps => ({ + node: getNodeConfig(state), + network: getNetworkConfig(state), + allNodes: getAllNodes(state), + allNetworks: getAllNetworkConfigs(state) + }), + { + changeNodeRequested, + changeNetworkRequested + } +)(NetworkSelector); diff --git a/common/components/NetworkSelector/NodeOption.scss b/common/components/NetworkSelector/NodeOption.scss new file mode 100644 index 00000000..51c4e661 --- /dev/null +++ b/common/components/NetworkSelector/NodeOption.scss @@ -0,0 +1,64 @@ +@import 'common/sass/variables'; +@import 'common/sass/mixins'; + +$radio-size: 10px; + +.NodeOption { + display: flex; + align-items: center; + justify-content: space-between; + font-size: $font-size-xs-bump; + + &-name { + flex: 1; + padding: 0.2rem 0; + cursor: pointer; + @include ellipsis; + + &:before { + content: ''; + display: inline-block; + border: 1px solid rgba($gray-light, 0.4); + border-radius: 100%; + height: $radio-size; + width: $radio-size; + margin-right: $space-xs; + background: #FFF; + box-shadow: 0 0 0 1px #FFF inset; + } + + &:hover { + &:before { + border-color: rgba($gray-light, 0.8); + } + } + + &.is-selected { + &:before { + border-color: $brand-primary; + background: $brand-primary; + } + } + + &.is-auto-selected { + &:before { + background: rgba($brand-primary, 0.3); + } + + &:hover:before { + background: rgba($brand-primary, 0.8); + } + } + } + + &-remove { + @include reset-button; + opacity: 0.25; + font-size: 14px; + + &:hover { + color: $brand-danger; + opacity: 0.8; + } + } +} diff --git a/common/components/NetworkSelector/NodeOption.tsx b/common/components/NetworkSelector/NodeOption.tsx new file mode 100644 index 00000000..9fd3894b --- /dev/null +++ b/common/components/NetworkSelector/NodeOption.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import classnames from 'classnames'; +import { translateRaw } from 'translations'; +import { TRemoveCustomNode, removeCustomNode } from 'actions/config'; +import { NodeConfig } from 'types/node'; +import './NodeOption.scss'; + +interface OwnProps { + node: NodeConfig; + isSelected: boolean; + isAutoSelected: boolean; + select(node: NodeConfig): void; +} + +interface DispatchProps { + removeCustomNode: TRemoveCustomNode; +} + +type Props = OwnProps & DispatchProps; + +class NodeOption extends React.PureComponent { + public render() { + const { node, isSelected, isAutoSelected } = this.props; + return ( +
    +
    + {node.isCustom ? node.name : node.service} +
    + {node.isCustom && ( + + )} +
    + ); + } + + private handleSelect = () => { + this.props.select(this.props.node); + }; + + private handleRemove = () => { + if (this.props.node.isCustom) { + this.props.removeCustomNode(this.props.node.id); + } + }; +} + +export default connect(undefined, { removeCustomNode })(NodeOption); diff --git a/common/components/NetworkSelector/index.tsx b/common/components/NetworkSelector/index.tsx new file mode 100644 index 00000000..b6e29471 --- /dev/null +++ b/common/components/NetworkSelector/index.tsx @@ -0,0 +1,2 @@ +import NetworkSelector from './NetworkSelector'; +export default NetworkSelector; diff --git a/common/components/ui/ColorDropdown.scss b/common/components/ui/ColorDropdown.scss deleted file mode 100644 index 8d061712..00000000 --- a/common/components/ui/ColorDropdown.scss +++ /dev/null @@ -1,23 +0,0 @@ -.ColorDropdown { - &-item { - position: relative; - padding-right: 10px; - border-left: 2px solid #fff; - - &-remove { - position: absolute; - top: 50%; - right: 5px; - width: 15px; - height: 15px; - opacity: 0.5; - cursor: pointer; - // Z fixes clipping issue - transform: translateY(-50%) translateZ(0); - - &:hover { - opacity: 1; - } - } - } -} diff --git a/common/components/ui/ColorDropdown.tsx b/common/components/ui/ColorDropdown.tsx deleted file mode 100644 index f46e81de..00000000 --- a/common/components/ui/ColorDropdown.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { PureComponent } from 'react'; -import classnames from 'classnames'; -import DropdownShell from './DropdownShell'; -import removeIcon from 'assets/images/icon-remove.svg'; -import './ColorDropdown.scss'; - -interface Option { - name: any; - value: T; - color?: string; - hidden?: boolean | undefined; - onRemove?(): void; -} - -interface Props { - value: T; - options: Option[]; - label?: string; - ariaLabel: string; - extra?: any; - size?: string; - color?: string; - menuAlign?: string; - disabled?: boolean; - onChange(value: T): void; -} - -export default class ColorDropdown extends PureComponent, {}> { - private dropdownShell: DropdownShell | null; - - public render() { - const { ariaLabel, disabled, color, size } = this.props; - - return ( - (this.dropdownShell = el)} - disabled={disabled} - /> - ); - } - - private renderLabel = () => { - const label = this.props.label ? `${this.props.label}:` : ''; - const activeOption = this.getActiveOption(); - - return ( - - {label} {activeOption ? activeOption.name : '-'} - - ); - }; - - private renderOptions = () => { - const { options, value, menuAlign, extra } = this.props; - - const listItems = options.filter(opt => !opt.hidden).reduce((prev: any[], opt) => { - const prevOpt = prev.length ? prev[prev.length - 1] : null; - if (prevOpt && !prevOpt.divider && prevOpt.color !== opt.color) { - prev.push({ divider: true }); - } - prev.push(opt); - return prev; - }, []); - - const menuClass = classnames({ - ColorDropdown: true, - 'dropdown-menu': true, - [`dropdown-menu-${menuAlign || ''}`]: !!menuAlign - }); - - return ( - - ); - }; - - private onChange = (value: any) => { - this.props.onChange(value); - if (this.dropdownShell) { - this.dropdownShell.close(); - } - }; - - private onRemove(onRemove: () => void, ev?: React.FormEvent) { - if (ev) { - ev.preventDefault(); - ev.stopPropagation(); - } - onRemove(); - } - - private getActiveOption() { - return this.props.options.find(opt => opt.value === this.props.value); - } -} diff --git a/common/components/ui/DropdownShell.tsx b/common/components/ui/DropdownShell.tsx index ebc97e4f..35f10285 100644 --- a/common/components/ui/DropdownShell.tsx +++ b/common/components/ui/DropdownShell.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { Component } from 'react'; import classnames from 'classnames'; interface Props { @@ -14,7 +14,7 @@ interface State { expanded: boolean; } -export default class DropdownComponent extends PureComponent { +export default class DropdownComponent extends Component { public static defaultProps = { color: 'default', size: 'sm' diff --git a/common/components/ui/index.ts b/common/components/ui/index.ts index a8df4308..602e8cb5 100644 --- a/common/components/ui/index.ts +++ b/common/components/ui/index.ts @@ -1,5 +1,4 @@ export { default as Dropdown } from './Dropdown'; -export { default as ColorDropdown } from './ColorDropdown'; export { default as OldDropDown } from './OldDropdown'; export { default as DropdownShell } from './DropdownShell'; export { default as Identicon } from './Identicon'; diff --git a/common/containers/TabSection/WebTemplate.tsx b/common/containers/TabSection/WebTemplate.tsx index abb6dad3..fd3a768c 100644 --- a/common/containers/TabSection/WebTemplate.tsx +++ b/common/containers/TabSection/WebTemplate.tsx @@ -6,6 +6,7 @@ import Notifications from './Notifications'; import OfflineTab from './OfflineTab'; import { getOffline, getLatestBlock } from 'selectors/config'; import { Query } from 'components/renderCbs'; +import { makeAutoNodeName } from 'libs/nodes'; import './WebTemplate.scss'; interface StateProps { @@ -29,7 +30,7 @@ class WebTemplate extends Component { ( -
    +
    )} />
    diff --git a/common/containers/Tabs/CheckTransaction/index.tsx b/common/containers/Tabs/CheckTransaction/index.tsx index 8210a7f9..132db895 100644 --- a/common/containers/Tabs/CheckTransaction/index.tsx +++ b/common/containers/Tabs/CheckTransaction/index.tsx @@ -45,7 +45,7 @@ class CheckTransaction extends React.Component { const { network } = this.props; const { hash } = this.state; const CHECK_TX_KEY = - network.name === 'ETH' + network.id === 'ETH' ? 'CHECK_TX_STATUS_DESCRIPTION_MULTIPLE' : 'CHECK_TX_STATUS_DESCRIPTION_2'; diff --git a/common/libs/nodes/configs.ts b/common/libs/nodes/configs.ts new file mode 100644 index 00000000..3c9a99e4 --- /dev/null +++ b/common/libs/nodes/configs.ts @@ -0,0 +1,144 @@ +import { RawNodeConfig } from 'types/node'; +import { StaticNetworkIds } from 'types/network'; + +export const makeNodeName = (network: string, name: string) => { + return `${network.toLowerCase()}_${name}`; +}; + +export const NODE_CONFIGS: { [key in StaticNetworkIds]: RawNodeConfig[] } = { + ETH: [ + { + name: makeNodeName('ETH', 'mycrypto'), + type: 'rpc', + service: 'MyCrypto', + url: 'https://api.mycryptoapi.com/eth', + estimateGas: true + }, + { + name: makeNodeName('ETH', 'ethscan'), + type: 'etherscan', + service: 'Etherscan', + url: 'https://api.etherscan.io/api', + estimateGas: false + }, + { + name: makeNodeName('ETH', 'infura'), + type: 'infura', + service: 'Infura', + url: 'https://mainnet.infura.io/mycrypto', + estimateGas: false + }, + { + name: makeNodeName('ETH', 'blockscale'), + type: 'rpc', + service: 'Blockscale', + url: 'https://api.dev.blockscale.net/dev/parity', + estimateGas: true + } + ], + + Ropsten: [ + { + name: makeNodeName('Ropsten', 'infura'), + type: 'infura', + service: 'Infura', + url: 'https://ropsten.infura.io/mycrypto', + estimateGas: false + } + ], + + Kovan: [ + { + name: makeNodeName('Kovan', 'ethscan'), + type: 'etherscan', + service: 'Etherscan', + url: 'https://kovan.etherscan.io/api', + estimateGas: false + } + ], + + Rinkeby: [ + { + name: makeNodeName('Rinkeby', 'infura'), + type: 'infura', + service: 'Infura', + url: 'https://rinkeby.infura.io/mycrypto', + estimateGas: false + }, + { + name: makeNodeName('Rinkeby', 'ethscan'), + type: 'etherscan', + service: 'Etherscan', + url: 'https://rinkeby.etherscan.io/api', + estimateGas: false + } + ], + + ETC: [ + { + name: makeNodeName('ETC', 'epool'), + type: 'rpc', + service: 'Epool.io', + url: 'https://mewapi.epool.io', + estimateGas: false + }, + { + name: makeNodeName('ETC', 'commonwealth'), + type: 'rpc', + service: 'Ethereum Commonwealth', + url: 'https://etc-geth.0xinfra.com/', + estimateGas: false + } + ], + + UBQ: [ + { + name: makeNodeName('UBQ', 'ubiqscan'), + type: 'rpc', + service: 'ubiqscan.io', + url: 'https://pyrus2.ubiqscan.io', + estimateGas: true + } + ], + + EXP: [ + { + name: makeNodeName('EXP', 'tech'), + type: 'rpc', + service: 'expanse.tech', + url: 'https://node.expanse.tech/', + estimateGas: true + } + ], + POA: [ + { + name: makeNodeName('POA', 'core'), + type: 'rpc', + service: 'poa.network', + url: 'https://core.poa.network', + estimateGas: true + } + ], + + TOMO: [ + { + name: makeNodeName('TOMO', 'tomocoin'), + type: 'rpc', + service: 'tomocoin.io', + url: 'https://core.tomocoin.io', + estimateGas: true + } + ], + + ELLA: [ + { + name: makeNodeName('ELLA', 'ellaism'), + type: 'rpc', + service: 'ellaism.org', + url: 'https://jsonrpc.ellaism.org', + estimateGas: true + } + ] +}; + +export default NODE_CONFIGS; diff --git a/common/libs/nodes/index.ts b/common/libs/nodes/index.ts index 43fc7c9a..ddb82d6c 100644 --- a/common/libs/nodes/index.ts +++ b/common/libs/nodes/index.ts @@ -2,6 +2,8 @@ import { shepherd, redux } from 'mycrypto-shepherd'; import { INode } from '.'; import { tokenBalanceHandler } from './tokenBalanceProxy'; import { IProviderConfig } from 'mycrypto-shepherd/dist/lib/ducks/providerConfigs'; +import { NODE_CONFIGS, makeNodeName } from './configs'; +import { NodeConfig } from 'types/node'; type DeepPartial = Partial<{ [key in keyof T]: Partial }>; const { selectors, store } = redux; @@ -59,130 +61,41 @@ export const getShepherdNetwork = () => balancerConfigSelectors.getNetwork(store export const getShepherdPending = () => balancerConfigSelectors.isSwitchingNetworks(store.getState()); -export const makeWeb3Network = (network: string) => `WEB3_${network}`; -export const stripWeb3Network = (network: string) => network.replace('WEB3_', ''); -export const isAutoNode = (nodeName: string) => nodeName.endsWith('_auto') || nodeName === 'web3'; - -const regEthConf = makeProviderConfig({ network: 'ETH' }); -shepherd.useProvider('rpc', 'eth_mycrypto', regEthConf, 'https://api.mycryptoapi.com/eth'); -shepherd.useProvider('etherscan', 'eth_ethscan', regEthConf, 'https://api.etherscan.io/api'); -shepherd.useProvider('infura', 'eth_infura', regEthConf, 'https://mainnet.infura.io/mycrypto'); -shepherd.useProvider( - 'rpc', - 'eth_blockscale', - regEthConf, - 'https://api.dev.blockscale.net/dev/parity' -); - -const regRopConf = makeProviderConfig({ network: 'Ropsten' }); -shepherd.useProvider('infura', 'rop_infura', regRopConf, 'https://ropsten.infura.io/mycrypto'); - -const regKovConf = makeProviderConfig({ network: 'Kovan' }); -shepherd.useProvider('etherscan', 'kov_ethscan', regKovConf, 'https://kovan.etherscan.io/api'); - -const regRinConf = makeProviderConfig({ network: 'Rinkeby' }); -shepherd.useProvider('infura', 'rin_ethscan', regRinConf, 'https://rinkeby.infura.io/mycrypto'); -shepherd.useProvider('etherscan', 'rin_infura', regRinConf, 'https://rinkeby.etherscan.io/api'); - -const regEtcConf = makeProviderConfig({ network: 'ETC' }); -shepherd.useProvider('rpc', 'etc_epool', regEtcConf, 'https://mewapi.epool.io'); -shepherd.useProvider('rpc', 'etc_commonwealth', regEtcConf, 'https://etc-geth.0xinfra.com/'); - -const regUbqConf = makeProviderConfig({ network: 'UBQ' }); -shepherd.useProvider('rpc', 'ubq', regUbqConf, 'https://pyrus2.ubiqscan.io'); - -const regExpConf = makeProviderConfig({ network: 'EXP' }); -shepherd.useProvider('rpc', 'exp_tech', regExpConf, 'https://node.expanse.tech/'); - -const regPoaConf = makeProviderConfig({ network: 'POA' }); -shepherd.useProvider('rpc', 'poa', regPoaConf, 'https://core.poa.network'); - -const regTomoConf = makeProviderConfig({ network: 'TOMO' }); -shepherd.useProvider('rpc', 'tomo', regTomoConf, 'https://core.tomocoin.io'); - -const regEllaConf = makeProviderConfig({ network: 'ELLA' }); -shepherd.useProvider('rpc', 'ella', regEllaConf, 'https://jsonrpc.ellaism.org'); +const autoNodeSuffix = 'auto'; +const web3NodePrefix = 'WEB3_'; +export const makeWeb3Network = (network: string) => `${web3NodePrefix}${network}`; +export const stripWeb3Network = (network: string) => network.replace(web3NodePrefix, ''); +export const isAutoNode = (nodeName: string) => + nodeName.endsWith(autoNodeSuffix) || nodeName === 'web3'; +export const isAutoNodeConfig = (node: NodeConfig) => !node.isCustom && node.isAuto; +export const makeAutoNodeName = (network: string) => makeNodeName(network, autoNodeSuffix); /** - * Pseudo-networks to support metamask / web3 interaction + * Assemble shepherd providers from node configs. Includes pseudo-configs */ -const web3EthConf = makeProviderConfig({ - network: makeWeb3Network('ETH'), - supportedMethods: { - sendRawTx: false, - sendTransaction: false, - signMessage: false, - getNetVersion: false - } -}); -shepherd.useProvider('rpc', 'web3_eth_mycrypto', web3EthConf, 'https://api.mycryptoapi.com/eth'); -shepherd.useProvider('etherscan', 'web3_eth_ethscan', web3EthConf, 'https://api.etherscan.io/api'); -shepherd.useProvider( - 'infura', - 'web3_eth_infura', - web3EthConf, - 'https://mainnet.infura.io/mycrypto' -); -shepherd.useProvider( - 'rpc', - 'web3_eth_blockscale', - web3EthConf, - 'https://api.dev.blockscale.net/dev/parity' -); +const WEB3_NETWORKS = ['ETH', 'Ropsten', 'Kovan', 'Rinkeby', 'ETC']; +Object.entries(NODE_CONFIGS).forEach(([network, nodes]) => { + const nodeProviderConf = makeProviderConfig({ network }); + const web3ProviderConf = WEB3_NETWORKS.includes(network) + ? makeProviderConfig({ + network: makeWeb3Network(network), + supportedMethods: { + sendRawTx: false, + sendTransaction: false, + signMessage: false, + getNetVersion: false + } + }) + : null; -const web3RopConf = makeProviderConfig({ - network: makeWeb3Network('Ropsten'), - supportedMethods: { - sendRawTx: false, - sendTransaction: false, - signMessage: false, - getNetVersion: false - } + nodes.forEach(n => { + shepherd.useProvider(n.type, n.name, nodeProviderConf, n.url); + if (web3ProviderConf) { + shepherd.useProvider(n.type, `web3_${n.name}`, web3ProviderConf, n.url); + } + }); }); -shepherd.useProvider( - 'infura', - 'web3_rop_infura', - web3RopConf, - 'https://ropsten.infura.io/mycrypto' -); - -const web3KovConf = makeProviderConfig({ - network: makeWeb3Network('Kovan'), - supportedMethods: { - sendRawTx: false, - sendTransaction: false, - signMessage: false, - getNetVersion: false - } -}); -shepherd.useProvider( - 'etherscan', - 'web3_kov_ethscan', - web3KovConf, - 'https://kovan.etherscan.io/api' -); - -const web3RinConf = makeProviderConfig({ - network: makeWeb3Network('Rinkeby'), - supportedMethods: { - sendRawTx: false, - sendTransaction: false, - signMessage: false, - getNetVersion: false - } -}); -shepherd.useProvider( - 'infura', - 'web3_rin_ethscan', - web3RinConf, - 'https://rinkeby.infura.io/mycrypto' -); -shepherd.useProvider( - 'etherscan', - 'web3_rin_infura', - web3RinConf, - 'https://rinkeby.etherscan.io/api' -); export { shepherdProvider, shepherd }; export * from './INode'; +export * from './configs'; diff --git a/common/libs/nodes/web3/index.ts b/common/libs/nodes/web3/index.ts index b2942438..56a45f57 100644 --- a/common/libs/nodes/web3/index.ts +++ b/common/libs/nodes/web3/index.ts @@ -64,18 +64,18 @@ export async function setupWeb3Node() { } const lib = new Web3Node(); - const networkId = await lib.getNetVersion(); + const chainId = await lib.getNetVersion(); const accounts = await lib.getAccounts(); if (!accounts.length) { throw new Error('No accounts found in MetaMask / Mist.'); } - if (networkId === 'loading') { + if (chainId === 'loading') { throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.'); } - return { networkId, lib }; + return { chainId, lib }; } export async function isWeb3NodeAvailable(): Promise { diff --git a/common/libs/wallet/non-deterministic/web3.ts b/common/libs/wallet/non-deterministic/web3.ts index 97d0ff1a..8a834361 100644 --- a/common/libs/wallet/non-deterministic/web3.ts +++ b/common/libs/wallet/non-deterministic/web3.ts @@ -2,7 +2,7 @@ import { getTransactionFields, makeTransaction } from 'libs/transaction'; import { IFullWallet } from '../IWallet'; import { bufferToHex, toChecksumAddress } from 'ethereumjs-util'; import { configuredStore } from 'store'; -import { getNodeLib, getNetworkNameByChainId } from 'selectors/config'; +import { getNodeLib, getNetworkByChainId } from 'selectors/config'; import Web3Node from 'libs/nodes/web3'; import { INode } from 'libs/nodes/INode'; @@ -75,12 +75,14 @@ export default class Web3Wallet implements IFullWallet { private async networkCheck(lib: Web3Node) { const netId = await lib.getNetVersion(); - const netName = getNetworkNameByChainId(configuredStore.getState(), netId); - if (this.network !== netName) { + const networkConfig = getNetworkByChainId(configuredStore.getState(), netId); + if (!networkConfig) { + throw new Error(`MyCrypto doesn’t support the network with chain ID '${netId}'`); + } else if (this.network !== networkConfig.id) { throw new Error( - `Expected MetaMask / Mist network to be ${ - this.network - }, but got ${netName}. Please change the network or refresh the page.` + `Expected MetaMask / Mist network to be ${this.network}, but got ${ + networkConfig.id + }. Please change the network or refresh the page.` ); } } diff --git a/common/reducers/config/networks/customNetworks.ts b/common/reducers/config/networks/customNetworks.ts index 55bedf31..b7800ec9 100644 --- a/common/reducers/config/networks/customNetworks.ts +++ b/common/reducers/config/networks/customNetworks.ts @@ -8,12 +8,12 @@ import { CustomNetworksState as State } from './types'; const addCustomNetwork = (state: State, { payload }: AddCustomNetworkAction): State => ({ ...state, - [payload.id]: payload.config + [payload.id]: payload }); function removeCustomNetwork(state: State, { payload }: RemoveCustomNetworkAction): State { const stateCopy = { ...state }; - Reflect.deleteProperty(stateCopy, payload.id); + Reflect.deleteProperty(stateCopy, payload); return stateCopy; } diff --git a/common/reducers/config/networks/staticNetworks.ts b/common/reducers/config/networks/staticNetworks.ts index 492a057a..16fcfed1 100644 --- a/common/reducers/config/networks/staticNetworks.ts +++ b/common/reducers/config/networks/staticNetworks.ts @@ -30,7 +30,8 @@ const testnetDefaultGasPrice = { export const INITIAL_STATE: State = { ETH: { - name: 'ETH', + id: 'ETH', + name: 'Ethereum', unit: 'ETH', chainId: 1, isCustom: false, @@ -54,6 +55,7 @@ export const INITIAL_STATE: State = { shouldEstimateGasPrice: true }, Ropsten: { + id: 'Ropsten', name: 'Ropsten', unit: 'ETH', chainId: 3, @@ -74,6 +76,7 @@ export const INITIAL_STATE: State = { gasPriceSettings: testnetDefaultGasPrice }, Kovan: { + id: 'Kovan', name: 'Kovan', unit: 'ETH', chainId: 42, @@ -94,6 +97,7 @@ export const INITIAL_STATE: State = { gasPriceSettings: testnetDefaultGasPrice }, Rinkeby: { + id: 'Rinkeby', name: 'Rinkeby', unit: 'ETH', chainId: 4, @@ -114,7 +118,8 @@ export const INITIAL_STATE: State = { gasPriceSettings: testnetDefaultGasPrice }, ETC: { - name: 'ETC', + id: 'ETC', + name: 'Ethereum Classic', unit: 'ETC', chainId: 61, isCustom: false, @@ -138,7 +143,8 @@ export const INITIAL_STATE: State = { } }, UBQ: { - name: 'UBQ', + id: 'UBQ', + name: 'Ubiq', unit: 'UBQ', chainId: 8, isCustom: false, @@ -161,7 +167,8 @@ export const INITIAL_STATE: State = { } }, EXP: { - name: 'EXP', + id: 'EXP', + name: 'Expanse', unit: 'EXP', chainId: 2, isCustom: false, @@ -184,6 +191,7 @@ export const INITIAL_STATE: State = { } }, POA: { + id: 'POA', name: 'POA', unit: 'POA', chainId: 99, @@ -209,7 +217,8 @@ export const INITIAL_STATE: State = { } }, TOMO: { - name: 'TOMO', + id: 'TOMO', + name: 'TomoChain', unit: 'TOMO', chainId: 40686, isCustom: false, @@ -233,7 +242,8 @@ export const INITIAL_STATE: State = { } }, ELLA: { - name: 'ELLA', + id: 'ELLA', + name: 'Ellaism', unit: 'ELLA', chainId: 64, isCustom: false, diff --git a/common/reducers/config/nodes/customNodes.ts b/common/reducers/config/nodes/customNodes.ts index 12a1e176..32d6cfcf 100644 --- a/common/reducers/config/nodes/customNodes.ts +++ b/common/reducers/config/nodes/customNodes.ts @@ -8,12 +8,12 @@ import { CustomNodesState as State } from './types'; const addCustomNode = (state: State, { payload }: AddCustomNodeAction): State => ({ ...state, - [payload.id]: payload.config + [payload.id]: payload }); function removeCustomNode(state: State, { payload }: RemoveCustomNodeAction): State { const stateCopy = { ...state }; - Reflect.deleteProperty(stateCopy, payload.id); + Reflect.deleteProperty(stateCopy, payload); return stateCopy; } diff --git a/common/reducers/config/nodes/selectedNode.ts b/common/reducers/config/nodes/selectedNode.ts index 0feabfda..29f117a9 100644 --- a/common/reducers/config/nodes/selectedNode.ts +++ b/common/reducers/config/nodes/selectedNode.ts @@ -1,44 +1,40 @@ import { - ChangeNodeAction, - ChangeNodeIntentAction, + ChangeNodeRequestedAction, + ChangeNodeSucceededAction, NodeAction, TypeKeys, - RemoveCustomNodeAction, CustomNodeAction } from 'actions/config'; +import { makeAutoNodeName } from 'libs/nodes'; import { SelectedNodeState as State } from './types'; export const INITIAL_STATE: State = { - nodeId: 'eth_auto', - prevNode: 'eth_auto', + nodeId: makeAutoNodeName('ETH'), + prevNode: makeAutoNodeName('ETH'), pending: false }; -const changeNode = (state: State, { payload }: ChangeNodeAction): State => ({ +const changeNodeRequested = (state: State, _: ChangeNodeRequestedAction): State => ({ + ...state, + pending: true +}); + +const changeNodeSucceeded = (state: State, { payload }: ChangeNodeSucceededAction): State => ({ nodeId: payload.nodeId, // make sure we dont accidentally switch back to a web3 node prevNode: state.nodeId === 'web3' ? state.prevNode : state.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); + case TypeKeys.CONFIG_CHANGE_NODE_SUCCEEDED: + return changeNodeSucceeded(state, action); + case TypeKeys.CONFIG_CHANGE_NODE_REQUESTED: + return changeNodeRequested(state, action); default: return state; } diff --git a/common/reducers/config/nodes/staticNodes.ts b/common/reducers/config/nodes/staticNodes.ts index d8c141e5..6ccbcad8 100644 --- a/common/reducers/config/nodes/staticNodes.ts +++ b/common/reducers/config/nodes/staticNodes.ts @@ -1,191 +1,37 @@ import { TypeKeys, NodeAction } from 'actions/config'; -import { shepherdProvider } from 'libs/nodes'; +import { NODE_CONFIGS, makeAutoNodeName } from 'libs/nodes'; import { StaticNodesState } from './types'; +import { RawNodeConfig } from 'types/node'; +import { StaticNetworkIds } from 'types/network'; -export const INITIAL_STATE: StaticNodesState = { - eth_auto: { - network: 'ETH', +function makeStateFromNodeConfigs(prev: Partial, network: StaticNetworkIds) { + // Auto network + const autoId = makeAutoNodeName(network); + prev[autoId] = { + network, + id: autoId, + isAuto: true, isCustom: false, - lib: shepherdProvider, - service: 'AUTO', - estimateGas: true - }, - eth_mycrypto: { - network: 'ETH', - isCustom: false, - lib: shepherdProvider, - service: 'MyCrypto', - estimateGas: true - }, - eth_ethscan: { - network: 'ETH', - isCustom: false, - service: 'Etherscan.io', - lib: shepherdProvider, - estimateGas: false - }, + service: 'AUTO' + }; - eth_infura: { - network: 'ETH', - isCustom: false, - service: 'infura.io', - lib: shepherdProvider, - estimateGas: false - }, - eth_blockscale: { - network: 'ETH', - isCustom: false, - lib: shepherdProvider, - service: 'Blockscale beta', - estimateGas: true - }, + // Static networks + NODE_CONFIGS[network].forEach((config: RawNodeConfig) => { + prev[config.name] = { + network, + id: config.name, + isCustom: false, + service: config.service + }; + }); - rop_auto: { - network: 'Ropsten', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: false - }, - rop_infura: { - network: 'Ropsten', - isCustom: false, - service: 'infura.io', - lib: shepherdProvider, - estimateGas: false - }, + return prev; +} - kov_auto: { - network: 'Kovan', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: false - }, - kov_ethscan: { - network: 'Kovan', - isCustom: false, - service: 'Etherscan.io', - lib: shepherdProvider, - estimateGas: false - }, - - rin_auto: { - network: 'Rinkeby', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: false - }, - rin_ethscan: { - network: 'Rinkeby', - isCustom: false, - service: 'Etherscan.io', - lib: shepherdProvider, - estimateGas: false - }, - rin_infura: { - network: 'Rinkeby', - isCustom: false, - service: 'infura.io', - lib: shepherdProvider, - estimateGas: false - }, - - etc_auto: { - network: 'ETC', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: false - }, - etc_epool: { - network: 'ETC', - isCustom: false, - service: 'Epool.io', - lib: shepherdProvider, - estimateGas: false - }, - etc_commonwealth: { - network: 'ETC', - isCustom: false, - service: 'Ethereum Commonwealth', - lib: shepherdProvider, - estimateGas: false - }, - - ubq_auto: { - network: 'UBQ', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: true - }, - ubq: { - network: 'UBQ', - isCustom: false, - service: 'ubiqscan.io', - lib: shepherdProvider, - estimateGas: true - }, - - exp_auto: { - network: 'EXP', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: true - }, - exp_tech: { - network: 'EXP', - isCustom: false, - service: 'Expanse.tech', - lib: shepherdProvider, - estimateGas: true - }, - poa_auto: { - network: 'POA', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: true - }, - poa: { - network: 'POA', - isCustom: false, - service: 'poa.network', - lib: shepherdProvider, - estimateGas: true - }, - tomo_auto: { - network: 'TOMO', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: true - }, - tomo: { - network: 'TOMO', - isCustom: false, - service: 'tomocoin.io', - lib: shepherdProvider, - estimateGas: true - }, - ella_auto: { - network: 'ELLA', - isCustom: false, - service: 'AUTO', - lib: shepherdProvider, - estimateGas: true - }, - ella: { - network: 'ELLA', - isCustom: false, - service: 'ellaism.org', - lib: shepherdProvider, - estimateGas: true - } -}; +export const INITIAL_STATE: StaticNodesState = Object.keys(NODE_CONFIGS).reduce( + makeStateFromNodeConfigs, + {} +); const staticNodes = (state: StaticNodesState = INITIAL_STATE, action: NodeAction) => { switch (action.type) { diff --git a/common/sagas/config/network.ts b/common/sagas/config/network.ts index 3974f0fd..2128f00e 100644 --- a/common/sagas/config/network.ts +++ b/common/sagas/config/network.ts @@ -14,15 +14,14 @@ export function* pruneCustomNetworks(): SagaIterator { ); //construct lookup table of networks - const linkedNetworks: { [key: string]: boolean } = 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 })); + for (const customNetwork of Object.values(customNetworks)) { + if (!linkedNetworks[customNetwork.id]) { + yield put(removeCustomNetwork(customNetwork.id)); } } } diff --git a/common/sagas/config/node.ts b/common/sagas/config/node.ts index 39ba5c4e..110a3f0e 100644 --- a/common/sagas/config/node.ts +++ b/common/sagas/config/node.ts @@ -17,19 +17,23 @@ import { isStaticNodeId, getCustomNodeFromId, getStaticNodeFromId, - getNetworkConfigById + getNetworkConfigById, + getAllNodes } from 'selectors/config'; import { TypeKeys } from 'actions/config/constants'; import { setOnline, setOffline, - changeNode, - changeNodeIntent, + changeNodeRequested, + changeNodeSucceeded, + changeNodeForce, setLatestBlock, AddCustomNodeAction, ChangeNodeForceAction, - ChangeNodeIntentAction, - ChangeNodeIntentOneTimeAction + ChangeNodeRequestedAction, + ChangeNodeRequestedOneTimeAction, + ChangeNetworkRequestedAction, + RemoveCustomNodeAction } from 'actions/config'; import { showNotification } from 'actions/notifications'; import { resetWallet } from 'actions/wallet'; @@ -44,8 +48,10 @@ import { stripWeb3Network, makeProviderConfig, getShepherdNetwork, - getShepherdPending + getShepherdPending, + makeAutoNodeName } from 'libs/nodes'; +import { INITIAL_STATE as selectedNodeInitialState } from 'reducers/config/nodes/selectedNode'; export function* pollOfflineStatus(): SagaIterator { let hasCheckedOnline = false; @@ -111,25 +117,28 @@ export function* reload(): SagaIterator { setTimeout(() => location.reload(), 1150); } -export function* handleNodeChangeIntentOneTime(): SagaIterator { - const action: ChangeNodeIntentOneTimeAction = yield take( - TypeKeys.CONFIG_NODE_CHANGE_INTENT_ONETIME +export function* handleChangeNodeRequestedOneTime(): SagaIterator { + const action: ChangeNodeRequestedOneTimeAction = yield take( + TypeKeys.CONFIG_CHANGE_NODE_REQUESTED_ONETIME ); // allow shepherdProvider async init to complete. TODO - don't export shepherdProvider as promise yield call(delay, 100); - yield put(changeNodeIntent(action.payload)); + yield put(changeNodeRequested(action.payload)); } -export function* handleNodeChangeIntent({ +export function* handleChangeNodeRequested({ payload: nodeIdToSwitchTo -}: ChangeNodeIntentAction): SagaIterator { +}: ChangeNodeRequestedAction): SagaIterator { const isStaticNode: boolean = yield select(isStaticNodeId, nodeIdToSwitchTo); const currentConfig: NodeConfig = yield select(getNodeConfig); + // Bail out if they're switching to the same node + if (currentConfig.id === nodeIdToSwitchTo) { + return; + } + 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; @@ -186,7 +195,7 @@ export function* handleNodeChangeIntent({ } yield put(setLatestBlock(currentBlock)); - yield put(changeNode({ networkId: nextNodeConfig.network, nodeId: nodeIdToSwitchTo })); + yield put(changeNodeSucceeded({ networkId: nextNodeConfig.network, nodeId: nodeIdToSwitchTo })); if (currentConfig.network !== nextNodeConfig.network) { yield fork(handleNewNetwork); @@ -194,14 +203,14 @@ export function* handleNodeChangeIntent({ } export function* handleAddCustomNode(action: AddCustomNodeAction): SagaIterator { - const { payload: { config } } = action; + const config = action.payload; shepherd.useProvider( 'myccustom', config.id, makeProviderConfig({ network: config.network }), config ); - yield put(changeNodeIntent(action.payload.id)); + yield put(changeNodeRequested(config.id)); } export function* handleNewNetwork() { @@ -222,18 +231,58 @@ export function* handleNodeChangeForce({ payload: staticNodeIdToSwitchTo }: Chan const nodeConfig = yield select(getStaticNodeFromId, staticNodeIdToSwitchTo); // force the node change - yield put(changeNode({ networkId: nodeConfig.network, nodeId: staticNodeIdToSwitchTo })); + yield put(changeNodeSucceeded({ networkId: nodeConfig.network, nodeId: staticNodeIdToSwitchTo })); // also put the change through as usual so status check and // error messages occur if the node is unavailable - yield put(changeNodeIntent(staticNodeIdToSwitchTo)); + yield put(changeNodeRequested(staticNodeIdToSwitchTo)); +} + +export function* handleChangeNetworkRequested({ payload: network }: ChangeNetworkRequestedAction) { + let desiredNode = ''; + const autoNodeName = makeAutoNodeName(network); + const isStaticNode: boolean = yield select(isStaticNodeId, autoNodeName); + + if (isStaticNode) { + desiredNode = autoNodeName; + } else { + const allNodes: { [id: string]: NodeConfig } = yield select(getAllNodes); + const networkNode = Object.values(allNodes).find(n => n.network === network); + if (networkNode) { + desiredNode = networkNode.id; + } + } + + if (desiredNode) { + yield put(changeNodeRequested(desiredNode)); + } else { + yield put( + showNotification( + 'danger', + translateRaw('NETWORK_UNKNOWN_ERROR', { + $network: network + }), + 5000 + ) + ); + } +} + +export function* handleRemoveCustomNode({ payload: nodeId }: RemoveCustomNodeAction): SagaIterator { + // If custom node is currently selected, go back to default node + const currentNodeId = yield select(getNodeId); + if (nodeId === currentNodeId) { + yield put(changeNodeForce(selectedNodeInitialState.nodeId)); + } } export const node = [ - fork(handleNodeChangeIntentOneTime), - takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent), - takeEvery(TypeKeys.CONFIG_NODE_CHANGE_FORCE, handleNodeChangeForce), + fork(handleChangeNodeRequestedOneTime), + takeEvery(TypeKeys.CONFIG_CHANGE_NODE_REQUESTED, handleChangeNodeRequested), + takeEvery(TypeKeys.CONFIG_CHANGE_NODE_FORCE, handleNodeChangeForce), + takeEvery(TypeKeys.CONFIG_CHANGE_NETWORK_REQUESTED, handleChangeNetworkRequested), takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus), takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload), - takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, handleAddCustomNode) + takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, handleAddCustomNode), + takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, handleRemoveCustomNode) ]; diff --git a/common/sagas/config/web3.ts b/common/sagas/config/web3.ts index dae06c86..ba52e05d 100644 --- a/common/sagas/config/web3.ts +++ b/common/sagas/config/web3.ts @@ -7,12 +7,12 @@ import { TypeKeys, web3SetNode, web3UnsetNode, - changeNodeIntent + changeNodeRequested } from 'actions/config'; import { getNodeId, getPreviouslySelectedNode, - getNetworkNameByChainId, + getNetworkByChainId, getWeb3Node } from 'selectors/config'; import { setupWeb3Node, Web3Service, isWeb3Node } from 'libs/nodes/web3'; @@ -22,8 +22,7 @@ import { makeProviderConfig, getShepherdManualMode, makeWeb3Network, - stripWeb3Network, - shepherdProvider + stripWeb3Network } from 'libs/nodes'; import { StaticNodeConfig } from 'shared/types/node'; import { showNotification } from 'actions/notifications'; @@ -32,16 +31,24 @@ import translate from 'translations'; let web3Added = false; export function* initWeb3Node(): SagaIterator { - const { networkId, lib } = yield call(setupWeb3Node); - const network: string = yield select(getNetworkNameByChainId, networkId); - const web3Network = makeWeb3Network(network); + const { chainId, lib } = yield call(setupWeb3Node); + const network: ReturnType = yield select( + getNetworkByChainId, + chainId + ); + + if (!network) { + throw new Error(`MyCrypto doesn’t support the network with chain ID '${chainId}'`); + } + + const web3Network = makeWeb3Network(network.id); + const id = 'web3'; const config: StaticNodeConfig = { + id, isCustom: false, network: web3Network as any, service: Web3Service, - lib: shepherdProvider, - estimateGas: false, hidden: true }; @@ -50,12 +57,12 @@ export function* initWeb3Node(): SagaIterator { } if (!web3Added) { - shepherd.useProvider('web3', 'web3', makeProviderConfig({ network: web3Network })); + shepherd.useProvider('web3', id, makeProviderConfig({ network: web3Network })); } web3Added = true; - yield put(web3SetNode({ id: 'web3', config })); + yield put(web3SetNode({ id, config })); return lib; } @@ -64,10 +71,10 @@ export function* initWeb3Node(): SagaIterator { export function* unlockWeb3(): SagaIterator { try { const nodeLib = yield call(initWeb3Node); - yield put(changeNodeIntent('web3')); + yield put(changeNodeRequested('web3')); yield take( (action: any) => - action.type === TypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeId === 'web3' + action.type === TypeKeys.CONFIG_CHANGE_NODE_SUCCEEDED && action.payload.nodeId === 'web3' ); const web3Node: any | null = yield select(getWeb3Node); diff --git a/common/sagas/transactions.ts b/common/sagas/transactions.ts index 4c231ca3..566d569b 100644 --- a/common/sagas/transactions.ts +++ b/common/sagas/transactions.ts @@ -117,5 +117,5 @@ export function* resetTxData() { export default function* transactions(): SagaIterator { yield takeEvery(TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA, fetchTxData); yield takeEvery(TxTypeKeys.BROADCAST_TRANSACTION_QUEUED, saveBroadcastedTx); - yield takeEvery(ConfigTypeKeys.CONFIG_NODE_CHANGE, resetTxData); + yield takeEvery(ConfigTypeKeys.CONFIG_CHANGE_NODE_SUCCEEDED, resetTxData); } diff --git a/common/sass/styles/overrides/dropdowns.scss b/common/sass/styles/overrides/dropdowns.scss index d9e64e01..136fe43e 100644 --- a/common/sass/styles/overrides/dropdowns.scss +++ b/common/sass/styles/overrides/dropdowns.scss @@ -15,6 +15,7 @@ span.dropdown { .dropdown-menu { padding: 0; border: none; + box-shadow: $dropdown-shadow; > li { margin: 0; diff --git a/common/sass/variables.scss b/common/sass/variables.scss index c7aa17b7..832a879f 100644 --- a/common/sass/variables.scss +++ b/common/sass/variables.scss @@ -307,3 +307,4 @@ $page-header-border-color: $gray-lighter; $dl-horizontal-offset: $component-offset-horizontal; $dl-horizontal-breakpoint: $grid-float-breakpoint; $hr-border: $gray-lighter; +$dropdown-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); diff --git a/common/selectors/config/networks.ts b/common/selectors/config/networks.ts index a3107af3..fcb5a82d 100644 --- a/common/selectors/config/networks.ts +++ b/common/selectors/config/networks.ts @@ -16,14 +16,14 @@ export const getNetworkConfigById = (state: AppState, networkId: string) => ? getStaticNetworkConfigs(state)[networkId] : getCustomNetworkConfigs(state)[networkId]; -export const getNetworkNameByChainId = (state: AppState, chainId: number | string) => { +export const getNetworkByChainId = (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; + return network; }; export const getStaticNetworkIds = (state: AppState): StaticNetworkIds[] => @@ -82,6 +82,11 @@ export const getCustomNetworkConfigs = (state: AppState) => getNetworks(state).c export const getStaticNetworkConfigs = (state: AppState) => getNetworks(state).staticNetworks; +export const getAllNetworkConfigs = (state: AppState) => ({ + ...getStaticNetworkConfigs(state), + ...getCustomNetworkConfigs(state) +}); + export const isNetworkUnit = (state: AppState, unit: string) => { return unit === getNetworkUnit(state); }; diff --git a/common/selectors/config/nodes.ts b/common/selectors/config/nodes.ts index e4e37ad1..ef34d42e 100644 --- a/common/selectors/config/nodes.ts +++ b/common/selectors/config/nodes.ts @@ -1,13 +1,8 @@ import { AppState } from 'reducers'; -import { - getStaticNetworkConfigs, - getCustomNetworkConfigs, - isStaticNetworkId -} from 'selectors/config'; -import { CustomNodeConfig, StaticNodeConfig, StaticNodeId } from 'types/node'; -import { StaticNetworkIds } from 'types/network'; +import { CustomNodeConfig, StaticNodeConfig, StaticNodeId, NodeConfig } from 'types/node'; const getConfig = (state: AppState) => state.config; -import { shepherdProvider, INode, stripWeb3Network } from 'libs/nodes'; +import { shepherdProvider, INode, isAutoNodeConfig } from 'libs/nodes'; +import { getNetworkConfig } from './networks'; export const getNodes = (state: AppState) => getConfig(state).nodes; @@ -84,8 +79,8 @@ export function getNodeConfig(state: AppState): StaticNodeConfig | CustomNodeCon 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`); + const nodeId = getNodeId(state); + throw Error(`No node config found for ${nodeId} in either static or custom nodes`); } return config; } @@ -94,70 +89,45 @@ export function getNodeLib(_: AppState): INode { return shepherdProvider; } -export interface NodeOption { - isCustom: false; - value: string; - label: { network: 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 associatedNetwork = - staticNetworkConfigs[stripWeb3Network(node.network) as StaticNetworkIds]; - const opt: NodeOption = { - isCustom: node.isCustom, - value: nodeId, - label: { - network: stripWeb3Network(node.network), - service: node.service - }, - color: associatedNetwork.color, - hidden: node.hidden - }; - return opt; - }); -} - -export interface CustomNodeOption { - isCustom: true; - id: string; - value: string; - label: { - network: string; - nodeName: string; +export function getAllNodes(state: AppState): { [key: string]: NodeConfig } { + return { + ...getStaticNodes(state), + ...getCustomNodeConfigs(state) }; - color?: string; - hidden?: boolean; } -export function getCustomNodeOptions(state: AppState): CustomNodeOption[] { - const staticNetworkConfigs = getStaticNetworkConfigs(state); - const customNetworkConfigs = getCustomNetworkConfigs(state); - return Object.entries(getCustomNodeConfigs(state)).map( - ([_, node]: [string, CustomNodeConfig]) => { - const chainId = node.network; - const associatedNetwork = isStaticNetworkId(state, chainId) - ? staticNetworkConfigs[chainId] - : customNetworkConfigs[chainId]; - const opt: CustomNodeOption = { - isCustom: node.isCustom, - value: node.id, - label: { - network: associatedNetwork.unit, - nodeName: node.name - }, - color: associatedNetwork.isCustom ? undefined : associatedNetwork.color, - hidden: false, - id: node.id - }; - return opt; +export interface INodeLabel { + network: string; + info: string; +} + +export function getSelectedNodeLabel(state: AppState): INodeLabel { + const allNodes = getAllNodes(state); + const node = getNodeConfig(state); + const network = getNetworkConfig(state); + let info; + + if (node.isCustom) { + // Custom nodes have names + info = node.name; + } else if (node.isAuto) { + // Auto nodes should show the count of all nodes it uses. If only one, + // show the service name of the node. + const networkNodes = Object.values(allNodes).filter( + n => !isAutoNodeConfig(n) && n.network === node.network + ); + + if (networkNodes.length > 1) { + info = `${networkNodes.length} Nodes`; + } else { + info = networkNodes[0].service; } - ); -} + } else { + info = node.service; + } -export function getNodeOptions(state: AppState) { - return [...getStaticNodeOptions(state), ...getCustomNodeOptions(state)]; + return { + network: network.name, + info + }; } diff --git a/common/store/configAndTokens.ts b/common/store/configAndTokens.ts index 5d927d1f..95d4ec38 100644 --- a/common/store/configAndTokens.ts +++ b/common/store/configAndTokens.ts @@ -15,7 +15,7 @@ import { } from 'selectors/config'; import RootReducer, { AppState } from 'reducers'; import { CustomNodeConfig } from 'types/node'; -import { shepherd, makeProviderConfig, shepherdProvider, isAutoNode } from 'libs/nodes'; +import { shepherd, makeProviderConfig, isAutoNode } from 'libs/nodes'; const appInitialState = RootReducer(undefined as any, { type: 'inital_state' }); type DeepPartial = { [P in keyof T]?: DeepPartial }; @@ -168,8 +168,7 @@ function rehydrateCustomNodes( configToHydrate ); - const lib = shepherdProvider; - const hydratedNode: CustomNodeConfig = { ...configToHydrate, lib }; + const hydratedNode: CustomNodeConfig = { ...configToHydrate }; return { ...hydratedNodes, [customNodeId]: hydratedNode }; }, {} as ConfigState['nodes']['customNodes'] diff --git a/common/translations/lang/en.json b/common/translations/lang/en.json index 89f36d1d..de3bf4c6 100644 --- a/common/translations/lang/en.json +++ b/common/translations/lang/en.json @@ -12,7 +12,7 @@ "LABEL_CANNOT_CONTAIN_ENS_SUFFIX": "Address labels may not contain \".eth\", \".test\" or \".reverse\".", "SENDING_TO": "Sending to", "NEW_ADDRESS": "New address", - "NEW_LABEL": "New label", + "NEW_LABEL": "New label", "X_TXHASH": "TX Hash", "X_PASSWORDDESC": "This password * encrypts * your private key. This does not act as a seed to generate your keys. **You will need this password + your keystore file to unlock your wallet.**", "NAV_CHECKTXSTATUS": "Check TX Status", @@ -538,7 +538,8 @@ "TOOLTIP_MORE_INFO": "More info", "TOOLTIP_INSECURE_WALLET_TYPE": "This wallet type is insecure", "TOOLTIP_SECURE_WALLET_TYPE": "This wallet type is secure", - "CUSTOM_NODE_CONFLICT": "You already have a node called $conflictedNode that matches this one, saving will overwrite it", + "CUSTOM_NODE_NAME_CONFLICT": "You already have a node called '$node' that matches this one, saving will overwrite it", + "CUSTOM_NODE_CHAINID_CONFLICT": "Custom network cannot share chain ID with '$network'", "CUSTOM_NETWORK": "Network", "CUSTOM_NODE_NAME": "Node Name", "CUSTOM_NETWORK_NAME": "Network Name", @@ -546,6 +547,11 @@ "CUSTOM_NETWORK_CHAIN_ID": "Chain ID", "CUSTOM_NETWORK_URL": "URL", "CUSTOM_NETWORK_HTTP_AUTH": "HTTP Basic Authentication", + "NETWORKS_SWITCH": "Switch to the $network network", + "NETWORKS_SWITCH_NODE": "Switch to the $node node on the $network network", + "NETWORKS_EXPAND_NODES": "Show node options for the $network network", + "NETWORKS_ALTERNATIVE": "Other Networks", + "NETWORK_UNKNOWN_ERROR": "Unknown network '$network', try adding a custom node or connecting to a different one", "BROADCAST_TX_TITLE": "Broadcast Signed Transaction", "BROADCAST_TX_DESCRIPTION": "Paste a signed transaction and click 'send transaction'", "NAME_AUCTION_PROMPT_BID_1": "Want to place a bid on {name}.eth? ", @@ -575,6 +581,8 @@ "CONTRACTS_INTERACT": "Interact", "CONTRACTS_DEPLOY": "Deploy", "SELECT_A_THING": "Select a $thing", + "SHOW_THING": "Show $thing", + "HIDE_THING": "Hide $thing", "NO_CONTRACTS_AVAILABLE": "No contracts available", "NETWORK_STATUS_ONLINE": "Connected to $network network", "NETWORK_STATUS_OFFLINE": "Disconnected from $network network", @@ -592,6 +600,7 @@ "WELCOME_MODAL_FEATURE_5": "A downloadable desktop app", "WELCOME_MODAL_FEATURE_MORE": "...and much, much more!", "WELCOME_MODAL_LINKS": "Help out with any issues you find by [reporting bugs on GitHub](https://github.com/MyCryptoHQ/MyCrypto/issues) or [HackerOne](https://hackerone.com/mycrypto). Need something from the old site, or just miss that clunky feel? We've kept it up as [MyCrypto Legacy](https://legacy.mycrypto.com).", - "WELCOME_MODAL_CONTINUE": "Show me the new site!" + "WELCOME_MODAL_CONTINUE": "Show me the new site!", + "TESTNET": "Testnet" } } diff --git a/shared/types/network.d.ts b/shared/types/network.d.ts index 4b63ad4b..dd3b31b2 100644 --- a/shared/types/network.d.ts +++ b/shared/types/network.d.ts @@ -47,7 +47,8 @@ export interface GasPriceSetting { interface StaticNetworkConfig { isCustom: false; // used for type guards - name: StaticNetworkIds; + id: StaticNetworkIds; + name: string; unit: string; color?: string; blockExplorer: BlockExplorerConfig; @@ -67,6 +68,7 @@ interface StaticNetworkConfig { interface CustomNetworkConfig { isCustom: true; // used for type guards isTestnet?: boolean; + id: string; name: string; unit: string; chainId: number; diff --git a/shared/types/node.d.ts b/shared/types/node.d.ts index 5f18d821..88c49464 100644 --- a/shared/types/node.d.ts +++ b/shared/types/node.d.ts @@ -1,12 +1,11 @@ -import { INode } from 'libs/nodes'; import { StaticNetworkIds } from './network'; import { StaticNodesState, CustomNodesState } from 'reducers/config/nodes'; interface CustomNodeConfig { id: string; isCustom: true; + isAuto?: undefined; name: string; - lib: INode; service: 'your custom node'; url: string; network: string; @@ -17,42 +16,24 @@ interface CustomNodeConfig { } interface StaticNodeConfig { + id: string; isCustom: false; + isAuto?: boolean; network: StaticNetworkIds; - lib: INode; service: string; - estimateGas?: boolean; hidden?: boolean; } -declare enum StaticNodeId { - ETH_AUTO = 'eth_auto', - ETH_MYCRYPTO = 'eth_mycrypto', - ETH_ETHSCAN = 'eth_ethscan', - ETH_INFURA = 'eth_infura', - ETH_BLOCKSCALE = 'eth_blockscale', - ROP_AUTO = 'rop_auto', - ROP_INFURA = 'rop_infura', - KOV_AUTO = 'kov_auto', - KOV_ETHSCAN = 'kov_ethscan', - RIN_AUTO = 'rin_auto', - RIN_ETHSCAN = 'rin_ethscan', - RIN_INFURA = 'rin_infura', - ETC_AUTO = 'etc_auto', - ETC_EPOOL = 'etc_epool', - ETC_COMMONWEALTH = 'etc_commonwealth', - UBQ_AUTO = 'ubq_auto', - UBQ = 'ubq', - EXP_AUTO = 'exp_auto', - EXP_TECH = 'exp_tech', - POA_AUTO = 'poa_auto', - POA = 'poa', - TOMO_AUTO = 'tomo_auto', - TOMO = 'tomo', - ELLA_AUTO = 'ella_auto', - ELLA = 'ella' +interface RawNodeConfig { + name: string; + type: 'rpc' | 'etherscan' | 'infura' | 'web3' | 'myccustom'; + service: string; + url: string; + estimateGas: boolean; } -type StaticNodeConfigs = { [key in StaticNodeId]: StaticNodeConfig } & { web3?: StaticNodeConfig }; +type StaticNodeId = string; + +type StaticNodeConfigs = { [id: string]: StaticNodeConfig } & { web3?: StaticNodeConfig }; type NodeConfig = StaticNodesState[StaticNodeId] | CustomNodesState[string]; diff --git a/spec/integration/data.int.ts b/spec/integration/data.int.ts index 23c4c1a3..5f042647 100644 --- a/spec/integration/data.int.ts +++ b/spec/integration/data.int.ts @@ -1,10 +1,10 @@ import { Validator, ValidatorResult } from 'jsonschema'; import { schema } from '../../common/libs/validators'; import 'url-search-params-polyfill'; -import RPCNode from 'libs/nodes/rpc'; -import RpcNodeTestConfig from './RpcNodeTestConfig'; +import INodeTestConfig from './RpcNodeTestConfig'; import { StaticNodeConfig } from 'types/node'; import { staticNodesExpectedState } from '../reducers/config/nodes/staticNodes.spec'; +import { INode, shepherd, shepherdProvider } from 'libs/nodes'; const v = new Validator(); @@ -24,26 +24,20 @@ const validRequests = { }; interface RPCTestList { - [key: string]: ((n: RPCNode) => Promise); + [key: string]: ((n: INode) => Promise); } -const testGetBalance = (n: RPCNode) => { - return n.client - .call(n.requests.getBalance(validRequests.address)) - .then(data => v.validate(data, schema.RpcNode)); +const testGetBalance = (n: INode) => { + return n.getBalance(validRequests.address).then(data => v.validate(data, schema.RpcNode)); }; -const testEstimateGas = (n: RPCNode) => { - return n.client - .call(n.requests.estimateGas(validRequests.transaction)) - .then(data => v.validate(data, schema.RpcNode)); +const testEstimateGas = (n: INode) => { + return n.estimateGas(validRequests.transaction).then(data => v.validate(data, schema.RpcNode)); }; -const testGetTokenBalance = (n: RPCNode) => { +const testGetTokenBalance = (n: INode) => { const { address, token } = validRequests; - return n.client - .call(n.requests.getTokenBalance(address, token)) - .then(data => v.validate(data, schema.RpcNode)); + return n.getTokenBalance(address, token).then(data => v.validate(data, schema.RpcNode)); }; const RPCTests: RPCTestList = { @@ -52,7 +46,7 @@ const RPCTests: RPCTestList = { getTokenBalance: testGetTokenBalance }; -function testRpcRequests(node: RPCNode, service: string) { +function testRpcRequests(node: INode, service: string) { Object.keys(RPCTests).forEach(testType => { describe(`RPC (${service}) should work`, () => { it( @@ -67,10 +61,11 @@ function testRpcRequests(node: RPCNode, service: string) { } const mapNodeEndpoints = (nodes: { [key: string]: StaticNodeConfig }) => { - const { RpcNodes } = RpcNodeTestConfig; + const { INodes } = INodeTestConfig; - RpcNodes.forEach(n => { - testRpcRequests(nodes[n].lib as RPCNode, `${nodes[n].service} ${nodes[n].network}`); + INodes.forEach((n: string) => { + shepherd.manual(n, true); + testRpcRequests(shepherdProvider, `${nodes[n].service} ${nodes[n].network}`); }); }; diff --git a/spec/reducers/config/__snapshots__/config.spec.ts.snap b/spec/reducers/config/__snapshots__/config.spec.ts.snap index 4dc590b3..5ddb4aa6 100644 --- a/spec/reducers/config/__snapshots__/config.spec.ts.snap +++ b/spec/reducers/config/__snapshots__/config.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`handleNodeChangeIntent* should get the next network 1`] = ` +exports[`handleChangeNodeRequested* should get the next network 1`] = ` Object { "@@redux-saga/IO": true, "SELECT": Object { @@ -12,7 +12,7 @@ Object { } `; -exports[`handleNodeChangeIntent* should select getCustomNodeConfig and match race snapshot 1`] = ` +exports[`handleChangeNodeRequested* should select getCustomNodeConfig and match race snapshot 1`] = ` Object { "@@redux-saga/IO": true, "SELECT": Object { diff --git a/spec/reducers/config/config.spec.ts b/spec/reducers/config/config.spec.ts index dc19a322..de33d0c2 100644 --- a/spec/reducers/config/config.spec.ts +++ b/spec/reducers/config/config.spec.ts @@ -5,20 +5,20 @@ import { cloneableGenerator, createMockTask } from 'redux-saga/utils'; import { setOffline, setOnline, - changeNode, - changeNodeIntent, + changeNodeSucceeded, + changeNodeRequested, changeNodeForce, setLatestBlock, TypeKeys, - ChangeNodeIntentOneTimeAction, - changeNodeIntentOneTime + ChangeNodeRequestedOneTimeAction, + changeNodeRequestedOneTime } from 'actions/config'; import { - handleNodeChangeIntent, + handleChangeNodeRequested, handlePollOfflineStatus, pollOfflineStatus, handleNewNetwork, - handleNodeChangeIntentOneTime + handleChangeNodeRequestedOneTime } from 'sagas/config/node'; import { getNodeId, @@ -35,7 +35,7 @@ import { translateRaw } from 'translations'; import { StaticNodeConfig } from 'types/node'; import { staticNodesExpectedState } from './nodes/staticNodes.spec'; import { selectedNodeExpectedState } from './nodes/selectedNode.spec'; -import { customNodesExpectedState, firstCustomNodeId } from './nodes/customNodes.spec'; +import { customNodesExpectedState, firstCustomNode } from './nodes/customNodes.spec'; import { unsetWeb3Node, unsetWeb3NodeOnWalletEvent } from 'sagas/config/web3'; import { shepherd } from 'mycrypto-shepherd'; import { getShepherdOffline, getShepherdPending } from 'libs/nodes'; @@ -120,7 +120,7 @@ describe('handlePollOfflineStatus*', () => { }); }); -describe('handleNodeChangeIntent*', () => { +describe('handleChangeNodeRequested*', () => { let originalRandom: any; // normal operation variables @@ -134,18 +134,14 @@ describe('handleNodeChangeIntent*', () => { ); const newNodeConfig: StaticNodeConfig = (staticNodesExpectedState as any).initialState[newNodeId]; const isOffline = false; - const changeNodeIntentAction = changeNodeIntent(newNodeId); + const changeNodeRequestedAction = changeNodeRequested(newNodeId); const latestBlock = '0xa'; const data = {} as any; - data.gen = cloneableGenerator(handleNodeChangeIntent)(changeNodeIntentAction); + data.gen = cloneableGenerator(handleChangeNodeRequested)(changeNodeRequestedAction); function shouldBailOut(gen: SagaIterator, nextVal: any, errMsg: string) { - 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({ networkId: defaultNodeConfig.network, nodeId: defaultNodeId })) - ); + expect(gen.next(nextVal).value).toEqual(put(showNotification('danger', errMsg, 5000))); expect(gen.next().done).toEqual(true); } @@ -178,17 +174,13 @@ describe('handleNodeChangeIntent*', () => { expect(data.gen.next(true).value).toEqual(select(getOffline)); }); - it('should show error and revert to previous node if online check times out', () => { - data.nodeError = data.gen.clone(); - data.nodeError.next(isOffline); - expect(data.nodeError.throw('err').value).toEqual(select(getNodeId)); - expect(data.nodeError.next(defaultNodeId).value).toEqual( + it('should show error if check times out', () => { + data.clone1 = data.gen.clone(); + data.clone1.next(true); + expect(data.clone1.throw('err').value).toEqual( put(showNotification('danger', translateRaw('ERROR_32'), 5000)) ); - expect(data.nodeError.next().value).toEqual( - put(changeNode({ networkId: defaultNodeConfig.network, nodeId: defaultNodeId })) - ); - expect(data.nodeError.next().done).toEqual(true); + expect(data.clone1.next().done).toEqual(true); }); it('should sucessfully switch to the manual node', () => { @@ -207,7 +199,7 @@ describe('handleNodeChangeIntent*', () => { it('should put changeNode', () => { expect(data.gen.next().value).toEqual( - put(changeNode({ networkId: newNodeConfig.network, nodeId: newNodeId })) + put(changeNodeSucceeded({ networkId: newNodeConfig.network, nodeId: newNodeId })) ); }); @@ -221,22 +213,22 @@ describe('handleNodeChangeIntent*', () => { // custom node variables const customNodeConfigs = customNodesExpectedState.addFirstCustomNode; - const customNodeAction = changeNodeIntent(firstCustomNodeId); - data.customNode = handleNodeChangeIntent(customNodeAction); + const customNodeAction = changeNodeRequested(firstCustomNode.id); + data.customNode = handleChangeNodeRequested(customNodeAction); // test custom node it('should select getCustomNodeConfig and match race snapshot', () => { data.customNode.next(); data.customNode.next(false); expect(data.customNode.next(defaultNodeConfig).value).toEqual( - select(getCustomNodeFromId, firstCustomNodeId) + select(getCustomNodeFromId, firstCustomNode.id) ); expect(data.customNode.next(customNodeConfigs.customNode1).value).toMatchSnapshot(); }); - const customNodeIdNotFound = firstCustomNodeId + 'notFound'; - const customNodeNotFoundAction = changeNodeIntent(customNodeIdNotFound); - data.customNodeNotFound = handleNodeChangeIntent(customNodeNotFoundAction); + const customNodeIdNotFound = firstCustomNode.id + 'notFound'; + const customNodeNotFoundAction = changeNodeRequested(customNodeIdNotFound); + data.customNodeNotFound = handleChangeNodeRequested(customNodeNotFoundAction); // test custom node not found it('should handle unknown / missing custom node', () => { @@ -244,13 +236,13 @@ describe('handleNodeChangeIntent*', () => { data.customNodeNotFound.next(false); }); - it('should blah', () => { + it('should select getCustomNodeFromId', () => { expect(data.customNodeNotFound.next(defaultNodeConfig).value).toEqual( select(getCustomNodeFromId, customNodeIdNotFound) ); }); - it('should blahah', () => { + it('should show an error if was an unknown custom node', () => { shouldBailOut( data.customNodeNotFound, null, @@ -259,17 +251,17 @@ describe('handleNodeChangeIntent*', () => { }); }); -describe('handleNodeChangeIntentOneTime', () => { - const saga = handleNodeChangeIntentOneTime(); - const action: ChangeNodeIntentOneTimeAction = changeNodeIntentOneTime('eth_auto'); +describe('handleChangeNodeRequestedOneTime', () => { + const saga = handleChangeNodeRequestedOneTime(); + const action: ChangeNodeRequestedOneTimeAction = changeNodeRequestedOneTime('eth_auto'); it('should take a one time action based on the url containing a valid network to switch to', () => { - expect(saga.next().value).toEqual(take(TypeKeys.CONFIG_NODE_CHANGE_INTENT_ONETIME)); + expect(saga.next().value).toEqual(take(TypeKeys.CONFIG_CHANGE_NODE_REQUESTED_ONETIME)); }); it(`should delay for 10 ms to allow shepherdProvider async init to complete`, () => { expect(saga.next(action).value).toEqual(call(delay, 100)); }); it('should dispatch the change node intent', () => { - expect(saga.next().value).toEqual(put(changeNodeIntent(action.payload))); + expect(saga.next().value).toEqual(put(changeNodeRequested(action.payload))); }); it('should be done', () => { expect(saga.next().done).toEqual(true); diff --git a/spec/reducers/config/networks/customNetworks.spec.ts b/spec/reducers/config/networks/customNetworks.spec.ts index ce3f0f3f..9014f035 100644 --- a/spec/reducers/config/networks/customNetworks.spec.ts +++ b/spec/reducers/config/networks/customNetworks.spec.ts @@ -2,41 +2,36 @@ import { CustomNetworkConfig } from 'types/network'; import { addCustomNetwork, removeCustomNetwork } from 'actions/config'; import { customNetworks } from 'reducers/config/networks/customNetworks'; -const firstCustomNetworkId = 'firstCustomNetwork'; -const firstCustomNetworkConfig: CustomNetworkConfig = { +const firstCustomNetwork: CustomNetworkConfig = { isCustom: true, - chainId: 1, - name: firstCustomNetworkId, + id: '111', + chainId: 111, + name: 'First Custom Network', unit: 'customNetworkUnit', dPathFormats: null }; -const secondCustomNetworkId = 'secondCustomNetwork'; -const secondCustomNetworkConfig: CustomNetworkConfig = { - ...firstCustomNetworkConfig, - name: secondCustomNetworkId +const secondCustomNetwork: CustomNetworkConfig = { + ...firstCustomNetwork, + id: '222', + chainId: 222, + name: 'Second Custom Network' }; const expectedState = { initialState: {}, - addFirstCustomNetwork: { [firstCustomNetworkId]: firstCustomNetworkConfig }, + addFirstCustomNetwork: { [firstCustomNetwork.id]: firstCustomNetwork }, addSecondCustomNetwork: { - [firstCustomNetworkId]: firstCustomNetworkConfig, - [secondCustomNetworkId]: secondCustomNetworkConfig + [firstCustomNetwork.id]: firstCustomNetwork, + [secondCustomNetwork.id]: secondCustomNetwork }, - removeFirstCustomNetwork: { [secondCustomNetworkId]: secondCustomNetworkConfig } + removeFirstCustomNetwork: { [secondCustomNetwork.id]: secondCustomNetwork } }; const actions = { - addFirstCustomNetwork: addCustomNetwork({ - id: firstCustomNetworkId, - config: firstCustomNetworkConfig - }), - addSecondCustomNetwork: addCustomNetwork({ - config: secondCustomNetworkConfig, - id: secondCustomNetworkId - }), - removeFirstCustomNetwork: removeCustomNetwork({ id: firstCustomNetworkId }) + addFirstCustomNetwork: addCustomNetwork(firstCustomNetwork), + addSecondCustomNetwork: addCustomNetwork(secondCustomNetwork), + removeFirstCustomNetwork: removeCustomNetwork(firstCustomNetwork.id) }; describe('custom networks reducer', () => { diff --git a/spec/reducers/config/nodes/customNodes.spec.ts b/spec/reducers/config/nodes/customNodes.spec.ts index cf6e9df2..5e41b9ba 100644 --- a/spec/reducers/config/nodes/customNodes.spec.ts +++ b/spec/reducers/config/nodes/customNodes.spec.ts @@ -2,37 +2,34 @@ 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 = { +export const firstCustomNode: CustomNodeConfig = { isCustom: true, - id: firstCustomNodeId, - lib: jest.fn() as any, + id: 'customNode1', name: 'My cool custom node', network: 'CustomNetworkId', service: 'your custom node', url: '127.0.0.1' }; -const secondCustomNodeId = 'customNode2'; const secondCustomNode: CustomNodeConfig = { ...firstCustomNode, - id: secondCustomNodeId + id: 'customNode2' }; const expectedState = { initialState: {}, - addFirstCustomNode: { [firstCustomNodeId]: firstCustomNode }, + addFirstCustomNode: { [firstCustomNode.id]: firstCustomNode }, addSecondCustomNode: { - [firstCustomNodeId]: firstCustomNode, - [secondCustomNodeId]: secondCustomNode + [firstCustomNode.id]: firstCustomNode, + [secondCustomNode.id]: secondCustomNode }, - removeFirstCustomNode: { [secondCustomNodeId]: secondCustomNode } + removeFirstCustomNode: { [secondCustomNode.id]: secondCustomNode } }; const actions = { - addFirstCustomNode: addCustomNode({ id: firstCustomNodeId, config: firstCustomNode }), - addSecondCustomNode: addCustomNode({ id: secondCustomNodeId, config: secondCustomNode }), - removeFirstCustomNode: removeCustomNode({ id: firstCustomNodeId }) + addFirstCustomNode: addCustomNode(firstCustomNode), + addSecondCustomNode: addCustomNode(secondCustomNode), + removeFirstCustomNode: removeCustomNode(firstCustomNode.id) }; describe('custom nodes reducer', () => { diff --git a/spec/reducers/config/nodes/selectedNode.spec.ts b/spec/reducers/config/nodes/selectedNode.spec.ts index 362e3ac9..9714cf60 100644 --- a/spec/reducers/config/nodes/selectedNode.spec.ts +++ b/spec/reducers/config/nodes/selectedNode.spec.ts @@ -1,4 +1,4 @@ -import { changeNodeIntent, changeNode } from 'actions/config'; +import { changeNodeRequested, changeNodeSucceeded } from 'actions/config'; import { selectedNode } from 'reducers/config/nodes/selectedNode'; import { SelectedNodeState } from 'reducers/config/nodes/types'; @@ -9,8 +9,8 @@ export const expectedState = { }; export const actions = { - changeNode: changeNode({ nodeId: 'nodeToChangeTo', networkId: 'networkToChangeTo' }), - changeNodeIntent: changeNodeIntent('eth_mycrypto') + changeNode: changeNodeSucceeded({ nodeId: 'nodeToChangeTo', networkId: 'networkToChangeTo' }), + changeNodeRequested: changeNodeRequested('eth_mycrypto') }; describe('selected node reducer', () => { @@ -19,7 +19,7 @@ describe('selected node reducer', () => { it('should handle the intent to change a node', () => expect( - selectedNode(expectedState.initialState as SelectedNodeState, actions.changeNodeIntent) + selectedNode(expectedState.initialState as SelectedNodeState, actions.changeNodeRequested) ).toEqual(expectedState.nodeChangeIntent)); }); diff --git a/spec/reducers/config/nodes/staticNodes.spec.ts b/spec/reducers/config/nodes/staticNodes.spec.ts index 78c4c8cc..5588a6c0 100644 --- a/spec/reducers/config/nodes/staticNodes.spec.ts +++ b/spec/reducers/config/nodes/staticNodes.spec.ts @@ -7,11 +7,10 @@ configuredStore.getState(); const web3Id = 'web3'; const web3Node: StaticNodeConfig = { + id: web3Id, isCustom: false, network: 'ETH', service: Web3Service, - lib: jest.fn() as any, - estimateGas: false, hidden: true }; diff --git a/spec/sagas/config/node.spec.ts b/spec/sagas/config/node.spec.ts index d9f9ab93..1b411bff 100644 --- a/spec/sagas/config/node.spec.ts +++ b/spec/sagas/config/node.spec.ts @@ -1,9 +1,23 @@ import { configuredStore } from 'store'; -import { cloneableGenerator } from 'redux-saga/utils'; -import { handleNodeChangeForce } from 'sagas/config/node'; +import { cloneableGenerator, SagaIteratorClone } from 'redux-saga/utils'; +import { + handleNodeChangeForce, + handleChangeNetworkRequested, + handleRemoveCustomNode +} from 'sagas/config/node'; import { put, select } from 'redux-saga/effects'; -import { isStaticNodeId, getStaticNodeFromId } from 'selectors/config'; -import { changeNode, changeNodeIntent } from 'actions/config'; +import { isStaticNodeId, getStaticNodeFromId, getNodeId, getAllNodes } from 'selectors/config'; +import { + TypeKeys, + changeNodeSucceeded, + changeNodeRequested, + changeNodeForce, + ChangeNetworkRequestedAction, + RemoveCustomNodeAction +} from 'actions/config'; +import { makeAutoNodeName } from 'libs/nodes'; +import { INITIAL_STATE as selectedNodeInitialState } from 'reducers/config/nodes/selectedNode'; +import { CustomNodeConfig } from 'types/node'; // init module configuredStore.getState(); @@ -30,7 +44,7 @@ describe('handleNodeChangeForce*', () => { it('should force the node change', () => { expect(gen.next(nodeConfig).value).toEqual( put( - changeNode({ + changeNodeSucceeded({ networkId: nodeConfig.network, nodeId: payload }) @@ -39,10 +53,79 @@ describe('handleNodeChangeForce*', () => { }); it('should put a change node intent', () => { - expect(gen.next().value).toEqual(put(changeNodeIntent(payload))); + expect(gen.next().value).toEqual(put(changeNodeRequested(payload))); }); it('should be done', () => { expect(gen.next().done).toEqual(true); }); }); + +describe('handleChangeNetworkRequested*', () => { + const action: ChangeNetworkRequestedAction = { + payload: 'ETH', + type: TypeKeys.CONFIG_CHANGE_NETWORK_REQUESTED + }; + const nextNodeName = makeAutoNodeName(action.payload); + const customNode: CustomNodeConfig = { + id: 'id', + url: 'url', + name: 'Custom Node', + service: 'your custom node', + network: action.payload, + isCustom: true + }; + const gen = cloneableGenerator(handleChangeNetworkRequested); + const staticCase = gen(action); + let customCase: SagaIteratorClone; + let failureCase: SagaIteratorClone; + + it('should select isStaticNodeId', () => { + expect(staticCase.next().value).toEqual(select(isStaticNodeId, nextNodeName)); + }); + + it('should put changeNodeRequested for auto node if static network', () => { + customCase = staticCase.clone(); + expect(staticCase.next(true).value).toEqual(put(changeNodeRequested(nextNodeName))); + expect(staticCase.next().done).toBeTruthy(); + }); + + it('should select getAllNodes if non-static network', () => { + expect(customCase.next(false).value).toEqual(select(getAllNodes)); + }); + + it('should put changeNodeRequested on the first custom node if found', () => { + failureCase = customCase.clone(); + expect(customCase.next([customNode]).value).toEqual(put(changeNodeRequested(customNode.id))); + }); + + it('should put showNotification if not a valid network', () => { + const value = failureCase.next([]).value as any; + expect(value.PUT.action.type).toBe('SHOW_NOTIFICATION'); + }); +}); + +describe('handleRemoveCustomNode*', () => { + const customNodeUrl = 'https://mycustomnode.com'; + const action: RemoveCustomNodeAction = { + type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, + payload: customNodeUrl + }; + const sameCase = cloneableGenerator(handleRemoveCustomNode)(action); + let diffCase: SagaIteratorClone; + + it('Should select getNodeId', () => { + expect(sameCase.next().value).toEqual(select(getNodeId)); + }); + + it('Should put changeNodeForce to default network if current node id === removed node id', () => { + diffCase = sameCase.clone(); + expect(sameCase.next(customNodeUrl).value).toEqual( + put(changeNodeForce(selectedNodeInitialState.nodeId)) + ); + }); + + it('Should do nothing if current node id !== removed node id', () => { + expect(diffCase.next('Different').done).toBeTruthy(); + }); +}); diff --git a/spec/sagas/gas.spec.ts b/spec/sagas/gas.spec.ts index 421fecb1..4da7e78a 100644 --- a/spec/sagas/gas.spec.ts +++ b/spec/sagas/gas.spec.ts @@ -127,7 +127,8 @@ describe('setDefaultEstimates*', () => { it('Should use config defaults if network has no defaults', () => { const customNetwork = { isCustom: true as true, - name: 'Custon', + id: '123', + name: 'Custom', unit: 'CST', chainId: 123, dPathFormats: null diff --git a/spec/sagas/wallet.spec.tsx b/spec/sagas/wallet.spec.tsx index b294a3de..f982bcc1 100644 --- a/spec/sagas/wallet.spec.tsx +++ b/spec/sagas/wallet.spec.tsx @@ -12,7 +12,7 @@ import { TypeKeys } from 'actions/wallet'; import { Wei } from 'libs/units'; -import { changeNodeIntent, web3UnsetNode } from 'actions/config'; +import { changeNodeRequested, web3UnsetNode } from 'actions/config'; import { INode } from 'libs/nodes/INode'; import { apply, call, fork, put, select, take, cancel } from 'redux-saga/effects'; import { getNodeLib, getOffline, getWeb3Node } from 'selectors/config'; @@ -326,14 +326,15 @@ describe('unlockWeb3*', () => { expect(data.gen.next().value).toEqual(call(initWeb3Node)); }); - it('should put changeNodeIntent', () => { - expect(data.gen.next(nodeLib).value).toEqual(put(changeNodeIntent('web3'))); + it('should put changeNodeRequested', () => { + expect(data.gen.next(nodeLib).value).toEqual(put(changeNodeRequested('web3'))); }); it('should yield take on node change', () => { const expected = take( (action: any) => - action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeSelection === 'web3' + action.type === ConfigTypeKeys.CONFIG_CHANGE_NODE_SUCCEEDED && + action.payload.nodeSelection === 'web3' ); const result = data.gen.next().value; expect(JSON.stringify(expected)).toEqual(JSON.stringify(result));