diff --git a/common/reducers/config/index.ts b/common/reducers/config/index.ts index e69de29b..8389d994 100644 --- a/common/reducers/config/index.ts +++ b/common/reducers/config/index.ts @@ -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({ meta, networks, nodes }); diff --git a/common/reducers/config/meta/index.ts b/common/reducers/config/meta/index.ts index e69de29b..c9942f46 100644 --- a/common/reducers/config/meta/index.ts +++ b/common/reducers/config/meta/index.ts @@ -0,0 +1 @@ +export * from './meta'; diff --git a/common/reducers/config.ts b/common/reducers/config/meta/meta.ts similarity index 72% rename from common/reducers/config.ts rename to common/reducers/config/meta/meta.ts index 0f59d06b..5a804457 100644 --- a/common/reducers/config.ts +++ b/common/reducers/config/meta/meta.ts @@ -1,27 +1,18 @@ -import { ChangeLanguageAction, SetLatestBlockAction, ConfigAction } from 'actions/config'; +import { ChangeLanguageAction, SetLatestBlockAction, MetaAction } from 'actions/config'; import { TypeKeys } from 'actions/config/constants'; export interface State { // FIXME languageSelection: string; - nodeSelection: string; - isChangingNode: boolean; offline: boolean; autoGasLimit: boolean; latestBlock: string; } -const defaultNode = 'eth_mew'; -export const INITIAL_STATE: State = { +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: '???' }; @@ -53,7 +44,7 @@ function setLatestBlock(state: State, action: SetLatestBlockAction): State { }; } -export function config(state: State = INITIAL_STATE, action: ConfigAction): State { +export function meta(state: State = INITIAL_STATE, action: MetaAction): State { switch (action.type) { case TypeKeys.CONFIG_LANGUAGE_CHANGE: return changeLanguage(state, action); diff --git a/common/reducers/config/networks/customNetworks.ts b/common/reducers/config/networks/customNetworks.ts index 557c64d6..b7269f42 100644 --- a/common/reducers/config/networks/customNetworks.ts +++ b/common/reducers/config/networks/customNetworks.ts @@ -6,10 +6,13 @@ import { } from 'actions/config'; import { CustomNetworkConfig } from 'reducers/config/networks/typings'; -export interface State { +// TODO: this doesn't accurately represent state, as +interface State1 { [customNetworkId: string]: CustomNetworkConfig; } +export type State = Partial; + const addCustomNetwork = (state: State, { payload }: AddCustomNetworkAction): State => ({ ...state, [payload.id]: payload.config diff --git a/common/reducers/config/networks/defaultNetworks.ts b/common/reducers/config/networks/defaultNetworks.ts index b88ff8bf..a38dcf2b 100644 --- a/common/reducers/config/networks/defaultNetworks.ts +++ b/common/reducers/config/networks/defaultNetworks.ts @@ -10,13 +10,13 @@ import { UBQ_DEFAULT } from 'config/dpaths'; import { - NetworkConfig, + DefaultNetworkConfig, BlockExplorerConfig, DefaultNetworkNames } from 'reducers/config/networks/typings'; import { ConfigAction } from 'actions/config'; -export type State = { [key in DefaultNetworkNames]: NetworkConfig }; +export type State = { [key in DefaultNetworkNames]: DefaultNetworkConfig }; // Must be a website that follows the ethplorer convention of /tx/[hash] and // address/[address] to generate the correct functions. diff --git a/common/reducers/config/networks/typings.ts b/common/reducers/config/networks/typings.ts index d6449ade..201d5d93 100644 --- a/common/reducers/config/networks/typings.ts +++ b/common/reducers/config/networks/typings.ts @@ -27,7 +27,7 @@ export interface DPathFormats { mnemonicPhrase: DPath; } -export interface NetworkConfig { +export interface DefaultNetworkConfig { // TODO really try not to allow strings due to custom networks name: DefaultNetworkNames; unit: string; diff --git a/common/reducers/config/nodes/selectedNode.ts b/common/reducers/config/nodes/selectedNode.ts index 12a747dd..907ff581 100644 --- a/common/reducers/config/nodes/selectedNode.ts +++ b/common/reducers/config/nodes/selectedNode.ts @@ -7,7 +7,7 @@ interface NodeLoaded { interface NodeChangePending { pending: true; - nodeName: null; + nodeName: undefined; } export type State = NodeLoaded | NodeChangePending; @@ -23,7 +23,7 @@ const changeNode = (_: State, { payload }: ChangeNodeAction): State => ({ }); const changeNodeIntent = (_: State, _2: ChangeNodeIntentAction): State => ({ - nodeName: null, + nodeName: undefined, pending: true }); diff --git a/common/sagas/config.ts b/common/sagas/config.ts index db184fce..2c72d3c1 100644 --- a/common/sagas/config.ts +++ b/common/sagas/config.ts @@ -25,7 +25,7 @@ import { } from 'utils/node'; import { makeCustomNetworkId, getNetworkConfigFromId } from 'utils/network'; import { - getNode, + getNodeName, getNodeConfig, getCustomNodeConfigs, getCustomNetworkConfigs, @@ -120,7 +120,7 @@ export function* reload(): SagaIterator { } export function* handleNodeChangeIntent(action: ChangeNodeIntentAction): SagaIterator { - const currentNode: string = yield select(getNode); + const currentNode: string = yield select(getNodeName); const currentConfig: NodeConfig = yield select(getNodeConfig); const customNets: CustomNetworkConfig[] = yield select(getCustomNetworkConfigs); const currentNetwork = @@ -213,7 +213,7 @@ export function* cleanCustomNetworks(): SagaIterator { // 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 node = yield select(getNodeName); const nodeConfig = yield select(getNodeConfig); const newWallet = action.payload; const isWeb3Wallet = newWallet instanceof Web3Wallet; @@ -227,7 +227,7 @@ export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator { } export function* unsetWeb3Node(): SagaIterator { - const node = yield select(getNode); + const node = yield select(getNodeName); if (node !== 'web3') { return; diff --git a/common/selectors/config.ts b/common/selectors/config.ts deleted file mode 100644 index e9fd8f4e..00000000 --- a/common/selectors/config.ts +++ /dev/null @@ -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; -} diff --git a/common/selectors/config/index.ts b/common/selectors/config/index.ts new file mode 100644 index 00000000..a413631a --- /dev/null +++ b/common/selectors/config/index.ts @@ -0,0 +1,7 @@ +import { AppState } from 'reducers'; +export * from './meta'; +export * from './networks'; +export * from './nodes'; +export * from './tokens'; + +export const getConfig = (state: AppState) => state.config; diff --git a/common/selectors/config/meta.ts b/common/selectors/config/meta.ts new file mode 100644 index 00000000..48b1ddbe --- /dev/null +++ b/common/selectors/config/meta.ts @@ -0,0 +1,20 @@ +import { AppState } from 'reducers'; +import { getConfig } from 'sagas/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 { + return getMeta(state).autoGasLimit; +} + +export function getLanguageSelection(state: AppState): string { + return getMeta(state).languageSelection; +} + +export function getLatestBlock(state: AppState) { + return getMeta(state).latestBlock; +} diff --git a/common/selectors/config/networks.ts b/common/selectors/config/networks.ts new file mode 100644 index 00000000..bd0fe758 --- /dev/null +++ b/common/selectors/config/networks.ts @@ -0,0 +1,41 @@ +import { AppState } from 'reducers'; +import { getConfig } from 'selectors/config'; +import { + DefaultNetworkConfig, + CustomNetworkConfig, + DefaultNetworkNames, + NetworkContract +} from 'reducers/config/networks/typings'; + +export const getNetworks = (state: AppState) => getConfig(state).networks; + +export const isCurrentNetworkDefault = (state: AppState): DefaultNetworkConfig | undefined => { + const { defaultNetworks, selectedNetwork } = getNetworks(state); + const isDefaultNetworkName = (networkName: string): networkName is DefaultNetworkNames => + Object.keys(defaultNetworks).includes(networkName); + const defaultNetwork = isDefaultNetworkName(selectedNetwork) + ? defaultNetworks[selectedNetwork] + : undefined; + return defaultNetwork; +}; + +export const isCurrentNetworkCustom = (state: AppState): CustomNetworkConfig | undefined => { + const { customNetworks, selectedNetwork } = getNetworks(state); + const customNetwork = customNetworks[selectedNetwork]; + return customNetwork; +}; + +export const getNetworkConfig = ( + state: AppState +): DefaultNetworkConfig | CustomNetworkConfig | undefined => + isCurrentNetworkDefault(state) || isCurrentNetworkCustom(state); + +export const getNetworkContracts = (state: AppState): NetworkContract[] | null => { + const network = isCurrentNetworkDefault(state); + return network ? network.contracts : []; +}; + +export const getCustomNetworkConfigs = (state: AppState): (CustomNetworkConfig | undefined)[] => { + const { customNetworks } = getNetworks(state); + return Object.values(customNetworks); +}; diff --git a/common/selectors/config/nodes.ts b/common/selectors/config/nodes.ts new file mode 100644 index 00000000..5da87d34 --- /dev/null +++ b/common/selectors/config/nodes.ts @@ -0,0 +1,55 @@ +import { AppState } from 'reducers'; +import { getConfig } from 'selectors/config'; +import { + DefaultNodeConfig, + DefaultNodeName, + CustomNodeConfig +} from 'reducers/config/nodes/typings'; +import { INode } from 'libs/nodes/INode'; + +export const getNodes = (state: AppState) => getConfig(state).nodes; + +export const isCurrentNodeDefault = (state: AppState): DefaultNodeConfig | undefined => { + const { defaultNodes, selectedNode: { nodeName } } = getNodes(state); + if (nodeName === undefined) { + return nodeName; + } + + const isDefaultNodeName = (networkName: string): networkName is DefaultNodeName => + Object.keys(defaultNodes).includes(networkName); + + const defaultNetwork = isDefaultNodeName(nodeName) ? defaultNodes[nodeName] : undefined; + return defaultNetwork; +}; + +export const isCurrentNetworkCustom = (state: AppState): CustomNodeConfig | undefined => { + const { customNodes, selectedNode: { nodeName } } = getNodes(state); + + if (nodeName === undefined) { + return nodeName; + } + + const customNetwork = customNodes[nodeName]; + return customNetwork; +}; + +export function getCustomNodeConfigs(state: AppState): CustomNodeConfig[] { + return Object.values(getNodes(state).customNodes); +} + +export function getNodeName(state: AppState): string | undefined { + return getNodes(state).selectedNode.nodeName; +} + +export function getIsWeb3Node(state: AppState): boolean { + return getNodeName(state) === 'web3'; +} + +export function getNodeConfig(state: AppState): DefaultNodeConfig | CustomNodeConfig | undefined { + return isCurrentNodeDefault(state) || isCurrentNetworkCustom(state); +} + +export function getNodeLib(state: AppState): INode | undefined { + const config = isCurrentNodeDefault(state); + return config ? config.lib : undefined; +} diff --git a/common/selectors/config/tokens.ts b/common/selectors/config/tokens.ts new file mode 100644 index 00000000..877b61e5 --- /dev/null +++ b/common/selectors/config/tokens.ts @@ -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 { isCurrentNetworkDefault } from 'selectors/config'; +import { Token } from 'reducers/config/networks/typings'; + +export function getNetworkTokens(state: AppState): Token[] { + const network = isCurrentNetworkDefault(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; +} diff --git a/spec/sagas/config.spec.ts b/spec/sagas/config.spec.ts index 1ce5bed8..965e6e86 100644 --- a/spec/sagas/config.spec.ts +++ b/spec/sagas/config.spec.ts @@ -14,7 +14,7 @@ import { } from 'sagas/config'; import { NODES, NodeConfig, NETWORKS } from 'config'; import { - getNode, + getNodeName, getNodeConfig, getOffline, getCustomNodeConfigs, @@ -176,7 +176,7 @@ describe('handleNodeChangeIntent*', () => { }); it('should select getNode', () => { - expect(data.gen.next().value).toEqual(select(getNode)); + expect(data.gen.next().value).toEqual(select(getNodeName)); }); it('should select nodeConfig', () => { @@ -263,7 +263,7 @@ describe('unsetWeb3Node*', () => { const gen = unsetWeb3Node(); it('should select getNode', () => { - expect(gen.next().value).toEqual(select(getNode)); + expect(gen.next().value).toEqual(select(getNodeName)); }); it('should select getNodeConfig', () => { @@ -293,7 +293,7 @@ describe('unsetWeb3NodeOnWalletEvent*', () => { const gen = unsetWeb3NodeOnWalletEvent(fakeAction); it('should select getNode', () => { - expect(gen.next().value).toEqual(select(getNode)); + expect(gen.next().value).toEqual(select(getNodeName)); }); it('should select getNodeConfig', () => {