Split out nodes and networks into their own reducers

This commit is contained in:
henrynguyen5 2018-01-26 18:33:57 -05:00
parent 63ed9c1871
commit 583654f94c
14 changed files with 383 additions and 292 deletions

View File

@ -32,7 +32,7 @@ export function changeNode(
): interfaces.ChangeNodeAction {
return {
type: TypeKeys.CONFIG_NODE_CHANGE,
payload: { nodeSelection, node, network }
payload: { nodeSelection, nodeName, networkName }
};
}

View File

@ -1,5 +1,6 @@
import { TypeKeys } from './constants';
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config';
import { CustomNodeConfig } from 'config';
import { CustomNetworkConfig } from 'reducers/config/networks/typings';
/*** Toggle Offline ***/
export interface ToggleOfflineAction {
@ -21,9 +22,8 @@ export interface ChangeNodeAction {
type: TypeKeys.CONFIG_NODE_CHANGE;
// FIXME $keyof?
payload: {
nodeSelection: string;
node: NodeConfig;
network: NetworkConfig;
nodeName: string;
networkName: string;
};
}
@ -41,25 +41,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 +73,18 @@ export interface Web3UnsetNodeAction {
type: TypeKeys.CONFIG_NODE_WEB3_UNSET;
}
/*** Union Type ***/
export type ConfigAction =
| ChangeNodeAction
export type CustomNetworkAction = AddCustomNetworkAction | RemoveCustomNetworkAction;
export type CustomNodeAction = AddCustomNodeAction | RemoveCustomNodeAction;
export type NodeAction = ChangeNodeAction | ChangeNodeIntentAction | Web3UnsetNodeAction;
export type MetaAction =
| ChangeLanguageAction
| ToggleOfflineAction
| ToggleAutoGasLimitAction
| PollOfflineStatus
| ChangeNodeIntentAction
| AddCustomNodeAction
| RemoveCustomNodeAction
| AddCustomNetworkAction
| RemoveCustomNetworkAction
| SetLatestBlockAction
| Web3UnsetNodeAction;
| SetLatestBlockAction;
/*** Union Type ***/
export type ConfigAction = CustomNetworkAction | CustomNodeAction | NodeAction | MetaAction;

View File

@ -1,176 +0,0 @@
import { EtherscanNode, InfuraNode, RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
export interface CustomNodeConfig {
name: string;
url: string;
port: number;
network: string;
auth?: {
username: string;
password: string;
};
}
export interface NodeConfig {
network: NetworkKeys;
lib: RPCNode | Web3Node;
service: string;
estimateGas?: boolean;
hidden?: boolean;
}
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 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;
}
}
export const Web3Service = 'MetaMask / Mist';
export interface NodeConfigOverride extends NodeConfig {
network: any;
}
export async function initWeb3Node(): Promise<void> {
const { networkId, lib } = await setupWeb3Node();
const web3: NodeConfigOverride = {
network: networkIdToName(networkId),
service: Web3Service,
lib,
estimateGas: false,
hidden: true
};
NODES.web3 = web3;
}

View File

@ -1,36 +1,13 @@
import {
ChangeLanguageAction,
ChangeNodeAction,
AddCustomNodeAction,
RemoveCustomNodeAction,
AddCustomNetworkAction,
RemoveCustomNetworkAction,
SetLatestBlockAction,
ConfigAction
} from 'actions/config';
import { ChangeLanguageAction, 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;
}
@ -55,23 +32,6 @@ function changeLanguage(state: State, action: ChangeLanguageAction): State {
};
}
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,
@ -86,44 +46,6 @@ function toggleAutoGasLimitEstimation(state: State): State {
};
}
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,
@ -135,22 +57,12 @@ export function config(state: State = INITIAL_STATE, action: ConfigAction): Stat
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:

View File

@ -0,0 +1,33 @@
import {
AddCustomNetworkAction,
RemoveCustomNetworkAction,
CustomNetworkAction,
TypeKeys
} from 'actions/config';
import { CustomNetworkConfig } from 'reducers/config/networks/typings';
export interface State {
[customNetworkId: string]: CustomNetworkConfig;
}
const addCustomNetwork = (state: State, { payload }: AddCustomNetworkAction): State => ({
...state,
[payload.id]: payload.config
});
function removeCustomNetwork(state: State, { payload }: RemoveCustomNetworkAction): State {
const stateCopy = { ...state };
Reflect.deleteProperty(stateCopy, payload.id);
return stateCopy;
}
export const customNetworks = (state: State = {}, action: CustomNetworkAction) => {
switch (action.type) {
case TypeKeys.CONFIG_ADD_CUSTOM_NETWORK:
return addCustomNetwork(state, action);
case TypeKeys.CONFIG_REMOVE_CUSTOM_NETWORK:
return removeCustomNetwork(state, action);
default:
return state;
}
};

