MyCrypto/common/sagas/config/node.ts

303 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { delay, SagaIterator } from 'redux-saga';
import { call, fork, put, take, takeEvery, select, apply } from 'redux-saga/effects';
import { bindActionCreators } from 'redux';
import {
getNodeId,
getNodeConfig,
getOffline,
isStaticNodeId,
getCustomNodeFromId,
getStaticNodeFromId,
getNetworkConfigById,
getAllNodes
} from 'selectors/config';
import { TypeKeys } from 'actions/config/constants';
import {
setOnline,
setOffline,
changeNodeRequested,
changeNodeSucceeded,
changeNodeFailed,
changeNodeForce,
setLatestBlock,
AddCustomNodeAction,
ChangeNodeForceAction,
ChangeNodeRequestedAction,
ChangeNodeRequestedOneTimeAction,
ChangeNetworkRequestedAction,
RemoveCustomNodeAction
} from 'actions/config';
import { showNotification } from 'actions/notifications';
import { resetWallet } from 'actions/wallet';
import { translateRaw } from 'translations';
import { StaticNodeConfig, CustomNodeConfig, NodeConfig } from 'types/node';
import { CustomNetworkConfig, StaticNetworkConfig } from 'types/network';
import {
getShepherdOffline,
isAutoNode,
shepherd,
shepherdProvider,
stripWeb3Network,
makeProviderConfig,
getShepherdNetwork,
getShepherdPending,
makeAutoNodeName
} from 'libs/nodes';
import { INITIAL_STATE as selectedNodeInitialState } from 'reducers/config/nodes/selectedNode';
import { configuredStore as store } from 'store';
window.addEventListener('load', () => {
const getShepherdStatus = () => ({
pending: getShepherdPending(),
isOnline: !getShepherdOffline()
});
const { online, offline, lostNetworkNotif, offlineNotif, restoreNotif } = bindActionCreators(
{
offline: setOffline,
online: setOnline,
restoreNotif: () =>
showNotification('success', 'Your connection to the network has been restored!', 3000),
lostNetworkNotif: () =>
showNotification(
'danger',
`Youve lost your connection to the network, check your internet
connection or try changing networks from the dropdown at the
top right of the page.`,
Infinity
),
offlineNotif: () =>
showNotification(
'info',
'You are currently offline. Some features will be unavailable.',
5000
)
},
store.dispatch
);
const getAppOnline = () => !getOffline(store.getState());
/**
* @description Repeatedly polls itself to check for online state conflict occurs, implemented in recursive style for flexible polling times
* as network requests take a variable amount of time.
*
* Whenever an app online state conflict occurs, it resolves the conflict with the following priority:
* * If shepherd is online but app is offline -> do a ping request via shepherd provider, with the result of the ping being the set app state
* * If shepherd is offline but app is online -> set app to offline as it wont be able to make requests anyway
*/
async function detectOnlineStateConflict() {
const shepherdStatus = getShepherdStatus();
const appOffline = getAppOnline();
const onlineStateConflict = shepherdStatus.isOnline !== appOffline;
if (shepherdStatus.pending || !onlineStateConflict) {
return setTimeout(detectOnlineStateConflict, 1000);
}
// if app reports online but shepherd offline, then set app offline
if (appOffline && !shepherdStatus.isOnline) {
lostNetworkNotif();
offline();
} else if (!appOffline && shepherdStatus.isOnline) {
// if app reports offline but shepherd reports online
// send a request to shepherd provider to see if we can still send out requests
const success = await shepherdProvider.ping().catch(() => false);
if (success) {
restoreNotif();
online();
}
}
detectOnlineStateConflict();
}
detectOnlineStateConflict();
window.addEventListener('offline', () => {
const previouslyOnline = getAppOnline();
// if browser reports as offline and we were previously online
// then set offline without checking balancer state
if (!navigator.onLine && previouslyOnline) {
offlineNotif();
offline();
}
});
});
// @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* handleChangeNodeRequestedOneTime(): SagaIterator {
const action: ChangeNodeRequestedOneTimeAction = yield take(
TypeKeys.CONFIG_CHANGE_NODE_REQUESTED_ONETIME
);
// allow shepherdProvider async init to complete. TODO - don't export shepherdProvider as promise
yield call(delay, 100);
yield put(changeNodeRequested(action.payload));
}
export function* handleChangeNodeRequested({
payload: nodeIdToSwitchTo
}: ChangeNodeRequestedAction): SagaIterator {
const isStaticNode: boolean = yield select(isStaticNodeId, nodeIdToSwitchTo);
const currentConfig: NodeConfig = yield select(getNodeConfig);
// Bail out if they're switching to the same node
if (currentConfig.id === nodeIdToSwitchTo) {
return;
}
function* bailOut(message: string) {
yield put(showNotification('danger', message, 5000));
yield put(changeNodeFailed());
}
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);
}
const nextNetwork: StaticNetworkConfig | CustomNetworkConfig = yield select(
getNetworkConfigById,
stripWeb3Network(nextNodeConfig.network)
);
if (!nextNetwork) {
return yield* bailOut(
`Unknown custom network for your node '${nodeIdToSwitchTo}', try re-adding it`
);
}
const isOffline = yield select(getOffline);
if (isAutoNode(nodeIdToSwitchTo)) {
shepherd.auto();
if (getShepherdNetwork() !== nextNodeConfig.network) {
yield apply(shepherd, shepherd.switchNetworks, [nextNodeConfig.network]);
}
} else {
try {
yield apply(shepherd, shepherd.manual, [nodeIdToSwitchTo, isOffline]);
} catch (err) {
console.error(err);
return yield* bailOut(translateRaw('ERROR_32'));
}
}
let currentBlock = '???';
try {
currentBlock = yield apply(shepherdProvider, shepherdProvider.getCurrentBlock);
} catch (err) {
if (!isOffline) {
console.error(err);
return yield* bailOut(translateRaw('ERROR_32'));
}
}
yield put(setLatestBlock(currentBlock));
yield put(changeNodeSucceeded({ networkId: nextNodeConfig.network, nodeId: nodeIdToSwitchTo }));
if (currentConfig.network !== nextNodeConfig.network) {
yield fork(handleNewNetwork);
}
}
export function* handleAddCustomNode(action: AddCustomNodeAction): SagaIterator {
const config = action.payload;
shepherd.useProvider(
'myccustom',
config.id,
makeProviderConfig({ network: config.network }),
config
);
yield put(changeNodeRequested(config.id));
}
export function* handleNewNetwork() {
yield put(resetWallet());
}
export function* handleNodeChangeForce({ payload: staticNodeIdToSwitchTo }: ChangeNodeForceAction) {
// does not perform node online check before changing nodes
// necessary when switching back from Web3 provider so node
// dropdown does not get stuck if node is offline
const isStaticNode: boolean = yield select(isStaticNodeId, staticNodeIdToSwitchTo);
if (!isStaticNode) {
return;
}
const nodeConfig = yield select(getStaticNodeFromId, staticNodeIdToSwitchTo);
// force the node change
yield put(changeNodeSucceeded({ networkId: nodeConfig.network, nodeId: staticNodeIdToSwitchTo }));
// also put the change through as usual so status check and
// error messages occur if the node is unavailable
yield put(changeNodeRequested(staticNodeIdToSwitchTo));
}
export function* handleChangeNetworkRequested({ payload: network }: ChangeNetworkRequestedAction) {
let desiredNode = '';
const autoNodeName = makeAutoNodeName(network);
const isStaticNode: boolean = yield select(isStaticNodeId, autoNodeName);
if (isStaticNode) {
desiredNode = autoNodeName;
} else {
const allNodes: { [id: string]: NodeConfig } = yield select(getAllNodes);
const networkNode = Object.values(allNodes).find(n => n.network === network);
if (networkNode) {
desiredNode = networkNode.id;
}
}
if (desiredNode) {
yield put(changeNodeRequested(desiredNode));
} else {
yield put(
showNotification(
'danger',
translateRaw('NETWORK_UNKNOWN_ERROR', {
$network: network
}),
5000
)
);
}
}
export function* handleRemoveCustomNode({ payload: nodeId }: RemoveCustomNodeAction): SagaIterator {
// If custom node is currently selected, go back to default node
const currentNodeId = yield select(getNodeId);
if (nodeId === currentNodeId) {
yield put(changeNodeForce(selectedNodeInitialState.nodeId));
}
}
export const node = [
fork(handleChangeNodeRequestedOneTime),
takeEvery(TypeKeys.CONFIG_CHANGE_NODE_REQUESTED, handleChangeNodeRequested),
takeEvery(TypeKeys.CONFIG_CHANGE_NODE_FORCE, handleNodeChangeForce),
takeEvery(TypeKeys.CONFIG_CHANGE_NETWORK_REQUESTED, handleChangeNetworkRequested),
takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload),
takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, handleAddCustomNode),
takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, handleRemoveCustomNode)
];