From 2f8908804a7d67455e1a5f351c4282c752c17ebb Mon Sep 17 00:00:00 2001 From: ryanml Date: Thu, 15 Apr 2021 10:41:40 -0700 Subject: [PATCH] Handling infura blockage (#10883) * Handling infura blockage * Adding blockage home notification * Updating copy, adding temporary notification dismissal * Addressing review feedback * Using eth_blockNumber method to check Infura availability * Handling network changes in availability check --- app/_locales/en/messages.json | 4 ++ app/scripts/controllers/network/network.js | 55 ++++++++++++++++++++++ app/scripts/controllers/preferences.js | 27 +++++++++++ shared/constants/network.js | 2 + ui/app/pages/home/home.component.js | 27 +++++++++++ ui/app/pages/home/home.container.js | 2 + ui/app/selectors/selectors.js | 4 ++ 7 files changed, 121 insertions(+) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 537257326..614649c1a 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -878,6 +878,10 @@ "infoHelp": { "message": "Info & Help" }, + "infuraBlockedNotification": { + "message": "MetaMask is unable to connect to the blockchain host. Review possible reasons $1.", + "description": "$1 is a clickable link with with text defined by the 'here' key" + }, "initialTransactionConfirmed": { "message": "Your initial transaction was confirmed by the network. Click OK to go back." }, diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 1c9c80600..cd0fc0bce 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -17,16 +17,19 @@ import { NETWORK_TYPE_TO_ID_MAP, MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, + INFURA_BLOCKED_KEY, } from '../../../../shared/constants/network'; import { isPrefixedFormattedHexString, isSafeChainId, } from '../../../../shared/modules/network.utils'; +import getFetchWithTimeout from '../../../../shared/modules/fetch-with-timeout'; import createMetamaskMiddleware from './createMetamaskMiddleware'; import createInfuraClient from './createInfuraClient'; import createJsonRpcClient from './createJsonRpcClient'; const env = process.env.METAMASK_ENV; +const fetchWithTimeout = getFetchWithTimeout(30000); let defaultProviderConfigOpts; if (process.env.IN_TEST === 'true') { @@ -52,6 +55,10 @@ export const NETWORK_EVENTS = { NETWORK_DID_CHANGE: 'networkDidChange', // Fired when the actively selected network *will* change NETWORK_WILL_CHANGE: 'networkWillChange', + // Fired when Infura returns an error indicating no support + INFURA_IS_BLOCKED: 'infuraIsBlocked', + // Fired when not using an Infura network or when Infura returns no error, indicating support + INFURA_IS_UNBLOCKED: 'infuraIsUnblocked', }; export default class NetworkController extends EventEmitter { @@ -152,6 +159,15 @@ export default class NetworkController extends EventEmitter { // Ping the RPC endpoint so we can confirm that it works const ethQuery = new EthQuery(this._provider); const initialNetwork = this.getNetworkState(); + const { type } = this.getProviderConfig(); + const isInfura = INFURA_PROVIDER_TYPES.includes(type); + + if (isInfura) { + this._checkInfuraAvailability(type); + } else { + this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED); + } + ethQuery.sendAsync({ method: 'net_version' }, (err, networkVersion) => { const currentNetwork = this.getNetworkState(); if (initialNetwork === currentNetwork) { @@ -235,6 +251,45 @@ export default class NetworkController extends EventEmitter { // Private // + async _checkInfuraAvailability(network) { + const rpcUrl = `https://${network}.infura.io/v3/${this._infuraProjectId}`; + + let networkChanged = false; + this.once(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { + networkChanged = true; + }); + + try { + const response = await fetchWithTimeout(rpcUrl, { + method: 'POST', + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_blockNumber', + params: [], + id: 1, + }), + }); + + if (networkChanged) { + return; + } + + if (response.ok) { + this.emit(NETWORK_EVENTS.INFURA_IS_UNBLOCKED); + } else { + const responseMessage = await response.json(); + if (networkChanged) { + return; + } + if (responseMessage.error === INFURA_BLOCKED_KEY) { + this.emit(NETWORK_EVENTS.INFURA_IS_BLOCKED); + } + } + } catch (err) { + log.warn(`MetaMask - Infura availability check failed`, err); + } + } + _switchNetwork(opts) { this.emit(NETWORK_EVENTS.NETWORK_WILL_CHANGE); this.setNetworkState('loading'); diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 9fe84c3ff..9470df50c 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -66,6 +66,7 @@ export default class PreferencesController { completedOnboarding: false, // ENS decentralized website resolution ipfsGateway: 'dweb.link', + infuraBlocked: null, ...opts.initState, }; @@ -75,6 +76,7 @@ export default class PreferencesController { this.openPopup = opts.openPopup; this.migrateAddressBookState = opts.migrateAddressBookState; this._subscribeToNetworkDidChange(); + this._subscribeToInfuraAvailability(); global.setPreference = (key, value) => { return this.setFeatureFlag(key, value); @@ -679,6 +681,31 @@ export default class PreferencesController { }); } + _subscribeToInfuraAvailability() { + this.network.on(NETWORK_EVENTS.INFURA_IS_BLOCKED, () => { + this._setInfuraBlocked(true); + }); + this.network.on(NETWORK_EVENTS.INFURA_IS_UNBLOCKED, () => { + this._setInfuraBlocked(false); + }); + } + + /** + * + * A setter for the `infuraBlocked` property + * @param {boolean} isBlocked - Bool indicating whether Infura is blocked + * + */ + _setInfuraBlocked(isBlocked) { + const { infuraBlocked } = this.store.getState(); + + if (infuraBlocked === isBlocked) { + return; + } + + this.store.updateState({ infuraBlocked: isBlocked }); + } + /** * Updates `accountTokens`, `tokens`, `accountHiddenTokens` and `hiddenTokens` of current account and network according to it. * diff --git a/shared/constants/network.js b/shared/constants/network.js index 89e2b7e52..6a71ad68a 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -96,3 +96,5 @@ export const NATIVE_CURRENCY_TOKEN_IMAGE_MAP = { [TEST_ETH_SYMBOL]: TEST_ETH_TOKEN_IMAGE_URL, [BNB_SYMBOL]: BNB_TOKEN_IMAGE_URL, }; + +export const INFURA_BLOCKED_KEY = 'countryBlocked'; diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 84ecd217f..fb66fc964 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -34,6 +34,8 @@ const LEARN_MORE_URL = 'https://metamask.zendesk.com/hc/en-us/articles/360045129011-Intro-to-MetaMask-v8-extension'; const LEGACY_WEB3_URL = 'https://metamask.zendesk.com/hc/en-us/articles/360053147012'; +const INFURA_BLOCKAGE_URL = + 'https://metamask.zendesk.com/hc/en-us/articles/360059386712'; export default class Home extends PureComponent { static contextTypes = { @@ -74,10 +76,12 @@ export default class Home extends PureComponent { originOfCurrentTab: PropTypes.string, disableWeb3ShimUsageAlert: PropTypes.func.isRequired, pendingConfirmations: PropTypes.arrayOf(PropTypes.object).isRequired, + infuraBlocked: PropTypes.bool.isRequired, }; state = { mounted: false, + canShowBlockageNotification: true, }; componentDidMount() { @@ -176,6 +180,7 @@ export default class Home extends PureComponent { setWeb3ShimUsageAlertDismissed, originOfCurrentTab, disableWeb3ShimUsageAlert, + infuraBlocked, } = this.props; return ( @@ -241,6 +246,28 @@ export default class Home extends PureComponent { key="home-privacyModeDefault" /> ) : null} + {infuraBlocked && this.state.canShowBlockageNotification ? ( + + global.platform.openTab({ url: INFURA_BLOCKAGE_URL }) + } + > + {t('here')} + , + ])} + ignoreText={t('dismiss')} + onIgnore={() => { + this.setState({ + canShowBlockageNotification: false, + }); + }} + key="home-infuraBlockedNotification" + /> + ) : null} ); } diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js index 929833e14..721c7a15e 100644 --- a/ui/app/pages/home/home.container.js +++ b/ui/app/pages/home/home.container.js @@ -11,6 +11,7 @@ import { getUnapprovedTemplatedConfirmations, getWeb3ShimUsageStateForOrigin, unconfirmedTransactionsCountSelector, + getInfuraBlocked, } from '../../selectors'; import { @@ -104,6 +105,7 @@ const mapStateToProps = (state) => { originOfCurrentTab, shouldShowWeb3ShimUsageNotification, pendingConfirmations, + infuraBlocked: getInfuraBlocked(state), }; }; diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 80f84b71d..26c423c56 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -417,6 +417,10 @@ export function getIpfsGateway(state) { return state.metamask.ipfsGateway; } +export function getInfuraBlocked(state) { + return Boolean(state.metamask.infuraBlocked); +} + export function getUSDConversionRate(state) { return state.metamask.usdConversionRate; }