1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Give the user ability to opt out of get balance batch request for all loaded accounts (#16746)

This commit is contained in:
Pedro Figueiredo 2022-12-01 21:16:04 +00:00 committed by GitHub
parent 6689f8b71d
commit d096560d85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 326 additions and 29 deletions

View File

@ -4318,6 +4318,12 @@
"useDefault": {
"message": "Use default"
},
"useMultiAccountBalanceChecker": {
"message": "Batch account balance requests"
},
"useMultiAccountBalanceCheckerDescription": {
"message": "We batch accounts and query Infura to responsively show your balances. If you turn this off, only active accounts will be queried. Some dapps won't work unless you connect your wallet."
},
"usePhishingDetection": {
"message": "Use phishing detection"
},

View File

@ -35,6 +35,7 @@ export default class PreferencesController {
useNonceField: false,
usePhishDetect: true,
dismissSeedBackUpReminder: false,
useMultiAccountBalanceChecker: true,
// set to true means the dynamic list from the API is being used
// set to false will be using the static list from contract-metadata
@ -126,6 +127,16 @@ export default class PreferencesController {
this.store.updateState({ usePhishDetect: val });
}
/**
* Setter for the `useMultiAccountBalanceChecker` property
*
* @param {boolean} val - Whether or not the user wants to fetch balances for
* all accounts that he has added to the MetaMask wallet state.
*/
setUseMultiAccountBalanceChecker(val) {
this.store.updateState({ useMultiAccountBalanceChecker: val });
}
/**
* Setter for the `useTokenDetection` property
*

View File

@ -287,6 +287,28 @@ describe('preferences controller', function () {
);
});
});
describe('setUseMultiAccountBalanceChecker', function () {
it('should default to true', function () {
const state = preferencesController.store.getState();
assert.equal(state.useMultiAccountBalanceChecker, true);
});
it('should set the setUseMultiAccountBalanceChecker property in state', function () {
assert.equal(
preferencesController.store.getState().useMultiAccountBalanceChecker,
true,
);
preferencesController.setUseMultiAccountBalanceChecker(false);
assert.equal(
preferencesController.store.getState().useMultiAccountBalanceChecker,
false,
);
});
});
describe('setUseTokenDetection', function () {
it('should default to false', function () {
const state = preferencesController.store.getState();

View File

@ -78,6 +78,7 @@ export default class AccountTracker {
this._updateForBlock = this._updateForBlock.bind(this);
this.getCurrentChainId = opts.getCurrentChainId;
this.getNetworkIdentifier = opts.getNetworkIdentifier;
this.preferencesController = opts.preferencesController;
this.ethersProvider = new ethers.providers.Web3Provider(this._provider);
}
@ -205,8 +206,20 @@ export default class AccountTracker {
* @returns {Promise} after all account balances updated
*/
async _updateAccounts() {
const { accounts } = this.store.getState();
const addresses = Object.keys(accounts);
const { useMultiAccountBalanceChecker } =
this.preferencesController.store.getState();
let addresses = [];
if (useMultiAccountBalanceChecker) {
const { accounts } = this.store.getState();
addresses = Object.keys(accounts);
} else {
const selectedAddress = this.preferencesController.getSelectedAddress();
addresses = [selectedAddress];
}
const chainId = this.getCurrentChainId();
const networkId = this.getNetworkIdentifier();
const rpcUrl = 'http://127.0.0.1:8545';

View File

@ -550,6 +550,7 @@ export default class MetamaskController extends EventEmitter {
getNetworkIdentifier: this.networkController.getNetworkIdentifier.bind(
this.networkController,
),
preferencesController: this.preferencesController,
});
// start and stop polling for balances based on activeControllerConnections
@ -1605,6 +1606,10 @@ export default class MetamaskController extends EventEmitter {
setUsePhishDetect: preferencesController.setUsePhishDetect.bind(
preferencesController,
),
setUseMultiAccountBalanceChecker:
preferencesController.setUseMultiAccountBalanceChecker.bind(
preferencesController,
),
setUseTokenDetection: preferencesController.setUseTokenDetection.bind(
preferencesController,
),

View File

@ -12,6 +12,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
import {
setCompletedOnboarding,
setFeatureFlag,
setUseMultiAccountBalanceChecker,
setUsePhishDetect,
setUseTokenDetection,
} from '../../../store/actions';
@ -26,6 +27,10 @@ export default function PrivacySettings() {
const [turnOnTokenDetection, setTurnOnTokenDetection] = useState(true);
const [showIncomingTransactions, setShowIncomingTransactions] =
useState(true);
const [
isMultiAccountBalanceCheckerEnabled,
setMultiAccountBalanceCheckerEnabled,
] = useState(true);
const handleSubmit = () => {
dispatch(
@ -33,6 +38,9 @@ export default function PrivacySettings() {
);
dispatch(setUsePhishDetect(usePhishingDetection));
dispatch(setUseTokenDetection(turnOnTokenDetection));
dispatch(
setUseMultiAccountBalanceChecker(isMultiAccountBalanceCheckerEnabled),
);
dispatch(setCompletedOnboarding());
history.push(ONBOARDING_PIN_EXTENSION_ROUTE);
};
@ -104,6 +112,12 @@ export default function PrivacySettings() {
title={t('turnOnTokenDetection')}
description={t('useTokenDetectionPrivacyDesc')}
/>
<Setting
value={isMultiAccountBalanceCheckerEnabled}
setValue={setMultiAccountBalanceCheckerEnabled}
title={t('useMultiAccountBalanceChecker')}
description={t('useMultiAccountBalanceCheckerDescription')}
/>
</div>
<Button type="primary" rounded onClick={handleSubmit}>
{t('done')}

View File

@ -24,12 +24,14 @@ describe('Privacy Settings Onboarding View', () => {
const completeOnboardingStub = jest
.fn()
.mockImplementation(() => Promise.resolve());
const setUseMultiAccountBalanceCheckerStub = jest.fn();
setBackgroundConnection({
setFeatureFlag: setFeatureFlagStub,
setUsePhishDetect: setUsePhishDetectStub,
setUseTokenDetection: setUseTokenDetectionStub,
completeOnboarding: completeOnboardingStub,
setUseMultiAccountBalanceChecker: setUseMultiAccountBalanceCheckerStub,
});
it('should update preferences', () => {
@ -41,6 +43,8 @@ describe('Privacy Settings Onboarding View', () => {
expect(setFeatureFlagStub).toHaveBeenCalledTimes(0);
expect(setUsePhishDetectStub).toHaveBeenCalledTimes(0);
expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(0);
expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(0);
const toggles = container.querySelectorAll('input[type=checkbox]');
const submitButton = getByText('Done');
@ -48,25 +52,35 @@ describe('Privacy Settings Onboarding View', () => {
fireEvent.click(toggles[0]);
fireEvent.click(toggles[1]);
fireEvent.click(toggles[2]);
fireEvent.click(toggles[3]);
fireEvent.click(submitButton);
expect(setFeatureFlagStub).toHaveBeenCalledTimes(1);
expect(setUsePhishDetectStub).toHaveBeenCalledTimes(1);
expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(1);
expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(1);
expect(setFeatureFlagStub.mock.calls[0][1]).toStrictEqual(false);
expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false);
expect(setUseTokenDetectionStub.mock.calls[0][0]).toStrictEqual(false);
expect(setUseMultiAccountBalanceCheckerStub.mock.calls[0][0]).toStrictEqual(
false,
);
// toggle back to true
fireEvent.click(toggles[0]);
fireEvent.click(toggles[1]);
fireEvent.click(toggles[2]);
fireEvent.click(toggles[3]);
fireEvent.click(submitButton);
expect(setFeatureFlagStub).toHaveBeenCalledTimes(2);
expect(setUsePhishDetectStub).toHaveBeenCalledTimes(2);
expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(2);
expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(2);
expect(setFeatureFlagStub.mock.calls[1][1]).toStrictEqual(true);
expect(setUsePhishDetectStub.mock.calls[1][0]).toStrictEqual(true);
expect(setUseTokenDetectionStub.mock.calls[1][0]).toStrictEqual(true);
expect(setUseMultiAccountBalanceCheckerStub.mock.calls[1][0]).toStrictEqual(
true,
);
});
});

View File

@ -241,6 +241,77 @@ exports[`Security Tab should match snapshot 1`] = `
</div>
</div>
</div>
<div
class="settings-page__content-row"
>
<div
class="settings-page__content-item"
>
<span>
Batch account balance requests
</span>
<div
class="settings-page__content-description"
>
<span>
We batch accounts and query Infura to responsively show your balances. If you turn this off, only active accounts will be queried. Some dapps won't work unless you connect your wallet.
</span>
</div>
</div>
<div
class="settings-page__content-item"
>
<div
class="settings-page__content-item-col"
>
<label
class="toggle-button toggle-button--off"
tabindex="0"
>
<div
style="display: flex; width: 52px; align-items: center; justify-content: flex-start; position: relative; cursor: pointer; background-color: transparent; border: 0px; padding: 0px; user-select: none;"
>
<div
style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);"
>
<div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 0; width: 26px; height: 20px; left: 4px;"
/>
<div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 1;"
/>
</div>
<div
style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;"
>
<div
style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(106, 115, 125); left: 3px;"
/>
</div>
<input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox"
value="false"
/>
</div>
<div
class="toggle-button__status"
>
<span
class="toggle-button__label-off"
>
Off
</span>
<span
class="toggle-button__label-on"
>
On
</span>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -24,6 +24,8 @@ export default class SecurityTab extends PureComponent {
setShowIncomingTransactionsFeatureFlag: PropTypes.func.isRequired,
setUsePhishDetect: PropTypes.func.isRequired,
usePhishDetect: PropTypes.bool.isRequired,
useMultiAccountBalanceChecker: PropTypes.bool.isRequired,
setUseMultiAccountBalanceChecker: PropTypes.func.isRequired,
};
settingsRefs = Array(
@ -83,33 +85,6 @@ export default class SecurityTab extends PureComponent {
);
}
renderMetaMetricsOptIn() {
const { t } = this.context;
const { participateInMetaMetrics, setParticipateInMetaMetrics } =
this.props;
return (
<div ref={this.settingsRefs[3]} className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{t('participateInMetaMetrics')}</span>
<div className="settings-page__content-description">
<span>{t('participateInMetaMetricsDescription')}</span>
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<ToggleButton
value={participateInMetaMetrics}
onToggle={(value) => setParticipateInMetaMetrics(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
</div>
</div>
</div>
);
}
renderIncomingTransactionsOptIn() {
const { t } = this.context;
const { showIncomingTransactions, setShowIncomingTransactionsFeatureFlag } =
@ -165,6 +140,60 @@ export default class SecurityTab extends PureComponent {
);
}
renderMetaMetricsOptIn() {
const { t } = this.context;
const { participateInMetaMetrics, setParticipateInMetaMetrics } =
this.props;
return (
<div ref={this.settingsRefs[3]} className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{t('participateInMetaMetrics')}</span>
<div className="settings-page__content-description">
<span>{t('participateInMetaMetricsDescription')}</span>
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<ToggleButton
value={participateInMetaMetrics}
onToggle={(value) => setParticipateInMetaMetrics(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
</div>
</div>
</div>
);
}
renderMultiAccountBalanceCheckerOptIn() {
const { t } = this.context;
const { useMultiAccountBalanceChecker, setUseMultiAccountBalanceChecker } =
this.props;
return (
<div ref={this.settingsRefs[3]} className="settings-page__content-row">
<div className="settings-page__content-item">
<span>{t('useMultiAccountBalanceChecker')}</span>
<div className="settings-page__content-description">
<span>{t('useMultiAccountBalanceCheckerDescription')}</span>
</div>
</div>
<div className="settings-page__content-item">
<div className="settings-page__content-item-col">
<ToggleButton
value={useMultiAccountBalanceChecker}
onToggle={(value) => setUseMultiAccountBalanceChecker(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
</div>
</div>
</div>
);
}
render() {
const { warning } = this.props;
@ -175,6 +204,7 @@ export default class SecurityTab extends PureComponent {
{this.renderIncomingTransactionsOptIn()}
{this.renderPhishingDetectionToggle()}
{this.renderMetaMetricsOptIn()}
{this.renderMultiAccountBalanceCheckerOptIn()}
</div>
);
}

View File

@ -5,6 +5,7 @@ import {
setFeatureFlag,
setParticipateInMetaMetrics,
setUsePhishDetect,
setUseMultiAccountBalanceChecker,
} from '../../../store/actions';
import SecurityTab from './security-tab.component';
@ -17,6 +18,7 @@ const mapStateToProps = (state) => {
featureFlags: { showIncomingTransactions } = {},
participateInMetaMetrics,
usePhishDetect,
useMultiAccountBalanceChecker,
} = metamask;
return {
@ -24,6 +26,7 @@ const mapStateToProps = (state) => {
showIncomingTransactions,
participateInMetaMetrics,
usePhishDetect,
useMultiAccountBalanceChecker,
};
};
@ -34,6 +37,8 @@ const mapDispatchToProps = (dispatch) => {
setShowIncomingTransactionsFeatureFlag: (shouldShow) =>
dispatch(setFeatureFlag('showIncomingTransactions', shouldShow)),
setUsePhishDetect: (val) => dispatch(setUsePhishDetect(val)),
setUseMultiAccountBalanceChecker: (val) =>
dispatch(setUseMultiAccountBalanceChecker(val)),
};
};

View File

@ -83,4 +83,19 @@ describe('Security Tab', () => {
expect(showIncomingCheckbox).toHaveAttribute('value', 'true');
});
it('toggles batch balance checks', () => {
const { queryAllByRole } = renderWithProvider(<SecurityTab />, mockStore);
const checkboxes = queryAllByRole('checkbox');
const batchBalanceChecksCheckbox = checkboxes[3];
expect(batchBalanceChecksCheckbox).toHaveAttribute('value', 'false');
fireEvent.change(batchBalanceChecksCheckbox, {
target: { value: true },
});
expect(batchBalanceChecksCheckbox).toHaveAttribute('value', 'true');
});
});

View File

@ -2705,6 +2705,19 @@ export function setUsePhishDetect(val) {
};
}
export function setUseMultiAccountBalanceChecker(val) {
return (dispatch) => {
dispatch(showLoadingIndication());
log.debug(`background.setUseMultiAccountBalanceChecker`);
callBackgroundMethod('setUseMultiAccountBalanceChecker', [val], (err) => {
dispatch(hideLoadingIndication());
if (err) {
dispatch(displayWarning(err.message));
}
});
};
}
export function setUseTokenDetection(val) {
return (dispatch) => {
dispatch(showLoadingIndication());

View File

@ -1522,6 +1522,84 @@ describe('Actions', () => {
});
});
describe('#setUsePhishDetect', () => {
afterEach(() => {
sinon.restore();
});
it('calls setUsePhishDetect in background', () => {
const store = mockStore();
const setUsePhishDetectStub = sinon.stub().callsFake((_, cb) => cb());
_setBackgroundConnection({
setUsePhishDetect: setUsePhishDetectStub,
});
store.dispatch(actions.setUsePhishDetect());
expect(setUsePhishDetectStub.callCount).toStrictEqual(1);
});
it('errors when setUsePhishDetect in background throws', () => {
const store = mockStore();
const setUsePhishDetectStub = sinon.stub().callsFake((_, cb) => {
cb(new Error('error'));
});
_setBackgroundConnection({
setUsePhishDetect: setUsePhishDetectStub,
});
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ type: 'DISPLAY_WARNING', value: 'error' },
];
store.dispatch(actions.setUsePhishDetect());
expect(store.getActions()).toStrictEqual(expectedActions);
});
});
describe('#setUseMultiAccountBalanceChecker', () => {
afterEach(() => {
sinon.restore();
});
it('calls setUseMultiAccountBalanceChecker in background', () => {
const store = mockStore();
const setUseMultiAccountBalanceCheckerStub = sinon
.stub()
.callsFake((_, cb) => cb());
_setBackgroundConnection({
setUseMultiAccountBalanceChecker: setUseMultiAccountBalanceCheckerStub,
});
store.dispatch(actions.setUseMultiAccountBalanceChecker());
expect(setUseMultiAccountBalanceCheckerStub.callCount).toStrictEqual(1);
});
it('errors when setUseMultiAccountBalanceChecker in background throws', () => {
const store = mockStore();
const setUseMultiAccountBalanceCheckerStub = sinon
.stub()
.callsFake((_, cb) => {
cb(new Error('error'));
});
_setBackgroundConnection({
setUseMultiAccountBalanceChecker: setUseMultiAccountBalanceCheckerStub,
});
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ type: 'DISPLAY_WARNING', value: 'error' },
];
store.dispatch(actions.setUseMultiAccountBalanceChecker());
expect(store.getActions()).toStrictEqual(expectedActions);
});
});
describe('#updateCurrentLocale', () => {
beforeEach(() => {
sinon.stub(window, 'fetch').resolves({