Node Refactor (#1603)

* Initial work on refactoring node definitions to reduce number of places theyre defined, amount of copy pasting.

* Use makeAutoNodeNAme instead of manually appending _auto

* Add getNetVersion to list of unsupported methods

* PR feedback

* Rework web template node selector to be a network selector. Refactor some types to help with that. Better handle removing custom nodes.

* Remove color dropdown.

* Fix selecting custom networks. Show notification if change network intent fails.

* Use selectors for current node / network instead of intuiting from nodeSelection

* Add id key to all networks, simplify add and remove custom node and network functions.

* Fix a lot of uses of network.name to use network.id instead.

* Dont allow network chainid conflicts

* Fix web3 network by chainid

* Add testnet badge to network selector

* Change nomenclature from change(Node|Network)(Intent)? to change(Node|Network)(Requested|Succeeded)

* tscheck

* Better code for chainid collision

* Remove console logs

* Fix tests

* Network selector becomes self contained component used both by web header and electron nav.

* Dont select node again

* Additional title text

* tscheck

* Custom node behavior in Electron

* Close panel too

* Convert node label data into selector function

* tscheck

* Parens & space
This commit is contained in:
William O'Beirne 2018-05-29 10:51:42 -04:00 committed by Daniel Ternyak
parent 9bdeab2307
commit a043334685
56 changed files with 1406 additions and 1048 deletions

View File

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

View File

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

View File

@ -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',

View File

@ -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<Props, State> {
</NewTabLink>
</li>
)}
{network.name === 'ETH' && (
{network.id === 'ETH' && (
<li className="AccountInfo-list-item">
<NewTabLink href={etherChainExplorerInst.addressUrl(address)}>
{`${network.name} (${etherChainExplorerInst.origin})`}
@ -186,7 +186,7 @@ class AccountInfo extends React.Component<Props, State> {
private setSymbol(network: NetworkConfig) {
if (network.isTestnet) {
return network.unit + ' (' + network.name + ')';
return `${network.unit} (${translateRaw('TESTNET')})`;
}
return network.unit;
}

View File

@ -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<Props, State> {
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<Props, State> {
}
];
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<Props, State> {
>
{isHttps && <div className="alert alert-warning small">{translate('NODE_WARNING')}</div>}
{conflictedNode && (
{nameConflictNode && (
<div className="alert alert-warning small">
{translate('CUSTOM_NODE_CONFLICT', { conflictedNode: conflictedNode.name })}
{translate('CUSTOM_NODE_NAME_CONFLICT', { $node: nameConflictNode.name })}
</div>
)}
@ -171,6 +173,11 @@ class CustomNodeModal extends React.Component<Props, State> {
</label>
</div>
)}
{chainidConflictNetwork && (
<div className="alert alert-warning small">
{translate('CUSTOM_NODE_CHAINID_CONFLICT', { $network: chainidConflictNetwork.name })}
</div>
)}
<label className="input-group input-group-inline">
<div className="input-group-header">{translate('CUSTOM_NETWORK_URL')}</div>
@ -267,16 +274,36 @@ class CustomNodeModal extends React.Component<Props, State> {
invalids.customNetworkUnit = true;
}
// Numeric chain ID (if provided)
const iChainId = parseInt(customNetworkChainId, 10);
if (!iChainId || iChainId < 0) {
// Numeric chain ID
if (this.getChainIdCollisionNetwork(customNetworkChainId)) {
invalids.customNetworkChainId = true;
} else {
const iChainId = parseInt(customNetworkChainId, 10);
if (!customNetworkChainId || !iChainId || iChainId < 0) {
invalids.customNetworkChainId = true;
}
}
}
return invalids;
}
private getChainIdCollisionNetwork(chainId: string) {
if (!chainId) {
return false;
}
const chainIdInt = parseInt(chainId, 10);
const allNetworks = [
...Object.values(this.props.staticNetworks),
...Object.values(this.props.customNetworks)
];
return allNetworks.reduce(
(collision, network) => (network.chainId === chainIdInt ? network : collision),
null
);
}
private makeCustomNetworkConfigFromState(): CustomNetworkConfig {
const similarNetworkConfig = Object.values(this.props.staticNetworks).find(
n => n.chainId === +this.state.customNetworkChainId
@ -285,9 +312,10 @@ class CustomNodeModal extends React.Component<Props, State> {
return {
isCustom: true,
id: this.state.customNetworkChainId,
name: this.state.customNetworkId,
unit: this.state.customNetworkUnit,
chainId: this.state.customNetworkChainId ? parseInt(this.state.customNetworkChainId, 10) : 0,
chainId: parseInt(this.state.customNetworkChainId, 10),
dPathFormats
};
}
@ -300,7 +328,7 @@ class CustomNodeModal extends React.Component<Props, State> {
? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
: network;
const node: Omit<CustomNodeConfig, 'lib'> = {
return {
isCustom: true,
service: 'your custom node',
id: url,
@ -316,11 +344,9 @@ class CustomNodeModal extends React.Component<Props, State> {
}
: {})
};
return { ...node, lib: shepherdProvider };
}
private getConflictedNode(): CustomNodeConfig | undefined {
private getNameConflictNode(): CustomNodeConfig | undefined {
const { customNodes } = this.props;
const config = this.makeCustomNodeConfigFromState();
@ -333,14 +359,14 @@ class CustomNodeModal extends React.Component<Props, State> {
if (this.state.network === CUSTOM.value) {
const network = this.makeCustomNetworkConfigFromState();
this.props.addCustomNetwork({ config: network, id: node.network });
this.props.addCustomNetwork(network);
}
this.props.addCustomNode({ config: node, id: node.id });
this.props.addCustomNode(node);
};
private makeCustomNetworkId(config: CustomNetworkConfig): string {
return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`;
return config.chainId.toString();
}
}

View File

@ -3,8 +3,8 @@ import classnames from 'classnames';
import translate from 'translations';
import { navigationLinks } from 'config';
import NavigationLink from 'components/NavigationLink';
import NetworkSelect from './NetworkSelect';
import LanguageSelect from './LanguageSelect';
import NodeSelect from './NodeSelect';
import NetworkStatus from './NetworkStatus';
import './ElectronNav.scss';
@ -80,7 +80,7 @@ export default class ElectronNav extends React.Component<{}, State> {
};
private openNodeSelect = () => {
const panelContent = <NodeSelect closePanel={this.closePanel} />;
const panelContent = <NetworkSelect closePanel={this.closePanel} />;
this.setState({
panelContent,
isPanelOpen: true

View File

@ -0,0 +1,59 @@
import React from 'react';
import { connect } from 'react-redux';
import NetworkSelector from 'components/NetworkSelector';
import CustomNodeModal from 'components/CustomNodeModal';
import { TAddCustomNode, AddCustomNodeAction, addCustomNode } from 'actions/config';
interface OwnProps {
closePanel(): void;
}
interface DispatchProps {
addCustomNode: TAddCustomNode;
}
type Props = OwnProps & DispatchProps;
interface State {
isAddingCustomNode: boolean;
}
class NetworkSelect extends React.Component<Props, State> {
public state: State = {
isAddingCustomNode: false
};
public render() {
const { isAddingCustomNode } = this.state;
return (
<React.Fragment>
<NetworkSelector
onSelectNetwork={this.props.closePanel}
onSelectNode={this.props.closePanel}
openCustomNodeModal={this.openCustomNodeModal}
/>
<CustomNodeModal
isOpen={isAddingCustomNode}
addCustomNode={this.addCustomNode}
handleClose={this.closeCustomNodeModal}
/>
</React.Fragment>
);
}
private openCustomNodeModal = () => {
this.setState({ isAddingCustomNode: true });
};
private closeCustomNodeModal = () => {
this.setState({ isAddingCustomNode: false });
};
private addCustomNode = (payload: AddCustomNodeAction['payload']) => {
this.closeCustomNodeModal();
this.props.addCustomNode(payload);
this.props.closePanel();
};
}
export default connect(undefined, { addCustomNode })(NetworkSelect);

View File

@ -1,31 +0,0 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
.NodeSelect {
&-node {
@include reset-button;
display: block;
width: 100%;
height: 48px;
line-height: 48px;
padding: 0 10px;
color: $text-color;
border-bottom: 1px solid $gray-lighter;
border-left: 4px solid;
text-align: left;
@include ellipsis;
&:hover {
color: $link-hover-color;
}
&.is-active {
color: $link-color;
background: $gray-lightest;
}
small {
font-size: 12px;
}
}
}

View File

@ -1,126 +0,0 @@
import React from 'react';
import classnames from 'classnames';
import { connect } from 'react-redux';
import translate from 'translations';
import CustomNodeModal from 'components/CustomNodeModal';
import {
TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode,
changeNodeIntent,
addCustomNode,
removeCustomNode,
AddCustomNodeAction
} from 'actions/config';
import {
isNodeChanging,
getNodeId,
CustomNodeOption,
NodeOption,
getNodeOptions
} from 'selectors/config';
import { AppState } from 'reducers';
import './NodeSelect.scss';
interface OwnProps {
closePanel(): void;
}
interface StateProps {
nodeSelection: AppState['config']['nodes']['selectedNode']['nodeId'];
isChangingNode: AppState['config']['nodes']['selectedNode']['pending'];
nodeOptions: (CustomNodeOption | NodeOption)[];
}
interface DispatchProps {
changeNodeIntent: TChangeNodeIntent;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
}
type Props = OwnProps & StateProps & DispatchProps;
interface State {
isAddingCustomNode: boolean;
}
class NodeSelect extends React.Component<Props, State> {
public state: State = {
isAddingCustomNode: false
};
public render() {
const { nodeSelection, nodeOptions } = this.props;
const { isAddingCustomNode } = this.state;
return (
<div className="NodeSelect">
{nodeOptions.map(node => (
<button
key={node.value}
className={classnames({
'NodeSelect-node': true,
'is-active': node.value === nodeSelection
})}
onClick={() => this.handleNodeSelect(node.value)}
style={{ borderLeftColor: node.color }}
>
{this.renderNodeLabel(node)}
</button>
))}
<button className="NodeSelect-node is-add" onClick={this.openCustomNodeModal}>
{translate('NODE_ADD')}
</button>
<CustomNodeModal
isOpen={isAddingCustomNode}
addCustomNode={this.addCustomNode}
handleClose={this.closeCustomNodeModal}
/>
</div>
);
}
private handleNodeSelect = (node: string) => {
this.props.changeNodeIntent(node);
this.props.closePanel();
};
private renderNodeLabel(node: CustomNodeOption | NodeOption) {
return node.isCustom ? (
<span>
{node.label.network} - {node.label.nodeName} <small>(custom)</small>
</span>
) : (
<span>
{node.label.network} - <small>({node.label.service})</small>
</span>
);
}
private openCustomNodeModal = () => {
this.setState({ isAddingCustomNode: true });
};
private closeCustomNodeModal = () => {
this.setState({ isAddingCustomNode: false });
};
private addCustomNode = (payload: AddCustomNodeAction['payload']) => {
this.props.addCustomNode(payload);
this.closeCustomNodeModal();
};
}
export default connect(
(state: AppState): StateProps => ({
isChangingNode: isNodeChanging(state),
nodeSelection: getNodeId(state),
nodeOptions: getNodeOptions(state)
}),
{
changeNodeIntent,
addCustomNode,
removeCustomNode
}
)(NodeSelect);

View File

@ -0,0 +1,14 @@
@import 'common/sass/variables';
.NetworkDropdown {
&-options {
position: absolute;
top: 100%;
right: 0;
width: 180px;
z-index: $zindex-dropdown;
background: #FFF;
box-shadow: $dropdown-shadow;
transform: translateY(2px);
}
}

View File

@ -0,0 +1,77 @@
import React from 'react';
import { connect } from 'react-redux';
import { DropdownShell } from 'components/ui';
import NetworkSelector from 'components/NetworkSelector';
import { getNodeConfig, getSelectedNodeLabel } from 'selectors/config';
import { AppState } from 'reducers';
import './NetworkDropdown.scss';
interface OwnProps {
openCustomNodeModal(): void;
}
interface StateProps {
node: ReturnType<typeof getNodeConfig>;
nodeLabel: ReturnType<typeof getSelectedNodeLabel>;
}
type Props = OwnProps & StateProps;
class NetworkDropdown extends React.Component<Props> {
private dropdown: DropdownShell | null;
public render() {
const { node } = this.props;
return (
<DropdownShell
ariaLabel="Dropdown"
renderLabel={this.renderLabel}
renderOptions={this.renderOptions}
disabled={node.id === 'web3'}
size="smr"
color="white"
ref={el => (this.dropdown = el)}
/>
);
}
private renderLabel = () => {
const { nodeLabel } = this.props;
return (
<span>
{nodeLabel.network} <small>({nodeLabel.info})</small>
</span>
);
};
private renderOptions = () => {
return (
<div className="NetworkDropdown-options">
<NetworkSelector
openCustomNodeModal={this.openModal}
onSelectNetwork={this.onSelect}
onSelectNode={this.onSelect}
/>
</div>
);
};
private onSelect = () => {
if (this.dropdown) {
this.dropdown.close();
}
};
private openModal = () => {
this.props.openCustomNodeModal();
if (this.dropdown) {
this.dropdown.close();
}
};
}
export default connect((state: AppState): StateProps => ({
node: getNodeConfig(state),
nodeLabel: getSelectedNodeLabel(state)
}))(NetworkDropdown);

View File

@ -1,20 +1,18 @@
import {
TChangeLanguage,
TChangeNodeIntent,
TChangeNodeIntentOneTime,
TChangeNodeRequestedOneTime,
TAddCustomNode,
TRemoveCustomNode,
TAddCustomNetwork,
AddCustomNodeAction,
changeLanguage,
changeNodeIntent,
changeNodeIntentOneTime,
changeNodeRequestedOneTime,
addCustomNode,
removeCustomNode,
addCustomNetwork
} from 'actions/config';
import logo from 'assets/images/logo-mycrypto.svg';
import { OldDropDown, ColorDropdown } from 'components/ui';
import { OldDropDown } from 'components/ui';
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
@ -22,26 +20,20 @@ import { TSetGasPriceField, setGasPriceField } from 'actions/transaction';
import { ANNOUNCEMENT_MESSAGE, ANNOUNCEMENT_TYPE, languages } from 'config';
import Navigation from './components/Navigation';
import OnlineStatus from './components/OnlineStatus';
import NetworkDropdown from './components/NetworkDropdown';
import CustomNodeModal from 'components/CustomNodeModal';
import { getKeyByValue } from 'utils/helpers';
import { NodeConfig } from 'types/node';
import './index.scss';
import { AppState } from 'reducers';
import {
getOffline,
isNodeChanging,
getLanguageSelection,
getNodeId,
getNodeConfig,
CustomNodeOption,
NodeOption,
getNodeOptions,
getNetworkConfig,
isStaticNodeId
} from 'selectors/config';
import { NetworkConfig } from 'types/network';
import { connect, MapStateToProps } from 'react-redux';
import translate from 'translations';
import './index.scss';
interface OwnProps {
networkParam: string | null;
@ -49,8 +41,7 @@ interface OwnProps {
interface DispatchProps {
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
changeNodeIntentOneTime: TChangeNodeIntentOneTime;
changeNodeRequestedOneTime: TChangeNodeRequestedOneTime;
setGasPriceField: TSetGasPriceField;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
@ -61,11 +52,8 @@ interface StateProps {
shouldSetNodeFromQS: boolean;
network: NetworkConfig;
languageSelection: AppState['config']['meta']['languageSelection'];
node: NodeConfig;
nodeSelection: AppState['config']['nodes']['selectedNode']['nodeId'];
isChangingNode: AppState['config']['nodes']['selectedNode']['pending'];
isOffline: AppState['config']['meta']['offline'];
nodeOptions: (CustomNodeOption | NodeOption)[];
}
const mapStateToProps: MapStateToProps<StateProps, OwnProps, AppState> = (
@ -76,17 +64,13 @@ const mapStateToProps: MapStateToProps<StateProps, OwnProps, AppState> = (
isOffline: getOffline(state),
isChangingNode: isNodeChanging(state),
languageSelection: getLanguageSelection(state),
nodeSelection: getNodeId(state),
node: getNodeConfig(state),
nodeOptions: getNodeOptions(state),
network: getNetworkConfig(state)
});
const mapDispatchToProps: DispatchProps = {
setGasPriceField,
changeLanguage,
changeNodeIntent,
changeNodeIntentOneTime,
changeNodeRequestedOneTime,
addCustomNode,
removeCustomNode,
addCustomNetwork
@ -108,42 +92,10 @@ class Header extends Component<Props, State> {
}
public render() {
const {
languageSelection,
node,
nodeSelection,
isChangingNode,
isOffline,
nodeOptions,
network
} = this.props;
const { languageSelection, isChangingNode, isOffline, network } = this.props;
const { isAddingCustomNode } = this.state;
const selectedLanguage = languageSelection;
const LanguageDropDown = OldDropDown as new () => OldDropDown<typeof selectedLanguage>;
const options = nodeOptions.map(n => {
if (n.isCustom) {
const { label, isCustom, id, ...rest } = n;
return {
...rest,
name: (
<span>
{label.network} - {label.nodeName} <small>(custom)</small>
</span>
),
onRemove: () => this.props.removeCustomNode({ id })
};
} else {
const { label, isCustom, ...rest } = n;
return {
...rest,
name: (
<span>
{label.network} <small>({label.service})</small>
</span>
)
};
}
});
return (
<div className="Header">
@ -185,24 +137,7 @@ class Header extends Component<Props, State> {
'is-flashing': isChangingNode
})}
>
<ColorDropdown
ariaLabel={`
change node. current node is on the ${node.network} network
provided by ${node.service}
`}
options={options}
value={nodeSelection || ''}
extra={
<li>
<a onClick={this.openCustomNodeModal}>{translate('NODE_ADD')}</a>
</li>
}
disabled={nodeSelection === 'web3'}
onChange={this.props.changeNodeIntent}
size="smr"
color="white"
menuAlign="right"
/>
<NetworkDropdown openCustomNodeModal={this.openCustomNodeModal} />
</div>
</div>
</section>
@ -242,7 +177,7 @@ class Header extends Component<Props, State> {
private attemptSetNodeFromQueryParameter() {
const { shouldSetNodeFromQS, networkParam } = this.props;
if (shouldSetNodeFromQS) {
this.props.changeNodeIntentOneTime(networkParam!);
this.props.changeNodeRequestedOneTime(networkParam!);
}
}
}

View File

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

View File

@ -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<Props> {
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 (
<div className="NetworkOption" style={{ borderLeftColor }}>
<div className="NetworkOption-label">
<div
className={classnames({
'NetworkOption-label-name': true,
'is-selected': isNetworkSelected,
'is-specific-node': isNetworkSelected && !isAutoSelected && singleNodes.length > 1,
'is-long-name': isLongName
})}
title={translateRaw('NETWORKS_SWITCH', { $network: network.name })}
onClick={this.handleSelect}
>
{network.name}
{network.isTestnet && (
<small className="NetworkOption-label-name-badge">({translate('TESTNET')})</small>
)}
</div>
<button
className={classnames('NetworkOption-label-expand', isExpanded && 'is-expanded')}
onClick={this.handleToggleExpand}
title={translateRaw('NETWORKS_EXPAND_NODES', { $network: network.name })}
>
<i className="fa fa-chevron-down" />
</button>
</div>
{isExpanded && (
<div className="NetworkOption-nodes">
{singleNodes.map(node => (
<NodeOption
key={node.id}
node={node}
isSelected={node.id === nodeSelection}
isAutoSelected={isAutoSelected}
select={this.props.selectNode}
/>
))}
</div>
)}
</div>
);
}
private handleSelect = () => {
this.props.selectNetwork(this.props.network);
};
private handleToggleExpand = () => {
this.props.toggleExpand(this.props.network);
};
}

View File

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

View File

@ -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<Props> {
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<any>[],
alt: [] as React.ReactElement<any>[]
};
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(
<NetworkOption
key={netKey}
network={allNetworks[netKey]}
nodes={nodesByNetwork[netKey]}
nodeSelection={node.id}
isNetworkSelected={node.network === netKey}
isExpanded={expandedNetwork === allNetworks[netKey]}
selectNetwork={this.selectNetwork}
selectNode={this.selectNode}
toggleExpand={this.toggleNetworkExpand}
/>
);
});
return (
<div className="NetworkSelector">
{options.core}
<button className="NetworkSelector-alts" onClick={this.toggleShowAltNetworks}>
<i className="fa fa-flask" />
{translate(isShowingAltNetworks ? 'HIDE_THING' : 'SHOW_THING', {
$thing: translateRaw('NETWORKS_ALTERNATIVE')
})}
</button>
{isShowingAltNetworks && options.alt}
<button className="NetworkSelector-add" onClick={this.props.openCustomNodeModal}>
<i className="fa fa-plus" />
{translate('NODE_ADD')}
</button>
</div>
);
}
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);

View File

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

View File

@ -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<Props> {
public render() {
const { node, isSelected, isAutoSelected } = this.props;
return (
<div className="NodeOption" key={node.service}>
<div
className={classnames(
'NodeOption-name',
isSelected && 'is-selected',
isAutoSelected && 'is-auto-selected'
)}
title={translateRaw('NETWORKS_SWITCH_NODE', {
$node: node.isCustom ? node.name : node.service,
$network: node.network
})}
onClick={this.handleSelect}
>
{node.isCustom ? node.name : node.service}
</div>
{node.isCustom && (
<button className="NodeOption-remove" onClick={this.handleRemove}>
<i className="fa fa-times-circle" />
</button>
)}
</div>
);
}
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);

View File

@ -0,0 +1,2 @@
import NetworkSelector from './NetworkSelector';
export default NetworkSelector;

View File

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

View File

@ -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<T> {
name: any;
value: T;
color?: string;
hidden?: boolean | undefined;
onRemove?(): void;
}
interface Props<T> {
value: T;
options: Option<T>[];
label?: string;
ariaLabel: string;
extra?: any;
size?: string;
color?: string;
menuAlign?: string;
disabled?: boolean;
onChange(value: T): void;
}
export default class ColorDropdown<T> extends PureComponent<Props<T>, {}> {
private dropdownShell: DropdownShell | null;
public render() {
const { ariaLabel, disabled, color, size } = this.props;
return (
<DropdownShell
renderLabel={this.renderLabel}
renderOptions={this.renderOptions}
size={size}
color={color}
ariaLabel={ariaLabel}
ref={el => (this.dropdownShell = el)}
disabled={disabled}
/>
);
}
private renderLabel = () => {
const label = this.props.label ? `${this.props.label}:` : '';
const activeOption = this.getActiveOption();
return (
<span>
{label} {activeOption ? activeOption.name : '-'}
</span>
);
};
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 (
<ul className={menuClass}>
{listItems.map((option, i) => {
if (option.divider) {
return <li key={i} role="separator" className="divider" />;
} else {
return (
<li key={i} className="ColorDropdown-item" style={{ borderColor: option.color }}>
<a
className={option.value === value ? 'active' : ''}
onClick={this.onChange.bind(null, option.value)}
>
{option.name}
{option.onRemove && (
<img
className="ColorDropdown-item-remove"
onClick={this.onRemove.bind(null, option.onRemove)}
src={removeIcon}
alt="remove"
/>
)}
</a>
</li>
);
}
})}
{extra && <li key="separator" role="separator" className="divider" />}
{extra}
</ul>
);
};
private onChange = (value: any) => {
this.props.onChange(value);
if (this.dropdownShell) {
this.dropdownShell.close();
}
};
private onRemove(onRemove: () => void, ev?: React.FormEvent<HTMLButtonElement>) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
onRemove();
}
private getActiveOption() {
return this.props.options.find(opt => opt.value === this.props.value);
}
}

View File

@ -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<Props, State> {
export default class DropdownComponent extends Component<Props, State> {
public static defaultProps = {
color: 'default',
size: 'sm'

View File

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

View File

@ -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<Props, {}> {
<Query
params={['network']}
withQuery={({ network }) => (
<Header networkParam={network && `${network.toLowerCase()}_auto`} />
<Header networkParam={network && makeAutoNodeName(network)} />
)}
/>
<div className="Tab container">

View File

@ -45,7 +45,7 @@ class CheckTransaction extends React.Component<Props, State> {
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';

View File

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

View File

@ -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<T> = Partial<{ [key in keyof T]: Partial<T[key]> }>;
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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<StaticNodesState>, 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) {

View File

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

View File

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

View File

@ -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<typeof getNetworkByChainId> = yield select(
getNetworkByChainId,
chainId
);
if (!network) {
throw new Error(`MyCrypto doesnt 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);

View File

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

View File

@ -15,6 +15,7 @@ span.dropdown {
.dropdown-menu {
padding: 0;
border: none;
box-shadow: $dropdown-shadow;
> li {
margin: 0;

View File

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

View File

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

View File

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

View File

@ -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<T> = { [P in keyof T]?: DeepPartial<T[P]> };
@ -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']

View File

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

View File

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

View File

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

View File

@ -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<ValidatorResult>);
[key: string]: ((n: INode) => Promise<ValidatorResult>);
}
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}`);
});
};

View File

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

View File

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

View File

@ -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', () => {

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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