1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Stop GasFeeController polling when pop closes (#11746)

* Stop GasFeeController polling when pop closes

* Stop estimate gas polling on window unload

* lint + comments

* Improve client closed logic

* lint

* Add back _beforeUnload on unmount in gas-modal-page-container

* Add full check and call onClientClosed method for notifcation environment

* Add gas pollingToken tracking to appStateController and use to disconnect polling for each environment type

* remove unused method

* move controller manipulation logic from background.js to metamask-controller, disaggregate methods

* add beforeunload handling to reset gas polling tokens from root of send page

* cleanup, lint and address feedback

* clear appState gasPollingTokens when all instances of all env types are closed, fix pollingTokenType arg from onEnvironmentTypeClosed call in metamask-controller

* mock new methods to fix tests

* final bit of cleanup + comments

Co-authored-by: Dan Miller <danjm.com@gmail.com>
This commit is contained in:
Alex Donesky 2021-08-04 16:53:13 -05:00 committed by GitHub
parent 96a13dddb5
commit d359429f04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 221 additions and 27 deletions

View File

@ -302,6 +302,24 @@ function setupController(initState, initLangCode) {
); );
}; };
const onCloseEnvironmentInstances = (isClientOpen, environmentType) => {
// if all instances of metamask are closed we call a method on the controller to stop gasFeeController polling
if (isClientOpen === false) {
controller.onClientClosed();
// otherwise we want to only remove the polling tokens for the environment type that has closed
} else {
// in the case of fullscreen environment a user might have multiple tabs open so we don't want to disconnect all of
// its corresponding polling tokens unless all tabs are closed.
if (
environmentType === ENVIRONMENT_TYPE_FULLSCREEN &&
Boolean(Object.keys(openMetamaskTabsIDs).length)
) {
return;
}
controller.onEnvironmentTypeClosed(environmentType);
}
};
/** /**
* A runtime.Port object, as provided by the browser: * A runtime.Port object, as provided by the browser:
* @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/Port * @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/Port
@ -330,10 +348,11 @@ function setupController(initState, initLangCode) {
if (processName === ENVIRONMENT_TYPE_POPUP) { if (processName === ENVIRONMENT_TYPE_POPUP) {
popupIsOpen = true; popupIsOpen = true;
endOfStream(portStream, () => { endOfStream(portStream, () => {
popupIsOpen = false; popupIsOpen = false;
controller.isClientOpen = isClientOpenStatus(); const isClientOpen = isClientOpenStatus();
controller.isClientOpen = isClientOpen;
onCloseEnvironmentInstances(isClientOpen, ENVIRONMENT_TYPE_POPUP);
}); });
} }
@ -342,7 +361,12 @@ function setupController(initState, initLangCode) {
endOfStream(portStream, () => { endOfStream(portStream, () => {
notificationIsOpen = false; notificationIsOpen = false;
controller.isClientOpen = isClientOpenStatus(); const isClientOpen = isClientOpenStatus();
controller.isClientOpen = isClientOpen;
onCloseEnvironmentInstances(
isClientOpen,
ENVIRONMENT_TYPE_NOTIFICATION,
);
}); });
} }
@ -352,7 +376,12 @@ function setupController(initState, initLangCode) {
endOfStream(portStream, () => { endOfStream(portStream, () => {
delete openMetamaskTabsIDs[tabId]; delete openMetamaskTabsIDs[tabId];
controller.isClientOpen = isClientOpenStatus(); const isClientOpen = isClientOpenStatus();
controller.isClientOpen = isClientOpen;
onCloseEnvironmentInstances(
isClientOpen,
ENVIRONMENT_TYPE_FULLSCREEN,
);
}); });
} }
} else { } else {

View File

@ -25,6 +25,9 @@ export default class AppStateController extends EventEmitter {
connectedStatusPopoverHasBeenShown: true, connectedStatusPopoverHasBeenShown: true,
defaultHomeActiveTabName: null, defaultHomeActiveTabName: null,
browserEnvironment: {}, browserEnvironment: {},
popupGasPollTokens: [],
notificationGasPollTokens: [],
fullScreenGasPollTokens: [],
recoveryPhraseReminderHasBeenShown: false, recoveryPhraseReminderHasBeenShown: false,
recoveryPhraseReminderLastShown: new Date().getTime(), recoveryPhraseReminderLastShown: new Date().getTime(),
...initState, ...initState,
@ -191,4 +194,38 @@ export default class AppStateController extends EventEmitter {
setBrowserEnvironment(os, browser) { setBrowserEnvironment(os, browser) {
this.store.updateState({ browserEnvironment: { os, browser } }); this.store.updateState({ browserEnvironment: { os, browser } });
} }
/**
* Adds a pollingToken for a given environmentType
* @returns {void}
*/
addPollingToken(pollingToken, pollingTokenType) {
const prevState = this.store.getState()[pollingTokenType];
this.store.updateState({
[pollingTokenType]: [...prevState, pollingToken],
});
}
/**
* removes a pollingToken for a given environmentType
* @returns {void}
*/
removePollingToken(pollingToken, pollingTokenType) {
const prevState = this.store.getState()[pollingTokenType];
this.store.updateState({
[pollingTokenType]: prevState.filter((token) => token !== pollingToken),
});
}
/**
* clears all pollingTokens
* @returns {void}
*/
clearPollingTokens() {
this.store.updateState({
popupGasPollTokens: [],
notificationGasPollTokens: [],
fullScreenGasPollTokens: [],
});
}
} }

