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:
parent
9bdeab2307
commit
a043334685
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1,2 @@
|
|||
import NetworkSelector from './NetworkSelector';
|
||||
export default NetworkSelector;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { getTransactionFields, makeTransaction } from 'libs/transaction';
|
|||
import { IFullWallet } from '../IWallet';
|
||||
import { bufferToHex, toChecksumAddress } from 'ethereumjs-util';
|
||||
import { configuredStore } from 'store';
|
||||
import { getNodeLib, getNetworkNameByChainId } from 'selectors/config';
|
||||
import { getNodeLib, getNetworkByChainId } from 'selectors/config';
|
||||
import Web3Node from 'libs/nodes/web3';
|
||||
import { INode } from 'libs/nodes/INode';
|
||||
|
||||
|
@ -75,12 +75,14 @@ export default class Web3Wallet implements IFullWallet {
|
|||
|
||||
private async networkCheck(lib: Web3Node) {
|
||||
const netId = await lib.getNetVersion();
|
||||
const netName = getNetworkNameByChainId(configuredStore.getState(), netId);
|
||||
if (this.network !== netName) {
|
||||
const networkConfig = getNetworkByChainId(configuredStore.getState(), netId);
|
||||
if (!networkConfig) {
|
||||
throw new Error(`MyCrypto doesn’t support the network with chain ID '${netId}'`);
|
||||
} else if (this.network !== networkConfig.id) {
|
||||
throw new Error(
|
||||
`Expected MetaMask / Mist network to be ${
|
||||
this.network
|
||||
}, but got ${netName}. Please change the network or refresh the page.`
|
||||
`Expected MetaMask / Mist network to be ${this.network}, but got ${
|
||||
networkConfig.id
|
||||
}. Please change the network or refresh the page.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
];
|
||||
|
|
|
@ -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 doesn’t support the network with chain ID '${chainId}'`);
|
||||
}
|
||||
|
||||
const web3Network = makeWeb3Network(network.id);
|
||||
const id = 'web3';
|
||||
|
||||
const config: StaticNodeConfig = {
|
||||
id,
|
||||
isCustom: false,
|
||||
network: web3Network as any,
|
||||
service: Web3Service,
|
||||
lib: shepherdProvider,
|
||||
estimateGas: false,
|
||||
hidden: true
|
||||
};
|
||||
|
||||
|
@ -50,12 +57,12 @@ export function* initWeb3Node(): SagaIterator {
|
|||
}
|
||||
|
||||
if (!web3Added) {
|
||||
shepherd.useProvider('web3', 'web3', makeProviderConfig({ network: web3Network }));
|
||||
shepherd.useProvider('web3', id, makeProviderConfig({ network: web3Network }));
|
||||
}
|
||||
|
||||
web3Added = true;
|
||||
|
||||
yield put(web3SetNode({ id: 'web3', config }));
|
||||
yield put(web3SetNode({ id, config }));
|
||||
return lib;
|
||||
}
|
||||
|
||||
|
@ -64,10 +71,10 @@ export function* initWeb3Node(): SagaIterator {
|
|||
export function* unlockWeb3(): SagaIterator {
|
||||
try {
|
||||
const nodeLib = yield call(initWeb3Node);
|
||||
yield put(changeNodeIntent('web3'));
|
||||
yield put(changeNodeRequested('web3'));
|
||||
yield take(
|
||||
(action: any) =>
|
||||
action.type === TypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeId === 'web3'
|
||||
action.type === TypeKeys.CONFIG_CHANGE_NODE_SUCCEEDED && action.payload.nodeId === 'web3'
|
||||
);
|
||||
|
||||
const web3Node: any | null = yield select(getWeb3Node);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ span.dropdown {
|
|||
.dropdown-menu {
|
||||
padding: 0;
|
||||
border: none;
|
||||
box-shadow: $dropdown-shadow;
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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}`);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue