Skip to content

Commit 177517c

Browse files
feat: disable enforced simulations (#33802)
## **Description** Support disabling enforced simulations via a new simulation settings modal. Specifically: - Add `enableEnforcedSimulations` and `enableEnforcedSimulationsForTransactions` to `AppStateController`. - Add `applyTransactionContainersExisting` utility method to apply transaction containers to an existing transaction. - Add `SimulationSettingsModal` component to configure enforced simulation settings. - Add `useIsEnforcedSimulationsSupported` hook to determine if settings should be displayed. - Check `AppStateController` state in `EnforceSimulationHook`. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/33802?quickstart=1) ## **Related issues** Fixes: [#5173](MetaMask/MetaMask-planning#5173) ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** <img width="393" alt="Modal" src="https://github.com/user-attachments/assets/fc5b8703-c853-4a9e-8d67-9b31337ed68d" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent b2a760c commit 177517c

File tree

33 files changed

+1053
-150
lines changed

33 files changed

+1053
-150
lines changed

app/_locales/en/messages.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/_locales/en_GB/messages.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/scripts/constants/sentry-state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export const SENTRY_BACKGROUND_STATE = {
6060
onboardingDate: false,
6161
currentExtensionPopupId: false,
6262
defaultHomeActiveTabName: true,
63+
enableEnforcedSimulations: true,
64+
enableEnforcedSimulationsForTransactions: false,
6365
fullScreenGasPollTokens: true,
6466
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
6567
// eslint-disable-next-line @typescript-eslint/naming-convention

app/scripts/controller-init/messengers/transaction-controller-messenger.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@metamask/network-controller';
1313
import {
1414
TransactionControllerEstimateGasAction,
15+
TransactionControllerGetStateAction,
1516
TransactionControllerMessenger,
1617
TransactionControllerPostTransactionBalanceUpdatedEvent,
1718
TransactionControllerTransactionApprovedEvent,
@@ -35,6 +36,7 @@ import {
3536
SwapsControllerSetApproveTxIdAction,
3637
SwapsControllerSetTradeTxIdAction,
3738
} from '../../controllers/swaps/swaps.types';
39+
import { AppStateControllerGetStateAction } from '../../controllers/app-state-controller';
3840
import {
3941
InstitutionalSnapControllerPublishHookAction,
4042
InstitutionalSnapControllerBeforeCheckPendingTransactionHookAction,
@@ -44,6 +46,7 @@ type MessengerActions =
4446
| ApprovalControllerActions
4547
| AccountsControllerGetSelectedAccountAction
4648
| AccountsControllerGetStateAction
49+
| AppStateControllerGetStateAction
4750
| DelegationControllerSignDelegationAction
4851
| InstitutionalSnapControllerPublishHookAction
4952
| InstitutionalSnapControllerBeforeCheckPendingTransactionHookAction
@@ -55,7 +58,8 @@ type MessengerActions =
5558
| RemoteFeatureFlagControllerGetStateAction
5659
| SwapsControllerSetApproveTxIdAction
5760
| SwapsControllerSetTradeTxIdAction
58-
| TransactionControllerEstimateGasAction;
61+
| TransactionControllerEstimateGasAction
62+
| TransactionControllerGetStateAction;
5963

6064
type MessengerEvents =
6165
| TransactionControllerTransactionApprovedEvent
@@ -117,6 +121,7 @@ export function getTransactionControllerInitMessenger(
117121
'ApprovalController:endFlow',
118122
'ApprovalController:startFlow',
119123
'ApprovalController:updateRequestState',
124+
'AppStateController:getState',
120125
'DelegationController:signDelegation',
121126
'InstitutionalSnapController:beforeCheckPendingTransactionHook',
122127
'InstitutionalSnapController:publishHook',
@@ -127,6 +132,7 @@ export function getTransactionControllerInitMessenger(
127132
'SwapsController:setApproveTxId',
128133
'SwapsController:setTradeTxId',
129134
'TransactionController:estimateGas',
135+
'TransactionController:getState',
130136
],
131137
});
132138
}

app/scripts/controllers/app-state-controller.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ const extensionMock = {
4545
},
4646
} as unknown as jest.Mocked<Browser>;
4747

48+
const TRANSACTION_ID_MOCK = '123-456';
49+
4850
describe('AppStateController', () => {
4951
describe('setOutdatedBrowserWarningLastShown', () => {
5052
it('sets the last shown time', async () => {
@@ -610,6 +612,46 @@ describe('AppStateController', () => {
610612
});
611613
});
612614
});
615+
616+
describe('setEnableEnforcedSimulations', () => {
617+
it('updates the enableEnforcedSimulations state', async () => {
618+
await withController(({ controller }) => {
619+
controller.setEnableEnforcedSimulations(false);
620+
expect(controller.state.enableEnforcedSimulations).toBe(false);
621+
622+
controller.setEnableEnforcedSimulations(true);
623+
expect(controller.state.enableEnforcedSimulations).toBe(true);
624+
});
625+
});
626+
});
627+
628+
describe('setEnableEnforcedSimulationsForTransaction', () => {
629+
it('updates the enableEnforcedSimulationsForTransactions state', async () => {
630+
await withController(({ controller }) => {
631+
controller.setEnableEnforcedSimulationsForTransaction(
632+
TRANSACTION_ID_MOCK,
633+
true,
634+
);
635+
636+
expect(
637+
controller.state.enableEnforcedSimulationsForTransactions,
638+
).toStrictEqual({
639+
[TRANSACTION_ID_MOCK]: true,
640+
});
641+
642+
controller.setEnableEnforcedSimulationsForTransaction(
643+
TRANSACTION_ID_MOCK,
644+
false,
645+
);
646+
647+
expect(
648+
controller.state.enableEnforcedSimulationsForTransactions,
649+
).toStrictEqual({
650+
[TRANSACTION_ID_MOCK]: false,
651+
});
652+
});
653+
});
654+
});
613655
});
614656

615657
type WithControllerOptions = {

app/scripts/controllers/app-state-controller.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ export type AppStateControllerState = {
9090
isUpdateAvailable: boolean;
9191
updateModalLastDismissedAt: number | null;
9292
lastUpdatedAt: number | null;
93+
enableEnforcedSimulations: boolean;
94+
enableEnforcedSimulationsForTransactions: Record<string, boolean>;
9395
};
9496

9597
const controllerName = 'AppStateController';
@@ -209,6 +211,8 @@ const getDefaultAppStateControllerState = (): AppStateControllerState => ({
209211
isUpdateAvailable: false,
210212
updateModalLastDismissedAt: null,
211213
lastUpdatedAt: null,
214+
enableEnforcedSimulations: true,
215+
enableEnforcedSimulationsForTransactions: {},
212216
...getInitialStateOverrides(),
213217
});
214218

@@ -386,6 +390,14 @@ const controllerMetadata = {
386390
persist: true,
387391
anonymous: true,
388392
},
393+
enableEnforcedSimulations: {
394+
persist: true,
395+
anonymous: true,
396+
},
397+
enableEnforcedSimulationsForTransactions: {
398+
persist: false,
399+
anonymous: true,
400+
},
389401
};
390402

391403
export class AppStateController extends BaseController<
@@ -1121,4 +1133,19 @@ export class AppStateController extends BaseController<
11211133
state.throttledOrigins[origin] = throttledOriginState;
11221134
});
11231135
}
1136+
1137+
setEnableEnforcedSimulations(enabled: boolean): void {
1138+
this.update((state) => {
1139+
state.enableEnforcedSimulations = enabled;
1140+
});
1141+
}
1142+
1143+
setEnableEnforcedSimulationsForTransaction(
1144+
transactionId: string,
1145+
enabled: boolean,
1146+
): void {
1147+
this.update((state) => {
1148+
state.enableEnforcedSimulationsForTransactions[transactionId] = enabled;
1149+
});
1150+
}
11241151
}

app/scripts/lib/transaction/containers/enforced-simulations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ function generateDelegation({
106106
return delegation;
107107
}
108108

109-
export function generateCalldata({
109+
function generateCalldata({
110110
transaction,
111111
delegation,
112112
}: {

app/scripts/lib/transaction/containers/util.test.ts

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,40 @@
11
import {
22
TransactionContainerType,
33
TransactionControllerEstimateGasAction,
4+
TransactionControllerGetStateAction,
45
TransactionMeta,
56
} from '@metamask/transaction-controller';
67
import { cloneDeep } from 'lodash';
78
import { Messenger } from '@metamask/base-controller';
89
import { TransactionControllerInitMessenger } from '../../../controller-init/messengers/transaction-controller-messenger';
9-
import { applyTransactionContainers } from './util';
10+
import {
11+
applyTransactionContainers,
12+
applyTransactionContainersExisting,
13+
} from './util';
1014
import { enforceSimulations } from './enforced-simulations';
1115

1216
jest.mock('./enforced-simulations');
1317

14-
const TRANSACTION_META_MOCK = { txParams: {} } as TransactionMeta;
15-
const CHAIN_ID_MOCK = '0x123' as const;
18+
const TRANSACTION_ID_MOCK = '123-456';
1619
const ESTIMATE_GAS_MOCK = '0x456' as const;
20+
const NEW_DATA_MOCK = '0x789' as const;
21+
22+
const TRANSACTION_META_MOCK = {
23+
id: TRANSACTION_ID_MOCK,
24+
txParams: {},
25+
} as TransactionMeta;
1726

1827
describe('Container Utils', () => {
1928
const enforceSimulationsMock = jest.mocked(enforceSimulations);
29+
const getTransactionControllerStateMock = jest.fn();
2030
let messenger: TransactionControllerInitMessenger;
2131

2232
beforeEach(() => {
2333
jest.resetAllMocks();
2434

2535
const baseMessenger = new Messenger<
26-
TransactionControllerEstimateGasAction,
36+
| TransactionControllerEstimateGasAction
37+
| TransactionControllerGetStateAction,
2738
never
2839
>();
2940

@@ -32,17 +43,29 @@ describe('Container Utils', () => {
3243
async () => ({ gas: ESTIMATE_GAS_MOCK, simulationFails: { debug: {} } }),
3344
);
3445

46+
baseMessenger.registerActionHandler(
47+
'TransactionController:getState',
48+
getTransactionControllerStateMock,
49+
);
50+
3551
messenger = baseMessenger.getRestricted({
3652
name: 'TransactionControllerInitMessenger',
37-
allowedActions: ['TransactionController:estimateGas'],
53+
allowedActions: [
54+
'TransactionController:estimateGas',
55+
'TransactionController:getState',
56+
],
3857
allowedEvents: [],
3958
});
4059

4160
enforceSimulationsMock.mockResolvedValue({
4261
updateTransaction: (tx) => {
43-
tx.chainId = CHAIN_ID_MOCK;
62+
tx.txParams.data = NEW_DATA_MOCK;
4463
},
4564
});
65+
66+
getTransactionControllerStateMock.mockReturnValue({
67+
transactions: [],
68+
});
4669
});
4770

4871
describe('applyTransactionContainers', () => {
@@ -57,7 +80,7 @@ describe('Container Utils', () => {
5780
const transactionToUpdate = cloneDeep(TRANSACTION_META_MOCK);
5881
updateTransaction(transactionToUpdate);
5982

60-
expect(transactionToUpdate.chainId).toBe(CHAIN_ID_MOCK);
83+
expect(transactionToUpdate.txParams.data).toBe(NEW_DATA_MOCK);
6184
});
6285

6386
it('updates gas if not approved', async () => {
@@ -90,4 +113,45 @@ describe('Container Utils', () => {
90113
]);
91114
});
92115
});
116+
117+
describe('applyTransactionContainersExisting', () => {
118+
it('throws if transaction not found', async () => {
119+
const updateEditableParams = jest.fn();
120+
121+
await expect(
122+
applyTransactionContainersExisting({
123+
containerTypes: [TransactionContainerType.EnforcedSimulations],
124+
transactionId: TRANSACTION_ID_MOCK,
125+
messenger,
126+
updateEditableParams,
127+
}),
128+
).rejects.toThrow(
129+
`Transaction with ID ${TRANSACTION_ID_MOCK} not found.`,
130+
);
131+
});
132+
133+
it('calls updateEditableParams with new parameters', async () => {
134+
getTransactionControllerStateMock.mockReturnValue({
135+
transactions: [TRANSACTION_META_MOCK],
136+
});
137+
138+
const updateEditableParams = jest.fn();
139+
140+
await applyTransactionContainersExisting({
141+
containerTypes: [TransactionContainerType.EnforcedSimulations],
142+
transactionId: TRANSACTION_ID_MOCK,
143+
messenger,
144+
updateEditableParams,
145+
});
146+
147+
expect(updateEditableParams).toHaveBeenCalledWith(
148+
TRANSACTION_ID_MOCK,
149+
expect.objectContaining({
150+
containerTypes: [TransactionContainerType.EnforcedSimulations],
151+
data: NEW_DATA_MOCK,
152+
gas: ESTIMATE_GAS_MOCK,
153+
}),
154+
);
155+
});
156+
});
93157
});

0 commit comments

Comments
 (0)