View File

@ -32,6 +32,7 @@ import { MAINNET_CHAIN_ID } from '../../shared/constants/network';
import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { UI_NOTIFICATIONS } from '../../shared/notifications';
import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils';
import { MILLISECOND } from '../../shared/constants/time'; import { MILLISECOND } from '../../shared/constants/time';
import { POLLING_TOKEN_ENVIRONMENT_TYPES } from '../../shared/constants/app';
import { hexToDecimal } from '../../ui/helpers/utils/conversions.util'; import { hexToDecimal } from '../../ui/helpers/utils/conversions.util';
import ComposableObservableStore from './lib/ComposableObservableStore'; import ComposableObservableStore from './lib/ComposableObservableStore';
@ -1130,6 +1131,16 @@ export default class MetamaskController extends EventEmitter {
this.gasFeeController.getTimeEstimate, this.gasFeeController.getTimeEstimate,
this.gasFeeController, this.gasFeeController,
), ),
addPollingTokenToAppState: nodeify(
this.appStateController.addPollingToken,
this.appStateController,
),
removePollingTokenFromAppState: nodeify(
this.appStateController.removePollingToken,
this.appStateController,
),
}; };
} }
@ -2969,7 +2980,6 @@ export default class MetamaskController extends EventEmitter {
/* eslint-disable accessor-pairs */ /* eslint-disable accessor-pairs */
/** /**
* A method for recording whether the MetaMask user interface is open or not. * A method for recording whether the MetaMask user interface is open or not.
* @private
* @param {boolean} open * @param {boolean} open
*/ */
set isClientOpen(open) { set isClientOpen(open) {
@ -2978,6 +2988,38 @@ export default class MetamaskController extends EventEmitter {
} }
/* eslint-enable accessor-pairs */ /* eslint-enable accessor-pairs */
/**
* A method that is called by the background when all instances of metamask are closed.
* Currently used to stop polling in the gasFeeController.
*/
onClientClosed() {
try {
this.gasFeeController.stopPolling();
this.appStateController.clearPollingTokens();
} catch (error) {
console.error(error);
}
}
/**
* A method that is called by the background when a particular environment type is closed (fullscreen, popup, notification).
* Currently used to stop polling in the gasFeeController for only that environement type
*/
onEnvironmentTypeClosed(environmentType) {
const appStatePollingTokenType =
POLLING_TOKEN_ENVIRONMENT_TYPES[environmentType];
const pollingTokensToDisconnect = this.appStateController.store.getState()[
appStatePollingTokenType
];
pollingTokensToDisconnect.forEach((pollingToken) => {
this.gasFeeController.disconnectPoller(pollingToken);
this.appStateController.removePollingToken(
pollingToken,
appStatePollingTokenType,
);
});
}
/** /**
* Adds a domain to the PhishingController safelist * Adds a domain to the PhishingController safelist
* @param {string} hostname - the domain to safelist * @param {string} hostname - the domain to safelist

View File

@ -30,3 +30,9 @@ export const MESSAGE_TYPE = {
ADD_ETHEREUM_CHAIN: 'wallet_addEthereumChain', ADD_ETHEREUM_CHAIN: 'wallet_addEthereumChain',
SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain', SWITCH_ETHEREUM_CHAIN: 'wallet_switchEthereumChain',
}; };
export const POLLING_TOKEN_ENVIRONMENT_TYPES = {
[ENVIRONMENT_TYPE_POPUP]: 'popupGasPollTokens',
[ENVIRONMENT_TYPE_NOTIFICATION]: 'notificationGasPollTokens',
[ENVIRONMENT_TYPE_FULLSCREEN]: 'fullScreenGasPollTokens',
};

View File

@ -13,6 +13,7 @@ jest.mock('../../../../store/actions', () => ({
getGasFeeEstimatesAndStartPolling: jest getGasFeeEstimatesAndStartPolling: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve()), .mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(),
})); }));
const propsMethodSpies = { const propsMethodSpies = {

View File

@ -5,6 +5,8 @@ import { Tabs, Tab } from '../../../ui/tabs';
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling, getGasFeeEstimatesAndStartPolling,
addPollingTokenToAppState,
removePollingTokenFromAppState,
} from '../../../../store/actions'; } from '../../../../store/actions';
import AdvancedTabContent from './advanced-tab-content'; import AdvancedTabContent from './advanced-tab-content';
import BasicTabContent from './basic-tab-content'; import BasicTabContent from './basic-tab-content';
@ -52,18 +54,27 @@ export default class GasModalPageContainer extends Component {
this._isMounted = true; this._isMounted = true;
getGasFeeEstimatesAndStartPolling().then((pollingToken) => { getGasFeeEstimatesAndStartPolling().then((pollingToken) => {
if (this._isMounted) { if (this._isMounted) {
addPollingTokenToAppState(pollingToken);
this.setState({ pollingToken }); this.setState({ pollingToken });
} else { } else {
disconnectGasFeeEstimatePoller(pollingToken); disconnectGasFeeEstimatePoller(pollingToken);
removePollingTokenFromAppState(pollingToken);
} }
}); });
window.addEventListener('beforeunload', this._beforeUnload);
} }
componentWillUnmount() { _beforeUnload = () => {
this._isMounted = false; this._isMounted = false;
if (this.state.pollingToken) { if (this.state.pollingToken) {
disconnectGasFeeEstimatePoller(this.state.pollingToken); disconnectGasFeeEstimatePoller(this.state.pollingToken);
removePollingTokenFromAppState(this.state.pollingToken);
} }
};
componentWillUnmount() {
this._beforeUnload();
window.removeEventListener('beforeunload', this._beforeUnload);
} }
renderBasicTabContent(gasPriceButtonGroupProps) { renderBasicTabContent(gasPriceButtonGroupProps) {

View File

@ -50,6 +50,8 @@ import {
showLoadingIndication, showLoadingIndication,
updateTokenType, updateTokenType,
updateTransaction, updateTransaction,
addPollingTokenToAppState,
removePollingTokenFromAppState,
} from '../../store/actions'; } from '../../store/actions';
import { setCustomGasLimit } from '../gas/gas.duck'; import { setCustomGasLimit } from '../gas/gas.duck';
import { import {
@ -84,7 +86,6 @@ import {
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import { ETH, GWEI } from '../../helpers/constants/common'; import { ETH, GWEI } from '../../helpers/constants/common';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction'; import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
// typedefs // typedefs
/** /**
* @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction * @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction
@ -437,6 +438,9 @@ export const initializeSendState = createAsyncThunk(
// Instruct the background process that polling for gas prices should begin // Instruct the background process that polling for gas prices should begin
gasEstimatePollToken = await getGasFeeEstimatesAndStartPolling(); gasEstimatePollToken = await getGasFeeEstimatesAndStartPolling();
addPollingTokenToAppState(gasEstimatePollToken);
const { const {
metamask: { gasFeeEstimates, gasEstimateType }, metamask: { gasFeeEstimates, gasEstimateType },
} = thunkApi.getState(); } = thunkApi.getState();
@ -1298,6 +1302,7 @@ export function resetSendState() {
await disconnectGasFeeEstimatePoller( await disconnectGasFeeEstimatePoller(
state[name].gas.gasEstimatePollToken, state[name].gas.gasEstimatePollToken,
); );
removePollingTokenFromAppState(state[name].gas.gasEstimatePollToken);
} }
}; };
} }

View File

@ -16,6 +16,8 @@ import { useGasFeeEstimates } from './useGasFeeEstimates';
jest.mock('../store/actions', () => ({ jest.mock('../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(), disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(), getGasFeeEstimatesAndStartPolling: jest.fn(),
addPollingTokenToAppState: jest.fn(),
removePollingTokenFromAppState: jest.fn(),
})); }));
jest.mock('react-redux', () => { jest.mock('react-redux', () => {

View File

@ -2,6 +2,8 @@ import { useEffect } from 'react';
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling, getGasFeeEstimatesAndStartPolling,
addPollingTokenToAppState,
removePollingTokenFromAppState,
} from '../store/actions'; } from '../store/actions';
/** /**
@ -16,18 +18,30 @@ export function useSafeGasEstimatePolling() {
useEffect(() => { useEffect(() => {
let active = true; let active = true;
let pollToken; let pollToken;
getGasFeeEstimatesAndStartPolling().then((newPollToken) => {
if (active) { const cleanup = () => {
pollToken = newPollToken;
} else {
disconnectGasFeeEstimatePoller(newPollToken);
}
});
return () => {
active = false; active = false;
if (pollToken) { if (pollToken) {
disconnectGasFeeEstimatePoller(pollToken); disconnectGasFeeEstimatePoller(pollToken);
removePollingTokenFromAppState(pollToken);
} }
}; };
getGasFeeEstimatesAndStartPolling().then((newPollToken) => {
if (active) {
pollToken = newPollToken;
addPollingTokenToAppState(pollToken);
} else {
disconnectGasFeeEstimatePoller(newPollToken);
removePollingTokenFromAppState(pollToken);
}
});
window.addEventListener('beforeunload', cleanup);
return () => {
cleanup();
window.removeEventListener('beforeunload', cleanup);
};
}, []); }, []);
} }

View File

@ -40,6 +40,8 @@ import { COLORS } from '../../helpers/constants/design-system';
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling, getGasFeeEstimatesAndStartPolling,
addPollingTokenToAppState,
removePollingTokenFromAppState,
} from '../../store/actions'; } from '../../store/actions';
export default class ConfirmTransactionBase extends Component { export default class ConfirmTransactionBase extends Component {
@ -679,10 +681,19 @@ export default class ConfirmTransactionBase extends Component {
cancelTransaction({ id }); cancelTransaction({ id });
}; };
_beforeUnloadForGasPolling = () => {
this._isMounted = false;
if (this.state.pollingToken) {
disconnectGasFeeEstimatePoller(this.state.pollingToken);
removePollingTokenFromAppState(this.state.pollingToken);
}
};
_removeBeforeUnload = () => { _removeBeforeUnload = () => {
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
window.removeEventListener('beforeunload', this._beforeUnload); window.removeEventListener('beforeunload', this._beforeUnload);
} }
window.removeEventListener('beforeunload', this._beforeUnloadForGasPolling);
}; };
componentDidMount() { componentDidMount() {
@ -723,18 +734,18 @@ export default class ConfirmTransactionBase extends Component {
*/ */
getGasFeeEstimatesAndStartPolling().then((pollingToken) => { getGasFeeEstimatesAndStartPolling().then((pollingToken) => {
if (this._isMounted) { if (this._isMounted) {
addPollingTokenToAppState(pollingToken);
this.setState({ pollingToken }); this.setState({ pollingToken });
} else { } else {
disconnectGasFeeEstimatePoller(pollingToken); disconnectGasFeeEstimatePoller(pollingToken);
removePollingTokenFromAppState(this.state.pollingToken);
} }
}); });
window.addEventListener('beforeunload', this._beforeUnloadForGasPolling);
} }
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._beforeUnloadForGasPolling();
if (this.state.pollingToken) {
disconnectGasFeeEstimatePoller(this.state.pollingToken);
}
this._removeBeforeUnload(); this._removeBeforeUnload();
} }

