Custom Nodes - Final Touches (#501)
* Add warning about matching nodes, only allow one url:port combination of nodes. * Fix up alert styling. * Custom network form. * Add custom network to redux store. Setup infrastructure for removal and display. * Persist custom networks to LS, show them in display. * Force chain id, make typing happy. * Display custom networks in network dropdown. * Fix form validation, purge unused custom networks.
This commit is contained in:
parent
eb490a78b3
commit
b638b746de
|
@ -1,6 +1,6 @@
|
|||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
import { NodeConfig, CustomNodeConfig } from 'config/data';
|
||||
import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
|
||||
|
||||
export type TForceOfflineConfig = typeof forceOfflineConfig;
|
||||
export function forceOfflineConfig(): interfaces.ForceOfflineAction {
|
||||
|
@ -80,6 +80,26 @@ export function removeCustomNode(
|
|||
};
|
||||
}
|
||||
|
||||
export type TAddCustomNetwork = typeof addCustomNetwork;
|
||||
export function addCustomNetwork(
|
||||
payload: CustomNetworkConfig
|
||||
): interfaces.AddCustomNetworkAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TRemoveCustomNetwork = typeof removeCustomNetwork;
|
||||
export function removeCustomNetwork(
|
||||
payload: CustomNetworkConfig
|
||||
): interfaces.RemoveCustomNetworkAction {
|
||||
return {
|
||||
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetLatestBlock = typeof setLatestBlock;
|
||||
export function setLatestBlock(
|
||||
payload: string
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { TypeKeys } from './constants';
|
||||
import { CustomNodeConfig, NodeConfig } from 'config/data';
|
||||
import { NodeConfig, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
|
||||
|
||||
/*** Toggle Offline ***/
|
||||
export interface ToggleOfflineAction {
|
||||
|
@ -56,6 +56,18 @@ export interface RemoveCustomNodeAction {
|
|||
payload: CustomNodeConfig;
|
||||
}
|
||||
|
||||
/*** Add Custom Network ***/
|
||||
export interface AddCustomNetworkAction {
|
||||
type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK;
|
||||
payload: CustomNetworkConfig;
|
||||
}
|
||||
|
||||
/*** Remove Custom Network ***/
|
||||
export interface RemoveCustomNetworkAction {
|
||||
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK;
|
||||
payload: CustomNetworkConfig;
|
||||
}
|
||||
|
||||
/*** Set Latest Block ***/
|
||||
export interface SetLatestBlockAction {
|
||||
type: TypeKeys.CONFIG_SET_LATEST_BLOCK;
|
||||
|
@ -78,5 +90,7 @@ export type ConfigAction =
|
|||
| ChangeNodeIntentAction
|
||||
| AddCustomNodeAction
|
||||
| RemoveCustomNodeAction
|
||||
| AddCustomNetworkAction
|
||||
| RemoveCustomNetworkAction
|
||||
| SetLatestBlockAction
|
||||
| Web3UnsetNodeAction;
|
||||
|
|
|
@ -8,6 +8,8 @@ export enum TypeKeys {
|
|||
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
|
||||
CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
|
||||
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',
|
||||
CONFIG_ADD_CUSTOM_NETWORK = 'CONFIG_ADD_CUSTOM_NETWORK',
|
||||
CONFIG_REMOVE_CUSTOM_NETWORK = 'CONFIG_REMOVE_CUSTOM_NETWORK',
|
||||
CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK',
|
||||
CONFIG_NODE_WEB3_UNSET = 'CONFIG_NODE_WEB3_UNSET'
|
||||
}
|
||||
|
|
|
@ -2,9 +2,12 @@ import React from 'react';
|
|||
import classnames from 'classnames';
|
||||
import Modal, { IButton } from 'components/ui/Modal';
|
||||
import translate from 'translations';
|
||||
import { NETWORKS, CustomNodeConfig } from 'config/data';
|
||||
import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config/data';
|
||||
import { makeCustomNodeId } from 'utils/node';
|
||||
import { makeCustomNetworkId } from 'utils/network';
|
||||
|
||||
const NETWORK_KEYS = Object.keys(NETWORKS);
|
||||
const CUSTOM = 'custom';
|
||||
|
||||
interface Input {
|
||||
name: string;
|
||||
|
@ -13,7 +16,10 @@ interface Input {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
customNodes: CustomNodeConfig[];
|
||||
customNetworks: CustomNetworkConfig[];
|
||||
handleAddCustomNode(node: CustomNodeConfig): void;
|
||||
handleAddCustomNetwork(node: CustomNetworkConfig): void;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
||||
|
@ -22,6 +28,9 @@ interface State {
|
|||
url: string;
|
||||
port: string;
|
||||
network: string;
|
||||
customNetworkName: string;
|
||||
customNetworkUnit: string;
|
||||
customNetworkChainId: string;
|
||||
hasAuth: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
|
@ -33,13 +42,17 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
url: '',
|
||||
port: '',
|
||||
network: NETWORK_KEYS[0],
|
||||
customNetworkName: '',
|
||||
customNetworkUnit: '',
|
||||
customNetworkChainId: '',
|
||||
hasAuth: false,
|
||||
username: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { handleClose } = this.props;
|
||||
const { customNetworks, handleClose } = this.props;
|
||||
const { network } = this.state;
|
||||
const isHttps = window.location.protocol.includes('https');
|
||||
const invalids = this.getInvalids();
|
||||
|
||||
|
@ -56,6 +69,8 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
}
|
||||
];
|
||||
|
||||
const conflictedNode = this.getConflictedNode();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={translate('NODE_Title')}
|
||||
|
@ -65,11 +80,18 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
>
|
||||
<div>
|
||||
{isHttps && (
|
||||
<div className="alert alert-danger small">
|
||||
<div className="alert alert-warning small">
|
||||
{translate('NODE_Warning')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{conflictedNode && (
|
||||
<div className="alert alert-warning small">
|
||||
You already have a node called '{conflictedNode.name}' that
|
||||
matches this one, saving this will overwrite it
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form>
|
||||
<div className="row">
|
||||
<div className="col-sm-7">
|
||||
|
@ -87,7 +109,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
<select
|
||||
className="form-control"
|
||||
name="network"
|
||||
value={this.state.network}
|
||||
value={network}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{NETWORK_KEYS.map(net => (
|
||||
|
@ -95,10 +117,56 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
{net}
|
||||
</option>
|
||||
))}
|
||||
{customNetworks.map(net => {
|
||||
const id = makeCustomNetworkId(net);
|
||||
return (
|
||||
<option key={id} value={id}>
|
||||
{net.name} (Custom)
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<option value={CUSTOM}>Custom...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{network === CUSTOM && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label className="is-required">Network Name</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkName',
|
||||
placeholder: 'My Custom Network'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<label className="is-required">Currency</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkUnit',
|
||||
placeholder: 'ETH'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<label>Chain ID</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'customNetworkChainId',
|
||||
placeholder: 'e.g. 1'
|
||||
},
|
||||
invalids
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-9">
|
||||
<label>URL</label>
|
||||
|
@ -123,6 +191,7 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<label>
|
||||
|
@ -139,11 +208,11 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
{this.state.hasAuth && (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<label>Username</label>
|
||||
<label className="is-required">Username</label>
|
||||
{this.renderInput({ name: 'username' }, invalids)}
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<label>Password</label>
|
||||
<label className="is-required">Password</label>
|
||||
{this.renderInput(
|
||||
{
|
||||
name: 'password',
|
||||
|
@ -175,7 +244,17 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private getInvalids(): { [key: string]: boolean } {
|
||||
const { url, port, hasAuth, username, password } = this.state;
|
||||
const {
|
||||
url,
|
||||
port,
|
||||
hasAuth,
|
||||
username,
|
||||
password,
|
||||
network,
|
||||
customNetworkName,
|
||||
customNetworkUnit,
|
||||
customNetworkChainId
|
||||
} = this.state;
|
||||
const required = ['name', 'url', 'port', 'network'];
|
||||
const invalids: { [key: string]: boolean } = {};
|
||||
|
||||
|
@ -207,9 +286,64 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
// If they have a custom network, make sure info is provided
|
||||
if (network === CUSTOM) {
|
||||
if (!customNetworkName) {
|
||||
invalids.customNetworkName = true;
|
||||
}
|
||||
if (!customNetworkUnit) {
|
||||
invalids.customNetworkUnit = true;
|
||||
}
|
||||
|
||||
// Numeric chain ID (if provided)
|
||||
const iChainId = parseInt(customNetworkChainId, 10);
|
||||
if (!iChainId || iChainId < 0) {
|
||||
invalids.customNetworkChainId = true;
|
||||
}
|
||||
}
|
||||
|
||||
return invalids;
|
||||
}
|
||||
|
||||
private makeCustomNetworkConfigFromState(): CustomNetworkConfig {
|
||||
return {
|
||||
name: this.state.customNetworkName,
|
||||
unit: this.state.customNetworkUnit,
|
||||
chainId: this.state.customNetworkChainId
|
||||
? parseInt(this.state.customNetworkChainId, 10)
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
private makeCustomNodeConfigFromState(): CustomNodeConfig {
|
||||
const { network } = this.state;
|
||||
const node: CustomNodeConfig = {
|
||||
name: this.state.name.trim(),
|
||||
url: this.state.url.trim(),
|
||||
port: parseInt(this.state.port, 10),
|
||||
network:
|
||||
network === CUSTOM
|
||||
? makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
|
||||
: network
|
||||
};
|
||||
|
||||
if (this.state.hasAuth) {
|
||||
node.auth = {
|
||||
username: this.state.username,
|
||||
password: this.state.password
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private getConflictedNode(): CustomNodeConfig | undefined {
|
||||
const { customNodes } = this.props;
|
||||
const config = this.makeCustomNodeConfigFromState();
|
||||
const thisId = makeCustomNodeId(config);
|
||||
return customNodes.find(conf => makeCustomNodeId(conf) === thisId);
|
||||
}
|
||||
|
||||
private handleChange = (
|
||||
ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
|
@ -223,18 +357,11 @@ export default class CustomNodeModal extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
private saveAndAdd = () => {
|
||||
const node: CustomNodeConfig = {
|
||||
name: this.state.name.trim(),
|
||||
url: this.state.url.trim(),
|
||||
port: parseInt(this.state.port, 10),
|
||||
network: this.state.network
|
||||
};
|
||||
const node = this.makeCustomNodeConfigFromState();
|
||||
|
||||
if (this.state.hasAuth) {
|
||||
node.auth = {
|
||||
username: this.state.username,
|
||||
password: this.state.password
|
||||
};
|
||||
if (this.state.network === CUSTOM) {
|
||||
const network = this.makeCustomNetworkConfigFromState();
|
||||
this.props.handleAddCustomNetwork(network);
|
||||
}
|
||||
|
||||
this.props.handleAddCustomNode(node);
|
||||
|
|
|
@ -3,7 +3,8 @@ import {
|
|||
TChangeLanguage,
|
||||
TChangeNodeIntent,
|
||||
TAddCustomNode,
|
||||
TRemoveCustomNode
|
||||
TRemoveCustomNode,
|
||||
TAddCustomNetwork
|
||||
} from 'actions/config';
|
||||
import logo from 'assets/images/logo-myetherwallet.svg';
|
||||
import { Dropdown, ColorDropdown } from 'components/ui';
|
||||
|
@ -14,17 +15,18 @@ import {
|
|||
ANNOUNCEMENT_MESSAGE,
|
||||
ANNOUNCEMENT_TYPE,
|
||||
languages,
|
||||
NETWORKS,
|
||||
NODES,
|
||||
VERSION,
|
||||
NodeConfig,
|
||||
CustomNodeConfig
|
||||
} from '../../config/data';
|
||||
CustomNodeConfig,
|
||||
CustomNetworkConfig
|
||||
} from 'config/data';
|
||||
import GasPriceDropdown from './components/GasPriceDropdown';
|
||||
import Navigation from './components/Navigation';
|
||||
import CustomNodeModal from './components/CustomNodeModal';
|
||||
import { getKeyByValue } from 'utils/helpers';
|
||||
import { makeCustomNodeId } from 'utils/node';
|
||||
import { getNetworkConfigFromId } from 'utils/network';
|
||||
import './index.scss';
|
||||
|
||||
interface Props {
|
||||
|
@ -34,12 +36,14 @@ interface Props {
|
|||
isChangingNode: boolean;
|
||||
gasPriceGwei: number;
|
||||
customNodes: CustomNodeConfig[];
|
||||
customNetworks: CustomNetworkConfig[];
|
||||
|
||||
changeLanguage: TChangeLanguage;
|
||||
changeNodeIntent: TChangeNodeIntent;
|
||||
changeGasPrice: TChangeGasPrice;
|
||||
addCustomNode: TAddCustomNode;
|
||||
removeCustomNode: TRemoveCustomNode;
|
||||
addCustomNetwork: TAddCustomNetwork;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -58,40 +62,47 @@ export default class Header extends Component<Props, State> {
|
|||
node,
|
||||
nodeSelection,
|
||||
isChangingNode,
|
||||
customNodes
|
||||
customNodes,
|
||||
customNetworks
|
||||
} = this.props;
|
||||
const { isAddingCustomNode } = this.state;
|
||||
const selectedLanguage = languageSelection;
|
||||
const selectedNetwork = NETWORKS[node.network];
|
||||
const selectedNetwork = getNetworkConfigFromId(
|
||||
node.network,
|
||||
customNetworks
|
||||
);
|
||||
const LanguageDropDown = Dropdown as new () => Dropdown<
|
||||
typeof selectedLanguage
|
||||
>;
|
||||
|
||||
const nodeOptions = Object.keys(NODES)
|
||||
.map(key => {
|
||||
const n = NODES[key];
|
||||
const network = getNetworkConfigFromId(n.network, customNetworks);
|
||||
return {
|
||||
value: key,
|
||||
name: (
|
||||
<span>
|
||||
{NODES[key].network} <small>({NODES[key].service})</small>
|
||||
{network && network.name} <small>({n.service})</small>
|
||||
</span>
|
||||
),
|
||||
color: NETWORKS[NODES[key].network].color,
|
||||
hidden: NODES[key].hidden
|
||||
color: network && network.color,
|
||||
hidden: n.hidden
|
||||
};
|
||||
})
|
||||
.concat(
|
||||
customNodes.map(customNode => {
|
||||
customNodes.map(cn => {
|
||||
const network = getNetworkConfigFromId(cn.network, customNetworks);
|
||||
return {
|
||||
value: makeCustomNodeId(customNode),
|
||||
value: makeCustomNodeId(cn),
|
||||
name: (
|
||||
<span>
|
||||
{customNode.network} - {customNode.name} <small>(custom)</small>
|
||||
{network && network.name} - {cn.name} <small>(custom)</small>
|
||||
</span>
|
||||
),
|
||||
color: '#000',
|
||||
color: network && network.color,
|
||||
hidden: false,
|
||||
onRemove: () => this.props.removeCustomNode(customNode)
|
||||
onRemove: () => this.props.removeCustomNode(cn)
|
||||
};
|
||||
})
|
||||
);
|
||||
|
@ -161,8 +172,8 @@ export default class Header extends Component<Props, State> {
|
|||
>
|
||||
<ColorDropdown
|
||||
ariaLabel={`
|
||||
change node. current node ${node.network}
|
||||
node by ${node.service}
|
||||
change node. current node is on the ${node.network} network
|
||||
provided by ${node.service}
|
||||
`}
|
||||
options={nodeOptions}
|
||||
value={nodeSelection}
|
||||
|
@ -182,11 +193,14 @@ export default class Header extends Component<Props, State> {
|
|||
</section>
|
||||
</section>
|
||||
|
||||
<Navigation color={selectedNetwork.color} />
|
||||
<Navigation color={selectedNetwork && selectedNetwork.color} />
|
||||
|
||||
{isAddingCustomNode && (
|
||||
<CustomNodeModal
|
||||
customNodes={customNodes}
|
||||
customNetworks={customNetworks}
|
||||
handleAddCustomNode={this.addCustomNode}
|
||||
handleAddCustomNetwork={this.props.addCustomNetwork}
|
||||
handleClose={this.closeCustomNodeModal}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -79,6 +79,12 @@ export interface NetworkConfig {
|
|||
contracts: NetworkContract[] | null;
|
||||
}
|
||||
|
||||
export interface CustomNetworkConfig {
|
||||
name: string;
|
||||
unit: string;
|
||||
chainId: number;
|
||||
}
|
||||
|
||||
export interface NodeConfig {
|
||||
network: string;
|
||||
lib: RPCNode | Web3Node;
|
||||
|
|
|
@ -1,40 +1,49 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
changeGasPrice as dChangeGasPrice,
|
||||
changeLanguage as dChangeLanguage,
|
||||
changeNodeIntent as dChangeNodeIntent,
|
||||
addCustomNode as dAddCustomNode,
|
||||
removeCustomNode as dRemoveCustomNode,
|
||||
addCustomNetwork as dAddCustomNetwork,
|
||||
TChangeGasPrice,
|
||||
TChangeLanguage,
|
||||
TChangeNodeIntent,
|
||||
TAddCustomNode,
|
||||
TRemoveCustomNode
|
||||
TRemoveCustomNode,
|
||||
TAddCustomNetwork
|
||||
} from 'actions/config';
|
||||
import { AlphaAgreement, Footer, Header } from 'components';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import Notifications from './Notifications';
|
||||
import { NodeConfig, CustomNodeConfig } from 'config/data';
|
||||
|
||||
interface Props {
|
||||
// FIXME
|
||||
children: any;
|
||||
|
||||
languageSelection: string;
|
||||
node: NodeConfig;
|
||||
nodeSelection: string;
|
||||
isChangingNode: boolean;
|
||||
gasPriceGwei: number;
|
||||
customNodes: CustomNodeConfig[];
|
||||
latestBlock: string;
|
||||
interface ReduxProps {
|
||||
languageSelection: AppState['config']['languageSelection'];
|
||||
node: AppState['config']['node'];
|
||||
nodeSelection: AppState['config']['nodeSelection'];
|
||||
isChangingNode: AppState['config']['isChangingNode'];
|
||||
gasPriceGwei: AppState['config']['gasPriceGwei'];
|
||||
customNodes: AppState['config']['customNodes'];
|
||||
customNetworks: AppState['config']['customNetworks'];
|
||||
latestBlock: AppState['config']['latestBlock'];
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
changeLanguage: TChangeLanguage;
|
||||
changeNodeIntent: TChangeNodeIntent;
|
||||
changeGasPrice: TChangeGasPrice;
|
||||
addCustomNode: TAddCustomNode;
|
||||
removeCustomNode: TRemoveCustomNode;
|
||||
addCustomNetwork: TAddCustomNetwork;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
// FIXME
|
||||
children: any;
|
||||
} & ReduxProps &
|
||||
ActionProps;
|
||||
|
||||
class TabSection extends Component<Props, {}> {
|
||||
public render() {
|
||||
const {
|
||||
|
@ -46,13 +55,15 @@ class TabSection extends Component<Props, {}> {
|
|||
languageSelection,
|
||||
gasPriceGwei,
|
||||
customNodes,
|
||||
customNetworks,
|
||||
latestBlock,
|
||||
|
||||
changeLanguage,
|
||||
changeNodeIntent,
|
||||
changeGasPrice,
|
||||
addCustomNode,
|
||||
removeCustomNode
|
||||
removeCustomNode,
|
||||
addCustomNetwork
|
||||
} = this.props;
|
||||
|
||||
const headerProps = {
|
||||
|
@ -62,12 +73,14 @@ class TabSection extends Component<Props, {}> {
|
|||
isChangingNode,
|
||||
gasPriceGwei,
|
||||
customNodes,
|
||||
customNetworks,
|
||||
|
||||
changeLanguage,
|
||||
changeNodeIntent,
|
||||
changeGasPrice,
|
||||
addCustomNode,
|
||||
removeCustomNode
|
||||
removeCustomNode,
|
||||
addCustomNetwork
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -84,7 +97,7 @@ class TabSection extends Component<Props, {}> {
|
|||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
function mapStateToProps(state: AppState): ReduxProps {
|
||||
return {
|
||||
node: state.config.node,
|
||||
nodeSelection: state.config.nodeSelection,
|
||||
|
@ -92,6 +105,7 @@ function mapStateToProps(state: AppState) {
|
|||
languageSelection: state.config.languageSelection,
|
||||
gasPriceGwei: state.config.gasPriceGwei,
|
||||
customNodes: state.config.customNodes,
|
||||
customNetworks: state.config.customNetworks,
|
||||
latestBlock: state.config.latestBlock
|
||||
};
|
||||
}
|
||||
|
@ -101,5 +115,6 @@ export default connect(mapStateToProps, {
|
|||
changeLanguage: dChangeLanguage,
|
||||
changeNodeIntent: dChangeNodeIntent,
|
||||
addCustomNode: dAddCustomNode,
|
||||
removeCustomNode: dRemoveCustomNode
|
||||
removeCustomNode: dRemoveCustomNode,
|
||||
addCustomNetwork: dAddCustomNetwork
|
||||
})(TabSection);
|
||||
|
|
|
@ -20,18 +20,21 @@ export interface IWithTx {
|
|||
showNotification: TShowNotification;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
wallet: state.wallet.inst,
|
||||
balance: state.wallet.balance,
|
||||
node: configSelectors.getNodeConfig(state),
|
||||
nodeLib: configSelectors.getNodeLib(state),
|
||||
chainId: configSelectors.getNetworkConfig(state).chainId,
|
||||
networkName: configSelectors.getNetworkConfig(state).name,
|
||||
gasPrice: toWei(
|
||||
`${configSelectors.getGasPriceGwei(state)}`,
|
||||
getDecimal('gwei')
|
||||
)
|
||||
});
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const network = configSelectors.getNetworkConfig(state);
|
||||
return {
|
||||
wallet: state.wallet.inst,
|
||||
balance: state.wallet.balance,
|
||||
node: configSelectors.getNodeConfig(state),
|
||||
nodeLib: configSelectors.getNodeLib(state),
|
||||
chainId: network ? network.chainId : 0,
|
||||
networkName: network ? network.name : 'Unknown network',
|
||||
gasPrice: toWei(
|
||||
`${configSelectors.getGasPriceGwei(state)}`,
|
||||
getDecimal('gwei')
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
export const withTx = passedComponent =>
|
||||
connect(mapStateToProps, {
|
||||
|
|
|
@ -198,7 +198,7 @@ export async function generateCompleteTransactionFromRawTransaction(
|
|||
to: toChecksumAddress(cleanHex(to)),
|
||||
value: token ? '0x00' : cleanHex(value.toString(16)),
|
||||
data: data ? cleanHex(data) : '',
|
||||
chainId: chainId || 1
|
||||
chainId: chainId || 0
|
||||
};
|
||||
|
||||
// Sign the transaction
|
||||
|
|
|
@ -4,23 +4,35 @@ import {
|
|||
ChangeNodeAction,
|
||||
AddCustomNodeAction,
|
||||
RemoveCustomNodeAction,
|
||||
AddCustomNetworkAction,
|
||||
RemoveCustomNetworkAction,
|
||||
SetLatestBlockAction,
|
||||
ConfigAction
|
||||
} from 'actions/config';
|
||||
import { TypeKeys } from 'actions/config/constants';
|
||||
import { NODES, NodeConfig, CustomNodeConfig } from '../config/data';
|
||||
import {
|
||||
NODES,
|
||||
NETWORKS,
|
||||
NodeConfig,
|
||||
CustomNodeConfig,
|
||||
NetworkConfig,
|
||||
CustomNetworkConfig
|
||||
} from '../config/data';
|
||||
import { makeCustomNodeId } from 'utils/node';
|
||||
import { makeCustomNetworkId } from 'utils/network';
|
||||
|
||||
export interface State {
|
||||
// FIXME
|
||||
languageSelection: string;
|
||||
nodeSelection: string;
|
||||
node: NodeConfig;
|
||||
network: NetworkConfig;
|
||||
isChangingNode: boolean;
|
||||
gasPriceGwei: number;
|
||||
offline: boolean;
|
||||
forceOffline: boolean;
|
||||
customNodes: CustomNodeConfig[];
|
||||
customNetworks: CustomNetworkConfig[];
|
||||
latestBlock: string;
|
||||
}
|
||||
|
||||
|
@ -29,11 +41,13 @@ export const INITIAL_STATE: State = {
|
|||
languageSelection: 'en',
|
||||
nodeSelection: defaultNode,
|
||||
node: NODES[defaultNode],
|
||||
network: NETWORKS[NODES[defaultNode].network],
|
||||
isChangingNode: false,
|
||||
gasPriceGwei: 21,
|
||||
offline: false,
|
||||
forceOffline: false,
|
||||
customNodes: [],
|
||||
customNetworks: [],
|
||||
latestBlock: '???'
|
||||
};
|
||||
|
||||
|
@ -82,9 +96,13 @@ function forceOffline(state: State): State {
|
|||
}
|
||||
|
||||
function addCustomNode(state: State, action: AddCustomNodeAction): State {
|
||||
const newId = makeCustomNodeId(action.payload);
|
||||
return {
|
||||
...state,
|
||||
customNodes: [...state.customNodes, action.payload]
|
||||
customNodes: [
|
||||
...state.customNodes.filter(node => makeCustomNodeId(node) !== newId),
|
||||
action.payload
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -98,6 +116,29 @@ function removeCustomNode(state: State, action: RemoveCustomNodeAction): State {
|
|||
};
|
||||
}
|
||||
|
||||
function addCustomNetwork(state: State, action: AddCustomNetworkAction): State {
|
||||
const newId = makeCustomNetworkId(action.payload);
|
||||
return {
|
||||
...state,
|
||||
customNetworks: [
|
||||
...state.customNetworks.filter(
|
||||
node => makeCustomNetworkId(node) !== newId
|
||||
),
|
||||
action.payload
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function removeCustomNetwork(
|
||||
state: State,
|
||||
action: RemoveCustomNetworkAction
|
||||
): State {
|
||||
return {
|
||||
...state,
|
||||
customNetworks: state.customNetworks.filter(cn => cn !== action.payload)
|
||||
};
|
||||
}
|
||||
|
||||
function setLatestBlock(state: State, action: SetLatestBlockAction): State {
|
||||
return {
|
||||
...state,
|
||||
|
@ -126,6 +167,10 @@ export function config(
|
|||
return addCustomNode(state, action);
|
||||
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:
|
||||
return removeCustomNode(state, action);
|
||||
case TypeKeys.CONFIG_ADD_CUSTOM_NETWORK:
|
||||
return addCustomNetwork(state, action);
|
||||
case TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK:
|
||||
return removeCustomNetwork(state, action);
|
||||
case TypeKeys.CONFIG_SET_LATEST_BLOCK:
|
||||
return setLatestBlock(state, action);
|
||||
default:
|
||||
|
|
|
@ -16,10 +16,12 @@ import {
|
|||
getCustomNodeConfigFromId,
|
||||
makeNodeConfigFromCustomConfig
|
||||
} from 'utils/node';
|
||||
import { makeCustomNetworkId } from 'utils/network';
|
||||
import {
|
||||
getNode,
|
||||
getNodeConfig,
|
||||
getCustomNodeConfigs,
|
||||
getCustomNetworkConfigs,
|
||||
getOffline,
|
||||
getForceOffline
|
||||
} from 'selectors/config';
|
||||
|
@ -30,6 +32,7 @@ import {
|
|||
changeNode,
|
||||
changeNodeIntent,
|
||||
setLatestBlock,
|
||||
removeCustomNetwork,
|
||||
AddCustomNodeAction,
|
||||
ChangeNodeIntentAction
|
||||
} from 'actions/config';
|
||||
|
@ -188,6 +191,22 @@ export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator {
|
|||
yield put(changeNodeIntent(nodeId));
|
||||
}
|
||||
|
||||
// If there are any orphaned custom networks, purge them
|
||||
export function* cleanCustomNetworks(): SagaIterator {
|
||||
const customNodes = yield select(getCustomNodeConfigs);
|
||||
const customNetworks = yield select(getCustomNetworkConfigs);
|
||||
const networksInUse = customNodes.reduce((prev, conf) => {
|
||||
prev[conf.network] = true;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
for (const net of customNetworks) {
|
||||
if (!networksInUse[makeCustomNetworkId(net)]) {
|
||||
yield put(removeCustomNetwork(net));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// unset web3 as the selected node if a non-web3 wallet has been selected
|
||||
export function* unsetWeb3Node(action): SagaIterator {
|
||||
const node = yield select(getNode);
|
||||
|
@ -230,6 +249,7 @@ export default function* configSaga(): SagaIterator {
|
|||
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
||||
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
||||
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
|
||||
yield takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, cleanCustomNetworks);
|
||||
yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3Node);
|
||||
yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3Node);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,19 @@
|
|||
@import "common/sass/mixins";
|
||||
@import "~bootstrap-sass/assets/stylesheets/bootstrap/alerts";
|
||||
|
||||
.alert {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
a {
|
||||
color: #FFF;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alert icons
|
||||
.alert:after {
|
||||
content: '';
|
||||
|
@ -23,15 +36,6 @@
|
|||
@media screen and (max-width: $screen-xs) {
|
||||
left: 1%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #FFF;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert,
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
label {
|
||||
margin-bottom: $space-xs;
|
||||
font-size: $font-size-bump-more;
|
||||
|
||||
&.is-required:after {
|
||||
content: '*';
|
||||
padding-left: 2px;
|
||||
color: $brand-warning;
|
||||
}
|
||||
}
|
||||
|
||||
label + .form-control,
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {
|
||||
NetworkConfig,
|
||||
NetworkContract,
|
||||
NETWORKS,
|
||||
NodeConfig,
|
||||
CustomNodeConfig
|
||||
CustomNodeConfig,
|
||||
CustomNetworkConfig
|
||||
} from 'config/data';
|
||||
import { INode } from 'libs/nodes/INode';
|
||||
import { AppState } from 'reducers';
|
||||
import { getNetworkConfigFromId } from 'utils/network';
|
||||
|
||||
export function getNode(state: AppState): string {
|
||||
return state.config.nodeSelection;
|
||||
|
@ -20,12 +21,16 @@ export function getNodeLib(state: AppState): INode {
|
|||
return getNodeConfig(state).lib;
|
||||
}
|
||||
|
||||
export function getNetworkConfig(state: AppState): NetworkConfig {
|
||||
return NETWORKS[getNodeConfig(state).network];
|
||||
export function getNetworkConfig(state: AppState): NetworkConfig | undefined {
|
||||
return getNetworkConfigFromId(
|
||||
getNodeConfig(state).network,
|
||||
getCustomNetworkConfigs(state)
|
||||
);
|
||||
}
|
||||
|
||||
export function getNetworkContracts(state: AppState): NetworkContract[] | null {
|
||||
return getNetworkConfig(state).contracts;
|
||||
const network = getNetworkConfig(state);
|
||||
return network ? network.contracts : [];
|
||||
}
|
||||
|
||||
export function getGasPriceGwei(state: AppState): number {
|
||||
|
@ -40,6 +45,12 @@ export function getCustomNodeConfigs(state: AppState): CustomNodeConfig[] {
|
|||
return state.config.customNodes;
|
||||
}
|
||||
|
||||
export function getCustomNetworkConfigs(
|
||||
state: AppState
|
||||
): CustomNetworkConfig[] {
|
||||
return state.config.customNetworks;
|
||||
}
|
||||
|
||||
export function getOffline(state: AppState): boolean {
|
||||
return state.config.offline;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ export type MergedToken = Token & {
|
|||
};
|
||||
|
||||
export function getTokens(state: AppState): MergedToken[] {
|
||||
const tokens: Token[] = getNetworkConfig(state).tokens;
|
||||
const network = getNetworkConfig(state);
|
||||
const tokens: Token[] = network ? network.tokens : [];
|
||||
return tokens.concat(
|
||||
state.customTokens.map((token: Token) => {
|
||||
const mergedToken = { ...token, custom: true };
|
||||
|
|
|
@ -109,7 +109,8 @@ const configureStore = () => {
|
|||
gasPriceGwei: state.config.gasPriceGwei,
|
||||
nodeSelection: state.config.nodeSelection,
|
||||
languageSelection: state.config.languageSelection,
|
||||
customNodes: state.config.customNodes
|
||||
customNodes: state.config.customNodes,
|
||||
customNetworks: state.config.customNetworks
|
||||
},
|
||||
swap: { ...state.swap, bityRates: {} },
|
||||
customTokens: state.customTokens
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { NETWORKS, NetworkConfig, CustomNetworkConfig } from 'config/data';
|
||||
|
||||
export function makeCustomNetworkId(config: CustomNetworkConfig): string {
|
||||
return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`;
|
||||
}
|
||||
|
||||
export function makeNetworkConfigFromCustomConfig(
|
||||
config: CustomNetworkConfig
|
||||
): NetworkConfig {
|
||||
return {
|
||||
...config,
|
||||
color: '#000',
|
||||
tokens: [],
|
||||
contracts: []
|
||||
};
|
||||
}
|
||||
|
||||
export function getNetworkConfigFromId(
|
||||
id: string,
|
||||
configs: CustomNetworkConfig[]
|
||||
): NetworkConfig | undefined {
|
||||
if (NETWORKS[id]) {
|
||||
return NETWORKS[id];
|
||||
}
|
||||
|
||||
const customConfig = configs.find(conf => makeCustomNetworkId(conf) === id);
|
||||
if (customConfig) {
|
||||
return makeNetworkConfigFromCustomConfig(customConfig);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue