Skip to content

Commit 41930af

Browse files
httpJunkiedanjmgeorgeweilerdan437
authored
feat: enable STX by default with migration and notification (#28854)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR enables Smart Transactions (STX) by default through migration number 135 for users who have either opted out or haven't interacted with the STX toggle, provided they have no recorded STX activity. How it works: - Upon Migration 135, alert displays on transaction confirmations: - Legacy transaction flow - New transaction flow (experimental) - Swaps confirmation flow - Contract deployment - Contract interactions (minting, etc.) In the case a user migrates from a previous version of the extension and the migration runs and sets STX toggle "ON" in `Settings > Advanced > Smart Transactions`, they will receive an STX Banner Alert on transaction confirmation screens until dismissed through a close button, or by clicking on the "Higher success rates" link within the alert that goes to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) for more information. Edge Cases: If a user is new and setting up a wallet for the first time, they will not receive the Banner Alert. If a user imports a new wallet during a fresh install of the extension on a new browser or recovers a wallet, it's possible they may not see the alert if STX was on in a previous install. The STX Banner Alert is dismissed and will not show again if a user is in the state to get shown the banner and toggles STX off independently even if they do not physically dismiss the STX Banner Alert. Migration Logic: 1. If `smartTransactionsOptInStatus` is `null` (new/never interacted) - Sets status to true - Enables notification flag 2. If status is false (previously opted out): - With no Ethereum Mainnet STX activity: Sets to true with notification - With existing Mainnet STX activity: Preserves user preference 3. If status is true: No changes needed UI Components: - Implements SmartTransactionsBannerAlert component for user notification The notification system bridges the migration changes with the UI, ensuring users are informed of the STX enablement while maintaining their ability to opt out through settings. **Target release:** TBD **Affected user base:** ~5.7M users who previously opted out of STX but have no STX activity. --- ## **Related issues** Fixes: N/A --- ## **Running Unit Tests** Migration Test: ```bash yarn jest app/scripts/migrations/135.test.ts --verbose ``` Smart Transaction Banner Component Test: ```bash yarn jest ui/pages/confirmations/components/smart-transactions-banner-alert/smart-transactions-banner-alert.test.ts --coverage=false ``` Confirm Transaction base Test: ```bash yarn jest ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js --coverage=false ``` Preferences Controller Test: ```bash yarn jest app/scripts/controllers/preferences-controller.test.ts --coverage=false ``` Transaction Alerts Component Test: ```bash yarn jest ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js --coverage=false ``` ## **Manual testing steps** **Test Migration (using a wallet/account with no STX Transactions)** 1. Switch branch: `git checkout tags/v12.5.0` and run: ```bash yarn yarn webpack ``` - Generate a `dist/chrome` directory 6. In Chrome Extension Manager, "Load Unpacked" from this directory 7. Import or setup a wallet without STX transactions, launch the wallet and choose "No Thanks" on the "Enhanced Transaction Protection" popup. 8. Check that toggle is OFF in: `Settings > Advanced > Smart Transactions` 9. Close MetaMask Extension and toggle the Extension "OFF" in the Extension Manager 10. Switch to branch from this PR `git checkout feat/enable-stx-migration` ```bash yarn yarn webpack ``` 11. Open MetaMask Extension and Check that toggle is ON in: `Settings > Advanced > Smart Transactions` **Test STX Banner Alert that it shows on Transaction Confirmations and not Sign Confirmations)** **_(using new confirmations flow)_** 12. Check that `Improved transaction requests` is ON in `Settings > Experimental` 13. Open the E2E TestDapp, try several Signs (ETH Sign, Personal Sign, Sign Typed Data, etc..) and ensure the STX Banner Alert does not show on those confirmations screens. 14. Create a Send transaction to your own wallet for `0.0001` ETH 15. Ensure that Smart Transactions Banner Alert IS showing 16. Start a Swaps transaction on Ethereum Mainnet 17. Ensure that Smart Transactions Banner Alert IS showing **Test STX Banner Alert that it shows on Transaction Confirmations and not Sign Confirmations)** **_(using old confirmations flow)_** 18. Check that `Improved transaction requests` is OFF in `Settings > Experimental` 19. Open the E2E TestDapp, try several Signs (ETH Sign, Personal Sign, Sign Typed Data, etc..) and ensure the STX Banner Alert does not show on those confirmations screens. 20. Create a Send transaction to your own wallet for `0.0001` ETH 21. Ensure that Smart Transactions Banner Alert IS showing 22. Start a Swaps transaction on Ethereum Mainnet 23. Ensure that Smart Transactions Banner Alert IS showing 24. Without clicking on Check that "Higher success rates" link (inspect) goes to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) 25. Dismiss the Smart Transactions Banner Alert and set `Improved transaction requests` back to ON in `Settings > Experimental` 26. Create a Send transaction to your own wallet for `0.0001` ETH 27. Ensure that Smart Transactions Banner Alert IS NOT showing **Congrats, you have manually tested the happy path, now we just need to test the edge cases:** 1. Remove the extensions from Extension Manager and Repeat steps 1 to 10 above to run migration again 2. Create a Send transaction to your own wallet for `0.0001` ETH 3. Ensure that Smart Transactions Banner Alert IS showing - DO NOT DISMISS THE ALERT. Instead 4. Open MetaMask Extension and Check that toggle is ON in: `Settings > Advanced > Smart Transactions` 5. Turn it off 6. Create a Send transaction to your own wallet for `0.0001` ETH 7. Ensure that Smart Transactions Banner Alert IS NOT showing 8. Perform any other Signing and/or Transaction Confirmations and ensure there are no modals that show errors and that the Banner Alert does not show anymore. **Test edge case that after STX migration runs and Banner is being shown that clicking on "Higher success rates" link:** 1. Remove the extensions from Extension Manager and Repeat steps 1 to 10 above to run migration again 2. Create a Send transaction to your own wallet for `0.0001` ETH 3. Ensure that Smart Transactions Banner Alert IS showing - DO NOT DISMISS THE ALERT WITH CLOSE BUTTON... INSTEAD 4. Click on "Higher success rates" link and ensure that it goes to: [What is 'Smart Transactions'?](https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/) 5. Open MetaMask Extension and Check that toggle is ON in: `Settings > Advanced > Smart Transactions` 6. Turn it off 7. Create a Send transaction to your own wallet for `0.0001` ETH 8. Ensure that Smart Transactions Banner Alert IS NOT showing 9. Perform any other Signing and/or Transaction Confirmations and ensure there are no modals that show errors and that the Banner Alert does not show anymore. Because the NEW confirmation flow does not support alerts using hooks that are dismissible, we have used the old style Banner Alert, and it is normal for their to be some variation on where the alert shows up and it's surroundings. But overall they should look similar. --- ## **Screenshots/Recordings** ### **Before** <img width="150" alt="01-stx_before" src="https://github.com/user-attachments/assets/31c0bd4e-1ce5-4bfc-88bc-70a3c1a8898a" /> <img width="150" alt="02-legacySend_before" src="https://github.com/user-attachments/assets/111662b8-182f-44ce-8a3e-e308e08e3c3f" /> <img width="150" alt="03-legacySwap_before" src="https://github.com/user-attachments/assets/25dafb19-bd63-4a29-b0bc-ac394b8f09cf" /> <img width="150" alt="04-signTypedDataV4_before" src="https://github.com/user-attachments/assets/c512ad84-d921-4b1e-8443-6d10d527ab67" /> <img width="150" alt="05-contractDeployment_before" src="https://github.com/user-attachments/assets/31624828-5e3d-4fc8-8264-64703c6a4087" /> <img width="150" alt="06-contractInteraction_before" src="https://github.com/user-attachments/assets/c132c7ff-64dc-4720-9209-331dbaf2bfe3" /> <img width="200" alt="07-wideSwap_before" src="https://github.com/user-attachments/assets/521e8129-368f-45cc-acbe-9c4f90eeb9ec" /> ### **After** <img width="150" alt="01-stx_after" src="https://github.com/user-attachments/assets/ce821edf-bb86-46af-97aa-fb816208c344" /> <img width="150" alt="02-legacySend_after" src="https://github.com/user-attachments/assets/98f3588c-3557-4f4f-9a10-969e27b14dd4" /> <img width="150" alt="03-legacySwap_after" src="https://github.com/user-attachments/assets/b60c91e8-63af-4d45-947d-d250f39fdb74" /> <img width="150" alt="04-signTypedDataV4_after" src="https://github.com/user-attachments/assets/a3600ed9-17eb-4bf2-b5a9-409857fe1911" /> <img width="150" alt="05-contractDeployment_after" src="https://github.com/user-attachments/assets/b6cf1e7e-6dba-4eae-8e78-ce16d12f7306" /> <img width="150" alt="06-contractInteraction_after" src="https://github.com/user-attachments/assets/d65e6a93-c6ca-4328-9622-089ab5fe5d97" /> <img width="200" alt="07-wideSwap_after" src="https://github.com/user-attachments/assets/2161a3a8-6a05-4b9b-a470-e004900c9fc1" /> --- ## **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/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests for the new behavior, covering: - [x] Version update handling - [x] All logic branches: - [x] `null` opt-in status - [x] `false` opt-in status with no STX activity - [x] `false` opt-in status with existing STX activity - [x] `true` opt-in status - [x] Notification flag setting - [x] Error handling - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format where applicable. - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. --- ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pulled and built the branch, ran the app, and tested the changes described above). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket and includes the necessary testing evidence (e.g., recordings, screenshots, or detailed descriptions). --------- Co-authored-by: Dan J Miller <[email protected]> Co-authored-by: georgeweiler <[email protected]> Co-authored-by: dan437 <[email protected]>
1 parent 3c220da commit 41930af

25 files changed

+1791
-48
lines changed

Diff for: app/_locales/en/messages.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: app/scripts/controllers/preferences-controller.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,7 @@ describe('preferences controller', () => {
733733
privacyMode: false,
734734
showFiatInTestnets: false,
735735
showTestNetworks: false,
736+
smartTransactionsMigrationApplied: false,
736737
smartTransactionsOptInStatus: true,
737738
useNativeCurrencyAsPrimaryCurrency: true,
738739
hideZeroBalanceTokens: false,
@@ -762,6 +763,7 @@ describe('preferences controller', () => {
762763
showExtensionInFullSizeView: false,
763764
showFiatInTestnets: false,
764765
showTestNetworks: false,
766+
smartTransactionsMigrationApplied: false,
765767
smartTransactionsOptInStatus: true,
766768
useNativeCurrencyAsPrimaryCurrency: true,
767769
hideZeroBalanceTokens: false,

Diff for: app/scripts/controllers/preferences-controller.ts

+13
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type Preferences = {
104104
showFiatInTestnets: boolean;
105105
showTestNetworks: boolean;
106106
smartTransactionsOptInStatus: boolean;
107+
smartTransactionsMigrationApplied: boolean;
107108
showNativeTokenAsMainBalance: boolean;
108109
useNativeCurrencyAsPrimaryCurrency: boolean;
109110
hideZeroBalanceTokens: boolean;
@@ -129,6 +130,7 @@ export type PreferencesControllerState = Omit<
129130
PreferencesState,
130131
| 'showTestNetworks'
131132
| 'smartTransactionsOptInStatus'
133+
| 'smartTransactionsMigrationApplied'
132134
| 'privacyMode'
133135
| 'tokenSortConfig'
134136
| 'useMultiRpcMigration'
@@ -217,6 +219,7 @@ export const getDefaultPreferencesControllerState =
217219
showFiatInTestnets: false,
218220
showTestNetworks: false,
219221
smartTransactionsOptInStatus: true,
222+
smartTransactionsMigrationApplied: false,
220223
showNativeTokenAsMainBalance: false,
221224
useNativeCurrencyAsPrimaryCurrency: true,
222225
hideZeroBalanceTokens: false,
@@ -406,6 +409,16 @@ const controllerMetadata = {
406409
preferences: {
407410
persist: true,
408411
anonymous: true,
412+
properties: {
413+
smartTransactionsOptInStatus: {
414+
persist: true,
415+
anonymous: true,
416+
},
417+
smartTransactionsMigrationApplied: {
418+
persist: true,
419+
anonymous: true,
420+
},
421+
},
409422
},
410423
ipfsGateway: {
411424
persist: true,

Diff for: app/scripts/migrations/135.test.ts

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { SmartTransaction } from '@metamask/smart-transactions-controller/dist/types';
2+
import { migrate, VersionedData } from './135';
3+
4+
const prevVersion = 134;
5+
6+
describe('migration #135', () => {
7+
const mockSmartTransaction: SmartTransaction = {
8+
uuid: 'test-uuid',
9+
};
10+
11+
it('should update the version metadata', async () => {
12+
const oldStorage: VersionedData = {
13+
meta: { version: prevVersion },
14+
data: {},
15+
};
16+
17+
const newStorage = await migrate(oldStorage);
18+
expect(newStorage.meta).toStrictEqual({ version: 135 });
19+
});
20+
21+
it('should set stx opt-in to true and migration flag when stx opt-in status is null', async () => {
22+
const oldStorage: VersionedData = {
23+
meta: { version: prevVersion },
24+
data: {
25+
PreferencesController: {
26+
preferences: {
27+
smartTransactionsOptInStatus: null,
28+
},
29+
},
30+
},
31+
};
32+
33+
const newStorage = await migrate(oldStorage);
34+
expect(
35+
newStorage.data.PreferencesController?.preferences
36+
?.smartTransactionsOptInStatus,
37+
).toBe(true);
38+
expect(
39+
newStorage.data.PreferencesController?.preferences
40+
?.smartTransactionsMigrationApplied,
41+
).toBe(true);
42+
});
43+
44+
it('should set stx opt-in to true and migration flag when stx opt-in status is undefined', async () => {
45+
const oldStorage: VersionedData = {
46+
meta: { version: prevVersion },
47+
data: {
48+
PreferencesController: {},
49+
},
50+
};
51+
52+
const newStorage = await migrate(oldStorage);
53+
expect(
54+
newStorage.data.PreferencesController?.preferences
55+
?.smartTransactionsOptInStatus,
56+
).toBe(true);
57+
expect(
58+
newStorage.data.PreferencesController?.preferences
59+
?.smartTransactionsMigrationApplied,
60+
).toBe(true);
61+
});
62+
63+
it('should set stx opt-in to true and migration flag when stx opt-in is false and no existing mainnet smart transactions', async () => {
64+
const oldStorage: VersionedData = {
65+
meta: { version: prevVersion },
66+
data: {
67+
PreferencesController: {
68+
preferences: {
69+
smartTransactionsOptInStatus: false,
70+
},
71+
},
72+
SmartTransactionsController: {
73+
smartTransactionsState: {
74+
smartTransactions: {
75+
'0x1': [], // Empty mainnet transactions
76+
'0xAA36A7': [mockSmartTransaction], // Sepolia has transactions
77+
},
78+
},
79+
},
80+
},
81+
};
82+
83+
const newStorage = await migrate(oldStorage);
84+
expect(
85+
newStorage.data.PreferencesController?.preferences
86+
?.smartTransactionsOptInStatus,
87+
).toBe(true);
88+
expect(
89+
newStorage.data.PreferencesController?.preferences
90+
?.smartTransactionsMigrationApplied,
91+
).toBe(true);
92+
});
93+
94+
it('should not change stx opt-in when stx opt-in is false but has existing smart transactions, but should set migration flag', async () => {
95+
const oldStorage: VersionedData = {
96+
meta: { version: prevVersion },
97+
data: {
98+
PreferencesController: {
99+
preferences: {
100+
smartTransactionsOptInStatus: false,
101+
},
102+
},
103+
SmartTransactionsController: {
104+
smartTransactionsState: {
105+
smartTransactions: {
106+
'0x1': [mockSmartTransaction],
107+
},
108+
},
109+
},
110+
},
111+
};
112+
113+
const newStorage = await migrate(oldStorage);
114+
expect(
115+
newStorage.data.PreferencesController?.preferences
116+
?.smartTransactionsOptInStatus,
117+
).toBe(false);
118+
expect(
119+
newStorage.data.PreferencesController?.preferences
120+
?.smartTransactionsMigrationApplied,
121+
).toBe(true);
122+
});
123+
124+
it('should not change stx opt-in when stx opt-in is already true, but should set migration flag', async () => {
125+
const oldStorage: VersionedData = {
126+
meta: { version: prevVersion },
127+
data: {
128+
PreferencesController: {
129+
preferences: {
130+
smartTransactionsOptInStatus: true,
131+
},
132+
},
133+
},
134+
};
135+
136+
const newStorage = await migrate(oldStorage);
137+
expect(
138+
newStorage.data.PreferencesController?.preferences
139+
?.smartTransactionsOptInStatus,
140+
).toBe(true);
141+
expect(
142+
newStorage.data.PreferencesController?.preferences
143+
?.smartTransactionsMigrationApplied,
144+
).toBe(true);
145+
});
146+
147+
it('should initialize preferences object if it does not exist', async () => {
148+
const oldStorage: VersionedData = {
149+
meta: { version: prevVersion },
150+
data: {
151+
PreferencesController: {
152+
preferences: {
153+
smartTransactionsOptInStatus: true,
154+
},
155+
},
156+
},
157+
};
158+
159+
const newStorage = await migrate(oldStorage);
160+
expect(newStorage.data.PreferencesController?.preferences).toBeDefined();
161+
expect(
162+
newStorage.data.PreferencesController?.preferences
163+
?.smartTransactionsMigrationApplied,
164+
).toBe(true);
165+
});
166+
167+
it('should capture exception if PreferencesController state is invalid', async () => {
168+
const sentryCaptureExceptionMock = jest.fn();
169+
global.sentry = {
170+
captureException: sentryCaptureExceptionMock,
171+
};
172+
173+
const oldStorage = {
174+
meta: { version: prevVersion },
175+
data: {
176+
PreferencesController: 'invalid',
177+
},
178+
} as unknown as VersionedData;
179+
180+
await migrate(oldStorage);
181+
182+
expect(sentryCaptureExceptionMock).toHaveBeenCalledTimes(1);
183+
expect(sentryCaptureExceptionMock).toHaveBeenCalledWith(
184+
new Error('Invalid PreferencesController state: string'),
185+
);
186+
});
187+
});

Diff for: app/scripts/migrations/135.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { hasProperty, isObject } from '@metamask/utils';
2+
import { cloneDeep } from 'lodash';
3+
import type { SmartTransaction } from '@metamask/smart-transactions-controller/dist/types';
4+
import { CHAIN_IDS } from '@metamask/transaction-controller';
5+
6+
export type VersionedData = {
7+
meta: {
8+
version: number;
9+
};
10+
data: {
11+
PreferencesController?: {
12+
preferences?: {
13+
smartTransactionsOptInStatus?: boolean | null;
14+
smartTransactionsMigrationApplied?: boolean;
15+
};
16+
};
17+
SmartTransactionsController?: {
18+
smartTransactionsState: {
19+
smartTransactions: Record<string, SmartTransaction[]>;
20+
};
21+
};
22+
};
23+
};
24+
25+
export const version = 135;
26+
27+
function transformState(state: VersionedData['data']) {
28+
if (
29+
!hasProperty(state, 'PreferencesController') ||
30+
!isObject(state.PreferencesController)
31+
) {
32+
global.sentry?.captureException?.(
33+
new Error(
34+
`Invalid PreferencesController state: ${typeof state.PreferencesController}`,
35+
),
36+
);
37+
return state;
38+
}
39+
40+
const { PreferencesController } = state;
41+
42+
const currentOptInStatus =
43+
PreferencesController.preferences?.smartTransactionsOptInStatus;
44+
45+
if (
46+
currentOptInStatus === undefined ||
47+
currentOptInStatus === null ||
48+
(currentOptInStatus === false && !hasExistingSmartTransactions(state))
49+
) {
50+
state.PreferencesController.preferences = {
51+
...state.PreferencesController.preferences,
52+
smartTransactionsOptInStatus: true,
53+
smartTransactionsMigrationApplied: true,
54+
};
55+
} else {
56+
state.PreferencesController.preferences = {
57+
...state.PreferencesController.preferences,
58+
smartTransactionsMigrationApplied: true,
59+
};
60+
}
61+
62+
return state;
63+
}
64+
65+
function hasExistingSmartTransactions(state: VersionedData['data']): boolean {
66+
const smartTransactions =
67+
state?.SmartTransactionsController?.smartTransactionsState
68+
?.smartTransactions;
69+
70+
if (!isObject(smartTransactions)) {
71+
return false;
72+
}
73+
74+
return (smartTransactions[CHAIN_IDS.MAINNET] || []).length > 0;
75+
}
76+
77+
export async function migrate(
78+
originalVersionedData: VersionedData,
79+
): Promise<VersionedData> {
80+
const versionedData = cloneDeep(originalVersionedData);
81+
versionedData.meta.version = version;
82+
transformState(versionedData.data);
83+
return versionedData;
84+
}

Diff for: app/scripts/migrations/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const migrations = [
158158
require('./133.1'),
159159
require('./133.2'),
160160
require('./134'),
161+
require('./135'),
161162
];
162163

163164
export default migrations;

Diff for: shared/constants/alerts.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export enum AlertTypes {
22
unconnectedAccount = 'unconnectedAccount',
33
web3ShimUsage = 'web3ShimUsage',
44
invalidCustomNetwork = 'invalidCustomNetwork',
5+
smartTransactionsMigration = 'smartTransactionsMigration',
56
}
67

78
/**
@@ -10,6 +11,7 @@ export enum AlertTypes {
1011
export const TOGGLEABLE_ALERT_TYPES = [
1112
AlertTypes.unconnectedAccount,
1213
AlertTypes.web3ShimUsage,
14+
AlertTypes.smartTransactionsMigration,
1315
];
1416

1517
export enum Web3ShimUsageAlertStates {

0 commit comments

Comments
 (0)