/ common / features / configAndTokens.ts
configAndTokens.ts
  1  import { dedupeCustomTokens } from 'utils/tokens';
  2  import { loadStatePropertyOrEmptyObject } from 'utils/localStorage';
  3  import { CustomNodeConfig } from 'types/node';
  4  import { shepherd, makeProviderConfig, isAutoNode } from 'libs/nodes';
  5  import RootReducer, { AppState } from './reducers';
  6  import { getLanguageSelection, getTheme } from './config/meta/selectors';
  7  import { getCustomNetworkConfigs } from './config/networks/custom/selectors';
  8  import { isStaticNetworkId } from './config/networks/static/selectors';
  9  import { isStaticNodeId } from './config/nodes/static/selectors';
 10  import { getCustomNodeConfigs } from './config/nodes/custom/selectors';
 11  import { getSelectedNode } from './config/nodes/selected/selectors';
 12  import { ConfigState } from './config/types';
 13  import { configReducer } from './config/reducer';
 14  import { CustomTokensState } from './customTokens/types';
 15  import { INITIAL_STATE as customTokensInitialState } from './customTokens/reducer';
 16  
 17  const appInitialState = RootReducer(undefined as any, { type: 'inital_state' });
 18  
 19  type DeepPartial<T> = { [P in keyof T]?: DeepPartial<T[P]> };
 20  export function getConfigAndCustomTokensStateToSubscribe(
 21    state: AppState
 22  ): Pick<DeepPartial<AppState>, 'config' | 'customTokens'> {
 23    const subscribedConfig: DeepPartial<ConfigState> = {
 24      meta: {
 25        languageSelection: getLanguageSelection(state),
 26        theme: getTheme(state)
 27      },
 28      nodes: { customNodes: getCustomNodeConfigs(state), selectedNode: getSelectedNode(state) },
 29      networks: {
 30        customNetworks: getCustomNetworkConfigs(state)
 31      }
 32    };
 33  
 34    const subscribedTokens = state.customTokens;
 35  
 36    return { config: subscribedConfig, customTokens: subscribedTokens };
 37  }
 38  
 39  export function rehydrateConfigAndCustomTokenState() {
 40    const configInitialState = configReducer(undefined as any, { type: 'inital_state' });
 41    const savedConfigState = loadStatePropertyOrEmptyObject<ConfigState>('config');
 42    const nextConfigState = { ...configInitialState };
 43  
 44    // If they have a saved node, make sure we assign that too. The node selected
 45    // isn't serializable, so we have to assign it here.
 46    if (savedConfigState) {
 47      // we assign networks first so that when we re-hydrate custom nodes, we can check that the network exists
 48      nextConfigState.networks = rehydrateNetworks(
 49        configInitialState.networks,
 50        savedConfigState.networks
 51      );
 52      nextConfigState.nodes = rehydrateNodes(
 53        configInitialState.nodes,
 54        savedConfigState.nodes,
 55        nextConfigState.networks
 56      );
 57      nextConfigState.meta = { ...nextConfigState.meta, ...savedConfigState.meta };
 58    }
 59  
 60    const { customNodes, selectedNode: { nodeId }, staticNodes } = nextConfigState.nodes;
 61    const selectedNode = isStaticNodeId(appInitialState, nodeId)
 62      ? staticNodes[nodeId]
 63      : customNodes[nodeId];
 64  
 65    if (!selectedNode) {
 66      return { config: configInitialState, customTokens: customTokensInitialState };
 67    }
 68  
 69    const nextCustomTokenState = rehydrateCustomTokens(
 70      nextConfigState.networks,
 71      selectedNode.network
 72    );
 73  
 74    return { config: nextConfigState, customTokens: nextCustomTokenState };
 75  }
 76  
 77  function rehydrateCustomTokens(networkState: ConfigState['networks'], selectedNetwork: string) {
 78    // Dedupe custom tokens initially
 79    const savedCustomTokensState =
 80      loadStatePropertyOrEmptyObject<CustomTokensState>('customTokens') || customTokensInitialState;
 81  
 82    const { customNetworks, staticNetworks } = networkState;
 83    const network = isStaticNetworkId(appInitialState, selectedNetwork)
 84      ? staticNetworks[selectedNetwork]
 85      : customNetworks[selectedNetwork];
 86  
 87    return network.isCustom
 88      ? savedCustomTokensState
 89      : dedupeCustomTokens(network.tokens, savedCustomTokensState);
 90  }
 91  
 92  function rehydrateNetworks(
 93    initialState: ConfigState['networks'],
 94    savedState: ConfigState['networks']
 95  ): ConfigState['networks'] {
 96    const nextNetworkState = { ...initialState };
 97    nextNetworkState.customNetworks = savedState.customNetworks;
 98    return nextNetworkState;
 99  }