View File

@ -28,6 +28,8 @@ import {
import { import {
disconnectGasFeeEstimatePoller, disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling, getGasFeeEstimatesAndStartPolling,
addPollingTokenToAppState,
removePollingTokenFromAppState,
} from '../../store/actions'; } from '../../store/actions';
import ConfTx from './conf-tx'; import ConfTx from './conf-tx';
@ -57,6 +59,14 @@ export default class ConfirmTransaction extends Component {
this.state = {}; this.state = {};
} }
_beforeUnload = () => {
this._isMounted = false;
if (this.state.pollingToken) {
disconnectGasFeeEstimatePoller(this.state.pollingToken);
removePollingTokenFromAppState(this.state.pollingToken);
}
};
componentDidMount() { componentDidMount() {
this._isMounted = true; this._isMounted = true;
const { const {
@ -75,11 +85,15 @@ export default class ConfirmTransaction extends Component {
getGasFeeEstimatesAndStartPolling().then((pollingToken) => { getGasFeeEstimatesAndStartPolling().then((pollingToken) => {
if (this._isMounted) { if (this._isMounted) {
this.setState({ pollingToken }); this.setState({ pollingToken });
addPollingTokenToAppState(pollingToken);
} else { } else {
disconnectGasFeeEstimatePoller(pollingToken); disconnectGasFeeEstimatePoller(pollingToken);
removePollingTokenFromAppState(pollingToken);
} }
}); });
window.addEventListener('beforeunload', this._beforeUnload);
if (!totalUnapprovedCount && !sendTo) { if (!totalUnapprovedCount && !sendTo) {
history.replace(mostRecentOverviewPage); history.replace(mostRecentOverviewPage);
return; return;
@ -96,10 +110,8 @@ export default class ConfirmTransaction extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._beforeUnload();
if (this.state.pollingToken) { window.removeEventListener('beforeunload', this._beforeUnload);
disconnectGasFeeEstimatePoller(this.state.pollingToken);
}
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { import {
@ -47,11 +47,17 @@ export default function SendTransactionScreen() {
}); });
const dispatch = useDispatch(); const dispatch = useDispatch();
const cleanup = useCallback(() => {
dispatch(resetSendState());
}, [dispatch]);
useEffect(() => { useEffect(() => {
if (chainId !== undefined) { if (chainId !== undefined) {
dispatch(initializeSendState()); dispatch(initializeSendState());
window.addEventListener('beforeunload', cleanup);
} }
}, [chainId, dispatch]); }, [chainId, dispatch, cleanup]);
useEffect(() => { useEffect(() => {
if (location.search === '?scan=true') { if (location.search === '?scan=true') {
@ -67,8 +73,9 @@ export default function SendTransactionScreen() {
useEffect(() => { useEffect(() => {
return () => { return () => {
dispatch(resetSendState()); dispatch(resetSendState());
window.removeEventListener('beforeunload', cleanup);
}; };
}, [dispatch]); }, [dispatch, cleanup]);
let content; let content;

View File

@ -10,7 +10,10 @@ import {
import { getMethodDataAsync } from '../helpers/utils/transactions.util'; import { getMethodDataAsync } from '../helpers/utils/transactions.util';
import { getSymbolAndDecimals } from '../helpers/utils/token-util'; import { getSymbolAndDecimals } from '../helpers/utils/token-util';
import switchDirection from '../helpers/utils/switch-direction'; import switchDirection from '../helpers/utils/switch-direction';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../shared/constants/app'; import {
ENVIRONMENT_TYPE_NOTIFICATION,
POLLING_TOKEN_ENVIRONMENT_TYPES,
} from '../../shared/constants/app';
import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util'; import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util';
import txHelper from '../helpers/utils/tx-helper'; import txHelper from '../helpers/utils/tx-helper';
import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util'; import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util';
@ -2787,6 +2790,20 @@ export function disconnectGasFeeEstimatePoller(pollToken) {
return promisifiedBackground.disconnectGasFeeEstimatePoller(pollToken); return promisifiedBackground.disconnectGasFeeEstimatePoller(pollToken);
} }
export async function addPollingTokenToAppState(pollingToken) {
return promisifiedBackground.addPollingTokenToAppState(
pollingToken,
POLLING_TOKEN_ENVIRONMENT_TYPES[getEnvironmentType()],
);
}
export async function removePollingTokenFromAppState(pollingToken) {
return promisifiedBackground.removePollingTokenFromAppState(
pollingToken,
POLLING_TOKEN_ENVIRONMENT_TYPES[getEnvironmentType()],
);
}
export function getGasFeeTimeEstimate(maxPriorityFeePerGas, maxFeePerGas) { export function getGasFeeTimeEstimate(maxPriorityFeePerGas, maxFeePerGas) {
return promisifiedBackground.getGasFeeTimeEstimate( return promisifiedBackground.getGasFeeTimeEstimate(
maxPriorityFeePerGas, maxPriorityFeePerGas,