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:
William O'Beirne 2017-12-01 08:09:51 -08:00 committed by Daniel Ternyak
parent eb490a78b3
commit b638b746de
17 changed files with 406 additions and 87 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

30
common/utils/network.ts Normal file
View File

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