100  
101  function rehydrateNodes(
102    initalState: ConfigState['nodes'],
103    savedState: ConfigState['nodes'],
104    networkState: ConfigState['networks']
105  ): ConfigState['nodes'] {
106    const nextNodeState = { ...initalState };
107  
108    // re-assign the hydrated nodes
109    nextNodeState.customNodes = rehydrateCustomNodes(savedState.customNodes, networkState);
110    const { customNodes, staticNodes } = nextNodeState;
111    nextNodeState.selectedNode = getSavedSelectedNode(
112      nextNodeState.selectedNode,
113      savedState.selectedNode,
114      customNodes,
115      staticNodes
116    );
117  
118    return nextNodeState;
119  }
120  
121  function getSavedSelectedNode(
122    initialState: ConfigState['nodes']['selectedNode'],
123    savedState: ConfigState['nodes']['selectedNode'],
124    customNodes: ConfigState['nodes']['customNodes'],
125    staticNodes: ConfigState['nodes']['staticNodes']
126  ): ConfigState['nodes']['selectedNode'] {
127    const { nodeId: savedNodeId } = savedState;
128  
129    // if 'web3' has persisted as node selection, reset to app default
130    // necessary because web3 is only initialized as a node upon MetaMask / Mist unlock
131  
132    if (savedNodeId === 'web3') {
133      return { nodeId: initialState.nodeId, prevNode: initialState.nodeId, pending: false };
134    }
135  
136    const nodeConfigExists = isStaticNodeId(appInitialState, savedNodeId)
137      ? staticNodes[savedNodeId]
138      : customNodes[savedNodeId];
139  
140    if (nodeConfigExists) {
141      if (isAutoNode(savedNodeId)) {
142        shepherd.switchNetworks(nodeConfigExists.network);
143      } else {
144        shepherd.manual(savedNodeId, false);
145      }
146    }
147    const nodeId = nodeConfigExists ? savedNodeId : initialState.nodeId;
148    return { nodeId, prevNode: nodeId, pending: false };
149  }
150  
151  function rehydrateCustomNodes(
152    state: ConfigState['nodes']['customNodes'],
153    networkState: ConfigState['networks']
154  ) {
155    const networkExists = (networkId: string) =>
156      Object.keys(networkState.customNetworks).includes(networkId) ||
157      Object.keys(networkState.staticNetworks).includes(networkId);
158  
159    const rehydratedCustomNodes = Object.entries(state).reduce(
160      (hydratedNodes, [customNodeId, configToHydrate]) => {
161        if (!networkExists(configToHydrate.network)) {
162          return hydratedNodes;
163        }
164  
165        shepherd.useProvider(
166          'myccustom',
167          configToHydrate.id,
168          makeProviderConfig({ network: configToHydrate.network }),
169          configToHydrate
170        );
171  
172        const hydratedNode: CustomNodeConfig = { ...configToHydrate };
173        return { ...hydratedNodes, [customNodeId]: hydratedNode };
174      },
175      {} as ConfigState['nodes']['customNodes']
176    );
177    return rehydratedCustomNodes;
178  }