diff --git a/src/actions/BlockchainActions.js b/src/actions/BlockchainActions.js index afb967bf..e2650ac9 100644 --- a/src/actions/BlockchainActions.js +++ b/src/actions/BlockchainActions.js @@ -1,6 +1,7 @@ /* @flow */ import * as BLOCKCHAIN from 'actions/constants/blockchain'; +import * as DiscoveryActions from 'actions/DiscoveryActions'; import * as EthereumBlockchainActions from 'actions/ethereum/BlockchainActions'; import * as RippleBlockchainActions from 'actions/ripple/BlockchainActions'; @@ -19,6 +20,10 @@ export type BlockchainAction = | { type: typeof BLOCKCHAIN.START_SUBSCRIBE, shortcut: string, + } + | { + type: typeof BLOCKCHAIN.FAIL_SUBSCRIBE, + shortcut: string, }; // Conditionally subscribe to blockchain backend @@ -128,6 +133,8 @@ export const onError = ( const network = config.networks.find(c => c.shortcut === shortcut); if (!network) return; + dispatch(autoReconnect(shortcut)); + switch (network.type) { case 'ethereum': await dispatch(EthereumBlockchainActions.onError(shortcut)); @@ -140,3 +147,28 @@ export const onError = ( break; } }; + +const autoReconnect = (shortcut: string): PromiseAction => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { + const MAX_ATTEMPTS = 4; + let blockchain = getState().blockchain.find(b => b.shortcut === shortcut); + // try to automatically reconnect and wait after each attemp (5s * #attempt) untill max number of attemps is reached + for (let i = 0; i < MAX_ATTEMPTS; i++) { + const waitTime = 5000 * (i + 1); /// 5s * #attempt + if (!blockchain || blockchain.connected) { + break; + } + + blockchain = getState().blockchain.find(b => b.shortcut === shortcut); + + // reconnect with 7s timeout + // eslint-disable-next-line no-await-in-loop + await dispatch(DiscoveryActions.reconnect(shortcut, 7000)); + + // wait before next try + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => setTimeout(resolve, waitTime)); + } +}; diff --git a/src/actions/DiscoveryActions.js b/src/actions/DiscoveryActions.js index 22ca82a0..d61f2085 100644 --- a/src/actions/DiscoveryActions.js +++ b/src/actions/DiscoveryActions.js @@ -1,6 +1,7 @@ /* @flow */ import TrezorConnect, { UI } from 'trezor-connect'; +import * as BLOCKCHAIN_ACTION from 'actions/constants/blockchain'; import * as DISCOVERY from 'actions/constants/discovery'; import * as ACCOUNT from 'actions/constants/account'; import * as NOTIFICATION from 'actions/constants/notification'; @@ -325,11 +326,27 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction }); }; -export const reconnect = (network: string): PromiseAction => async ( +export const reconnect = (network: string, timeout: number = 30): PromiseAction => async ( dispatch: Dispatch ): Promise => { - await dispatch(BlockchainActions.subscribe(network)); - dispatch(restore()); + // Runs two promises. + // First promise is a subscribe action which will never resolve in case of completely lost connection to the backend + // That's why there is a second promise that rejects after the specified timeout. + return Promise.race([ + dispatch(BlockchainActions.subscribe(network)), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)), + ]) + .catch(() => { + // catch error from first promises that rejects (most likely timeout) + dispatch({ + type: BLOCKCHAIN_ACTION.FAIL_SUBSCRIBE, + shortcut: network, + }); + }) + .then(() => { + // dispatch restore when subscribe promise resolves + dispatch(restore()); + }); }; // Called after DEVICE.CONNECT ('trezor-connect') or CONNECT.AUTH_DEVICE actions in WalletService diff --git a/src/actions/constants/blockchain.js b/src/actions/constants/blockchain.js index 3cdd0a2e..3cd6b50d 100644 --- a/src/actions/constants/blockchain.js +++ b/src/actions/constants/blockchain.js @@ -1,5 +1,6 @@ /* @flow */ export const START_SUBSCRIBE: 'blockchain__start_subscribe' = 'blockchain__start_subscribe'; +export const FAIL_SUBSCRIBE: 'blockchain__fail_subscribe' = 'blockchain__fail_subscribe'; export const READY: 'blockchain__ready' = 'blockchain__ready'; export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee'; diff --git a/src/reducers/BlockchainReducer.js b/src/reducers/BlockchainReducer.js index 745a44b4..865f1f2f 100644 --- a/src/reducers/BlockchainReducer.js +++ b/src/reducers/BlockchainReducer.js @@ -48,6 +48,30 @@ const onStartSubscribe = (state: State, shortcut: string): State => { ]); }; +const onFailSubscribe = (state: State, shortcut: string): State => { + const network = state.find(b => b.shortcut === shortcut); + if (network) { + const others = state.filter(b => b !== network); + return others.concat([ + { + ...network, + connecting: false, + }, + ]); + } + + return state.concat([ + { + shortcut, + connected: false, + connecting: false, + block: 0, + feeTimestamp: 0, + feeLevels: [], + }, + ]); +}; + const onConnect = (state: State, action: BlockchainConnect): State => { const shortcut = action.payload.coin.shortcut.toLowerCase(); const network = state.find(b => b.shortcut === shortcut); @@ -136,6 +160,8 @@ export default (state: State = initialState, action: Action): State => { switch (action.type) { case BLOCKCHAIN_ACTION.START_SUBSCRIBE: return onStartSubscribe(state, action.shortcut); + case BLOCKCHAIN_ACTION.FAIL_SUBSCRIBE: + return onFailSubscribe(state, action.shortcut); case BLOCKCHAIN_EVENT.CONNECT: return onConnect(state, action); case BLOCKCHAIN_EVENT.ERROR: