Move Nodes/Networks to Redux (#961)

* Start splitting networks into their own reducers

* Split out nodes and networks into their own reducers

* Cleanup file structure

* Make selectors for new state

* Change custom network typing

* re-type repo

* Fix up components to use selectors, work on fixing sagas

* Provide consistency in naming, fix more sagas

* Get non web3 node switching working

* Split config rehydration off into a different file for store

* Inline auth for custom nodes

* Include typing for app state

* moar selectors

* Get web3 working + cleanup sagas

* Cleanup tsc errors

* Use forof loop instead of foreach for clearing pruning custom networks

* Add reducer tests for new redux state

* Export needed variables

* Add console error

* Remove old comment

* Work on saga tests

* Get passing existing saga tests

* Fix more tests

* Remove irrlevant tests

* add console error

* Get rest of tests passing

* Fix merge errors

* Remove random text

* Fix store saving

* Fix selector lib only grabbing from static nodes

* Fix custom node removal crashing app

* Infer selected network via node

* Prune custom networks properly on node removal

* Infer network name from chainid from selecting state

* Cleanup tsc errors

* Remove MEW nodes for main and testnet
This commit is contained in:
HenryNguyen5 2018-02-12 15:43:07 -05:00 committed by Daniel Ternyak
parent 128471dc09
commit 01fc5f1a89
111 changed files with 2561 additions and 1868 deletions

View File

@ -1,9 +1,8 @@
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config';
export type TToggleOfflineConfig = typeof toggleOfflineConfig;
export function toggleOfflineConfig(): interfaces.ToggleOfflineAction {
export type TToggleOffline = typeof toggleOffline;
export function toggleOffline(): interfaces.ToggleOfflineAction {
return {
type: TypeKeys.CONFIG_TOGGLE_OFFLINE
};
@ -26,13 +25,11 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
export type TChangeNode = typeof changeNode;
export function changeNode(
nodeSelection: string,
node: NodeConfig,
network: NetworkConfig
payload: interfaces.ChangeNodeAction['payload']
): interfaces.ChangeNodeAction {
return {
type: TypeKeys.CONFIG_NODE_CHANGE,
payload: { nodeSelection, node, network }
payload
};
}
@ -52,7 +49,9 @@ export function changeNodeIntent(payload: string): interfaces.ChangeNodeIntentAc
}
export type TAddCustomNode = typeof addCustomNode;
export function addCustomNode(payload: CustomNodeConfig): interfaces.AddCustomNodeAction {
export function addCustomNode(
payload: interfaces.AddCustomNodeAction['payload']
): interfaces.AddCustomNodeAction {
return {
type: TypeKeys.CONFIG_ADD_CUSTOM_NODE,
payload
@ -60,7 +59,9 @@ export function addCustomNode(payload: CustomNodeConfig): interfaces.AddCustomNo
}
export type TRemoveCustomNode = typeof removeCustomNode;
export function removeCustomNode(payload: CustomNodeConfig): interfaces.RemoveCustomNodeAction {
export function removeCustomNode(
payload: interfaces.RemoveCustomNodeAction['payload']
): interfaces.RemoveCustomNodeAction {
return {
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE,
payload
@ -68,7 +69,9 @@ export function removeCustomNode(payload: CustomNodeConfig): interfaces.RemoveCu
}
export type TAddCustomNetwork = typeof addCustomNetwork;
export function addCustomNetwork(payload: CustomNetworkConfig): interfaces.AddCustomNetworkAction {
export function addCustomNetwork(
payload: interfaces.AddCustomNetworkAction['payload']
): interfaces.AddCustomNetworkAction {
return {
type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK,
payload
@ -77,7 +80,7 @@ export function addCustomNetwork(payload: CustomNetworkConfig): interfaces.AddCu
export type TRemoveCustomNetwork = typeof removeCustomNetwork;
export function removeCustomNetwork(
payload: CustomNetworkConfig
payload: interfaces.RemoveCustomNetworkAction['payload']
): interfaces.RemoveCustomNetworkAction {
return {
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK,
@ -93,6 +96,15 @@ export function setLatestBlock(payload: string): interfaces.SetLatestBlockAction
};
}
export function web3SetNode(
payload: interfaces.Web3setNodeAction['payload']
): interfaces.Web3setNodeAction {
return {
type: TypeKeys.CONFIG_NODE_WEB3_SET,
payload
};
}
export type TWeb3UnsetNode = typeof web3UnsetNode;
export function web3UnsetNode(): interfaces.Web3UnsetNodeAction {
return {

View File

@ -1,5 +1,6 @@
import { TypeKeys } from './constants';
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config';
import { CustomNodeConfig, Web3NodeConfig } from 'types/node';
import { CustomNetworkConfig } from 'types/network';
/*** Toggle Offline ***/
export interface ToggleOfflineAction {
@ -19,11 +20,9 @@ export interface ChangeLanguageAction {
/*** Change Node ***/
export interface ChangeNodeAction {
type: TypeKeys.CONFIG_NODE_CHANGE;
// FIXME $keyof?
payload: {
nodeSelection: string;
node: NodeConfig;
network: NetworkConfig;
nodeId: string;
networkId: string;
};
}
@ -41,25 +40,25 @@ export interface ChangeNodeIntentAction {
/*** Add Custom Node ***/
export interface AddCustomNodeAction {
type: TypeKeys.CONFIG_ADD_CUSTOM_NODE;
payload: CustomNodeConfig;
payload: { id: string; config: CustomNodeConfig };
}
/*** Remove Custom Node ***/
export interface RemoveCustomNodeAction {
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE;
payload: CustomNodeConfig;
payload: { id: string };
}
/*** Add Custom Network ***/
export interface AddCustomNetworkAction {
type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK;
payload: CustomNetworkConfig;
payload: { id: string; config: CustomNetworkConfig };
}
/*** Remove Custom Network ***/
export interface RemoveCustomNetworkAction {
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK;
payload: CustomNetworkConfig;
payload: { id: string };
}
/*** Set Latest Block ***/
@ -73,17 +72,28 @@ export interface Web3UnsetNodeAction {
type: TypeKeys.CONFIG_NODE_WEB3_UNSET;
}
/*** Union Type ***/
export type ConfigAction =
/*** Set Web3 as a Node ***/
export interface Web3setNodeAction {
type: TypeKeys.CONFIG_NODE_WEB3_SET;
payload: { id: 'web3'; config: Web3NodeConfig };
}
export type CustomNetworkAction = AddCustomNetworkAction | RemoveCustomNetworkAction;
export type CustomNodeAction = AddCustomNodeAction | RemoveCustomNodeAction;
export type NodeAction =
| ChangeNodeAction
| ChangeNodeIntentAction
| Web3UnsetNodeAction
| Web3setNodeAction;
export type MetaAction =
| ChangeLanguageAction
| ToggleOfflineAction
| ToggleAutoGasLimitAction
| PollOfflineStatus
| ChangeNodeIntentAction
| AddCustomNodeAction
| RemoveCustomNodeAction
| AddCustomNetworkAction
| RemoveCustomNetworkAction
| SetLatestBlockAction
| Web3UnsetNodeAction;
| SetLatestBlockAction;
/*** Union Type ***/
export type ConfigAction = CustomNetworkAction | CustomNodeAction | NodeAction | MetaAction;

View File

@ -1,14 +1,19 @@
export enum TypeKeys {
CONFIG_LANGUAGE_CHANGE = 'CONFIG_LANGUAGE_CHANGE',
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
CONFIG_TOGGLE_AUTO_GAS_LIMIT = 'CONFIG_TOGGLE_AUTO_GAS_LIMIT',
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
CONFIG_SET_LATEST_BLOCK = 'CONFIG_SET_LATEST_BLOCK',
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_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'
CONFIG_REMOVE_CUSTOM_NETWORK = 'CONFIG_REMOVE_CUSTOM_NETWORK'
}

View File

@ -1,6 +1,6 @@
import { Token } from 'config';
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
import { Token } from 'types/network';
export type TAddCustomToken = typeof addCustomToken;
export function addCustomToken(payload: Token): interfaces.AddCustomTokenAction {

View File

@ -1,5 +1,6 @@
import { Token } from 'config';
import { TypeKeys } from './constants';
import { Token } from 'types/network';
/*** Add custom token ***/
export interface AddCustomTokenAction {
type: TypeKeys.CUSTOM_TOKEN_ADD;

View File

@ -1,5 +1,4 @@
import { Identicon, UnitDisplay } from 'components/ui';
import { NetworkConfig } from 'config';
import { IWallet, Balance, TrezorWallet, LedgerWallet } from 'libs/wallet';
import React from 'react';
import translate from 'translations';
@ -8,6 +7,7 @@ import Spinner from 'components/ui/Spinner';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { NetworkConfig } from 'types/network';
import { TSetAccountBalance, setAccountBalance } from 'actions/wallet';
interface OwnProps {
@ -72,7 +72,14 @@ class AccountInfo extends React.Component<Props, State> {
public render() {
const { network, balance, isOffline } = this.props;
const { address, showLongBalance, confirmAddr } = this.state;
const { blockExplorer, tokenExplorer } = network;
let blockExplorer;
let tokenExplorer;
if (!network.isCustom) {
// this is kind of ugly but its the result of typeguards, maybe we can find a cleaner solution later on such as just dedicating it to a selector
blockExplorer = network.blockExplorer;
tokenExplorer = network.tokenExplorer;
}
const wallet = this.props.wallet as LedgerWallet | TrezorWallet;
return (
<div className="AccountInfo">

View File

@ -7,12 +7,12 @@ import { rateSymbols } from 'api/rates';
import { chain, flatMap } from 'lodash';
import { TokenBalance, getShownTokenBalances } from 'selectors/wallet';
import { Balance } from 'libs/wallet';
import { NetworkConfig } from 'config';
import './EquivalentValues.scss';
import { Wei } from 'libs/units';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { connect } from 'react-redux';
import { NetworkConfig } from 'types/network';
interface AllValue {
symbol: string;
@ -37,10 +37,11 @@ interface State {
interface StateProps {
balance: Balance;
network: NetworkConfig;
tokenBalances: TokenBalance[];
rates: AppState['rates']['rates'];
ratesError: AppState['rates']['ratesError'];
isOffline: AppState['config']['offline'];
isOffline: AppState['config']['meta']['offline'];
}
interface DispatchProps {
@ -68,7 +69,7 @@ class EquivalentValues extends React.Component<Props, State> {
public defaultOption(
balance: Balance,
tokenBalances: TokenBalance[],
network: NetworkConfig
network: StateProps['network']
): DefaultOption {
return {
label: 'All',
@ -257,7 +258,6 @@ class EquivalentValues extends React.Component<Props, State> {
this.requestedCurrencies = currencies;
}
}
function mapStateToProps(state: AppState): StateProps {
return {
balance: state.wallet.balance,
@ -265,7 +265,7 @@ function mapStateToProps(state: AppState): StateProps {
network: getNetworkConfig(state),
rates: state.rates.rates,
ratesError: state.rates.ratesError,
isOffline: state.config.offline
isOffline: getOffline(state)
};
}

View File

@ -1,10 +1,11 @@
import React from 'react';
import classnames from 'classnames';
import { Token, HELP_ARTICLE } from 'config';
import { HELP_ARTICLE } from 'config';
import { isPositiveIntegerOrZero, isValidETHAddress } from 'libs/validators';
import translate from 'translations';
import { HelpLink } from 'components/ui';
import './AddCustomTokenForm.scss';
import { Token } from 'types/network';
interface Props {
allTokens: Token[];

View File

@ -1,9 +1,9 @@
import React from 'react';
import translate from 'translations';
import { Token } from 'config';
import { TokenBalance } from 'selectors/wallet';
import AddCustomTokenForm from './AddCustomTokenForm';
import TokenRow from './TokenRow';
import { Token } from 'types/network';
interface Props {
allTokens: Token[];

View File

@ -13,12 +13,12 @@ import {
setWalletTokens,
TSetWalletTokens
} from 'actions/wallet';
import { getAllTokens } from 'selectors/config';
import { getAllTokens, getOffline } from 'selectors/config';
import { getTokenBalances, getWalletInst, getWalletConfig, TokenBalance } from 'selectors/wallet';
import { Token } from 'config';
import translate from 'translations';
import Balances from './Balances';
import Spinner from 'components/ui/Spinner';
import { Token } from 'types/network';
import './index.scss';
interface StateProps {
@ -29,7 +29,7 @@ interface StateProps {
tokensError: AppState['wallet']['tokensError'];
isTokensLoading: AppState['wallet']['isTokensLoading'];
hasSavedWalletTokens: AppState['wallet']['hasSavedWalletTokens'];
isOffline: AppState['config']['offline'];
isOffline: AppState['config']['meta']['offline'];
}
interface ActionProps {
addCustomToken: TAddCustomToken;
@ -118,7 +118,7 @@ function mapStateToProps(state: AppState): StateProps {
tokensError: state.wallet.tokensError,
isTokensLoading: state.wallet.isTokensLoading,
hasSavedWalletTokens: state.wallet.hasSavedWalletTokens,
isOffline: state.config.offline
isOffline: getOffline(state)
};
}

View File

@ -6,13 +6,14 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
import './Body.scss';
import { getNetworkConfig } from 'selectors/config';
import { NetworkConfig } from 'types/network';
interface State {
showDetails: boolean;
}
interface StateProps {
network: AppState['config']['network'];
network: NetworkConfig;
}
class BodyClass extends React.Component<StateProps, State> {

View File

@ -6,7 +6,7 @@ import { getAllUSDValuesFromSerializedTx, AllUSDValues } from 'selectors/rates';
import { SerializedTxParams, getParamsFromSerializedTx } from 'selectors/transaction';
import { connect } from 'react-redux';
import { getNetworkConfig } from 'selectors/config';
import { NetworkConfig } from 'config';
import { NetworkConfig } from 'types/network';
interface StateProps extends SerializedTxParams, AllUSDValues {
network: NetworkConfig;

View File

@ -4,9 +4,9 @@ import './Details.scss';
import { SerializedTransaction } from 'components/renderCbs';
import { AppState } from 'reducers';
import { getNodeConfig } from 'selectors/config';
import { NodeConfig } from 'config';
import { connect } from 'react-redux';
import { TokenValue } from 'libs/units';
import { NodeConfig } from 'types/node';
interface StateProps {
node: NodeConfig;

View File

@ -1,11 +1,11 @@
import { NodeConfig } from 'config';
import React, { Component } from 'react';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { getNodeConfig } from 'selectors/config';
import { StaticNodeConfig } from 'types/node';
interface StateProps {
node: NodeConfig;
node: StaticNodeConfig;
}
class NodeClass extends Component<StateProps, {}> {

View File

@ -4,7 +4,8 @@ import { AppState } from 'reducers';
import { getCurrentTo, ICurrentTo } from 'selectors/transaction';
import { getAllTokens } from 'selectors/config';
import { getWalletInst } from 'selectors/wallet';
import { getAddressMessage, Token } from 'config';
import { getAddressMessage } from 'config';
import { Token } from 'types/network';
interface ReduxProps {
currentTo: ICurrentTo;

View File

@ -1,6 +1,6 @@
import { BlockExplorerConfig } from 'config';
import React from 'react';
import { translateRaw } from 'translations';
import { BlockExplorerConfig } from 'types/network';
export interface TransactionSucceededProps {
txHash: string;

View File

@ -2,11 +2,18 @@ import React from 'react';
import classnames from 'classnames';
import Modal, { IButton } from 'components/ui/Modal';
import translate from 'translations';
import { NETWORKS, CustomNodeConfig, CustomNetworkConfig } from 'config';
import { makeCustomNodeId } from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';
import { CustomNetworkConfig } from 'types/network';
import { CustomNodeConfig } from 'types/node';
import { TAddCustomNetwork, addCustomNetwork, AddCustomNodeAction } from 'actions/config';
import { connect, Omit } from 'react-redux';
import { AppState } from 'reducers';
import {
getCustomNetworkConfigs,
getCustomNodeConfigs,
getStaticNetworkConfigs
} from 'selectors/config';
import { CustomNode } from 'libs/nodes';
const NETWORK_KEYS = Object.keys(NETWORKS);
const CUSTOM = 'custom';
interface Input {
@ -15,20 +22,27 @@ interface Input {
type?: string;
}
interface Props {
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
handleAddCustomNode(node: CustomNodeConfig): void;
handleAddCustomNetwork(node: CustomNetworkConfig): void;
interface OwnProps {
addCustomNode(payload: AddCustomNodeAction['payload']): void;
handleClose(): void;
}
interface DispatchProps {
addCustomNetwork: TAddCustomNetwork;
}
interface StateProps {
customNodes: AppState['config']['nodes']['customNodes'];
customNetworks: AppState['config']['networks']['customNetworks'];
staticNetworks: AppState['config']['networks']['staticNetworks'];
}
interface State {
name: string;
url: string;
port: string;
network: string;
customNetworkName: string;
customNetworkId: string;
customNetworkUnit: string;
customNetworkChainId: string;
hasAuth: boolean;
@ -36,13 +50,15 @@ interface State {
password: string;
}
export default class CustomNodeModal extends React.PureComponent<Props, State> {
type Props = OwnProps & StateProps & DispatchProps;
class CustomNodeModal extends React.Component<Props, State> {
public state: State = {
name: '',
url: '',
port: '',
network: NETWORK_KEYS[0],
customNetworkName: '',
network: Object.keys(this.props.staticNetworks)[0],
customNetworkId: '',
customNetworkUnit: '',
customNetworkChainId: '',
hasAuth: false,
@ -51,7 +67,7 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
};
public render() {
const { customNetworks, handleClose } = this.props;
const { customNetworks, handleClose, staticNetworks } = this.props;
const { network } = this.state;
const isHttps = window.location.protocol.includes('https');
const invalids = this.getInvalids();
@ -109,19 +125,16 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
value={network}
onChange={this.handleChange}
>
{NETWORK_KEYS.map(net => (
{Object.keys(staticNetworks).map(net => (
<option key={net} value={net}>
{net}
</option>
))}
{customNetworks.map(net => {
const id = makeCustomNetworkId(net);
return (
<option key={id} value={id}>
{net.name} (Custom)
</option>
);
})}
{Object.entries(customNetworks).map(([id, net]) => (
<option key={id} value={id}>
{net.name} (Custom)
</option>
))}
<option value={CUSTOM}>Custom...</option>
</select>
</div>
@ -133,7 +146,7 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
<label className="is-required">Network Name</label>
{this.renderInput(
{
name: 'customNetworkName',
name: 'customNetworkId',
placeholder: 'My Custom Network'
},
invalids
@ -248,7 +261,7 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
username,
password,
network,
customNetworkName,
customNetworkId,
customNetworkUnit,
customNetworkChainId
} = this.state;
@ -285,8 +298,8 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
// If they have a custom network, make sure info is provided
if (network === CUSTOM) {
if (!customNetworkName) {
invalids.customNetworkName = true;
if (!customNetworkId) {
invalids.customNetworkId = true;
}
if (!customNetworkUnit) {
invalids.customNetworkUnit = true;
@ -303,13 +316,14 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
}
private makeCustomNetworkConfigFromState(): CustomNetworkConfig {
const similarNetworkConfig = Object.values(NETWORKS).find(
const similarNetworkConfig = Object.values(this.props.staticNetworks).find(
n => n.chainId === +this.state.customNetworkChainId
);
const dPathFormats = similarNetworkConfig ? similarNetworkConfig.dPathFormats : null;
return {
name: this.state.customNetworkName,
isCustom: true,
name: this.state.customNetworkId,
unit: this.state.customNetworkUnit,
chainId: this.state.customNetworkChainId ? parseInt(this.state.customNetworkChainId, 10) : 0,
dPathFormats
@ -318,29 +332,42 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
private makeCustomNodeConfigFromState(): CustomNodeConfig {
const { network } = this.state;
const node: CustomNodeConfig = {
const networkId =
network === CUSTOM
? this.makeCustomNetworkId(this.makeCustomNetworkConfigFromState())
: network;
const port = parseInt(this.state.port, 10);
const url = this.state.url.trim();
const node: Omit<CustomNodeConfig, 'lib'> = {
isCustom: true,
service: 'your custom node',
id: `${url}:${port}`,
name: this.state.name.trim(),
url: this.state.url.trim(),
port: parseInt(this.state.port, 10),
network:
network === CUSTOM ? makeCustomNetworkId(this.makeCustomNetworkConfigFromState()) : network
url,
port,
network: networkId,
...(this.state.hasAuth
? {
auth: {
username: this.state.username,
password: this.state.password
}
}
: {})
};
if (this.state.hasAuth) {
node.auth = {
username: this.state.username,
password: this.state.password
};
}
const lib = new CustomNode(node);
return node;
return { ...node, lib };
}
private getConflictedNode(): CustomNodeConfig | undefined {
const { customNodes } = this.props;
const config = this.makeCustomNodeConfigFromState();
const thisId = makeCustomNodeId(config);
return customNodes.find(conf => makeCustomNodeId(conf) === thisId);
return customNodes[config.id];
}
private handleChange = (ev: React.FormEvent<HTMLInputElement | HTMLSelectElement>) => {
@ -359,9 +386,25 @@ export default class CustomNodeModal extends React.PureComponent<Props, State> {
if (this.state.network === CUSTOM) {
const network = this.makeCustomNetworkConfigFromState();
this.props.handleAddCustomNetwork(network);
this.props.addCustomNetwork({ config: network, id: node.network });
}
this.props.handleAddCustomNode(node);
this.props.addCustomNode({ config: node, id: node.id });
};
private makeCustomNetworkId(config: CustomNetworkConfig): string {
return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`;
}
}
const mapStateToProps = (state: AppState): StateProps => ({
customNetworks: getCustomNetworkConfigs(state),
customNodes: getCustomNodeConfigs(state),
staticNetworks: getStaticNetworkConfigs(state)
});
const mapDispatchToProps: DispatchProps = {
addCustomNetwork
};
export default connect(mapStateToProps, mapDispatchToProps)(CustomNodeModal);

View File

@ -46,7 +46,7 @@ const tabs: TabLink[] = [
];
interface Props {
color?: string;
color?: string | false;
}
interface State {

View File

@ -3,39 +3,43 @@ import {
TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode,
TAddCustomNetwork
TAddCustomNetwork,
AddCustomNodeAction,
changeLanguage,
changeNodeIntent,
addCustomNode,
removeCustomNode,
addCustomNetwork
} from 'actions/config';
import logo from 'assets/images/logo-mycrypto.svg';
import { Dropdown, ColorDropdown } from 'components/ui';
import React, { PureComponent } from 'react';
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { TSetGasPriceField } from 'actions/transaction';
import {
ANNOUNCEMENT_MESSAGE,
ANNOUNCEMENT_TYPE,
languages,
NODES,
NodeConfig,
CustomNodeConfig,
CustomNetworkConfig
} from 'config';
import { TSetGasPriceField, setGasPriceField } from 'actions/transaction';
import { ANNOUNCEMENT_MESSAGE, ANNOUNCEMENT_TYPE, languages } from 'config';
import Navigation from './components/Navigation';
import CustomNodeModal from './components/CustomNodeModal';
import OnlineStatus from './components/OnlineStatus';
import { getKeyByValue } from 'utils/helpers';
import { makeCustomNodeId } from 'utils/node';
import { getNetworkConfigFromId } from 'utils/network';
import { NodeConfig } from 'types/node';
import './index.scss';
import { AppState } from 'reducers';
import {
getOffline,
isNodeChanging,
getLanguageSelection,
getNodeId,
getNodeConfig,
CustomNodeOption,
NodeOption,
getNodeOptions,
getNetworkConfig
} from 'selectors/config';
import { NetworkConfig } from 'types/network';
import { connect } from 'react-redux';
interface Props {
languageSelection: string;
node: NodeConfig;
nodeSelection: string;
isChangingNode: boolean;
isOffline: boolean;
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
interface DispatchProps {
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
setGasPriceField: TSetGasPriceField;
@ -44,11 +48,42 @@ interface Props {
addCustomNetwork: TAddCustomNetwork;
}
interface StateProps {
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 = (state: AppState): StateProps => ({
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,
addCustomNode,
removeCustomNode,
addCustomNetwork
};
interface State {
isAddingCustomNode: boolean;
}
export default class Header extends PureComponent<Props, State> {
type Props = StateProps & DispatchProps;
class Header extends Component<Props, State> {
public state = {
isAddingCustomNode: false
};
@ -56,50 +91,40 @@ export default class Header extends PureComponent<Props, State> {
public render() {
const {
languageSelection,
changeNodeIntent,
node,
nodeSelection,
isChangingNode,
isOffline,
customNodes,
customNetworks
nodeOptions,
network
} = this.props;
const { isAddingCustomNode } = this.state;
const selectedLanguage = languageSelection;
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);
const options = nodeOptions.map(n => {
if (n.isCustom) {
const { name: { networkId, nodeId }, isCustom, id, ...rest } = n;
return {
value: key,
...rest,
name: (
<span>
{network && network.name} <small>({n.service})</small>
{networkId} - {nodeId} <small>(custom)</small>
</span>
),
color: network && network.color,
hidden: n.hidden
onRemove: () => this.props.removeCustomNode({ id })
};
})
.concat(
customNodes.map(cn => {
const network = getNetworkConfigFromId(cn.network, customNetworks);
return {
value: makeCustomNodeId(cn),
name: (
<span>
{network && network.name} - {cn.name} <small>(custom)</small>
</span>
),
color: network && network.color,
hidden: false,
onRemove: () => this.props.removeCustomNode(cn)
};
})
);
} else {
const { name: { networkId, service }, isCustom, ...rest } = n;
return {
...rest,
name: (
<span>
{networkId} <small>({service})</small>
</span>
)
};
}
});
return (
<div className="Header">
@ -154,15 +179,15 @@ export default class Header extends PureComponent<Props, State> {
change node. current node is on the ${node.network} network
provided by ${node.service}
`}
options={nodeOptions}
value={nodeSelection}
options={options}
value={nodeSelection || ''}
extra={
<li>
<a onClick={this.openCustomNodeModal}>Add Custom Node</a>
</li>
}
disabled={nodeSelection === 'web3'}
onChange={changeNodeIntent}
onChange={this.props.changeNodeIntent}
size="smr"
color="white"
menuAlign="right"
@ -172,14 +197,11 @@ export default class Header extends PureComponent<Props, State> {
</section>
</section>
<Navigation color={selectedNetwork && selectedNetwork.color} />
<Navigation color={!network.isCustom && network.color} />
{isAddingCustomNode && (
<CustomNodeModal
customNodes={customNodes}
customNetworks={customNetworks}
handleAddCustomNode={this.addCustomNode}
handleAddCustomNetwork={this.props.addCustomNetwork}
addCustomNode={this.addCustomNode}
handleClose={this.closeCustomNodeModal}
/>
)}
@ -202,8 +224,10 @@ export default class Header extends PureComponent<Props, State> {
this.setState({ isAddingCustomNode: false });
};
private addCustomNode = (node: CustomNodeConfig) => {
private addCustomNode = (payload: AddCustomNodeAction['payload']) => {
this.setState({ isAddingCustomNode: false });
this.props.addCustomNode(node);
this.props.addCustomNode(payload);
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);

View File

@ -6,7 +6,7 @@ import { getOffline } from 'selectors/config';
import { NewTabLink } from 'components/ui';
interface StateProps {
offline: AppState['config']['offline'];
offline: AppState['config']['meta']['offline'];
}
class OfflineBroadcastClass extends Component<StateProps> {
public render() {

View File

@ -20,13 +20,14 @@ import SimpleGas from './components/SimpleGas';
import AdvancedGas, { AdvancedOptions } from './components/AdvancedGas';
import './TXMetaDataPanel.scss';
import { getGasPrice } from 'selectors/transaction';
import { NetworkConfig } from 'types/network';
type SliderStates = 'simple' | 'advanced';
interface StateProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
offline: AppState['config']['offline'];
network: AppState['config']['network'];
offline: AppState['config']['meta']['offline'];
network: NetworkConfig;
}
interface DispatchProps {

View File

@ -27,7 +27,7 @@ interface OwnProps {
}
interface StateProps {
autoGasLimitEnabled: AppState['config']['autoGasLimit'];
autoGasLimitEnabled: AppState['config']['meta']['autoGasLimit'];
validGasPrice: boolean;
}

View File

@ -2,8 +2,9 @@ import React from 'react';
import BN from 'bn.js';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { UnitDisplay } from 'components/ui';
import { NetworkConfig } from 'types/network';
import './FeeSummary.scss';
interface RenderData {
@ -17,8 +18,8 @@ interface RenderData {
interface ReduxStateProps {
gasLimit: AppState['transaction']['fields']['gasLimit'];
rates: AppState['rates']['rates'];
network: AppState['config']['network'];
isOffline: AppState['config']['offline'];
network: NetworkConfig;
isOffline: AppState['config']['meta']['offline'];
}
interface OwnProps {
@ -75,7 +76,7 @@ function mapStateToProps(state: AppState): ReduxStateProps {
gasLimit: state.transaction.fields.gasLimit,
rates: state.rates.rates,
network: getNetworkConfig(state),
isOffline: state.config.offline
isOffline: getOffline(state)
};
}

View File

@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getUnit } from 'selectors/transaction';
import { getNetworkConfig } from 'selectors/config';
import { NetworkConfig } from 'types/network';
interface DispatchProps {
setUnitMeta: TSetUnitMeta;
@ -18,7 +19,7 @@ interface StateProps {
tokens: TokenBalance[];
allTokens: MergedToken[];
showAllTokens?: boolean;
network: AppState['config']['network'];
network: NetworkConfig;
}
const StringDropdown = Dropdown as new () => Dropdown<string>;

View File

@ -40,11 +40,10 @@ import {
InsecureWalletName,
MiscWalletName,
WalletName,
isWeb3NodeAvailable,
knowledgeBaseURL,
donationAddressMap
} from 'config';
import { isWeb3NodeAvailable } from 'libs/nodes/web3';
import LedgerIcon from 'assets/images/wallets/ledger.svg';
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
import MistIcon from 'assets/images/wallets/mist.svg';

View File

@ -8,7 +8,6 @@ import {
} from 'actions/deterministicWallets';
import Modal, { IButton } from 'components/ui/Modal';
import { AppState } from 'reducers';
import { NetworkConfig } from 'config';
import { isValidPath } from 'libs/validators';
import React from 'react';
import { connect } from 'react-redux';
@ -16,7 +15,7 @@ import { getNetworkConfig } from 'selectors/config';
import { getTokens, MergedToken } from 'selectors/wallet';
import { UnitDisplay } from 'components/ui';
import './DeterministicWalletsModal.scss';
import { DPath } from 'config/dpaths';
import { StaticNetworkConfig } from 'types/network';
import Select from 'react-select';
const WALLETS_PER_PAGE = 5;
@ -34,7 +33,7 @@ interface Props {
// Redux state
wallets: AppState['deterministicWallets']['wallets'];
desiredToken: AppState['deterministicWallets']['desiredToken'];
network: NetworkConfig;
network: StaticNetworkConfig;
tokens: MergedToken[];
// Redux actions

View File

@ -7,10 +7,8 @@ import ledger from 'ledgerco';
import { Spinner, NewTabLink } from 'components/ui';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { SecureWalletName, ledgerReferralURL } from 'config';
import { DPath } from 'config/dpaths';
import { getPaths, getSingleDPath } from 'utils/network';
import { getPaths, getSingleDPath } from 'selectors/config/wallet';
interface OwnProps {
onUnlock(param: any): void;
@ -18,6 +16,7 @@ interface OwnProps {
interface StateProps {
dPath: DPath;
dPaths: DPath[];
}
interface State {
@ -103,7 +102,7 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
publicKey={publicKey}
chainCode={chainCode}
dPath={dPath}
dPaths={getPaths(SecureWalletName.LEDGER_NANO_S)}
dPaths={this.props.dPaths}
onCancel={this.handleCancel}
onConfirmAddress={this.handleUnlock}
onPathChange={this.handlePathChange}
@ -169,9 +168,9 @@ class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
}
function mapStateToProps(state: AppState): StateProps {
const network = getNetworkConfig(state);
return {
dPath: getSingleDPath(SecureWalletName.LEDGER_NANO_S, network)
dPath: getSingleDPath(state, SecureWalletName.LEDGER_NANO_S),
dPaths: getPaths(state, SecureWalletName.LEDGER_NANO_S)
};
}

View File

@ -5,10 +5,8 @@ import DeterministicWalletsModal from './DeterministicWalletsModal';
import { formatMnemonic } from 'utils/formatters';
import { InsecureWalletName } from 'config';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { connect } from 'react-redux';
import { DPath } from 'config/dpaths';
import { getPaths, getSingleDPath } from 'utils/network';
import { getSingleDPath, getPaths } from 'selectors/config/wallet';
import { TogglablePassword } from 'components';
interface Props {
@ -17,6 +15,7 @@ interface Props {
interface StateProps {
dPath: DPath;
dPaths: DPath[];
}
interface State {
@ -80,7 +79,7 @@ class MnemonicDecryptClass extends PureComponent<Props & StateProps, State> {
isOpen={!!seed}
seed={seed}
dPath={dPath}
dPaths={getPaths(InsecureWalletName.MNEMONIC_PHRASE)}
dPaths={this.props.dPaths}
onCancel={this.handleCancel}
onConfirmAddress={this.handleUnlock}
onPathChange={this.handlePathChange}
@ -147,9 +146,9 @@ class MnemonicDecryptClass extends PureComponent<Props & StateProps, State> {
}
function mapStateToProps(state: AppState): StateProps {
const network = getNetworkConfig(state);
return {
dPath: getSingleDPath(InsecureWalletName.MNEMONIC_PHRASE, network)
dPath: getSingleDPath(state, InsecureWalletName.MNEMONIC_PHRASE),
dPaths: getPaths(state, InsecureWalletName.MNEMONIC_PHRASE)
};
}

View File

@ -5,12 +5,10 @@ import TrezorConnect from 'vendor/trezor-connect';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import './Trezor.scss';
import { Spinner, NewTabLink } from 'components/ui';
import { getNetworkConfig } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { SecureWalletName, trezorReferralURL } from 'config';
import { DPath } from 'config/dpaths';
import { getPaths, getSingleDPath } from 'utils/network';
import { getSingleDPath, getPaths } from 'selectors/config/wallet';
//todo: conflicts with comment in walletDecrypt -> onUnlock method
interface OwnProps {
@ -19,6 +17,7 @@ interface OwnProps {
interface StateProps {
dPath: DPath;
dPaths: DPath[];
}
// todo: nearly duplicates ledger component props
@ -80,7 +79,7 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
publicKey={publicKey}
chainCode={chainCode}
dPath={dPath}
dPaths={getPaths(SecureWalletName.TREZOR)}
dPaths={this.props.dPaths}
onCancel={this.handleCancel}
onConfirmAddress={this.handleUnlock}
onPathChange={this.handlePathChange}
@ -143,9 +142,9 @@ class TrezorDecryptClass extends PureComponent<Props, State> {
}
function mapStateToProps(state: AppState): StateProps {
const network = getNetworkConfig(state);
return {
dPath: getSingleDPath(SecureWalletName.TREZOR, network)
dPath: getSingleDPath(state, SecureWalletName.TREZOR),
dPaths: getPaths(state, SecureWalletName.TREZOR)
};
}

View File

@ -8,7 +8,7 @@ interface Option<T> {
name: any;
value: T;
color?: string;
hidden: boolean | undefined;
hidden?: boolean | undefined;
onRemove?(): void;
}

View File

@ -85,7 +85,7 @@ export default UnitDisplay;
* Circumvents typescript issue with union props on connected components.
*/
interface OfflineProps {
offline: AppState['config']['offline'];
offline: AppState['config']['meta']['offline'];
children: React.ReactElement<string>;
}

View File

@ -1,8 +1,3 @@
export interface DPath {
label: string;
value: string; // TODO determine method for more precise typing for path
}
export const ETH_DEFAULT: DPath = {
label: 'Default (ETH)',
value: "m/44'/60'/0'/0"

View File

@ -1,4 +1,3 @@
export * from './networks';
export * from './data';
export * from './bity';
export * from './addressMessages';

View File

@ -1,373 +0,0 @@
import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from './data';
import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
import {
ETH_DEFAULT,
ETH_TREZOR,
ETH_LEDGER,
ETC_LEDGER,
ETC_TREZOR,
ETH_TESTNET,
EXP_DEFAULT,
UBQ_DEFAULT,
DPath
} from 'config/dpaths';
export interface BlockExplorerConfig {
origin: string;
txUrl(txHash: string): string;
addressUrl(address: string): string;
}
export interface Token {
address: string;
symbol: string;
decimal: number;
error?: string | null;
}
export interface NetworkContract {
name: NetworkKeys;
address?: string;
abi: string;
}
export interface DPathFormats {
trezor: DPath;
ledgerNanoS: DPath;
mnemonicPhrase: DPath;
}
export interface NetworkConfig {
// TODO really try not to allow strings due to custom networks
name: NetworkKeys;
unit: string;
color?: string;
blockExplorer?: BlockExplorerConfig;
tokenExplorer?: {
name: string;
address(address: string): string;
};
chainId: number;
tokens: Token[];
contracts: NetworkContract[] | null;
dPathFormats: DPathFormats;
isTestnet?: boolean;
}
export interface CustomNetworkConfig {
name: string;
unit: string;
chainId: number;
dPathFormats: DPathFormats | null;
}
export interface NodeConfig {
network: NetworkKeys;
lib: RPCNode | Web3Node;
service: string;
estimateGas?: boolean;
hidden?: boolean;
}
export interface CustomNodeConfig {
name: string;
url: string;
port: number;
network: string;
auth?: {
username: string;
password: string;
};
}
// Must be a website that follows the ethplorer convention of /tx/[hash] and
// address/[address] to generate the correct functions.
function makeExplorer(origin: string): BlockExplorerConfig {
return {
origin,
txUrl: hash => `${origin}/tx/${hash}`,
addressUrl: address => `${origin}/address/${address}`
};
}
const ETH: NetworkConfig = {
name: 'ETH',
unit: 'ETH',
chainId: 1,
color: '#0e97c0',
blockExplorer: makeExplorer('https://etherscan.io'),
tokenExplorer: {
name: ethPlorer,
address: ETHTokenExplorer
},
tokens: require('./tokens/eth.json'),
contracts: require('./contracts/eth.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TREZOR,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
}
};
const Ropsten: NetworkConfig = {
name: 'Ropsten',
unit: 'ETH',
chainId: 3,
color: '#adc101',
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
};
const Kovan: NetworkConfig = {
name: 'Kovan',
unit: 'ETH',
chainId: 42,
color: '#adc101',
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
};
const Rinkeby: NetworkConfig = {
name: 'Rinkeby',
unit: 'ETH',
chainId: 4,
color: '#adc101',
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
tokens: require('./tokens/rinkeby.json'),
contracts: require('./contracts/rinkeby.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
};
const ETC: NetworkConfig = {
name: 'ETC',
unit: 'ETC',
chainId: 61,
color: '#669073',
blockExplorer: makeExplorer('https://gastracker.io'),
tokens: require('./tokens/etc.json'),
contracts: require('./contracts/etc.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETC_TREZOR,
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
}
};
const UBQ: NetworkConfig = {
name: 'UBQ',
unit: 'UBQ',
chainId: 8,
color: '#b37aff',
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
tokens: require('./tokens/ubq.json'),
contracts: require('./contracts/ubq.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
}
};
const EXP: NetworkConfig = {
name: 'EXP',
unit: 'EXP',
chainId: 2,
color: '#673ab7',
// tslint:disable:no-http-string - Unavailable behind HTTPS right now
blockExplorer: makeExplorer('http://www.gander.tech'),
// tslint:enable:no-http-string
tokens: require('./tokens/exp.json'),
contracts: require('./contracts/exp.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: EXP_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
}
};
export const NETWORKS = {
ETH,
Ropsten,
Kovan,
Rinkeby,
ETC,
UBQ,
EXP
};
export type NetworkKeys = keyof typeof NETWORKS;
enum NodeName {
ETH_MEW = 'eth_mew',
ETH_MYCRYPTO = 'eth_mycrypto',
ETH_ETHSCAN = 'eth_ethscan',
ETH_INFURA = 'eth_infura',
ROP_MEW = 'rop_mew',
ROP_INFURA = 'rop_infura',
KOV_ETHSCAN = 'kov_ethscan',
RIN_ETHSCAN = 'rin_ethscan',
RIN_INFURA = 'rin_infura',
ETC_EPOOL = 'etc_epool',
UBQ = 'ubq',
EXP_TECH = 'exp_tech'
}
type NonWeb3NodeConfigs = { [key in NodeName]: NodeConfig };
interface Web3NodeConfig {
web3?: NodeConfig;
}
type NodeConfigs = NonWeb3NodeConfigs & Web3NodeConfig;
export const NODES: NodeConfigs = {
eth_mew: {
network: 'ETH',
lib: new RPCNode('https://api.myetherapi.com/eth'),
service: 'MyEtherWallet',
estimateGas: true
},
eth_mycrypto: {
network: 'ETH',
lib: new RPCNode('https://api.mycryptoapi.com/eth'),
service: 'MyCrypto',
estimateGas: true
},
eth_ethscan: {
network: 'ETH',
service: 'Etherscan.io',
lib: new EtherscanNode('https://api.etherscan.io/api'),
estimateGas: false
},
eth_infura: {
network: 'ETH',
service: 'infura.io',
lib: new InfuraNode('https://mainnet.infura.io/mew'),
estimateGas: false
},
rop_mew: {
network: 'Ropsten',
service: 'MyEtherWallet',
lib: new RPCNode('https://api.myetherapi.com/rop'),
estimateGas: false
},
rop_infura: {
network: 'Ropsten',
service: 'infura.io',
lib: new InfuraNode('https://ropsten.infura.io/mew'),
estimateGas: false
},
kov_ethscan: {
network: 'Kovan',
service: 'Etherscan.io',
lib: new EtherscanNode('https://kovan.etherscan.io/api'),
estimateGas: false
},
rin_ethscan: {
network: 'Rinkeby',
service: 'Etherscan.io',
lib: new EtherscanNode('https://rinkeby.etherscan.io/api'),
estimateGas: false
},
rin_infura: {
network: 'Rinkeby',
service: 'infura.io',
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
estimateGas: false
},
etc_epool: {
network: 'ETC',
service: 'Epool.io',
lib: new RPCNode('https://mewapi.epool.io'),
estimateGas: false
},
ubq: {
network: 'UBQ',
service: 'ubiqscan.io',
lib: new RPCNode('https://pyrus2.ubiqscan.io'),
estimateGas: true
},
exp_tech: {
network: 'EXP',
service: 'Expanse.tech',
lib: new RPCNode('https://node.expanse.tech/'),
estimateGas: true
}
};
interface Web3NodeInfo {
networkId: string;
lib: Web3Node;
}
export async function setupWeb3Node(): Promise<Web3NodeInfo> {
const { web3 } = window as any;
if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) {
throw new Error(
'Web3 not found. Please check that MetaMask is installed, or that MyCrypto is open in Mist.'
);
}
const lib = new Web3Node();
const networkId = await lib.getNetVersion();
const accounts = await lib.getAccounts();
if (!accounts.length) {
throw new Error('No accounts found in MetaMask / Mist.');
}
if (networkId === 'loading') {
throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.');
}
return { networkId, lib };
}
export async function isWeb3NodeAvailable(): Promise<boolean> {
try {
await setupWeb3Node();
return true;
} catch (e) {
return false;
}
}
export const Web3Service = 'MetaMask / Mist';
export interface NodeConfigOverride extends NodeConfig {
network: any;
}
export async function initWeb3Node(): Promise<void> {
const { networkId, lib } = await setupWeb3Node();
const web3: NodeConfigOverride = {
network: networkIdToName(networkId),
service: Web3Service,
lib,
estimateGas: false,
hidden: true
};
NODES.web3 = web3;
}

View File

@ -1,91 +1,31 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
changeLanguage as dChangeLanguage,
changeNodeIntent as dChangeNodeIntent,
addCustomNode as dAddCustomNode,
removeCustomNode as dRemoveCustomNode,
addCustomNetwork as dAddCustomNetwork,
TChangeLanguage,
TChangeNodeIntent,
TAddCustomNode,
TRemoveCustomNode,
TAddCustomNetwork
} from 'actions/config';
import { TSetGasPriceField, setGasPriceField as dSetGasPriceField } from 'actions/transaction';
import { AlphaAgreement, Footer, Header } from 'components';
import { AppState } from 'reducers';
import Notifications from './Notifications';
import OfflineTab from './OfflineTab';
import { getOffline, getLatestBlock } from 'selectors/config';
interface ReduxProps {
languageSelection: AppState['config']['languageSelection'];
node: AppState['config']['node'];
nodeSelection: AppState['config']['nodeSelection'];
isChangingNode: AppState['config']['isChangingNode'];
isOffline: AppState['config']['offline'];
customNodes: AppState['config']['customNodes'];
customNetworks: AppState['config']['customNetworks'];
latestBlock: AppState['config']['latestBlock'];
interface StateProps {
isOffline: AppState['config']['meta']['offline'];
latestBlock: AppState['config']['meta']['latestBlock'];
}
interface ActionProps {
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
addCustomNetwork: TAddCustomNetwork;
setGasPriceField: TSetGasPriceField;
}
type Props = {
interface OwnProps {
isUnavailableOffline?: boolean;
children: string | React.ReactElement<string> | React.ReactElement<string>[];
} & ReduxProps &
ActionProps;
}
type Props = OwnProps & StateProps;
class TabSection extends Component<Props, {}> {
public render() {
const {
isUnavailableOffline,
children,
// APP
node,
nodeSelection,
isChangingNode,
isOffline,
languageSelection,
customNodes,
customNetworks,
latestBlock,
setGasPriceField,
changeLanguage,
changeNodeIntent,
addCustomNode,
removeCustomNode,
addCustomNetwork
} = this.props;
const headerProps = {
languageSelection,
node,
nodeSelection,
isChangingNode,
isOffline,
customNodes,
customNetworks,
changeLanguage,
changeNodeIntent,
setGasPriceField,
addCustomNode,
removeCustomNode,
addCustomNetwork
};
const { isUnavailableOffline, children, isOffline, latestBlock } = this.props;
return (
<div className="page-layout">
<main>
<Header {...headerProps} />
<Header />
<div className="Tab container">
{isUnavailableOffline && isOffline ? <OfflineTab /> : children}
</div>
@ -98,24 +38,11 @@ class TabSection extends Component<Props, {}> {
}
}
function mapStateToProps(state: AppState): ReduxProps {
function mapStateToProps(state: AppState): StateProps {
return {
node: state.config.node,
nodeSelection: state.config.nodeSelection,
isChangingNode: state.config.isChangingNode,
isOffline: state.config.offline,
languageSelection: state.config.languageSelection,
customNodes: state.config.customNodes,
customNetworks: state.config.customNetworks,
latestBlock: state.config.latestBlock
isOffline: getOffline(state),
latestBlock: getLatestBlock(state)
};
}
export default connect(mapStateToProps, {
setGasPriceField: dSetGasPriceField,
changeLanguage: dChangeLanguage,
changeNodeIntent: dChangeNodeIntent,
addCustomNode: dAddCustomNode,
removeCustomNode: dRemoveCustomNode,
addCustomNetwork: dAddCustomNetwork
})(TabSection);
export default connect(mapStateToProps, {})(TabSection);

View File

@ -4,7 +4,6 @@ import './InteractExplorer.scss';
import { TShowNotification, showNotification } from 'actions/notifications';
import { getNodeLib } from 'selectors/config';
import { getTo, getDataExists } from 'selectors/transaction';
import { INode } from 'libs/nodes/INode';
import { GenerateTransaction } from 'components/GenerateTransaction';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
@ -12,9 +11,11 @@ import { Fields } from './components';
import { setDataField, TSetDataField } from 'actions/transaction';
import { Data } from 'libs/units';
import Select from 'react-select';
import { Web3Node } from 'libs/nodes';
import RpcNode from 'libs/nodes/rpc';
interface StateProps {
nodeLib: INode;
nodeLib: RpcNode | Web3Node;
to: AppState['transaction']['fields']['to'];
dataExists: boolean;
}

View File

@ -1,12 +1,13 @@
import React, { Component } from 'react';
import translate from 'translations';
import { NetworkContract, donationAddressMap } from 'config';
import { getNetworkContracts } from 'selectors/config';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import classnames from 'classnames';
import Select from 'react-select';
import { NetworkContract } from 'types/network';
import { donationAddressMap } from 'config';
interface ContractOption {
name: string;

View File

@ -12,7 +12,6 @@ import {
ICurrentValue
} from 'selectors/transaction/current';
import BN from 'bn.js';
import { NetworkConfig } from 'config';
import { validNumber, validDecimal } from 'libs/validators';
import { getGasLimit } from 'selectors/transaction';
import { AddressField, AmountField, TXMetaDataPanel } from 'components';
@ -21,6 +20,7 @@ import { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values';
import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config';
import './RequestPayment.scss';
import { reset, TReset, setCurrentTo, TSetCurrentTo } from 'actions/transaction';
import { NetworkConfig } from 'types/network';
interface OwnProps {
wallet: AppState['wallet']['inst'];
@ -31,7 +31,7 @@ interface StateProps {
currentTo: ICurrentTo;
currentValue: ICurrentValue;
gasLimit: SetGasLimitFieldAction['payload'];
networkConfig: NetworkConfig | undefined;
networkConfig: NetworkConfig;
decimal: number;
tokenContractAddress: string;
}

View File

@ -15,9 +15,8 @@ import {
UnavailableWallets
} from 'containers/Tabs/SendTransaction/components';
import SubTabs, { Tab } from 'components/SubTabs';
import { getNetworkConfig } from 'selectors/config';
import { isNetworkUnit } from 'utils/network';
import { RouteNotFound } from 'components/RouteNotFound';
import { isNetworkUnit } from 'selectors/config/wallet';
const Send = () => (
<React.Fragment>
@ -28,7 +27,7 @@ const Send = () => (
interface StateProps {
wallet: AppState['wallet']['inst'];
network: AppState['config']['network'];
requestDisabled: boolean;
}
type Props = StateProps & RouteComponentProps<{}>;
@ -46,7 +45,7 @@ class SendTransaction extends React.Component<Props> {
{
path: 'request',
name: translate('Request Payment'),
disabled: !isNetworkUnit(this.props.network, 'ETH')
disabled: this.props.requestDisabled
},
{
path: 'info',
@ -100,5 +99,5 @@ class SendTransaction extends React.Component<Props> {
export default connect((state: AppState) => ({
wallet: getWalletInst(state),
network: getNetworkConfig(state)
requestDisabled: !isNetworkUnit(state, 'ETH')
}))(SendTransaction);

View File

@ -8,7 +8,7 @@ import { configureLiteSend, TConfigureLiteSend } from 'actions/swap';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { shouldDisplayLiteSend } from 'selectors/swap';
import { NetworkConfig } from 'config';
import { NetworkConfig } from 'types/network';
interface DispatchProps {
configureLiteSend: TConfigureLiteSend;

View File

@ -56,6 +56,7 @@ import TabSection from 'containers/TabSection';
import { merge } from 'lodash';
import { RouteNotFound } from 'components/RouteNotFound';
import { Switch, Route, RouteComponentProps } from 'react-router';
import { getOffline } from 'selectors/config';
interface ReduxStateProps {
step: number;
@ -284,7 +285,7 @@ function mapStateToProps(state: AppState) {
bityOrderStatus: state.swap.bityOrderStatus,
shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus,
paymentAddress: state.swap.paymentAddress,
isOffline: state.config.offline
isOffline: getOffline(state)
};
}

View File

@ -1,6 +1,6 @@
import { Token } from 'config';
import { Wei, TokenValue } from 'libs/units';
import { IHexStrTransaction } from 'libs/transaction';
import { Token } from 'types/network';
export interface TxObj {
to: string;

View File

@ -1,11 +1,11 @@
import RPCNode from '../rpc';
import RPCClient from '../rpc/client';
import { CustomNodeConfig } from 'config';
import { CustomNodeConfig } from 'types/node';
import { Omit } from 'react-router';
export default class CustomNode extends RPCNode {
constructor(config: CustomNodeConfig) {
const endpoint = `${config.url}:${config.port}`;
super(endpoint);
constructor(config: Omit<CustomNodeConfig, 'lib'>) {
super(config.id);
const headers: { [key: string]: string } = {};
if (config.auth) {
@ -13,6 +13,6 @@ export default class CustomNode extends RPCNode {
headers.Authorization = `Basic ${btoa(`${username}:${password}`)}`;
}
this.client = new RPCClient(endpoint, headers);
this.client = new RPCClient(config.id, headers);
}
}

View File

@ -1,4 +1,3 @@
import { Token } from 'config';
import ERC20 from 'libs/erc20';
import RPCRequests from '../rpc/requests';
import {
@ -10,6 +9,7 @@ import {
SendRawTxRequest,
GetCurrentBlockRequest
} from './types';
import { Token } from 'types/network';
export default class EtherscanRequests extends RPCRequests {
public sendRawTx(signedTx: string): SendRawTxRequest {

View File

@ -1,5 +1,4 @@
import BN from 'bn.js';
import { Token } from 'config';
import { IHexStrTransaction } from 'libs/transaction';
import { Wei, TokenValue } from 'libs/units';
import { stripHexPrefix } from 'libs/values';
@ -15,6 +14,7 @@ import {
isValidCurrentBlock,
isValidRawTxApi
} from '../../validators';
import { Token } from 'types/network';
export default class RpcNode implements INode {
public client: RPCClient;

View File

@ -1,4 +1,3 @@
import { Token } from 'config';
import ERC20 from 'libs/erc20';
import {
CallRequest,
@ -11,6 +10,8 @@ import {
} from './types';
import { hexEncodeData } from './utils';
import { TxObj } from '../INode';
import { Token } from 'types/network';
export default class RPCRequests {
public getNetVersion() {
return { method: 'net_version' };

View File

@ -53,3 +53,38 @@ export default class Web3Node extends RPCNode {
export function isWeb3Node(nodeLib: INode | Web3Node): nodeLib is Web3Node {
return nodeLib instanceof Web3Node;
}
export const Web3Service = 'MetaMask / Mist';
export async function setupWeb3Node() {
const { web3 } = window as any;
if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) {
throw new Error(
'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.'
);
}
const lib = new Web3Node();
const networkId = await lib.getNetVersion();
const accounts = await lib.getAccounts();
if (!accounts.length) {
throw new Error('No accounts found in MetaMask / Mist.');
}
if (networkId === 'loading') {
throw new Error('MetaMask / Mist is still loading. Please refresh the page and try again.');
}
return { networkId, lib };
}
export async function isWeb3NodeAvailable(): Promise<boolean> {
try {
await setupWeb3Node();
return true;
} catch (e) {
return false;
}
}

View File

@ -1,7 +1,6 @@
import { Wei, toTokenBase } from 'libs/units';
import { addHexPrefix } from 'ethereumjs-util';
import BN from 'bn.js';
import { NetworkKeys } from 'config';
export function stripHexPrefix(value: string) {
return value.replace('0x', '');
@ -24,21 +23,6 @@ export function sanitizeHex(hex: string) {
return hex !== '' ? `0x${padLeftEven(hexStr)}` : '';
}
export function networkIdToName(networkId: string | number): NetworkKeys {
switch (networkId.toString()) {
case '1':
return 'ETH';
case '3':
return 'Ropsten';
case '4':
return 'Rinkeby';
case '42':
return 'Kovan';
default:
throw new Error(`Network ${networkId} is unsupported.`);
}
}
export const buildEIP681EtherRequest = (
recipientAddr: string,
chainId: number,

View File

@ -1,9 +1,8 @@
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { IFullWallet } from '../IWallet';
import { networkIdToName } from 'libs/values';
import { bufferToHex } from 'ethereumjs-util';
import { configuredStore } from 'store';
import { getNodeLib } from 'selectors/config';
import { getNodeLib, getNetworkNameByChainId } from 'selectors/config';
import Web3Node, { isWeb3Node } from 'libs/nodes/web3';
import { INode } from 'libs/nodes/INode';
@ -29,6 +28,9 @@ export default class Web3Wallet implements IFullWallet {
const state = configuredStore.getState();
const nodeLib: Web3Node | INode = getNodeLib(state);
if (!nodeLib) {
throw new Error('');
}
if (!isWeb3Node(nodeLib)) {
throw new Error('Web3 wallets can only be used with a Web3 node.');
}
@ -55,7 +57,7 @@ export default class Web3Wallet implements IFullWallet {
};
const state = configuredStore.getState();
const nodeLib: Web3Node | INode = getNodeLib(state);
const nodeLib: Web3Node | INode | undefined = getNodeLib(state);
if (!isWeb3Node(nodeLib)) {
throw new Error('Web3 wallets can only be used with a Web3 node.');
@ -67,7 +69,7 @@ export default class Web3Wallet implements IFullWallet {
private async networkCheck(lib: Web3Node) {
const netId = await lib.getNetVersion();
const netName = networkIdToName(netId);
const netName = getNetworkNameByChainId(configuredStore.getState(), netId);
if (this.network !== netName) {
throw new Error(
`Expected MetaMask / Mist network to be ${

View File

@ -1,159 +0,0 @@
import {
ChangeLanguageAction,
ChangeNodeAction,
AddCustomNodeAction,
RemoveCustomNodeAction,
AddCustomNetworkAction,
RemoveCustomNetworkAction,
SetLatestBlockAction,
ConfigAction
} from 'actions/config';
import { TypeKeys } from 'actions/config/constants';
import {
NODES,
NETWORKS,
NodeConfig,
CustomNodeConfig,
NetworkConfig,
CustomNetworkConfig
} from 'config';
import { makeCustomNodeId } from 'utils/node';
import { makeCustomNetworkId } from 'utils/network';
export interface State {
// FIXME
languageSelection: string;
nodeSelection: string;
node: NodeConfig;
network: NetworkConfig;
isChangingNode: boolean;
offline: boolean;
autoGasLimit: boolean;
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
latestBlock: string;
}
const defaultNode = 'eth_mew';
export const INITIAL_STATE: State = {
languageSelection: 'en',
nodeSelection: defaultNode,
node: NODES[defaultNode],
network: NETWORKS[NODES[defaultNode].network],
isChangingNode: false,
offline: false,
autoGasLimit: true,
customNodes: [],
customNetworks: [],
latestBlock: '???'
};
function changeLanguage(state: State, action: ChangeLanguageAction): State {
return {
...state,
languageSelection: action.payload
};
}
function changeNode(state: State, action: ChangeNodeAction): State {
return {
...state,
nodeSelection: action.payload.nodeSelection,
node: action.payload.node,
network: action.payload.network,
isChangingNode: false
};
}
function changeNodeIntent(state: State): State {
return {
...state,
isChangingNode: true
};
}
function toggleOffline(state: State): State {
return {
...state,
offline: !state.offline
};
}
function toggleAutoGasLimitEstimation(state: State): State {
return {
...state,
autoGasLimit: !state.autoGasLimit
};
}
function addCustomNode(state: State, action: AddCustomNodeAction): State {
const newId = makeCustomNodeId(action.payload);
return {
...state,
customNodes: [
...state.customNodes.filter(node => makeCustomNodeId(node) !== newId),
action.payload
]
};
}
function removeCustomNode(state: State, action: RemoveCustomNodeAction): State {
const id = makeCustomNodeId(action.payload);
return {
...state,
customNodes: state.customNodes.filter(cn => cn !== action.payload),
nodeSelection: id === state.nodeSelection ? defaultNode : state.nodeSelection
};
}
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,
latestBlock: action.payload
};
}
export function config(state: State = INITIAL_STATE, action: ConfigAction): State {
switch (action.type) {
case TypeKeys.CONFIG_LANGUAGE_CHANGE:
return changeLanguage(state, action);
case TypeKeys.CONFIG_NODE_CHANGE:
return changeNode(state, action);
case TypeKeys.CONFIG_NODE_CHANGE_INTENT:
return changeNodeIntent(state);
case TypeKeys.CONFIG_TOGGLE_OFFLINE:
return toggleOffline(state);
case TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT:
return toggleAutoGasLimitEstimation(state);
case TypeKeys.CONFIG_ADD_CUSTOM_NODE:
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:
return state;
}
}

View File

@ -0,0 +1,12 @@
import { meta, State as MetaState } from './meta';
import { networks, State as NetworksState } from './networks';
import { nodes, State as NodesState } from './nodes';
import { combineReducers } from 'redux';
export interface State {
meta: MetaState;
networks: NetworksState;
nodes: NodesState;
}
export const config = combineReducers<State>({ meta, networks, nodes });

View File

@ -0,0 +1 @@
export * from './meta';

View File

@ -0,0 +1,61 @@
import { ChangeLanguageAction, SetLatestBlockAction, MetaAction } from 'actions/config';
import { TypeKeys } from 'actions/config/constants';
export interface State {
languageSelection: string;
offline: boolean;
autoGasLimit: boolean;
latestBlock: string;
}
const INITIAL_STATE: State = {
languageSelection: 'en',
offline: false,
autoGasLimit: true,
latestBlock: '???'
};
function changeLanguage(state: State, action: ChangeLanguageAction): State {
return {
...state,
languageSelection: action.payload
};
}
function toggleOffline(state: State): State {
return {
...state,
offline: !state.offline
};
}
function toggleAutoGasLimitEstimation(state: State): State {
return {
...state,
autoGasLimit: !state.autoGasLimit
};
}
function setLatestBlock(state: State, action: SetLatestBlockAction): State {
return {
...state,
latestBlock: action.payload
};
}
export function meta(state: State = INITIAL_STATE, action: MetaAction): State {
switch (action.type) {
case TypeKeys.CONFIG_LANGUAGE_CHANGE:
return changeLanguage(state, action);
case TypeKeys.CONFIG_TOGGLE_OFFLINE:
return toggleOffline(state);
case TypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT:
return toggleAutoGasLimitEstimation(state);
case TypeKeys.CONFIG_SET_LATEST_BLOCK:
return setLatestBlock(state, action);
default:
return state;
}
}

View File

@ -0,0 +1,34 @@
import {
AddCustomNetworkAction,
RemoveCustomNetworkAction,
CustomNetworkAction,
TypeKeys
} from 'actions/config';
import { CustomNetworkConfig } from 'types/network';
// TODO: this doesn't accurately represent state, as
export interface State {
[customNetworkId: string]: CustomNetworkConfig;
}
const addCustomNetwork = (state: State, { payload }: AddCustomNetworkAction): State => ({
...state,
[payload.id]: payload.config
});
function removeCustomNetwork(state: State, { payload }: RemoveCustomNetworkAction): State {
const stateCopy = { ...state };
Reflect.deleteProperty(stateCopy, payload.id);
return stateCopy;
}
export const customNetworks = (state: State = {}, action: CustomNetworkAction) => {
switch (action.type) {
case TypeKeys.CONFIG_ADD_CUSTOM_NETWORK:
return addCustomNetwork(state, action);
case TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK:
return removeCustomNetwork(state, action);
default:
return state;
}
};

View File

@ -0,0 +1,15 @@
import { customNetworks, State as CustomNetworksState } from './customNetworks';
import { staticNetworks, State as StaticNetworksState } from './staticNetworks';
import { combineReducers } from 'redux';
interface State {
customNetworks: CustomNetworksState;
staticNetworks: StaticNetworksState;
}
const networks = combineReducers<State>({
customNetworks,
staticNetworks
});
export { State, networks, StaticNetworksState, CustomNetworksState };

View File

@ -0,0 +1,148 @@
import { ethPlorer, ETHTokenExplorer, SecureWalletName, InsecureWalletName } from 'config/data';
import {
ETH_DEFAULT,
ETH_TREZOR,
ETH_LEDGER,
ETC_LEDGER,
ETC_TREZOR,
ETH_TESTNET,
EXP_DEFAULT,
UBQ_DEFAULT
} from 'config/dpaths';
import { ConfigAction } from 'actions/config';
import { StaticNetworkIds, StaticNetworkConfig, BlockExplorerConfig } from 'types/network';
export type State = { [key in StaticNetworkIds]: StaticNetworkConfig };
// Must be a website that follows the ethplorer convention of /tx/[hash] and
// address/[address] to generate the correct functions.
// TODO: put this in utils / libs
export function makeExplorer(origin: string): BlockExplorerConfig {
return {
origin,
txUrl: hash => `${origin}/tx/${hash}`,
addressUrl: address => `${origin}/address/${address}`
};
}
const INITIAL_STATE: State = {
ETH: {
name: 'ETH',
unit: 'ETH',
chainId: 1,
isCustom: false,
color: '#0e97c0',
blockExplorer: makeExplorer('https://etherscan.io'),
tokenExplorer: {
name: ethPlorer,
address: ETHTokenExplorer
},
tokens: require('config/tokens/eth.json'),
contracts: require('config/contracts/eth.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TREZOR,
[SecureWalletName.LEDGER_NANO_S]: ETH_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_DEFAULT
}
},
Ropsten: {
name: 'Ropsten',
unit: 'ETH',
chainId: 3,
isCustom: false,
color: '#adc101',
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
tokens: require('config/tokens/ropsten.json'),
contracts: require('config/contracts/ropsten.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
},
Kovan: {
name: 'Kovan',
unit: 'ETH',
chainId: 42,
isCustom: false,
color: '#adc101',
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
tokens: require('config/tokens/ropsten.json'),
contracts: require('config/contracts/ropsten.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
},
Rinkeby: {
name: 'Rinkeby',
unit: 'ETH',
chainId: 4,
isCustom: false,
color: '#adc101',
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
tokens: require('config/tokens/rinkeby.json'),
contracts: require('config/contracts/rinkeby.json'),
isTestnet: true,
dPathFormats: {
[SecureWalletName.TREZOR]: ETH_TESTNET,
[SecureWalletName.LEDGER_NANO_S]: ETH_TESTNET,
[InsecureWalletName.MNEMONIC_PHRASE]: ETH_TESTNET
}
},
ETC: {
name: 'ETC',
unit: 'ETC',
chainId: 61,
isCustom: false,
color: '#669073',
blockExplorer: makeExplorer('https://gastracker.io'),
tokens: require('config/tokens/etc.json'),
contracts: require('config/contracts/etc.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: ETC_TREZOR,
[SecureWalletName.LEDGER_NANO_S]: ETC_LEDGER,
[InsecureWalletName.MNEMONIC_PHRASE]: ETC_TREZOR
}
},
UBQ: {
name: 'UBQ',
unit: 'UBQ',
chainId: 8,
isCustom: false,
color: '#b37aff',
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
tokens: require('config/tokens/ubq.json'),
contracts: require('config/contracts/ubq.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: UBQ_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: UBQ_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: UBQ_DEFAULT
}
},
EXP: {
name: 'EXP',
unit: 'EXP',
chainId: 2,
isCustom: false,
color: '#673ab7',
blockExplorer: makeExplorer('https://www.gander.tech'),
tokens: require('config/tokens/exp.json'),
contracts: require('config/contracts/exp.json'),
dPathFormats: {
[SecureWalletName.TREZOR]: EXP_DEFAULT,
[SecureWalletName.LEDGER_NANO_S]: EXP_DEFAULT,
[InsecureWalletName.MNEMONIC_PHRASE]: EXP_DEFAULT
}
}
};
export const staticNetworks = (state: State = INITIAL_STATE, action: ConfigAction) => {
switch (action.type) {
default:
return state;
}
};

View File

@ -0,0 +1,33 @@
import {
TypeKeys,
CustomNodeAction,
AddCustomNodeAction,
RemoveCustomNodeAction
} from 'actions/config';
import { CustomNodeConfig } from 'types/node';
export interface State {
[customNodeId: string]: CustomNodeConfig;
}
const addCustomNode = (state: State, { payload }: AddCustomNodeAction): State => ({
...state,
[payload.id]: payload.config
});
function removeCustomNode(state: State, { payload }: RemoveCustomNodeAction): State {
const stateCopy = { ...state };
Reflect.deleteProperty(stateCopy, payload.id);
return stateCopy;
}
export const customNodes = (state: State = {}, action: CustomNodeAction): State => {
switch (action.type) {
case TypeKeys.CONFIG_ADD_CUSTOM_NODE:
return addCustomNode(state, action);
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:
return removeCustomNode(state, action);
default:
return state;
}
};

View File

@ -0,0 +1,14 @@
import { customNodes, State as CustomNodesState } from './customNodes';
import { staticNodes, State as StaticNodesState } from './staticNodes';
import { selectedNode, State as SelectedNodeState } from './selectedNode';
import { combineReducers } from 'redux';
interface State {
customNodes: CustomNodesState;
staticNodes: StaticNodesState;
selectedNode: SelectedNodeState;
}
const nodes = combineReducers<State>({ customNodes, staticNodes, selectedNode });
export { State, nodes, CustomNodesState, StaticNodesState, SelectedNodeState };

View File

@ -0,0 +1,53 @@
import {
ChangeNodeAction,
ChangeNodeIntentAction,
NodeAction,
TypeKeys,
RemoveCustomNodeAction,
CustomNodeAction
} from 'actions/config';
interface NodeLoaded {
pending: false;
nodeId: string;
}
interface NodeChangePending {
pending: true;
nodeId: string;
}
export type State = NodeLoaded | NodeChangePending;
export const INITIAL_STATE: NodeLoaded = {
nodeId: 'eth_mycrypto',
pending: false
};
const changeNode = (_: State, { payload }: ChangeNodeAction): State => ({
nodeId: payload.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);
default:
return state;
}
};

View File

@ -0,0 +1,91 @@
import { EtherscanNode, InfuraNode, RPCNode } from 'libs/nodes';
import { TypeKeys, NodeAction } from 'actions/config';
import { NonWeb3NodeConfigs, Web3NodeConfigs } from 'types/node';
export type State = NonWeb3NodeConfigs & Web3NodeConfigs;
export const INITIAL_STATE: State = {
eth_mycrypto: {
network: 'ETH',
isCustom: false,
lib: new RPCNode('https://api.mycryptoapi.com/eth'),
service: 'MyCrypto',
estimateGas: true
},
eth_ethscan: {
network: 'ETH',
isCustom: false,
service: 'Etherscan.io',
lib: new EtherscanNode('https://api.etherscan.io/api'),
estimateGas: false
},
eth_infura: {
network: 'ETH',
isCustom: false,
service: 'infura.io',
lib: new InfuraNode('https://mainnet.infura.io/mew'),
estimateGas: false
},
rop_infura: {
network: 'Ropsten',
isCustom: false,
service: 'infura.io',
lib: new InfuraNode('https://ropsten.infura.io/mew'),
estimateGas: false
},
kov_ethscan: {
network: 'Kovan',
isCustom: false,
service: 'Etherscan.io',
lib: new EtherscanNode('https://kovan.etherscan.io/api'),
estimateGas: false
},
rin_ethscan: {
network: 'Rinkeby',
isCustom: false,
service: 'Etherscan.io',
lib: new EtherscanNode('https://rinkeby.etherscan.io/api'),
estimateGas: false
},
rin_infura: {
network: 'Rinkeby',
isCustom: false,
service: 'infura.io',
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
estimateGas: false
},
etc_epool: {
network: 'ETC',
isCustom: false,
service: 'Epool.io',
lib: new RPCNode('https://mewapi.epool.io'),
estimateGas: false
},
ubq: {
network: 'UBQ',
isCustom: false,
service: 'ubiqscan.io',
lib: new RPCNode('https://pyrus2.ubiqscan.io'),
estimateGas: true
},
exp_tech: {
network: 'EXP',
isCustom: false,
service: 'Expanse.tech',
lib: new RPCNode('https://node.expanse.tech/'),
estimateGas: true
}
};
export const staticNodes = (state: State = INITIAL_STATE, action: NodeAction) => {
switch (action.type) {
case TypeKeys.CONFIG_NODE_WEB3_SET:
return { ...state, [action.payload.id]: action.payload.config };
case TypeKeys.CONFIG_NODE_WEB3_UNSET:
const stateCopy = { ...state };
Reflect.deleteProperty(stateCopy, 'web3');
return stateCopy;
default:
return state;
}
};

View File

@ -4,7 +4,7 @@ import {
RemoveCustomTokenAction
} from 'actions/customTokens';
import { TypeKeys } from 'actions/customTokens/constants';
import { Token } from 'config';
import { Token } from 'types/network';
export type State = Token[];

View File

@ -28,7 +28,7 @@ export interface AppState {
transaction: TransactionState;
}
export default combineReducers({
export default combineReducers<AppState>({
config,
swap,
notifications,

View File

@ -1,269 +0,0 @@
import { delay, SagaIterator } from 'redux-saga';
import {
call,
cancel,
fork,
put,
take,
takeLatest,
takeEvery,
select,
race
} from 'redux-saga/effects';
import {
NODES,
NETWORKS,
NodeConfig,
CustomNodeConfig,
CustomNetworkConfig,
Web3Service
} from 'config';
import {
makeCustomNodeId,
getCustomNodeConfigFromId,
makeNodeConfigFromCustomConfig
} from 'utils/node';
import { makeCustomNetworkId, getNetworkConfigFromId } from 'utils/network';
import {
getNode,
getNodeConfig,
getCustomNodeConfigs,
getCustomNetworkConfigs,
getOffline
} from 'selectors/config';
import { AppState } from 'reducers';
import { TypeKeys } from 'actions/config/constants';
import {
toggleOfflineConfig,
changeNode,
changeNodeIntent,
setLatestBlock,
removeCustomNetwork,
AddCustomNodeAction,
ChangeNodeIntentAction
} from 'actions/config';
import { showNotification } from 'actions/notifications';
import { translateRaw } from 'translations';
import { Web3Wallet } from 'libs/wallet';
import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants';
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
export const getConfig = (state: AppState): ConfigState => state.config;
let hasCheckedOnline = false;
export function* pollOfflineStatus(): SagaIterator {
while (true) {
const node: NodeConfig = yield select(getNodeConfig);
const isOffline: boolean = yield select(getOffline);
// If our offline state disagrees with the browser, run a check
// Don't check if the user is in another tab or window
const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline;
if (shouldPing && !document.hidden) {
const { pingSucceeded } = yield race({
pingSucceeded: call(node.lib.ping.bind(node.lib)),
timeout: call(delay, 5000)
});
if (pingSucceeded && isOffline) {
// If we were able to ping but redux says we're offline, mark online
yield put(
showNotification('success', 'Your connection to the network has been restored!', 3000)
);
yield put(toggleOfflineConfig());
} else if (!pingSucceeded && !isOffline) {
// If we were unable to ping but redux says we're online, mark offline
// If they had been online, show an error.
// If they hadn't been online, just inform them with a warning.
if (hasCheckedOnline) {
yield put(
showNotification(
'danger',
`Youve lost your connection to the network, check your internet
connection or try changing networks from the dropdown at the
top right of the page.`,
Infinity
)
);
} else {
yield put(
showNotification(
'info',
'You are currently offline. Some features will be unavailable.',
5000
)
);
}
yield put(toggleOfflineConfig());
} else {
// If neither case was true, try again in 5s
yield call(delay, 5000);
}
hasCheckedOnline = true;
} else {
yield call(delay, 1000);
}
}
}
// Fork our recurring API call, watch for the need to cancel.
export function* handlePollOfflineStatus(): SagaIterator {
const pollOfflineStatusTask = yield fork(pollOfflineStatus);
yield take('CONFIG_STOP_POLL_OFFLINE_STATE');
yield cancel(pollOfflineStatusTask);
}
// @HACK For now we reload the app when doing a language swap to force non-connected
// data to reload. Also the use of timeout to avoid using additional actions for now.
export function* reload(): SagaIterator {
setTimeout(() => location.reload(), 1150);
}
export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIterator {
const currentNode: string = yield select(getNode);
const currentConfig: NodeConfig = yield select(getNodeConfig);
const customNets: CustomNetworkConfig[] = yield select(getCustomNetworkConfigs);
const currentNetwork =
getNetworkConfigFromId(currentConfig.network, customNets) || NETWORKS[currentConfig.network];
function* bailOut(message: string) {
yield put(showNotification('danger', message, 5000));
yield put(changeNode(currentNode, currentConfig, currentNetwork));
}
let actionConfig = NODES[action.payload];
if (!actionConfig) {
const customConfigs: CustomNodeConfig[] = yield select(getCustomNodeConfigs);
const config = getCustomNodeConfigFromId(action.payload, customConfigs);
if (config) {
actionConfig = makeNodeConfigFromCustomConfig(config);
}
}
if (!actionConfig) {
return yield* bailOut(`Attempted to switch to unknown node '${action.payload}'`);
}
// Grab latest block from the node, before switching, to confirm it's online
// Give it 5 seconds before we call it offline
let latestBlock;
let timeout;
try {
const { lb, to } = yield race({
lb: call(actionConfig.lib.getCurrentBlock.bind(actionConfig.lib)),
to: call(delay, 5000)
});
latestBlock = lb;
timeout = to;
} catch (err) {
// Whether it times out or errors, same message
timeout = true;
}
if (timeout) {
return yield* bailOut(translateRaw('ERROR_32'));
}
const actionNetwork = getNetworkConfigFromId(actionConfig.network, customNets);
if (!actionNetwork) {
return yield* bailOut(
`Unknown custom network for your node '${action.payload}', try re-adding it`
);
}
yield put(setLatestBlock(latestBlock));
yield put(changeNode(action.payload, actionConfig, actionNetwork));
// TODO - re-enable once DeterministicWallet state is fixed to flush properly.
// DeterministicWallet keeps path related state we need to flush before we can stop reloading
// const currentWallet: IWallet | null = yield select(getWalletInst);
// if there's no wallet, do not reload as there's no component state to resync
// if (currentWallet && currentConfig.network !== actionConfig.network) {
const isNewNetwork = currentConfig.network !== actionConfig.network;
const newIsWeb3 = actionConfig.service === Web3Service;
// don't reload when web3 is selected; node will automatically re-set and state is not an issue here
if (isNewNetwork && !newIsWeb3) {
yield call(reload);
}
}
export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator {
const nodeId = makeCustomNodeId(action.payload);
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* unsetWeb3NodeOnWalletEvent(action): SagaIterator {
const node = yield select(getNode);
const nodeConfig = yield select(getNodeConfig);
const newWallet = action.payload;
const isWeb3Wallet = newWallet instanceof Web3Wallet;
if (node !== 'web3' || isWeb3Wallet) {
return;
}
// switch back to a node with the same network as MetaMask/Mist
yield put(changeNodeIntent(equivalentNodeOrDefault(nodeConfig)));
}
export function* unsetWeb3Node(): SagaIterator {
const node = yield select(getNode);
if (node !== 'web3') {
return;
}
const nodeConfig: NodeConfig = yield select(getNodeConfig);
const newNode = equivalentNodeOrDefault(nodeConfig);
yield put(changeNodeIntent(newNode));
}
export const equivalentNodeOrDefault = (nodeConfig: NodeConfig) => {
const node = Object.keys(NODES)
.filter(key => key !== 'web3')
.reduce((found, key) => {
const config = NODES[key];
if (found.length) {
return found;
}
if (nodeConfig.network === config.network) {
return (found = key);
}
return found;
}, '');
// if no equivalent node was found, use the app default
return node.length ? node : configInitialState.nodeSelection;
};
export default function* configSaga(): SagaIterator {
yield takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus);
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(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node);
yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent);
yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3NodeOnWalletEvent);
}

View File

@ -0,0 +1,9 @@
import { network } from './network';
import { node } from './node';
import { web3 } from './web3';
import { all } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
export default function*(): SagaIterator {
yield all([...network, ...node, ...web3]);
}

View File

@ -0,0 +1,30 @@
import { select, takeEvery, put } from 'redux-saga/effects';
import { getCustomNodeConfigs, getCustomNetworkConfigs } from 'selectors/config';
import { removeCustomNetwork, TypeKeys } from 'actions/config';
import { SagaIterator } from 'redux-saga';
import { AppState } from 'reducers';
// If there are any orphaned custom networks, prune them
export function* pruneCustomNetworks(): SagaIterator {
const customNodes: AppState['config']['nodes']['customNodes'] = yield select(
getCustomNodeConfigs
);
const customNetworks: AppState['config']['networks']['customNetworks'] = yield select(
getCustomNetworkConfigs
);
//construct lookup table of networks
const linkedNetworks = 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 }));
}
}
}
export const network = [takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, pruneCustomNetworks)];

195
common/sagas/config/node.ts Normal file
View File

@ -0,0 +1,195 @@
import { delay, SagaIterator } from 'redux-saga';
import {
call,
cancel,
fork,
put,
take,
takeEvery,
select,
race,
apply,
takeLatest
} from 'redux-saga/effects';
import {
getNodeId,
getNodeConfig,
getOffline,
isStaticNodeId,
getCustomNodeFromId,
getStaticNodeFromId,
getNetworkConfigById
} from 'selectors/config';
import { TypeKeys } from 'actions/config/constants';
import {
toggleOffline,
changeNode,
changeNodeIntent,
setLatestBlock,
AddCustomNodeAction,
ChangeNodeIntentAction
} from 'actions/config';
import { showNotification } from 'actions/notifications';
import { translateRaw } from 'translations';
import { StaticNodeConfig, CustomNodeConfig, NodeConfig } from 'types/node';
import { CustomNetworkConfig, StaticNetworkConfig } from 'types/network';
import { Web3Service } from 'libs/nodes/web3';
let hasCheckedOnline = false;
export function* pollOfflineStatus(): SagaIterator {
while (true) {
const nodeConfig: StaticNodeConfig = yield select(getNodeConfig);
const isOffline: boolean = yield select(getOffline);
// If our offline state disagrees with the browser, run a check
// Don't check if the user is in another tab or window
const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline;
if (shouldPing && !document.hidden) {
const { pingSucceeded } = yield race({
pingSucceeded: call(nodeConfig.lib.ping.bind(nodeConfig.lib)),
timeout: call(delay, 5000)
});
if (pingSucceeded && isOffline) {
// If we were able to ping but redux says we're offline, mark online
yield put(
showNotification('success', 'Your connection to the network has been restored!', 3000)
);
yield put(toggleOffline());
} else if (!pingSucceeded && !isOffline) {
// If we were unable to ping but redux says we're online, mark offline
// If they had been online, show an error.
// If they hadn't been online, just inform them with a warning.
if (hasCheckedOnline) {
yield put(
showNotification(
'danger',
`Youve lost your connection to the network, check your internet
connection or try changing networks from the dropdown at the
top right of the page.`,
Infinity
)
);
} else {
yield put(
showNotification(
'info',
'You are currently offline. Some features will be unavailable.',
5000
)
);
}
yield put(toggleOffline());
} else {
// If neither case was true, try again in 5s
yield call(delay, 5000);
}
hasCheckedOnline = true;
} else {
yield call(delay, 1000);
}
}
}
// Fork our recurring API call, watch for the need to cancel.
export function* handlePollOfflineStatus(): SagaIterator {
const pollOfflineStatusTask = yield fork(pollOfflineStatus);
yield take('CONFIG_STOP_POLL_OFFLINE_STATE');
yield cancel(pollOfflineStatusTask);
}
// @HACK For now we reload the app when doing a language swap to force non-connected
// data to reload. Also the use of timeout to avoid using additional actions for now.
export function* reload(): SagaIterator {
setTimeout(() => location.reload(), 1150);
}
export function* handleNodeChangeIntent({
payload: nodeIdToSwitchTo
}: ChangeNodeIntentAction): SagaIterator {
const isStaticNode: boolean = yield select(isStaticNodeId, nodeIdToSwitchTo);
const currentConfig: NodeConfig = yield select(getNodeConfig);
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;
if (!isStaticNode) {
const config: CustomNodeConfig | undefined = yield select(
getCustomNodeFromId,
nodeIdToSwitchTo
);
if (config) {
nextNodeConfig = config;
} else {
return yield* bailOut(`Attempted to switch to unknown node '${nodeIdToSwitchTo}'`);
}
} else {
nextNodeConfig = yield select(getStaticNodeFromId, nodeIdToSwitchTo);
}
// Grab current block from the node, before switching, to confirm it's online
// Give it 5 seconds before we call it offline
let currentBlock;
let timeout;
try {
const { lb, to } = yield race({
lb: apply(nextNodeConfig.lib, nextNodeConfig.lib.getCurrentBlock),
to: call(delay, 5000)
});
currentBlock = lb;
timeout = to;
} catch (err) {
console.error(err);
// Whether it times out or errors, same message
timeout = true;
}
if (timeout) {
return yield* bailOut(translateRaw('ERROR_32'));
}
const nextNetwork: StaticNetworkConfig | CustomNetworkConfig = yield select(
getNetworkConfigById,
nextNodeConfig.network
);
if (!nextNetwork) {
return yield* bailOut(
`Unknown custom network for your node '${nodeIdToSwitchTo}', try re-adding it`
);
}
yield put(setLatestBlock(currentBlock));
yield put(changeNode({ networkId: nextNodeConfig.network, nodeId: nodeIdToSwitchTo }));
// TODO - re-enable once DeterministicWallet state is fixed to flush properly.
// DeterministicWallet keeps path related state we need to flush before we can stop reloading
// const currentWallet: IWallet | null = yield select(getWalletInst);
// if there's no wallet, do not reload as there's no component state to resync
// if (currentWallet && currentConfig.network !== actionConfig.network) {
const isNewNetwork = currentConfig.network !== nextNodeConfig.network;
const newIsWeb3 = nextNodeConfig.service === Web3Service;
// don't reload when web3 is selected; node will automatically re-set and state is not an issue here
if (isNewNetwork && !newIsWeb3) {
yield call(reload);
}
}
export function* switchToNewNode(action: AddCustomNodeAction): SagaIterator {
yield put(changeNodeIntent(action.payload.id));
}
export const node = [
takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent),
takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus),
takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload),
takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode)
];

View File

@ -0,0 +1,57 @@
import { TypeKeys as WalletTypeKeys } from 'actions/wallet/constants';
import { Web3Wallet } from 'libs/wallet';
import { SagaIterator } from 'redux-saga';
import { select, put, takeEvery, call } from 'redux-saga/effects';
import { changeNodeIntent, TypeKeys, web3SetNode } from 'actions/config';
import { getNodeId, getStaticAltNodeIdToWeb3, getNetworkNameByChainId } from 'selectors/config';
import { setupWeb3Node, Web3Service } from 'libs/nodes/web3';
import { Web3NodeConfig } from 'types/node';
export function* initWeb3Node(): SagaIterator {
const { networkId, lib } = yield call(setupWeb3Node);
const network = yield select(getNetworkNameByChainId, networkId);
const config: Web3NodeConfig = {
isCustom: false,
network,
service: Web3Service,
lib,
estimateGas: false,
hidden: true
};
yield put(web3SetNode({ id: 'web3', config }));
}
// unset web3 as the selected node if a non-web3 wallet has been selected
export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator {
const node = yield select(getNodeId);
const newWallet = action.payload;
const isWeb3Wallet = newWallet instanceof Web3Wallet;
if (node !== 'web3' || isWeb3Wallet) {
return;
}
const altNode = yield select(getStaticAltNodeIdToWeb3);
// switch back to a node with the same network as MetaMask/Mist
yield put(changeNodeIntent(altNode));
}
export function* unsetWeb3Node(): SagaIterator {
const node = yield select(getNodeId);
if (node !== 'web3') {
return;
}
const altNode = yield select(getStaticAltNodeIdToWeb3);
// switch back to a node with the same network as MetaMask/Mist
yield put(changeNodeIntent(altNode));
}
export const web3 = [
takeEvery(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node),
takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent),
takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3NodeOnWalletEvent)
];

View File

@ -5,7 +5,6 @@ import {
updateDeterministicWallet
} from 'actions/deterministicWallets';
import { showNotification } from 'actions/notifications';
import { Token } from 'config';
import { publicToAddress, toChecksumAddress } from 'ethereumjs-util';
import HDKey from 'hdkey';
import { INode } from 'libs/nodes/INode';
@ -16,6 +15,7 @@ import { getDesiredToken, getWallets } from 'selectors/deterministicWallets';
import { getTokens } from 'selectors/wallet';
import translate from 'translations';
import { TokenValue } from 'libs/units';
import { Token } from 'types/network';
export function* getDeterministicWallets(action: GetDeterministicWalletsAction): SagaIterator {
const { seed, dPath, publicKey, chainCode, limit, offset } = action.payload;

View File

@ -11,9 +11,9 @@ import {
SignTransactionRequestedAction
} from 'actions/transaction';
import Tx from 'ethereumjs-tx';
import { NetworkConfig } from 'config';
import { SagaIterator } from 'redux-saga';
import { showNotification } from 'actions/notifications';
import { StaticNetworkConfig } from 'types/network';
interface IFullWalletAndTransaction {
wallet: IFullWallet;
@ -46,7 +46,7 @@ function* getWalletAndTransaction(partialTx: SignTransactionRequestedAction['pay
throw Error('Could not get wallet instance to sign transaction');
}
// get the chainId
const { chainId }: NetworkConfig = yield select(getNetworkConfig);
const { chainId }: StaticNetworkConfig = yield select(getNetworkConfig);
// get the rest of the transaction parameters
partialTx._chainId = chainId;

View File

@ -1,6 +1,5 @@
import { apply, select, call } from 'redux-saga/effects';
import { AppState } from 'reducers';
import { Token } from 'config';
import { INode } from 'libs/nodes/INode';
import { IWallet, WalletConfig } from 'libs/wallet';
import { TokenBalance } from 'selectors/wallet';
@ -8,6 +7,7 @@ import { getCustomTokens } from 'selectors/customTokens';
import { getNodeLib } from 'selectors/config';
import { loadWalletConfig } from 'utils/localStorage';
import { TokenBalanceLookup } from './wallet';
import { Token } from 'types/network';
export function* getTokenBalances(wallet: IWallet, tokens: Token[]) {
const node: INode = yield select(getNodeLib);

View File

@ -36,10 +36,9 @@ import {
Web3Wallet,
WalletConfig
} from 'libs/wallet';
import { NODES, initWeb3Node, Token } from 'config';
import { SagaIterator, delay, Task } from 'redux-saga';
import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects';
import { getNodeLib, getAllTokens, getOffline } from 'selectors/config';
import { getNodeLib, getAllTokens, getOffline, getWeb3Node } from 'selectors/config';
import {
getTokens,
getWalletInst,
@ -51,6 +50,9 @@ import translate from 'translations';
import Web3Node, { isWeb3Node } from 'libs/nodes/web3';
import { loadWalletConfig, saveWalletConfig } from 'utils/localStorage';
import { getTokenBalances, filterScannedTokenBalances } from './helpers';
import { Token } from 'types/network';
import { Web3NodeConfig } from '../../../shared/types/node';
import { initWeb3Node } from 'sagas/config/web3';
export interface TokenBalanceLookup {
[symbol: string]: TokenBalance;
@ -262,14 +264,15 @@ export function* unlockWeb3(): SagaIterator {
yield put(changeNodeIntent('web3'));
yield take(
action =>
action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeSelection === 'web3'
action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && action.payload.nodeId === 'web3'
);
if (!NODES.web3) {
const web3Node: Web3NodeConfig | null = yield select(getWeb3Node);
if (!web3Node) {
throw Error('Web3 node config not found!');
}
const network = NODES.web3.network;
const nodeLib: INode | Web3Node = yield select(getNodeLib);
const network = web3Node.network;
const nodeLib: Web3Node = web3Node.lib;
if (!isWeb3Node(nodeLib)) {
throw new Error('Cannot use Web3 wallet without a Web3 node.');
@ -284,6 +287,7 @@ export function* unlockWeb3(): SagaIterator {
const wallet = new Web3Wallet(address, network);
yield put(setWallet(wallet));
} catch (err) {
console.error(err);
// unset web3 node so node dropdown isn't disabled
yield put(web3UnsetNode());
yield put(showNotification('danger', translate(err.message)));

View File

@ -1,104 +0,0 @@
import {
CustomNetworkConfig,
CustomNodeConfig,
NetworkConfig,
NetworkContract,
NodeConfig,
Token
} from 'config';
import { INode } from 'libs/nodes/INode';
import { AppState } from 'reducers';
import { getUnit } from 'selectors/transaction/meta';
import { isEtherUnit } from 'libs/units';
import { SHAPESHIFT_TOKEN_WHITELIST } from 'api/shapeshift';
export function getNode(state: AppState): string {
return state.config.nodeSelection;
}
export function getIsWeb3Node(state: AppState): boolean {
return getNode(state) === 'web3';
}
export function getNodeConfig(state: AppState): NodeConfig {
return state.config.node;
}
export function getNodeLib(state: AppState): INode {
return getNodeConfig(state).lib;
}
export function getNetworkConfig(state: AppState): NetworkConfig {
return state.config.network;
}
export function getNetworkContracts(state: AppState): NetworkContract[] | null {
const network = getNetworkConfig(state);
return network ? network.contracts : [];
}
export function getNetworkTokens(state: AppState): Token[] {
const network = getNetworkConfig(state);
return network ? network.tokens : [];
}
export function getAllTokens(state: AppState): Token[] {
const networkTokens = getNetworkTokens(state);
return networkTokens.concat(state.customTokens);
}
export function getSelectedTokenContractAddress(state: AppState): string {
const allTokens = getAllTokens(state);
const currentUnit = getUnit(state);
if (currentUnit === 'ether') {
return '';
}
return allTokens.reduce((tokenAddr, tokenInfo) => {
if (tokenAddr && tokenAddr.length) {
return tokenAddr;
}
if (tokenInfo.symbol === currentUnit) {
return tokenInfo.address;
}
return tokenAddr;
}, '');
}
export function tokenExists(state: AppState, token: string): boolean {
const existInWhitelist = SHAPESHIFT_TOKEN_WHITELIST.includes(token);
const existsInNetwork = !!getAllTokens(state).find(t => t.symbol === token);
return existsInNetwork || existInWhitelist;
}
export function getLanguageSelection(state: AppState): string {
return state.config.languageSelection;
}
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;
}
export function getAutoGasLimitEnabled(state: AppState): boolean {
return state.config.autoGasLimit;
}
export function isSupportedUnit(state: AppState, unit: string) {
const isToken: boolean = tokenExists(state, unit);
const isEther: boolean = isEtherUnit(unit);
if (!isToken && !isEther) {
return false;
}
return true;
}

View File

@ -0,0 +1,4 @@
export * from './meta';
export * from './networks';
export * from './nodes';
export * from './tokens';

View File

@ -0,0 +1,21 @@
import { AppState } from 'reducers';
const getConfig = (state: AppState) => state.config;
export const getMeta = (state: AppState) => getConfig(state).meta;
export function getOffline(state: AppState): boolean {
return getMeta(state).offline;
}
export function getAutoGasLimitEnabled(state: AppState): boolean {
const meta = getMeta(state);
return meta.autoGasLimit;
}
export function getLanguageSelection(state: AppState): string {
return getMeta(state).languageSelection;
}
export function getLatestBlock(state: AppState) {
return getMeta(state).latestBlock;
}

View File

@ -0,0 +1,76 @@
import { AppState } from 'reducers';
import {
CustomNetworkConfig,
StaticNetworkConfig,
StaticNetworkIds,
NetworkContract
} from 'types/network';
import { getNodeConfig } from 'selectors/config';
const getConfig = (state: AppState) => state.config;
export const getNetworks = (state: AppState) => getConfig(state).networks;
export const getNetworkConfigById = (state: AppState, networkId: string) =>
isStaticNetworkId(state, networkId)
? getStaticNetworkConfigs(state)[networkId]
: getCustomNetworkConfigs(state)[networkId];
export const getNetworkNameByChainId = (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;
};
export const getStaticNetworkIds = (state: AppState): StaticNetworkIds[] =>
Object.keys(getNetworks(state).staticNetworks) as StaticNetworkIds[];
export const isStaticNetworkId = (
state: AppState,
networkId: string
): networkId is StaticNetworkIds => Object.keys(getStaticNetworkConfigs(state)).includes(networkId);
export const getStaticNetworkConfig = (state: AppState): StaticNetworkConfig | undefined => {
const selectedNetwork = getSelectedNetwork(state);
const { staticNetworks } = getNetworks(state);
const defaultNetwork = isStaticNetworkId(state, selectedNetwork)
? staticNetworks[selectedNetwork]
: undefined;
return defaultNetwork;
};
export const getSelectedNetwork = (state: AppState) => getNodeConfig(state).network;
export const getCustomNetworkConfig = (state: AppState): CustomNetworkConfig | undefined => {
const selectedNetwork = getSelectedNetwork(state);
const { customNetworks } = getNetworks(state);
const customNetwork = customNetworks[selectedNetwork];
return customNetwork;
};
export const getNetworkConfig = (state: AppState): StaticNetworkConfig | CustomNetworkConfig => {
const config = getStaticNetworkConfig(state) || getCustomNetworkConfig(state);
if (!config) {
const selectedNetwork = getSelectedNetwork(state);
throw Error(
`No network config found for ${selectedNetwork} in either static or custom networks`
);
}
return config;
};
export const getNetworkContracts = (state: AppState): NetworkContract[] | null => {
const network = getStaticNetworkConfig(state);
return network ? network.contracts : [];
};
export const getCustomNetworkConfigs = (state: AppState) => getNetworks(state).customNetworks;
export const getStaticNetworkConfigs = (state: AppState) => getNetworks(state).staticNetworks;

View File

@ -0,0 +1,176 @@
import { AppState } from 'reducers';
import {
getStaticNetworkConfigs,
getCustomNetworkConfigs,
isStaticNetworkId
} from 'selectors/config';
import {
CustomNodeConfig,
StaticNodeConfig,
StaticNodeId,
Web3NodeConfig,
StaticNodeWithWeb3Id
} from 'types/node';
const getConfig = (state: AppState) => state.config;
import { INITIAL_STATE as SELECTED_NODE_INITIAL_STATE } from 'reducers/config/nodes/selectedNode';
export const getNodes = (state: AppState) => getConfig(state).nodes;
export function isNodeCustom(state: AppState, nodeId: string): CustomNodeConfig | undefined {
return getCustomNodeConfigs(state)[nodeId];
}
export const getCustomNodeFromId = (
state: AppState,
nodeId: string
): CustomNodeConfig | undefined => getCustomNodeConfigs(state)[nodeId];
export const getStaticAltNodeIdToWeb3 = (state: AppState) => {
const { web3, ...configs } = getStaticNodeConfigs(state);
if (!web3) {
return SELECTED_NODE_INITIAL_STATE.nodeId;
}
const res = Object.entries(configs).find(
([_, config]: [StaticNodeId, StaticNodeConfig]) => web3.network === config.network
);
if (res) {
return res[0];
}
return SELECTED_NODE_INITIAL_STATE.nodeId;
};
export const getStaticNodeFromId = (state: AppState, nodeId: StaticNodeId) =>
getStaticNodeConfigs(state)[nodeId];
export const isStaticNodeId = (state: AppState, nodeId: string): nodeId is StaticNodeWithWeb3Id =>
Object.keys(getStaticNodeConfigs(state)).includes(nodeId);
const getStaticNodeConfigs = (state: AppState) => getNodes(state).staticNodes;
export const getStaticNodeConfig = (state: AppState) => {
const { staticNodes, selectedNode: { nodeId } } = getNodes(state);
const defaultNetwork = isStaticNodeId(state, nodeId) ? staticNodes[nodeId] : undefined;
return defaultNetwork;
};
export const getWeb3Node = (state: AppState): Web3NodeConfig | null => {
const isWeb3Node = (nodeId: string, _: StaticNodeConfig | Web3NodeConfig): _ is Web3NodeConfig =>
nodeId === 'web3';
const currNode = getStaticNodeConfig(state);
const currNodeId = getNodeId(state);
if (currNode && currNodeId && isWeb3Node(currNodeId, currNode)) {
return currNode;
}
return null;
};
export const getCustomNodeConfig = (state: AppState): CustomNodeConfig | undefined => {
const { customNodes, selectedNode: { nodeId } } = getNodes(state);
const customNode = customNodes[nodeId];
return customNode;
};
export function getCustomNodeConfigs(state: AppState) {
return getNodes(state).customNodes;
}
export function getStaticNodes(state: AppState) {
return getNodes(state).staticNodes;
}
export function getSelectedNode(state: AppState) {
return getNodes(state).selectedNode;
}
export function isNodeChanging(state): boolean {
return getSelectedNode(state).pending;
}
export function getNodeId(state: AppState): string {
return getSelectedNode(state).nodeId;
}
export function getIsWeb3Node(state: AppState): boolean {
return getNodeId(state) === 'web3';
}
export function getNodeConfig(state: AppState): StaticNodeConfig | CustomNodeConfig {
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`);
}
return config;
}
export function getNodeLib(state: AppState) {
const config = getNodeConfig(state);
if (!config) {
throw Error('No node lib found when trying to select from state');
}
return config.lib;
}
export interface NodeOption {
isCustom: false;
value: string;
name: { networkId?: 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 networkId = node.network;
const associatedNetwork = staticNetworkConfigs[networkId];
const opt: NodeOption = {
isCustom: node.isCustom,
value: nodeId,
name: { networkId, service: node.service },
color: associatedNetwork.color,
hidden: node.hidden
};
return opt;
});
}
export interface CustomNodeOption {
isCustom: true;
id: string;
value: string;
name: { networkId?: string; nodeId: string };
color?: string;
hidden?: boolean;
}
export function getCustomNodeOptions(state: AppState): CustomNodeOption[] {
const staticNetworkConfigs = getStaticNetworkConfigs(state);
const customNetworkConfigs = getCustomNetworkConfigs(state);
return Object.entries(getCustomNodeConfigs(state)).map(
([nodeId, node]: [string, CustomNodeConfig]) => {
const networkId = node.network;
const associatedNetwork = isStaticNetworkId(state, networkId)
? staticNetworkConfigs[networkId]
: customNetworkConfigs[networkId];
const opt: CustomNodeOption = {
isCustom: node.isCustom,
value: node.id,
name: { networkId, nodeId },
color: associatedNetwork.isCustom ? undefined : associatedNetwork.color,
hidden: false,
id: node.id
};
return opt;
}
);
}
export function getNodeOptions(state: AppState) {
return [...getStaticNodeOptions(state), ...getCustomNodeOptions(state)];
}

View File

@ -0,0 +1,52 @@
import { AppState } from 'reducers';
import { getUnit } from 'selectors/transaction/meta';
import { isEtherUnit } from 'libs/units';
import { SHAPESHIFT_TOKEN_WHITELIST } from 'api/shapeshift';
import { getStaticNetworkConfig } from 'selectors/config';
import { Token } from 'types/network';
export function getNetworkTokens(state: AppState): Token[] {
const network = getStaticNetworkConfig(state);
return network ? network.tokens : [];
}
export function getAllTokens(state: AppState): Token[] {
const networkTokens = getNetworkTokens(state);
return networkTokens.concat(state.customTokens);
}
export function getSelectedTokenContractAddress(state: AppState): string {
const allTokens = getAllTokens(state);
const currentUnit = getUnit(state);
if (isEtherUnit(currentUnit)) {
return '';
}
return allTokens.reduce((tokenAddr, tokenInfo) => {
if (tokenAddr && tokenAddr.length) {
return tokenAddr;
}
if (tokenInfo.symbol === currentUnit) {
return tokenInfo.address;
}
return tokenAddr;
}, '');
}
export function tokenExists(state: AppState, token: string): boolean {
const existInWhitelist = SHAPESHIFT_TOKEN_WHITELIST.includes(token);
const existsInNetwork = !!getAllTokens(state).find(t => t.symbol === token);
return existsInNetwork || existInWhitelist;
}
export function isSupportedUnit(state: AppState, unit: string) {
const isToken: boolean = tokenExists(state, unit);
const isEther: boolean = isEtherUnit(unit);
if (!isToken && !isEther) {
return false;
}
return true;
}

View File

@ -0,0 +1,82 @@
import { InsecureWalletName, SecureWalletName, WalletName, walletNames } from 'config';
import { EXTRA_PATHS } from 'config/dpaths';
import sortedUniq from 'lodash/sortedUniq';
import difference from 'lodash/difference';
import { StaticNetworkConfig, DPathFormats } from 'types/network';
import { AppState } from 'reducers';
import { getStaticNetworkConfigs, getStaticNetworkConfig } from 'selectors/config';
type PathType = keyof DPathFormats;
type DPathFormat =
| SecureWalletName.TREZOR
| SecureWalletName.LEDGER_NANO_S
| InsecureWalletName.MNEMONIC_PHRASE;
export function getPaths(state: AppState, pathType: PathType): DPath[] {
const paths = Object.values(getStaticNetworkConfigs(state))
.reduce(
(networkPaths: DPath[], { dPathFormats }) =>
dPathFormats ? [...networkPaths, dPathFormats[pathType]] : networkPaths,
[]
)
.concat(EXTRA_PATHS);
return sortedUniq(paths);
}
export function getSingleDPath(state: AppState, format: DPathFormat): DPath {
const network = getStaticNetworkConfig(state);
if (!network) {
throw Error('No static network config loaded');
}
const dPathFormats = network.dPathFormats;
return dPathFormats[format];
}
export function isNetworkUnit(state: AppState, unit: string) {
const currentNetwork = getStaticNetworkConfig(state);
//TODO: logic check
if (!currentNetwork) {
return false;
}
const networks = getStaticNetworkConfigs(state);
const validNetworks = Object.values(networks).filter((n: StaticNetworkConfig) => n.unit === unit);
return validNetworks.includes(currentNetwork);
}
export function isWalletFormatSupportedOnNetwork(state: AppState, format: WalletName): boolean {
const network = getStaticNetworkConfig(state);
const CHECK_FORMATS: DPathFormat[] = [
SecureWalletName.LEDGER_NANO_S,
SecureWalletName.TREZOR,
InsecureWalletName.MNEMONIC_PHRASE
];
const isHDFormat = (f: string): f is DPathFormat => CHECK_FORMATS.includes(f as DPathFormat);
// Ensure DPath's are found
if (isHDFormat(format)) {
if (!network) {
return false;
}
const dPath = network.dPathFormats && network.dPathFormats[format];
return !!dPath;
}
// Ensure Web3 is only enabled on ETH or ETH Testnets (MetaMask does not support other networks)
if (format === SecureWalletName.WEB3) {
return isNetworkUnit(state, 'ETH');
}
// All other wallet formats are supported
return true;
}
export function unSupportedWalletFormatsOnNetwork(state: AppState): WalletName[] {
const supportedFormats = walletNames.filter(walletName =>
isWalletFormatSupportedOnNetwork(state, walletName)
);
return difference(walletNames, supportedFormats);
}

View File

@ -1,11 +1,12 @@
import { TokenValue, Wei } from 'libs/units';
import { Token, SecureWalletName, WalletName } from 'config';
import { SecureWalletName, WalletName } from 'config';
import { AppState } from 'reducers';
import { getNetworkConfig, getOffline } from 'selectors/config';
import { getNetworkConfig, getOffline, getStaticNetworkConfig } from 'selectors/config';
import { IWallet, Web3Wallet, LedgerWallet, TrezorWallet, WalletConfig } from 'libs/wallet';
import { isEtherTransaction, getUnit } from './transaction';
import { unSupportedWalletFormatsOnNetwork } from 'utils/network';
import { DisabledWallets } from 'components/WalletDecrypt';
import { Token } from 'types/network';
import { unSupportedWalletFormatsOnNetwork } from 'selectors/config/wallet';
export function getWalletInst(state: AppState): IWallet | null | undefined {
return state.wallet.inst;
@ -32,7 +33,7 @@ export type MergedToken = Token & {
};
export function getTokens(state: AppState): MergedToken[] {
const network = getNetworkConfig(state);
const network = getStaticNetworkConfig(state);
const tokens: Token[] = network ? network.tokens : [];
return tokens.concat(
state.customTokens.map((token: Token) => {
@ -164,7 +165,7 @@ export function getDisabledWallets(state: AppState): DisabledWallets {
// Some wallets don't support some networks
addReason(
unSupportedWalletFormatsOnNetwork(network),
unSupportedWalletFormatsOnNetwork(state),
`${network.name} does not support this wallet`
);

View File

@ -1,128 +0,0 @@
import throttle from 'lodash/throttle';
import { routerMiddleware } from 'react-router-redux';
import { State as ConfigState, INITIAL_STATE as configInitialState } from 'reducers/config';
import {
State as CustomTokenState,
INITIAL_STATE as customTokensInitialState
} from 'reducers/customTokens';
import { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap';
import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage';
import RootReducer, { AppState } from './reducers';
import { getNodeConfigFromId } from 'utils/node';
import { getNetworkConfigFromId } from 'utils/network';
import { dedupeCustomTokens } from 'utils/tokens';
import sagas from './sagas';
const configureStore = () => {
const logger = createLogger({
collapsed: true
});
const sagaMiddleware = createSagaMiddleware();
let middleware;
let store;
if (process.env.NODE_ENV !== 'production') {
middleware = composeWithDevTools(
applyMiddleware(sagaMiddleware, logger, routerMiddleware(history as any))
);
} else {
middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history as any));
}
const localSwapState = loadStatePropertyOrEmptyObject<SwapState>('swap');
const swapState =
localSwapState && localSwapState.step === 3
? {
...swapInitialState,
...localSwapState
}
: { ...swapInitialState };
const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>('config');
// If they have a saved node, make sure we assign that too. The node selected
// isn't serializable, so we have to assign it here.
if (savedConfigState && savedConfigState.nodeSelection) {
const savedNode = getNodeConfigFromId(
savedConfigState.nodeSelection,
savedConfigState.customNodes
);
// If we couldn't find it, revert to defaults
if (savedNode) {
savedConfigState.node = savedNode;
const network = getNetworkConfigFromId(savedNode.network, savedConfigState.customNetworks);
if (network) {
savedConfigState.network = network;
}
} else {
savedConfigState.nodeSelection = configInitialState.nodeSelection;
}
}
// Dedupe custom tokens initially
const savedCustomTokensState =
loadStatePropertyOrEmptyObject<CustomTokenState>('customTokens') || customTokensInitialState;
const initialNetwork =
(savedConfigState && savedConfigState.network) || configInitialState.network;
const customTokens = dedupeCustomTokens(initialNetwork.tokens, savedCustomTokensState);
const persistedInitialState = {
config: {
...configInitialState,
...savedConfigState
},
customTokens,
// ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3
swap: swapState
};
// if 'web3' has persisted as node selection, reset to app default
// necessary because web3 is only initialized as a node upon MetaMask / Mist unlock
if (persistedInitialState.config.nodeSelection === 'web3') {
persistedInitialState.config.nodeSelection = configInitialState.nodeSelection;
}
store = createStore(RootReducer, persistedInitialState, middleware);
// Add all of the sagas to the middleware
Object.keys(sagas).forEach(saga => {
sagaMiddleware.run(sagas[saga]);
});
store.subscribe(
throttle(() => {
const state: AppState = store.getState();
saveState({
config: {
nodeSelection: state.config.nodeSelection,
languageSelection: state.config.languageSelection,
customNodes: state.config.customNodes,
customNetworks: state.config.customNetworks
},
swap: {
...state.swap,
options: {
byId: {},
allIds: []
},
bityRates: {
byId: {},
allIds: []
},
shapeshiftRates: {
byId: {},
allIds: []
}
},
customTokens: state.customTokens
});
}, 50)
);
return store;
};
export const configuredStore = configureStore();

View File

@ -0,0 +1,162 @@
import { State as ConfigState, config } from 'reducers/config';
import { dedupeCustomTokens } from 'utils/tokens';
import {
State as CustomTokenState,
INITIAL_STATE as customTokensInitialState
} from 'reducers/customTokens';
import { loadStatePropertyOrEmptyObject } from 'utils/localStorage';
import {
isStaticNodeId,
isStaticNetworkId,
getLanguageSelection,
getCustomNodeConfigs,
getSelectedNode,
getCustomNetworkConfigs
} from 'selectors/config';
import RootReducer, { AppState } from 'reducers';
import CustomNode from 'libs/nodes/custom';
import { CustomNodeConfig } from 'types/node';
const appInitialState = RootReducer(undefined as any, { type: 'inital_state' });
type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> };
export function getConfigAndCustomTokensStateToSubscribe(
state: AppState
): Pick<DeepPartial<AppState>, 'config' | 'customTokens'> {
const subscribedConfig: DeepPartial<ConfigState> = {
meta: { languageSelection: getLanguageSelection(state) },
nodes: { customNodes: getCustomNodeConfigs(state), selectedNode: getSelectedNode(state) },
networks: {
customNetworks: getCustomNetworkConfigs(state)
}
};
const subscribedTokens = state.customTokens;
return { config: subscribedConfig, customTokens: subscribedTokens };
}
export function rehydrateConfigAndCustomTokenState() {
const configInitialState = config(undefined as any, { type: 'inital_state' });
const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>('config');
const nextConfigState = { ...configInitialState };
// If they have a saved node, make sure we assign that too. The node selected
// isn't serializable, so we have to assign it here.
if (savedConfigState) {
// we assign networks first so that when we re-hydrate custom nodes, we can check that the network exists
nextConfigState.networks = rehydrateNetworks(
configInitialState.networks,
savedConfigState.networks
);
nextConfigState.nodes = rehydrateNodes(
configInitialState.nodes,
savedConfigState.nodes,
nextConfigState.networks
);
nextConfigState.meta = { ...nextConfigState.meta, ...savedConfigState.meta };
}
const { customNodes, selectedNode: { nodeId }, staticNodes } = nextConfigState.nodes;
const selectedNode = isStaticNodeId(appInitialState, nodeId)
? staticNodes[nodeId]
: customNodes[nodeId];
if (!selectedNode) {
return { config: configInitialState, customTokens: customTokensInitialState };
}
const nextCustomTokenState = rehydrateCustomTokens(
nextConfigState.networks,
selectedNode.network
);
return { config: nextConfigState, customTokens: nextCustomTokenState };
}
function rehydrateCustomTokens(networkState: ConfigState['networks'], selectedNetwork: string) {
// Dedupe custom tokens initially
const savedCustomTokensState =
loadStatePropertyOrEmptyObject<CustomTokenState>('customTokens') || customTokensInitialState;
const { customNetworks, staticNetworks } = networkState;
const network = isStaticNetworkId(appInitialState, selectedNetwork)
? staticNetworks[selectedNetwork]
: customNetworks[selectedNetwork];
return network.isCustom
? savedCustomTokensState
: dedupeCustomTokens(network.tokens, savedCustomTokensState);
}
function rehydrateNetworks(
initialState: ConfigState['networks'],
savedState: ConfigState['networks']
): ConfigState['networks'] {
const nextNetworkState = { ...initialState };
nextNetworkState.customNetworks = savedState.customNetworks;
return nextNetworkState;
}
function rehydrateNodes(
initalState: ConfigState['nodes'],
savedState: ConfigState['nodes'],
networkState: ConfigState['networks']
): ConfigState['nodes'] {
const nextNodeState = { ...initalState };
// re-assign the hydrated nodes
nextNodeState.customNodes = rehydrateCustomNodes(savedState.customNodes, networkState);
const { customNodes, staticNodes } = nextNodeState;
nextNodeState.selectedNode = getSavedSelectedNode(
nextNodeState.selectedNode,
savedState.selectedNode,
customNodes,
staticNodes
);
return nextNodeState;
}
function getSavedSelectedNode(
initialState: ConfigState['nodes']['selectedNode'],
savedState: ConfigState['nodes']['selectedNode'],
customNodes: ConfigState['nodes']['customNodes'],
staticNodes: ConfigState['nodes']['staticNodes']
): ConfigState['nodes']['selectedNode'] {
const { nodeId: savedNodeId } = savedState;
// if 'web3' has persisted as node selection, reset to app default
// necessary because web3 is only initialized as a node upon MetaMask / Mist unlock
if (savedNodeId === 'web3') {
return { nodeId: initialState.nodeId, pending: false };
}
const nodeConfigExists = isStaticNodeId(appInitialState, savedNodeId)
? staticNodes[savedNodeId]
: customNodes[savedNodeId];
return { nodeId: nodeConfigExists ? savedNodeId : initialState.nodeId, pending: false };
}
function rehydrateCustomNodes(
state: ConfigState['nodes']['customNodes'],
networkState: ConfigState['networks']
) {
const networkExists = (networkId: string) =>
Object.keys(networkState.customNetworks).includes(networkId) ||
Object.keys(networkState.staticNetworks).includes(networkId);
const rehydratedCustomNodes = Object.entries(state).reduce(
(hydratedNodes, [customNodeId, configToHydrate]) => {
if (!networkExists(configToHydrate.network)) {
return hydratedNodes;
}
const lib = new CustomNode(configToHydrate);
const hydratedNode: CustomNodeConfig = { ...configToHydrate, lib };
return { ...hydratedNodes, [customNodeId]: hydratedNode };
},
{} as ConfigState['nodes']['customNodes']
);
return rehydratedCustomNodes;
}

1
common/store/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './store';

107
common/store/store.ts Normal file
View File

@ -0,0 +1,107 @@
import throttle from 'lodash/throttle';
import { routerMiddleware } from 'react-router-redux';
import {
INITIAL_STATE as transactionInitialState,
State as TransactionState
} from 'reducers/transaction';
import { State as SwapState, INITIAL_STATE as swapInitialState } from 'reducers/swap';
import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import { loadStatePropertyOrEmptyObject, saveState } from 'utils/localStorage';
import RootReducer, { AppState } from 'reducers';
import sagas from 'sagas';
import { gasPricetoBase } from 'libs/units';
import {
rehydrateConfigAndCustomTokenState,
getConfigAndCustomTokensStateToSubscribe
} from './configAndTokens';
const configureStore = () => {
const logger = createLogger({
collapsed: true
});
const sagaMiddleware = createSagaMiddleware();
let middleware;
let store;
if (process.env.NODE_ENV !== 'production') {
middleware = composeWithDevTools(
applyMiddleware(sagaMiddleware, logger, routerMiddleware(history as any))
);
} else {
middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history as any));
}
const localSwapState = loadStatePropertyOrEmptyObject<SwapState>('swap');
const swapState =
localSwapState && localSwapState.step === 3
? {
...swapInitialState,
...localSwapState
}
: { ...swapInitialState };
const savedTransactionState = loadStatePropertyOrEmptyObject<TransactionState>('transaction');
const persistedInitialState = {
transaction: {
...transactionInitialState,
fields: {
...transactionInitialState.fields,
gasPrice:
savedTransactionState && savedTransactionState.fields.gasPrice
? {
raw: savedTransactionState.fields.gasPrice.raw,
value: gasPricetoBase(+savedTransactionState.fields.gasPrice.raw)
}
: transactionInitialState.fields.gasPrice
}
},
// ONLY LOAD SWAP STATE FROM LOCAL STORAGE IF STEP WAS 3
swap: swapState,
...rehydrateConfigAndCustomTokenState()
};
store = createStore(RootReducer, persistedInitialState, middleware);
// Add all of the sagas to the middleware
Object.keys(sagas).forEach(saga => {
sagaMiddleware.run(sagas[saga]);
});
store.subscribe(
throttle(() => {
const state: AppState = store.getState();
saveState({
transaction: {
fields: {
gasPrice: state.transaction.fields.gasPrice
}
},
swap: {
...state.swap,
options: {
byId: {},
allIds: []
},
bityRates: {
byId: {},
allIds: []
},
shapeshiftRates: {
byId: {},
allIds: []
}
},
...getConfigAndCustomTokensStateToSubscribe(state)
});
}, 50)
);
return store;
};
export const configuredStore = configureStore();

View File

@ -1,120 +0,0 @@
import {
CustomNetworkConfig,
DPathFormats,
InsecureWalletName,
NetworkConfig,
NETWORKS,
SecureWalletName,
WalletName,
walletNames
} from 'config';
import { DPath, EXTRA_PATHS } from 'config/dpaths';
import sortedUniq from 'lodash/sortedUniq';
import difference from 'lodash/difference';
export function makeCustomNetworkId(config: CustomNetworkConfig): string {
return config.chainId ? `${config.chainId}` : `${config.name}:${config.unit}`;
}
export function makeNetworkConfigFromCustomConfig(config: CustomNetworkConfig): NetworkConfig {
// TODO - re-enable this block and classify customConfig after user-inputted dPaths are implemented
// -------------------------------------------------
// this still provides the type safety we want
// as we know config coming in is CustomNetworkConfig
// meaning name will be a string
// then we cast it as any to keep it as a network key
// interface Override extends NetworkConfig {
// name: any;
// }
// -------------------------------------------------
// TODO - allow for user-inputted dPaths so we don't need to use any below and can use supplied dPaths
// In the meantime, networks with an unknown chainId will have HD wallets disabled
const customConfig: any = {
...config,
color: '#000',
tokens: [],
contracts: []
};
return customConfig;
}
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);
}
}
type PathType = keyof DPathFormats;
type DPathFormat =
| SecureWalletName.TREZOR
| SecureWalletName.LEDGER_NANO_S
| InsecureWalletName.MNEMONIC_PHRASE;
export function getPaths(pathType: PathType): DPath[] {
const networkPaths: DPath[] = [];
Object.values(NETWORKS).forEach(networkConfig => {
const path = networkConfig.dPathFormats ? networkConfig.dPathFormats[pathType] : [];
if (path) {
networkPaths.push(path as DPath);
}
});
const paths = networkPaths.concat(EXTRA_PATHS);
return sortedUniq(paths);
}
export function getSingleDPath(format: DPathFormat, network: NetworkConfig): DPath {
const dPathFormats = network.dPathFormats;
return dPathFormats[format];
}
export function isNetworkUnit(network: NetworkConfig, unit: string) {
const validNetworks = Object.values(NETWORKS).filter((n: NetworkConfig) => n.unit === unit);
return validNetworks.includes(network);
}
export function isWalletFormatSupportedOnNetwork(
format: WalletName,
network: NetworkConfig
): boolean {
const CHECK_FORMATS: DPathFormat[] = [
SecureWalletName.LEDGER_NANO_S,
SecureWalletName.TREZOR,
InsecureWalletName.MNEMONIC_PHRASE
];
const isHDFormat = (f: string): f is DPathFormat => CHECK_FORMATS.includes(f as DPathFormat);
// Ensure DPath's are found
if (isHDFormat(format)) {
const dPath = network.dPathFormats && network.dPathFormats[format];
return !!dPath;
}
// Ensure Web3 is only enabled on ETH or ETH Testnets (MetaMask does not support other networks)
if (format === SecureWalletName.WEB3) {
return isNetworkUnit(network, 'ETH');
}
// All other wallet formats are supported
return true;
}
export function allWalletFormatsSupportedOnNetwork(network: NetworkConfig): WalletName[] {
return walletNames.filter(walletName => isWalletFormatSupportedOnNetwork(walletName, network));
}
export function unSupportedWalletFormatsOnNetwork(network: NetworkConfig): WalletName[] {
const supportedFormats = allWalletFormatsSupportedOnNetwork(network);
return difference(walletNames, supportedFormats);
}

View File

@ -1,42 +0,0 @@
import { CustomNode } from 'libs/nodes';
import { NODES, NodeConfig, CustomNodeConfig } from 'config';
export function makeCustomNodeId(config: CustomNodeConfig): string {
return `${config.url}:${config.port}`;
}
export function getCustomNodeConfigFromId(
id: string,
configs: CustomNodeConfig[]
): CustomNodeConfig | undefined {
return configs.find(node => makeCustomNodeId(node) === id);
}
export function getNodeConfigFromId(
id: string,
configs: CustomNodeConfig[]
): NodeConfig | undefined {
if (NODES[id]) {
return NODES[id];
}
const config = getCustomNodeConfigFromId(id, configs);
if (config) {
return makeNodeConfigFromCustomConfig(config);
}
}
export function makeNodeConfigFromCustomConfig(config: CustomNodeConfig): NodeConfig {
interface Override extends NodeConfig {
network: any;
}
const customConfig: Override = {
network: config.network,
lib: new CustomNode(config),
service: 'your custom node',
estimateGas: true
};
return customConfig;
}

View File

@ -1,4 +1,4 @@
import { Token } from 'config';
import { Token } from 'types/network';
export function dedupeCustomTokens(networkTokens: Token[], customTokens: Token[]): Token[] {
if (!customTokens.length) {

View File

@ -38,7 +38,7 @@ const HELP_MENU = {
{
label: 'Help / FAQ',
click() {
shell.openExternal('https://support.mycrypto.com/');
shell.openExternal('https://support.mycrypto.com/');
}
},
{

4
shared/types/hardware-wallets.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
interface DPath {
label: string;
value: string; // TODO determine method for more precise typing for path
}

56
shared/types/network.d.ts vendored Normal file
View File

@ -0,0 +1,56 @@
import { StaticNetworksState, CustomNetworksState } from 'reducers/config/networks';
type StaticNetworkIds = 'ETH' | 'Ropsten' | 'Kovan' | 'Rinkeby' | 'ETC' | 'UBQ' | 'EXP';
interface BlockExplorerConfig {
origin: string;
txUrl(txHash: string): string;
addressUrl(address: string): string;
}
interface Token {
address: string;
symbol: string;
decimal: number;
error?: string | null;
}
interface NetworkContract {
name: StaticNetworkIds;
address?: string;
abi: string;
}
interface DPathFormats {
trezor: DPath;
ledgerNanoS: DPath;
mnemonicPhrase: DPath;
}
interface StaticNetworkConfig {
isCustom: false; // used for type guards
name: StaticNetworkIds;
unit: string;
color?: string;
blockExplorer?: BlockExplorerConfig;
tokenExplorer?: {
name: string;
address(address: string): string;
};
chainId: number;
tokens: Token[];
contracts: NetworkContract[] | null;
dPathFormats: DPathFormats;
isTestnet?: boolean;
}
interface CustomNetworkConfig {
isCustom: true; // used for type guards
isTestnet?: boolean;
name: string;
unit: string;
chainId: number;
dPathFormats: DPathFormats | null;
}
type NetworkConfig = StaticNetworksState[StaticNetworkIds] | CustomNetworksState[string];

55
shared/types/node.d.ts vendored Normal file
View File

@ -0,0 +1,55 @@
import { RPCNode, Web3Node } from 'libs/nodes';
import { StaticNetworkIds } from './network';
import { StaticNodesState, CustomNodesState } from 'reducers/config/nodes';
import CustomNode from 'libs/nodes/custom';
interface CustomNodeConfig {
id: string;
isCustom: true;
name: string;
lib: CustomNode;
service: 'your custom node';
url: string;
port: number;
network: string;
auth?: {
username: string;
password: string;
};
}
interface StaticNodeConfig {
isCustom: false;
network: StaticNetworkIds;
lib: RPCNode | Web3Node;
service: string;
estimateGas?: boolean;
hidden?: boolean;
}
interface Web3NodeConfig extends StaticNodeConfig {
lib: Web3Node;
}
declare enum StaticNodeId {
ETH_MYCRYPTO = 'eth_mycrypto',
ETH_ETHSCAN = 'eth_ethscan',
ETH_INFURA = 'eth_infura',
ROP_INFURA = 'rop_infura',
KOV_ETHSCAN = 'kov_ethscan',
RIN_ETHSCAN = 'rin_ethscan',
RIN_INFURA = 'rin_infura',
ETC_EPOOL = 'etc_epool',
UBQ = 'ubq',
EXP_TECH = 'exp_tech'
}
type StaticNodeWithWeb3Id = StaticNodeId | 'web3';
type NonWeb3NodeConfigs = { [key in StaticNodeId]: StaticNodeConfig };
interface Web3NodeConfigs {
web3?: Web3NodeConfig;
}
type NodeConfig = StaticNodesState[StaticNodeId] | CustomNodesState[string];

View File

@ -1,19 +0,0 @@
import { NETWORKS, NetworkConfig } from 'config';
describe('Networks', () => {
Object.keys(NETWORKS).forEach(networkId => {
it(`${networkId} contains non-null dPathFormats`, () => {
const network: NetworkConfig = NETWORKS[networkId];
Object.values(network.dPathFormats).forEach(dPathFormat => {
expect(dPathFormat).toBeTruthy();
});
});
});
it(`contain unique chainIds`, () => {
const networkValues = Object.values(NETWORKS);
const chainIds = networkValues.map(a => a.chainId);
const chainIdsSet = new Set(chainIds);
expect(Array.from(chainIdsSet).length).toEqual(chainIds.length);
});
});

View File

@ -1,5 +1,5 @@
module.exports = {
RpcNodes: ['eth_mew', 'etc_epool', 'etc_epool', 'rop_mew'],
RpcNodes: ['eth_mycrypto', 'etc_epool', 'etc_epool', 'rop_mew'],
EtherscanNodes: ['eth_ethscan', 'kov_ethscan', 'rin_ethscan'],
InfuraNodes: ['eth_infura', 'rop_infura', 'rin_infura']
};

View File

@ -1,4 +1,3 @@
import { NODES, NodeConfig } from 'config';
import { RPCNode } from '../../common/libs/nodes';
import { Validator } from 'jsonschema';
import { schema } from '../../common/libs/validators';
@ -6,6 +5,8 @@ import 'url-search-params-polyfill';
import EtherscanNode from 'libs/nodes/etherscan';
import InfuraNode from 'libs/nodes/infura';
import RpcNodeTestConfig from './RpcNodeTestConfig';
import { StaticNodeConfig } from 'types/node';
import { staticNodesExpectedState } from '../reducers/config/nodes/staticNodes.spec';
const v = new Validator();
@ -63,7 +64,7 @@ function testRpcRequests(node: RPCNode, service: string) {
});
}
const mapNodeEndpoints = (nodes: { [key: string]: NodeConfig }) => {
const mapNodeEndpoints = (nodes: { [key: string]: StaticNodeConfig }) => {
const { RpcNodes, EtherscanNodes, InfuraNodes } = RpcNodeTestConfig;
RpcNodes.forEach(n => {
@ -79,4 +80,6 @@ const mapNodeEndpoints = (nodes: { [key: string]: NodeConfig }) => {
});
};
mapNodeEndpoints(NODES);
mapNodeEndpoints((staticNodesExpectedState.initialState as any) as {
[key: string]: StaticNodeConfig;
});

View File

@ -4,21 +4,14 @@ import Adapter from 'enzyme-adapter-react-16';
import SendTransaction from 'containers/Tabs/SendTransaction';
import shallowWithStore from '../utils/shallowWithStore';
import { createMockStore } from 'redux-test-utils';
import { NODES } from 'config';
import { RouteComponentProps } from 'react-router';
import { createMockRouteComponentProps } from '../utils/mockRouteComponentProps';
import { config } from 'reducers/config';
Enzyme.configure({ adapter: new Adapter() });
it('render snapshot', () => {
const testNode = 'rop_mew';
const testStateConfig = {
languageSelection: 'en',
nodeSelection: testNode,
node: NODES[testNode],
gasPriceGwei: 21,
offline: false
};
const testStateConfig = config(undefined as any, {} as any);
const testState = {
wallet: {},
balance: {},

View File

@ -5,7 +5,7 @@ import Swap from 'containers/Tabs/Swap';
import shallowWithStore from '../utils/shallowWithStore';
import { createMockStore } from 'redux-test-utils';
import { INITIAL_STATE as swap } from 'reducers/swap';
import { INITIAL_STATE as config } from 'reducers/config';
import { config } from 'reducers/config';
import { RouteComponentProps } from 'react-router';
import { createMockRouteComponentProps } from '../utils/mockRouteComponentProps';
@ -22,7 +22,7 @@ const routeProps: RouteComponentProps<any> = createMockRouteComponentProps({
});
it('render snapshot', () => {
const store = createMockStore({ swap, config });
const store = createMockStore({ swap, config: config(undefined as any, {} as any) });
const component = shallowWithStore(<Swap {...routeProps} />, store);
expect(component).toMatchSnapshot();

View File

@ -41,5 +41,6 @@ exports[`render snapshot 1`] = `
"url": "/account",
}
}
requestDisabled={false}
/>
`;

View File

@ -1,91 +0,0 @@
import { config, INITIAL_STATE } from 'reducers/config';
import * as configActions from 'actions/config';
import { NODES, NETWORKS } from 'config';
import { makeCustomNodeId, makeNodeConfigFromCustomConfig } from 'utils/node';
const custNode = {
name: 'Test Config',
url: 'https://somecustomconfig.org/',
port: 443,
network: 'ETH'
};
describe('config reducer', () => {
it('should handle CONFIG_LANGUAGE_CHANGE', () => {
const language = 'en';
expect(config(undefined, configActions.changeLanguage(language))).toEqual({
...INITIAL_STATE,
languageSelection: language
});
});
it('should handle CONFIG_NODE_CHANGE', () => {
const key = Object.keys(NODES)[0];
const node = NODES[key];
const network = NETWORKS[node.network];
expect(config(undefined, configActions.changeNode(key, node, network))).toEqual({
...INITIAL_STATE,
node: NODES[key],
nodeSelection: key
});
});
it('should handle CONFIG_TOGGLE_OFFLINE', () => {
const offlineState = {
...INITIAL_STATE,
offline: true
};
const onlineState = {
...INITIAL_STATE,
offline: false
};
expect(config(offlineState, configActions.toggleOfflineConfig())).toEqual({
...offlineState,
offline: false
});
expect(config(onlineState, configActions.toggleOfflineConfig())).toEqual({
...onlineState,
offline: true
});
});
it('should handle CONFIG_ADD_CUSTOM_NODE', () => {
expect(config(undefined, configActions.addCustomNode(custNode))).toEqual({
...INITIAL_STATE,
customNodes: [custNode]
});
});
describe('should handle CONFIG_REMOVE_CUSTOM_NODE', () => {
const customNodeId = makeCustomNodeId(custNode);
const addedState = config(undefined, configActions.addCustomNode(custNode));
const addedAndActiveState = config(
addedState,
configActions.changeNode(
customNodeId,
makeNodeConfigFromCustomConfig(custNode),
NETWORKS[custNode.network]
)
);
const removedState = config(addedAndActiveState, configActions.removeCustomNode(custNode));
it('should remove the custom node from `customNodes`', () => {
expect(removedState.customNodes.length).toBe(0);
});
it('should change the active node, if the custom one was active', () => {
expect(removedState.nodeSelection === customNodeId).toBeFalsy();
});
});
it('should handle CONFIG_SET_LATEST_BLOCK', () => {
expect(config(undefined, configActions.setLatestBlock('12345'))).toEqual({
...INITIAL_STATE,
latestBlock: '12345'
});
});
});

View File

@ -8,7 +8,17 @@ Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [],
"context": null,
"context": RpcNode {
"client": RPCClient {
"batch": [Function],
"call": [Function],
"createHeaders": [Function],
"decorateRequest": [Function],
"endpoint": "https://node.expanse.tech/",
"headers": Object {},
},
"requests": RPCRequests {},
},
"fn": [Function],
},
},
@ -29,25 +39,9 @@ Object {
exports[`handleNodeChangeIntent* should select getCustomNodeConfig and match race snapshot 1`] = `
Object {
"@@redux-saga/IO": true,
"RACE": Object {
"lb": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [],
"context": null,
"fn": [Function],
},
},
"to": Object {
"@@redux-saga/IO": true,
"CALL": Object {
"args": Array [
5000,
],
"context": null,
"fn": [Function],
},
},
"SELECT": Object {
"args": Array [],
"selector": [Function],
},
}
`;

View File

@ -2,33 +2,38 @@ import { configuredStore } from 'store';
import { delay } from 'redux-saga';
import { call, cancel, fork, put, take, select } from 'redux-saga/effects';
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
import { toggleOfflineConfig, changeNode, changeNodeIntent, setLatestBlock } from 'actions/config';
import { toggleOffline, changeNode, changeNodeIntent, setLatestBlock } from 'actions/config';
import {
pollOfflineStatus,
handlePollOfflineStatus,
handleNodeChangeIntent,
unsetWeb3Node,
unsetWeb3NodeOnWalletEvent,
equivalentNodeOrDefault,
handlePollOfflineStatus,
pollOfflineStatus,
reload
} from 'sagas/config';
import { NODES, NodeConfig, NETWORKS } from 'config';
} from 'sagas/config/node';
import {
getNode,
getNodeId,
getNodeConfig,
getOffline,
getCustomNodeConfigs,
getCustomNetworkConfigs
isStaticNodeId,
getStaticNodeFromId,
getNetworkConfigById,
getCustomNodeFromId,
getStaticAltNodeIdToWeb3
} from 'selectors/config';
import { INITIAL_STATE as configInitialState } from 'reducers/config';
import { Web3Wallet } from 'libs/wallet';
import { RPCNode } from 'libs/nodes';
import { showNotification } from 'actions/notifications';
import { translateRaw } from 'translations';
import { StaticNodeConfig } from 'types/node';
import { staticNodesExpectedState } from './nodes/staticNodes.spec';
import { metaExpectedState } from './meta/meta.spec';
import { selectedNodeExpectedState } from './nodes/selectedNode.spec';
import { customNodesExpectedState, firstCustomNodeId } from './nodes/customNodes.spec';
import { unsetWeb3Node, unsetWeb3NodeOnWalletEvent } from 'sagas/config/web3';
// init module
configuredStore.getState();
describe('pollOfflineStatus*', () => {
const { togglingToOffline, togglingToOnline } = metaExpectedState;
const nav = navigator as any;
const doc = document as any;
const data = {} as any;
@ -38,7 +43,6 @@ describe('pollOfflineStatus*', () => {
ping: jest.fn()
}
};
const isOffline = true;
const raceSuccess = {
pingSucceeded: true,
timeout: false
@ -88,29 +92,29 @@ describe('pollOfflineStatus*', () => {
it('should call delay if document is hidden', () => {
data.hiddenDoc = data.gen.clone();
doc.hidden = true;
expect(data.hiddenDoc.next(!isOffline).value).toEqual(call(delay, 1000));
expect(data.hiddenDoc.next(togglingToOnline.offline).value).toEqual(call(delay, 1000));
doc.hidden = false;
});
it('should race pingSucceeded and timeout', () => {
data.isOfflineClone = data.gen.clone();
data.shouldDelayClone = data.gen.clone();
expect(data.gen.next(isOffline).value).toMatchSnapshot();
expect(data.gen.next(togglingToOffline.offline).value).toMatchSnapshot();
});
it('should toggle offline and show notification if navigator disagrees with isOffline and ping succeeds', () => {
expect(data.gen.next(raceSuccess).value).toEqual(
put(showNotification('success', 'Your connection to the network has been restored!', 3000))
);
expect(data.gen.next().value).toEqual(put(toggleOfflineConfig()));
expect(data.gen.next().value).toEqual(put(toggleOffline()));
});
it('should toggle offline and show notification if navigator agrees with isOffline and ping fails', () => {
nav.onLine = isOffline;
expect(data.isOfflineClone.next(!isOffline));
nav.onLine = togglingToOffline.offline;
expect(data.isOfflineClone.next(togglingToOnline.offline));
expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot();
expect(data.isOfflineClone.next().value).toEqual(put(toggleOfflineConfig()));
nav.onLine = !isOffline;
expect(data.isOfflineClone.next().value).toEqual(put(toggleOffline()));
nav.onLine = togglingToOnline.offline;
});
});
@ -136,17 +140,15 @@ describe('handleNodeChangeIntent*', () => {
let originalRandom;
// normal operation variables
const defaultNode = configInitialState.nodeSelection;
const defaultNodeConfig = NODES[defaultNode];
const customNetworkConfigs = [];
const defaultNodeNetwork = NETWORKS[defaultNodeConfig.network];
const newNode = Object.keys(NODES).reduce(
(acc, cur) => (NODES[cur].network !== defaultNodeConfig.network ? cur : acc)
const defaultNodeId = selectedNodeExpectedState.initialState.nodeId;
const defaultNodeConfig: StaticNodeConfig = staticNodesExpectedState.initialState[defaultNodeId];
const newNodeId = Object.keys(staticNodesExpectedState.initialState).reduce(
(acc, cur) =>
staticNodesExpectedState.initialState[cur].network !== defaultNodeConfig.network ? cur : acc
);
const newNodeConfig = NODES[newNode];
const newNodeNetwork = NETWORKS[newNodeConfig.network];
const newNodeConfig: StaticNodeConfig = staticNodesExpectedState.initialState[newNodeId];
const changeNodeIntentAction = changeNodeIntent(newNode);
const changeNodeIntentAction = changeNodeIntent(newNodeId);
const latestBlock = '0xa';
const raceSuccess = {
lb: latestBlock
@ -159,9 +161,10 @@ describe('handleNodeChangeIntent*', () => {
data.gen = cloneableGenerator(handleNodeChangeIntent)(changeNodeIntentAction);
function shouldBailOut(gen, nextVal, errMsg) {
expect(gen.next(nextVal).value).toEqual(put(showNotification('danger', errMsg, 5000)));
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(defaultNode, defaultNodeConfig, defaultNodeNetwork))
put(changeNode({ networkId: defaultNodeConfig.network, nodeId: defaultNodeId }))
);
expect(gen.next().done).toEqual(true);
}
@ -175,34 +178,38 @@ describe('handleNodeChangeIntent*', () => {
Math.random = originalRandom;
});
it('should select getNode', () => {
expect(data.gen.next().value).toEqual(select(getNode));
it('should select is static node', () => {
expect(data.gen.next().value).toEqual(select(isStaticNodeId, newNodeId));
});
it('should select nodeConfig', () => {
expect(data.gen.next(defaultNode).value).toEqual(select(getNodeConfig));
expect(data.gen.next(defaultNodeId).value).toEqual(select(getNodeConfig));
});
it('should select getCustomNetworkConfigs', () => {
expect(data.gen.next(defaultNodeConfig).value).toEqual(select(getCustomNetworkConfigs));
it('should select getStaticNodeFromId', () => {
expect(data.gen.next(defaultNodeConfig).value).toEqual(select(getStaticNodeFromId, newNodeId));
});
it('should race getCurrentBlock and delay', () => {
expect(data.gen.next(customNetworkConfigs).value).toMatchSnapshot();
expect(data.gen.next(newNodeConfig).value).toMatchSnapshot();
});
it('should show error and revert to previous node if check times out', () => {
data.clone1 = data.gen.clone();
shouldBailOut(data.clone1, raceFailure, translateRaw('ERROR_32'));
});
it('should getNetworkConfigById', () => {
expect(data.gen.next(raceSuccess).value).toEqual(
select(getNetworkConfigById, newNodeConfig.network)
);
});
it('should put setLatestBlock', () => {
expect(data.gen.next(raceSuccess).value).toEqual(put(setLatestBlock(latestBlock)));
});
it('should put changeNode', () => {
expect(data.gen.next().value).toEqual(
put(changeNode(changeNodeIntentAction.payload, newNodeConfig, newNodeNetwork))
put(changeNode({ networkId: newNodeConfig.network, nodeId: newNodeId }))
);
});
@ -216,41 +223,40 @@ describe('handleNodeChangeIntent*', () => {
});
// custom node variables
const customNodeConfigs = [
{
name: 'name',
url: 'url',
port: 443,
network: 'network'
}
];
const customNodeIdFound = 'url:443';
const customNodeIdNotFound = 'notFound';
const customNodeAction = changeNodeIntent(customNodeIdFound);
const customNodeNotFoundAction = changeNodeIntent(customNodeIdNotFound);
const customNodeConfigs = customNodesExpectedState.addFirstCustomNode;
const customNodeAction = changeNodeIntent(firstCustomNodeId);
data.customNode = handleNodeChangeIntent(customNodeAction);
data.customNodeNotFound = handleNodeChangeIntent(customNodeNotFoundAction);
// test custom node
it('should select getCustomNodeConfig and match race snapshot', () => {
data.customNode.next();
data.customNode.next(defaultNode);
data.customNode.next(defaultNodeConfig);
expect(data.customNode.next(customNetworkConfigs).value).toEqual(select(getCustomNodeConfigs));
data.customNode.next(false);
expect(data.customNode.next(defaultNodeConfig).value).toEqual(
select(getCustomNodeFromId, firstCustomNodeId)
);
expect(data.customNode.next(customNodeConfigs).value).toMatchSnapshot();
});
const customNodeIdNotFound = firstCustomNodeId + 'notFound';
const customNodeNotFoundAction = changeNodeIntent(customNodeIdNotFound);
data.customNodeNotFound = handleNodeChangeIntent(customNodeNotFoundAction);
// test custom node not found
it('should handle unknown / missing custom node', () => {
data.customNodeNotFound.next();
data.customNodeNotFound.next(defaultNode);
data.customNodeNotFound.next(defaultNodeConfig);
expect(data.customNodeNotFound.next(customNetworkConfigs).value).toEqual(
select(getCustomNodeConfigs)
data.customNodeNotFound.next(false);
});
it('should blah', () => {
expect(data.customNodeNotFound.next(defaultNodeConfig).value).toEqual(
select(getCustomNodeFromId, customNodeIdNotFound)
);
});
it('should blahah', () => {
shouldBailOut(
data.customNodeNotFound,
customNodeConfigs,
null,
`Attempted to switch to unknown node '${customNodeNotFoundAction.payload}'`
);
});
@ -258,20 +264,19 @@ describe('handleNodeChangeIntent*', () => {
describe('unsetWeb3Node*', () => {
const node = 'web3';
const mockNodeConfig = { network: 'ETH' } as any;
const newNode = equivalentNodeOrDefault(mockNodeConfig);
const alternativeNodeId = 'eth_mycrypto';
const gen = unsetWeb3Node();
it('should select getNode', () => {
expect(gen.next().value).toEqual(select(getNode));
expect(gen.next().value).toEqual(select(getNodeId));
});
it('should select getNodeConfig', () => {
expect(gen.next(node).value).toEqual(select(getNodeConfig));
it('should select an alternative node to web3', () => {
expect(gen.next(node).value).toEqual(select(getStaticAltNodeIdToWeb3));
});
it('should put changeNodeIntent', () => {
expect(gen.next(mockNodeConfig).value).toEqual(put(changeNodeIntent(newNode)));
expect(gen.next(alternativeNodeId).value).toEqual(put(changeNodeIntent(alternativeNodeId)));
});
it('should be done', () => {
@ -288,22 +293,20 @@ describe('unsetWeb3Node*', () => {
describe('unsetWeb3NodeOnWalletEvent*', () => {
const fakeAction = {};
const mockNode = 'web3';
const mockNodeConfig: Partial<NodeConfig> = { network: 'ETH' };
const mockNodeId = 'web3';
const alternativeNodeId = 'eth_mycrypto';
const gen = unsetWeb3NodeOnWalletEvent(fakeAction);
it('should select getNode', () => {
expect(gen.next().value).toEqual(select(getNode));
expect(gen.next().value).toEqual(select(getNodeId));
});
it('should select getNodeConfig', () => {
expect(gen.next(mockNode).value).toEqual(select(getNodeConfig));
it('should select an alternative node to web3', () => {
expect(gen.next(mockNodeId).value).toEqual(select(getStaticAltNodeIdToWeb3));
});
it('should put changeNodeIntent', () => {
expect(gen.next(mockNodeConfig).value).toEqual(
put(changeNodeIntent(equivalentNodeOrDefault(mockNodeConfig as any)))
);
expect(gen.next(alternativeNodeId).value).toEqual(put(changeNodeIntent(alternativeNodeId)));
});
it('should be done', () => {
@ -327,51 +330,3 @@ describe('unsetWeb3NodeOnWalletEvent*', () => {
expect(gen2.next().done).toEqual(true);
});
});
describe('equivalentNodeOrDefault', () => {
const originalNodeList = Object.keys(NODES);
const appDefaultNode = configInitialState.nodeSelection;
const mockNodeConfig = {
network: 'ETH',
service: 'fakeService',
lib: new RPCNode('fakeEndpoint'),
estimateGas: false
};
afterEach(() => {
Object.keys(NODES).forEach(node => {
if (originalNodeList.indexOf(node) === -1) {
delete NODES[node];
}
});
});
it('should return node with equivalent network', () => {
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'Kovan'
});
expect(NODES[node].network).toEqual('Kovan');
});
it('should return app default if no eqivalent is found', () => {
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'noEqivalentExists'
} as any);
expect(node).toEqual(appDefaultNode);
});
it('should ignore web3 from node list', () => {
NODES.web3 = {
...mockNodeConfig,
network: 'uniqueToWeb3'
} as any;
const node = equivalentNodeOrDefault({
...mockNodeConfig,
network: 'uniqueToWeb3'
} as any);
expect(node).toEqual(appDefaultNode);
});
});

View File

@ -0,0 +1,76 @@
import { meta } from 'reducers/config/meta';
import { changeLanguage, toggleOffline, toggleAutoGasLimit, setLatestBlock } from 'actions/config';
const expectedInitialState = {
languageSelection: 'en',
offline: false,
autoGasLimit: true,
latestBlock: '???'
};
const expectedState = {
initialState: expectedInitialState,
changingLanguage: {
...expectedInitialState,
languageSelection: 'langaugeToChange'
},
togglingToOffline: {
...expectedInitialState,
offline: true
},
togglingToOnline: {
...expectedInitialState,
offline: false
},
togglingToManualGasLimit: {
...expectedInitialState,
autoGasLimit: false
},
togglingToAutoGasLimit: {
...expectedInitialState,
autoGasLimit: true
},
settingLatestBlock: {
...expectedInitialState,
latestBlock: '12345'
}
};
const actions = {
changeLangauge: changeLanguage('langaugeToChange'),
toggleOffline: toggleOffline(),
toggleAutoGasLimit: toggleAutoGasLimit(),
setLatestBlock: setLatestBlock('12345')
};
describe('meta reducer', () => {
it('should return the inital state', () =>
expect(meta(undefined, {} as any)).toEqual(expectedState.initialState));
it('should handle toggling to offline', () =>
expect(meta(expectedState.initialState, actions.toggleOffline)).toEqual(
expectedState.togglingToOffline
));
it('should handle toggling back to online', () =>
expect(meta(expectedState.togglingToOffline, actions.toggleOffline)).toEqual(
expectedState.togglingToOnline
));
it('should handle toggling to manual gas limit', () =>
expect(meta(expectedState.initialState, actions.toggleAutoGasLimit)).toEqual(
expectedState.togglingToManualGasLimit
));
it('should handle toggling back to auto gas limit', () =>
expect(meta(expectedState.togglingToManualGasLimit, actions.toggleAutoGasLimit)).toEqual(
expectedState.togglingToAutoGasLimit
));
it('should handle setting the latest block', () =>
expect(meta(expectedState.initialState, actions.setLatestBlock)).toEqual(
expectedState.settingLatestBlock
));
});
export { actions as metaActions, expectedState as metaExpectedState };

Some files were not shown because too many files have changed in this diff Show More