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:
parent
128471dc09
commit
01fc5f1a89
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, {}> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -46,7 +46,7 @@ const tabs: TabLink[] = [
|
|||
];
|
||||
|
||||
interface Props {
|
||||
color?: string;
|
||||
color?: string | false;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -27,7 +27,7 @@ interface OwnProps {
|
|||
}
|
||||
|
||||
interface StateProps {
|
||||
autoGasLimitEnabled: AppState['config']['autoGasLimit'];
|
||||
autoGasLimitEnabled: AppState['config']['meta']['autoGasLimit'];
|
||||
validGasPrice: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ interface Option<T> {
|
|||
name: any;
|
||||
value: T;
|
||||
color?: string;
|
||||
hidden: boolean | undefined;
|
||||
hidden?: boolean | undefined;
|
||||
onRemove?(): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export * from './networks';
|
||||
export * from './data';
|
||||
export * from './bity';
|
||||
export * from './addressMessages';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ${
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 });
|
|
@ -0,0 +1 @@
|
|||
export * from './meta';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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 };
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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 };
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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[];
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ export interface AppState {
|
|||
transaction: TransactionState;
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
export default combineReducers<AppState>({
|
||||
config,
|
||||
swap,
|
||||
notifications,
|
||||
|
|
|
@ -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',
|
||||
`You’ve 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);
|
||||
}
|
|
@ -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]);
|
||||
}
|
|
@ -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)];
|
|
@ -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',
|
||||
`You’ve 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)
|
||||
];
|
|
@ -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)
|
||||
];
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from './meta';
|
||||
export * from './networks';
|
||||
export * from './nodes';
|
||||
export * from './tokens';
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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)];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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`
|
||||
);
|
||||
|
||||
|
|
128
common/store.ts
128
common/store.ts
|
@ -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();
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './store';
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Token } from 'config';
|
||||
import { Token } from 'types/network';
|
||||
|
||||
export function dedupeCustomTokens(networkTokens: Token[], customTokens: Token[]): Token[] {
|
||||
if (!customTokens.length) {
|
||||
|
|
|
@ -38,7 +38,7 @@ const HELP_MENU = {
|
|||
{
|
||||
label: 'Help / FAQ',
|
||||
click() {
|
||||
shell.openExternal('https://support.mycrypto.com/');
|
||||
shell.openExternal('https://support.mycrypto.com/');
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
interface DPath {
|
||||
label: string;
|
||||
value: string; // TODO determine method for more precise typing for path
|
||||
}
|
|
@ -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];
|
|
@ -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];
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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']
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -41,5 +41,6 @@ exports[`render snapshot 1`] = `
|
|||
"url": "/account",
|
||||
}
|
||||
}
|
||||
requestDisabled={false}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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],
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
Loading…
Reference in New Issue