View File

@ -12,13 +12,15 @@ import {
import {
NetworkConfig,
BlockExplorerConfig,
DefaultNetworkKeys
DefaultNetworkNames
} from 'reducers/config/networks/typings';
import { ConfigAction } from 'actions/config';
export type State = { [key in DefaultNetworkKeys]: NetworkConfig };
export type State = { [key in DefaultNetworkNames]: NetworkConfig };
// 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
function makeExplorer(origin: string): BlockExplorerConfig {
return {
origin,
@ -27,7 +29,7 @@ function makeExplorer(origin: string): BlockExplorerConfig {
};
}
const INITIAL_STATE = {
const INITIAL_STATE: State = {
ETH: {
name: 'ETH',
unit: 'ETH',
@ -134,3 +136,10 @@ const INITIAL_STATE = {
}
}
};
export const defaultNetworks = (state: State = INITIAL_STATE, action: ConfigAction) => {
switch (action.type) {
default:
return state;
}
};

View File

@ -0,0 +1,16 @@
import { customNetworks, State as CustomNetworksState } from './customNetworks';
import { defaultNetworks, State as DefaultNetworksState } from './defaultNetworks';
import { selectedNetwork, State as SelectedNetworkState } from './selectedNetwork';
import { combineReducers } from 'redux';
export interface State {
customNetworks: CustomNetworksState;
defaultNetworks: DefaultNetworksState;
selectedNetwork: SelectedNetworkState;
}
export const networks = combineReducers<State>({
customNetworks,
defaultNetworks,
selectedNetwork
});

View File

@ -0,0 +1,22 @@
import { NodeAction, TypeKeys, ChangeNodeAction } from 'actions/config';
import { DefaultNetworkNames } from 'reducers/config/networks/typings';
import { INITIAL_STATE as INITIAL_NODE_STATE } from '../nodes/selectedNode'; // could probably consolidate this in the index file of 'nodes' to make it easier to import
import { INITIAL_STATE as INITIAL_DEFAULT_NODE_STATE } from '../nodes/defaultNodes';
import { NonWeb3NodeConfigs } from 'reducers/config/nodes/typings';
const initalNode =
INITIAL_DEFAULT_NODE_STATE[INITIAL_NODE_STATE.nodeName as keyof NonWeb3NodeConfigs];
export type State = string | DefaultNetworkNames;
const INITIAL_STATE: State = initalNode.networkName;
const handleNodeChange = (_: State, { payload }: ChangeNodeAction) => payload.networkName;
export const selectedNetwork = (state: State = INITIAL_STATE, action: NodeAction) => {
switch (action.type) {
case TypeKeys.CONFIG_NODE_CHANGE:
return handleNodeChange(state, action);
default:
break;
}
};

View File

@ -1,6 +1,6 @@
import { DPath } from 'config/dpaths';
export type DefaultNetworkKeys = 'ETH' | 'Ropsten' | 'Kovan' | 'Rinkeby' | 'ETC' | 'UBQ' | 'EXP';
export type DefaultNetworkNames = 'ETH' | 'Ropsten' | 'Kovan' | 'Rinkeby' | 'ETC' | 'UBQ' | 'EXP';
export interface BlockExplorerConfig {
origin: string;
@ -16,7 +16,7 @@ export interface Token {
}
export interface NetworkContract {
name: DefaultNetworkKeys;
name: DefaultNetworkNames;
address?: string;
abi: string;
}
@ -29,7 +29,7 @@ export interface DPathFormats {
export interface NetworkConfig {
// TODO really try not to allow strings due to custom networks
name: DefaultNetworkKeys;
name: DefaultNetworkNames;
unit: string;
color?: string;
blockExplorer?: BlockExplorerConfig;

View File

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

View File

@ -0,0 +1,87 @@
import { NonWeb3NodeConfigs, Web3NodeConfig } from 'reducers/config/nodes/typings';
import { EtherscanNode, InfuraNode, RPCNode } from 'libs/nodes';
import { ConfigAction } from 'actions/config';
export type State = NonWeb3NodeConfigs & Web3NodeConfig;
export const INITIAL_STATE: State = {
eth_mew: {
networkName: 'ETH',
lib: new RPCNode('https://api.myetherapi.com/eth'),
service: 'MyEtherWallet',
estimateGas: true
},
eth_mycrypto: {
networkName: 'ETH',
lib: new RPCNode('https://api.mycryptoapi.com/eth'),
service: 'MyCrypto',
estimateGas: true
},
eth_ethscan: {
networkName: 'ETH',
service: 'Etherscan.io',
lib: new EtherscanNode('https://api.etherscan.io/api'),
estimateGas: false
},
eth_infura: {
networkName: 'ETH',
service: 'infura.io',
lib: new InfuraNode('https://mainnet.infura.io/mew'),
estimateGas: false
},
rop_mew: {
networkName: 'Ropsten',
service: 'MyEtherWallet',
lib: new RPCNode('https://api.myetherapi.com/rop'),
estimateGas: false
},
rop_infura: {
networkName: 'Ropsten',
service: 'infura.io',
lib: new InfuraNode('https://ropsten.infura.io/mew'),
estimateGas: false
},
kov_ethscan: {
networkName: 'Kovan',
service: 'Etherscan.io',
lib: new EtherscanNode('https://kovan.etherscan.io/api'),
estimateGas: false
},
rin_ethscan: {
networkName: 'Rinkeby',
service: 'Etherscan.io',
lib: new EtherscanNode('https://rinkeby.etherscan.io/api'),
estimateGas: false
},
rin_infura: {
networkName: 'Rinkeby',
service: 'infura.io',
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
estimateGas: false
},
etc_epool: {
networkName: 'ETC',
service: 'Epool.io',
lib: new RPCNode('https://mewapi.epool.io'),
estimateGas: false
},
ubq: {
networkName: 'UBQ',
service: 'ubiqscan.io',
lib: new RPCNode('https://pyrus2.ubiqscan.io'),
estimateGas: true
},
exp_tech: {
networkName: 'EXP',
service: 'Expanse.tech',
lib: new RPCNode('https://node.expanse.tech/'),
estimateGas: true
}
};
export const defaultNodes = (state: State = INITIAL_STATE, action: ConfigAction) => {
switch (action.type) {
default:
return state;
}
};

View File

@ -0,0 +1,12 @@
import { customNodes, State as CustomNodeState } from './customNodes';
import { defaultNodes, State as DefaultNodeState } from './defaultNodes';
import { selectedNode, State as SelectedNodeState } from './selectedNode';
import { combineReducers } from 'redux';
export interface State {
customNodes: CustomNodeState;
defaultNodes: DefaultNodeState;
selectedNode: SelectedNodeState;
}
export const nodes = combineReducers<State>({ customNodes, defaultNodes, selectedNode });

View File

@ -0,0 +1,39 @@
import { ChangeNodeAction, ChangeNodeIntentAction, NodeAction, TypeKeys } from 'actions/config';
interface NodeLoaded {
pending: false;
nodeName: string;
}
interface NodeChangePending {
pending: true;
nodeName: null;
}
export type State = NodeLoaded | NodeChangePending;
export const INITIAL_STATE: NodeLoaded = {
nodeName: 'eth_mew',
pending: false
};
const changeNode = (_: State, { payload }: ChangeNodeAction): State => ({
nodeName: payload.networkName,
pending: false
});
const changeNodeIntent = (_: State, _2: ChangeNodeIntentAction): State => ({
nodeName: null,
pending: true
});
export const selectedNode = (state: State = INITIAL_STATE, action: NodeAction) => {
switch (action.type) {
case TypeKeys.CONFIG_NODE_CHANGE:
return changeNode(state, action);
case TypeKeys.CONFIG_NODE_CHANGE_INTENT:
return changeNodeIntent(state, action);
default:
return state;
}
};

View File

@ -0,0 +1,103 @@
import { RPCNode, Web3Node } from 'libs/nodes';
import { networkIdToName } from 'libs/values';
import { DefaultNetworkNames } from 'reducers/config/networks/typings';
export interface CustomNodeConfig {
name: string;
url: string;
port: number;
network: string;
auth?: {
username: string;
password: string;
};
}
export interface DefaultNodeConfig {
networkName: DefaultNetworkNames;
lib: RPCNode | Web3Node;
service: string;
estimateGas?: boolean;
hidden?: boolean;
}
export enum DefaultNodeName {
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'
}
export type NonWeb3NodeConfigs = { [key in DefaultNodeName]: DefaultNodeConfig };
export interface Web3NodeConfig {
web3?: DefaultNodeConfig;
}
/**
* TODO: Put this in a saga that runs on app mount
*/
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 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;
}
}
export const Web3Service = 'MetaMask / Mist';
export interface NodeConfigOverride extends DefaultNodeConfig {
networkName: 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;
}