From 08e46815143559e66a960f7770697a88d760a3f3 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Fri, 1 Nov 2024 08:11:58 -0700 Subject: [PATCH 001/111] chore: improve token lookup performance in `useAccountTotalFiatBalance` (#28233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** `useAccountTotalFiatBalance` looks up each token in the token list to add additional fields. But it was O(n) searching through the entire list, which can be thousands of tokens. The token list is keyed on token address, and can be queried directly instead. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28233?quickstart=1) ## **Related issues** ## **Manual testing steps** No visual changes. In the account picker, erc20 tokens should still have icons on the right of each account. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- ui/hooks/useAccountTotalFiatBalance.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/ui/hooks/useAccountTotalFiatBalance.js b/ui/hooks/useAccountTotalFiatBalance.js index b0c9b293c906..7b4a4675225a 100644 --- a/ui/hooks/useAccountTotalFiatBalance.js +++ b/ui/hooks/useAccountTotalFiatBalance.js @@ -51,7 +51,6 @@ export const useAccountTotalFiatBalance = ( const tokens = detectedTokens?.[currentChainId]?.[account?.address] ?? []; // This selector returns all the tokens, we need it to get the image of token const allTokenList = useSelector(getTokenList); - const allTokenListValues = Object.values(allTokenList); const primaryTokenImage = useSelector(getNativeCurrencyImage); const nativeCurrency = useSelector(getNativeCurrency); @@ -92,20 +91,18 @@ export const useAccountTotalFiatBalance = ( }; // To match the list of detected tokens with the entire token list to find the image for tokens - const findMatchingTokens = (array1, array2) => { + const findMatchingTokens = (tokenList, _tokensWithBalances) => { const result = []; - array2.forEach((token2) => { - const matchingToken = array1.find( - (token1) => token1.symbol === token2.symbol, - ); + _tokensWithBalances.forEach((token) => { + const matchingToken = tokenList[token.address.toLowerCase()]; if (matchingToken) { result.push({ ...matchingToken, - balance: token2.balance, - string: token2.string, - balanceError: token2.balanceError, + balance: token.balance, + string: token.string, + balanceError: token.balanceError, }); } }); @@ -113,10 +110,7 @@ export const useAccountTotalFiatBalance = ( return result; }; - const matchingTokens = findMatchingTokens( - allTokenListValues, - tokensWithBalances, - ); + const matchingTokens = findMatchingTokens(allTokenList, tokensWithBalances); // Combine native token, detected token with image in an array const allTokensWithFiatValues = [ From 9b85efbe7953d2ed269ad51852bf0788e082fb80 Mon Sep 17 00:00:00 2001 From: Kanthesha Devaramane Date: Fri, 1 Nov 2024 17:00:22 +0000 Subject: [PATCH 002/111] feat: Upgrade alert controller to base controller v2 (#28054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Following the [Wallet Framework team's OKRs for Q3 2024](https://docs.google.com/document/d/1JLEzfUxHlT8lw8ntgMWG0vQb5BAATcrYZDj0wRB2ogI/edit#heading=h.kzzai3cfecro), we want to bring AlertController up to date with our latest controller patterns. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28054?quickstart=1) ## **Related issues** Fixes: #25915 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- .../controllers/alert-controller.test.ts | 303 +++++++----------- app/scripts/controllers/alert-controller.ts | 124 ++++--- app/scripts/metamask-controller.js | 6 +- 3 files changed, 189 insertions(+), 244 deletions(-) diff --git a/app/scripts/controllers/alert-controller.test.ts b/app/scripts/controllers/alert-controller.test.ts index a8aee606e02d..de314c31f050 100644 --- a/app/scripts/controllers/alert-controller.test.ts +++ b/app/scripts/controllers/alert-controller.test.ts @@ -2,16 +2,16 @@ * @jest-environment node */ import { ControllerMessenger } from '@metamask/base-controller'; -import { KeyringControllerStateChangeEvent } from '@metamask/keyring-controller'; -import { SnapControllerStateChangeEvent } from '@metamask/snaps-controllers'; import { EthAccountType } from '@metamask/keyring-api'; import { - AlertControllerActions, - AlertControllerEvents, AlertController, AllowedActions, AllowedEvents, - AlertControllerState, + AlertControllerMessenger, + AlertControllerGetStateAction, + AlertControllerStateChangeEvent, + AlertControllerOptions, + getDefaultAlertControllerState, } from './alert-controller'; const EMPTY_ACCOUNT = { @@ -28,230 +28,153 @@ const EMPTY_ACCOUNT = { importTime: 0, }, }; -describe('AlertController', () => { - let controllerMessenger: ControllerMessenger< - AlertControllerActions | AllowedActions, - | AlertControllerEvents - | KeyringControllerStateChangeEvent - | SnapControllerStateChangeEvent - | AllowedEvents + +type WithControllerOptions = Partial; + +type WithControllerCallback = ({ + controller, +}: { + controller: AlertController; + messenger: ControllerMessenger< + AllowedActions | AlertControllerGetStateAction, + AllowedEvents | AlertControllerStateChangeEvent >; - let alertController: AlertController; +}) => ReturnValue; + +type WithControllerArgs = + | [WithControllerCallback] + | [WithControllerOptions, WithControllerCallback]; - beforeEach(() => { - controllerMessenger = new ControllerMessenger< - AllowedActions, - AllowedEvents - >(); - controllerMessenger.registerActionHandler( - 'AccountsController:getSelectedAccount', - () => EMPTY_ACCOUNT, - ); +async function withController( + ...args: WithControllerArgs +): Promise { + const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; + const { ...alertControllerOptions } = rest; - const alertMessenger = controllerMessenger.getRestricted({ + const controllerMessenger = new ControllerMessenger< + AllowedActions | AlertControllerGetStateAction, + AllowedEvents | AlertControllerStateChangeEvent + >(); + + const alertControllerMessenger: AlertControllerMessenger = + controllerMessenger.getRestricted({ name: 'AlertController', - allowedActions: [`AccountsController:getSelectedAccount`], - allowedEvents: [`AccountsController:selectedAccountChange`], + allowedActions: ['AccountsController:getSelectedAccount'], + allowedEvents: ['AccountsController:selectedAccountChange'], }); - alertController = new AlertController({ - state: { - unconnectedAccountAlertShownOrigins: { - testUnconnectedOrigin: false, - }, - web3ShimUsageOrigins: { - testWeb3ShimUsageOrigin: 0, - }, - }, - controllerMessenger: alertMessenger, - }); + controllerMessenger.registerActionHandler( + 'AccountsController:getSelectedAccount', + jest.fn().mockReturnValue(EMPTY_ACCOUNT), + ); + + const controller = new AlertController({ + messenger: alertControllerMessenger, + ...alertControllerOptions, }); + return await fn({ + controller, + messenger: controllerMessenger, + }); +} + +describe('AlertController', () => { describe('default state', () => { - it('should be same as AlertControllerState initialized', () => { - expect(alertController.store.getState()).toStrictEqual({ - alertEnabledness: { - unconnectedAccount: true, - web3ShimUsage: true, - }, - unconnectedAccountAlertShownOrigins: { - testUnconnectedOrigin: false, - }, - web3ShimUsageOrigins: { - testWeb3ShimUsageOrigin: 0, - }, + it('should be same as AlertControllerState initialized', async () => { + await withController(({ controller }) => { + expect(controller.state).toStrictEqual( + getDefaultAlertControllerState(), + ); }); }); }); describe('alertEnabledness', () => { - it('should default unconnectedAccount of alertEnabledness to true', () => { - expect( - alertController.store.getState().alertEnabledness.unconnectedAccount, - ).toStrictEqual(true); + it('should default unconnectedAccount of alertEnabledness to true', async () => { + await withController(({ controller }) => { + expect( + controller.state.alertEnabledness.unconnectedAccount, + ).toStrictEqual(true); + }); }); - it('should set unconnectedAccount of alertEnabledness to false', () => { - alertController.setAlertEnabledness('unconnectedAccount', false); - expect( - alertController.store.getState().alertEnabledness.unconnectedAccount, - ).toStrictEqual(false); - expect( - controllerMessenger.call('AlertController:getState').alertEnabledness - .unconnectedAccount, - ).toStrictEqual(false); + it('should set unconnectedAccount of alertEnabledness to false', async () => { + await withController(({ controller }) => { + controller.setAlertEnabledness('unconnectedAccount', false); + expect( + controller.state.alertEnabledness.unconnectedAccount, + ).toStrictEqual(false); + }); }); }); describe('unconnectedAccountAlertShownOrigins', () => { - it('should default unconnectedAccountAlertShownOrigins', () => { - expect( - alertController.store.getState().unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: false, - }); - expect( - controllerMessenger.call('AlertController:getState') - .unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: false, + it('should default unconnectedAccountAlertShownOrigins', async () => { + await withController(({ controller }) => { + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); }); }); - it('should set unconnectedAccountAlertShownOrigins', () => { - alertController.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); - expect( - alertController.store.getState().unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: true, - }); - expect( - controllerMessenger.call('AlertController:getState') - .unconnectedAccountAlertShownOrigins, - ).toStrictEqual({ - testUnconnectedOrigin: true, + it('should set unconnectedAccountAlertShownOrigins', async () => { + await withController(({ controller }) => { + controller.setUnconnectedAccountAlertShown('testUnconnectedOrigin'); + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({ + testUnconnectedOrigin: true, + }); }); }); }); describe('web3ShimUsageOrigins', () => { - it('should default web3ShimUsageOrigins', () => { - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 0, - }); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 0, + it('should default web3ShimUsageOrigins', async () => { + await withController(({ controller }) => { + expect(controller.state.web3ShimUsageOrigins).toStrictEqual({}); }); }); - it('should set origin of web3ShimUsageOrigins to recorded', () => { - alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, - }); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, + it('should set origin of web3ShimUsageOrigins to recorded', async () => { + await withController(({ controller }) => { + controller.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); + expect(controller.state.web3ShimUsageOrigins).toStrictEqual({ + testWeb3ShimUsageOrigin: 1, + }); }); }); - it('should set origin of web3ShimUsageOrigins to dismissed', () => { - alertController.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin'); - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 2, - }); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 2, + it('should set origin of web3ShimUsageOrigins to dismissed', async () => { + await withController(({ controller }) => { + controller.setWeb3ShimUsageAlertDismissed('testWeb3ShimUsageOrigin'); + expect(controller.state.web3ShimUsageOrigins).toStrictEqual({ + testWeb3ShimUsageOrigin: 2, + }); }); }); }); describe('selectedAccount change', () => { - it('should set unconnectedAccountAlertShownOrigins to {}', () => { - controllerMessenger.publish('AccountsController:selectedAccountChange', { - id: '', - address: '0x1234567', - options: {}, - methods: [], - type: 'eip155:eoa', - metadata: { - name: '', - keyring: { - type: '', + it('should set unconnectedAccountAlertShownOrigins to {}', async () => { + await withController(({ controller, messenger }) => { + messenger.publish('AccountsController:selectedAccountChange', { + id: '', + address: '0x1234567', + options: {}, + methods: [], + type: 'eip155:eoa', + metadata: { + name: '', + keyring: { + type: '', + }, + importTime: 0, }, - importTime: 0, - }, - }); - expect( - alertController.store.getState().unconnectedAccountAlertShownOrigins, - ).toStrictEqual({}); - expect( - controllerMessenger.call('AlertController:getState') - .unconnectedAccountAlertShownOrigins, - ).toStrictEqual({}); - }); - }); - - describe('AlertController:getState', () => { - it('should return the current state of the property', () => { - const defaultWeb3ShimUsageOrigins = { - testWeb3ShimUsageOrigin: 0, - }; - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual(defaultWeb3ShimUsageOrigins); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual(defaultWeb3ShimUsageOrigins); - }); - }); - - describe('AlertController:stateChange', () => { - it('state will be published when there is state change', () => { - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 0, - }); - - controllerMessenger.subscribe( - 'AlertController:stateChange', - (state: Partial) => { - expect(state.web3ShimUsageOrigins).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, - }); - }, - ); - - alertController.setWeb3ShimUsageRecorded('testWeb3ShimUsageOrigin'); - - expect( - alertController.store.getState().web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, - }); - expect( - alertController.getWeb3ShimUsageState('testWeb3ShimUsageOrigin'), - ).toStrictEqual(1); - expect( - controllerMessenger.call('AlertController:getState') - .web3ShimUsageOrigins, - ).toStrictEqual({ - testWeb3ShimUsageOrigin: 1, + }); + expect( + controller.state.unconnectedAccountAlertShownOrigins, + ).toStrictEqual({}); }); }); }); diff --git a/app/scripts/controllers/alert-controller.ts b/app/scripts/controllers/alert-controller.ts index 9e1882035e02..90e177e9edca 100644 --- a/app/scripts/controllers/alert-controller.ts +++ b/app/scripts/controllers/alert-controller.ts @@ -1,9 +1,13 @@ -import { ObservableStore } from '@metamask/obs-store'; import { AccountsControllerGetSelectedAccountAction, AccountsControllerSelectedAccountChangeEvent, } from '@metamask/accounts-controller'; -import { RestrictedControllerMessenger } from '@metamask/base-controller'; +import { + BaseController, + ControllerGetStateAction, + ControllerStateChangeEvent, + RestrictedControllerMessenger, +} from '@metamask/base-controller'; import { TOGGLEABLE_ALERT_TYPES, Web3ShimUsageAlertStates, @@ -14,10 +18,10 @@ const controllerName = 'AlertController'; /** * Returns the state of the {@link AlertController}. */ -export type AlertControllerGetStateAction = { - type: 'AlertController:getState'; - handler: () => AlertControllerState; -}; +export type AlertControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + AlertControllerState +>; /** * Actions exposed by the {@link AlertController}. @@ -27,10 +31,10 @@ export type AlertControllerActions = AlertControllerGetStateAction; /** * Event emitted when the state of the {@link AlertController} changes. */ -export type AlertControllerStateChangeEvent = { - type: 'AlertController:stateChange'; - payload: [AlertControllerState, []]; -}; +export type AlertControllerStateChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + AlertControllerState +>; /** * Events emitted by {@link AlertController}. @@ -76,12 +80,15 @@ export type AlertControllerState = { * @property state - The initial controller state * @property controllerMessenger - The controller messenger */ -type AlertControllerOptions = { +export type AlertControllerOptions = { state?: Partial; - controllerMessenger: AlertControllerMessenger; + messenger: AlertControllerMessenger; }; -const defaultState: AlertControllerState = { +/** + * Function to get default state of the {@link AlertController}. + */ +export const getDefaultAlertControllerState = (): AlertControllerState => ({ alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( (alertEnabledness: Record, alertType: string) => { alertEnabledness[alertType] = true; @@ -91,61 +98,76 @@ const defaultState: AlertControllerState = { ), unconnectedAccountAlertShownOrigins: {}, web3ShimUsageOrigins: {}, +}); + +/** + * {@link AlertController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +const controllerMetadata = { + alertEnabledness: { + persist: true, + anonymous: true, + }, + unconnectedAccountAlertShownOrigins: { + persist: true, + anonymous: false, + }, + web3ShimUsageOrigins: { + persist: true, + anonymous: false, + }, }; /** * Controller responsible for maintaining alert-related state. */ -export class AlertController { - store: ObservableStore; - - readonly #controllerMessenger: AlertControllerMessenger; - +export class AlertController extends BaseController< + typeof controllerName, + AlertControllerState, + AlertControllerMessenger +> { #selectedAddress: string; constructor(opts: AlertControllerOptions) { - const state: AlertControllerState = { - ...defaultState, - ...opts.state, - }; - - this.store = new ObservableStore(state); - this.#controllerMessenger = opts.controllerMessenger; - this.#controllerMessenger.registerActionHandler( - 'AlertController:getState', - () => this.store.getState(), - ); - this.store.subscribe((alertState: AlertControllerState) => { - this.#controllerMessenger.publish( - 'AlertController:stateChange', - alertState, - [], - ); + super({ + messenger: opts.messenger, + metadata: controllerMetadata, + name: controllerName, + state: { + ...getDefaultAlertControllerState(), + ...opts.state, + }, }); - this.#selectedAddress = this.#controllerMessenger.call( + this.#selectedAddress = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ).address; - this.#controllerMessenger.subscribe( + this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', (account: { address: string }) => { - const currentState = this.store.getState(); + const currentState = this.state; if ( currentState.unconnectedAccountAlertShownOrigins && this.#selectedAddress !== account.address ) { this.#selectedAddress = account.address; - this.store.updateState({ unconnectedAccountAlertShownOrigins: {} }); + this.update((state) => { + state.unconnectedAccountAlertShownOrigins = {}; + }); } }, ); } setAlertEnabledness(alertId: string, enabledness: boolean): void { - const { alertEnabledness } = this.store.getState(); - alertEnabledness[alertId] = enabledness; - this.store.updateState({ alertEnabledness }); + this.update((state) => { + state.alertEnabledness[alertId] = enabledness; + }); } /** @@ -154,9 +176,9 @@ export class AlertController { * @param origin - The origin the alert has been shown for */ setUnconnectedAccountAlertShown(origin: string): void { - const { unconnectedAccountAlertShownOrigins } = this.store.getState(); - unconnectedAccountAlertShownOrigins[origin] = true; - this.store.updateState({ unconnectedAccountAlertShownOrigins }); + this.update((state) => { + state.unconnectedAccountAlertShownOrigins[origin] = true; + }); } /** @@ -167,7 +189,7 @@ export class AlertController { * origin, or undefined. */ getWeb3ShimUsageState(origin: string): number | undefined { - return this.store.getState().web3ShimUsageOrigins?.[origin]; + return this.state.web3ShimUsageOrigins?.[origin]; } /** @@ -194,10 +216,10 @@ export class AlertController { * @param value - The state value to set. */ #setWeb3ShimUsageState(origin: string, value: number): void { - const { web3ShimUsageOrigins } = this.store.getState(); - if (web3ShimUsageOrigins) { - web3ShimUsageOrigins[origin] = value; - this.store.updateState({ web3ShimUsageOrigins }); - } + this.update((state) => { + if (state.web3ShimUsageOrigins) { + state.web3ShimUsageOrigins[origin] = value; + } + }); } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 55f5e881de4a..009f87634caa 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1797,7 +1797,7 @@ export default class MetamaskController extends EventEmitter { this.alertController = new AlertController({ state: initState.AlertController, - controllerMessenger: this.controllerMessenger.getRestricted({ + messenger: this.controllerMessenger.getRestricted({ name: 'AlertController', allowedEvents: ['AccountsController:selectedAccountChange'], allowedActions: ['AccountsController:getSelectedAccount'], @@ -2383,7 +2383,7 @@ export default class MetamaskController extends EventEmitter { AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, NetworkController: this.networkController, - AlertController: this.alertController.store, + AlertController: this.alertController, OnboardingController: this.onboardingController, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController, @@ -2438,7 +2438,7 @@ export default class MetamaskController extends EventEmitter { this.metaMetricsDataDeletionController, AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, - AlertController: this.alertController.store, + AlertController: this.alertController, OnboardingController: this.onboardingController, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController, From 8c4af60886775f2bbffc475f5f7e12fd2200ec01 Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:00:33 -0400 Subject: [PATCH 003/111] fix: Error handling for the state log download failure (#26999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Display error message if the `exportAsFile()` for state log download in advanced tab in the settings fails. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/26999?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2875 ## **Manual testing steps** 1. Throw an error artificially in the `exportAsFile()` state logs download function 2. `Failed to download state log.` message is diaplayed ## **Screenshots/Recordings** ### **Before** Screenshot 2024-11-01 at 10 21 23 AM ### **After** Screenshot 2024-11-01 at 10 34 37 AM ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- ui/ducks/app/app.test.js | 19 ++++++ ui/ducks/app/app.ts | 33 +++++++++++ .../advanced-tab.component.test.js.snap | 1 + .../advanced-tab/advanced-tab.component.js | 34 +++++++---- .../advanced-tab.component.test.js | 59 ++++++++++++++++++- .../advanced-tab/advanced-tab.container.js | 13 ++-- .../advanced-tab/advanced-tab.stories.js | 3 +- ui/store/actionConstants.ts | 2 + 8 files changed, 146 insertions(+), 18 deletions(-) diff --git a/ui/ducks/app/app.test.js b/ui/ducks/app/app.test.js index 9a7a93ea958b..27b20a5841b3 100644 --- a/ui/ducks/app/app.test.js +++ b/ui/ducks/app/app.test.js @@ -339,4 +339,23 @@ describe('App State', () => { expect(state.showDataDeletionErrorModal).toStrictEqual(false); }); + + it('displays error in settings', () => { + const state = reduceApp(metamaskState, { + type: actions.SHOW_SETTINGS_PAGE_ERROR, + payload: 'settings page error', + }); + + expect(state.errorInSettings).toStrictEqual('settings page error'); + }); + + it('hides error in settings', () => { + const displayErrorInSettings = { errorInSettings: 'settings page error' }; + const oldState = { ...metamaskState, ...displayErrorInSettings }; + const state = reduceApp(oldState, { + type: actions.HIDE_SETTINGS_PAGE_ERROR, + }); + + expect(state.errorInSettings).toBeNull(); + }); }); diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index e6a7855ce7a5..81f875446f1e 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -105,6 +105,7 @@ type AppState = { snapsInstallPrivacyWarningShown: boolean; isAddingNewNetwork: boolean; isMultiRpcOnboarding: boolean; + errorInSettings: string | null; }; export type AppSliceState = { @@ -192,6 +193,7 @@ const initialState: AppState = { snapsInstallPrivacyWarningShown: false, isAddingNewNetwork: false, isMultiRpcOnboarding: false, + errorInSettings: null, }; export default function reduceApp( @@ -632,6 +634,16 @@ export default function reduceApp( ...appState, showDataDeletionErrorModal: false, }; + case actionConstants.SHOW_SETTINGS_PAGE_ERROR: + return { + ...appState, + errorInSettings: action.payload, + }; + case actionConstants.HIDE_SETTINGS_PAGE_ERROR: + return { + ...appState, + errorInSettings: null, + }; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT: return { @@ -720,6 +732,27 @@ export function setCustomTokenAmount(payload: string): PayloadAction { return { type: actionConstants.SET_CUSTOM_TOKEN_AMOUNT, payload }; } +/** + * An action creator for display a error to the user in various places in the + * UI. It will not be cleared until a new warning replaces it or `hideWarning` + * is called. + * + * @param payload - The warning to show. + * @returns The action to display the warning. + */ +export function displayErrorInSettings(payload: string): PayloadAction { + return { + type: actionConstants.SHOW_SETTINGS_PAGE_ERROR, + payload, + }; +} + +export function hideErrorInSettings() { + return { + type: actionConstants.HIDE_SETTINGS_PAGE_ERROR, + }; +} + // Selectors export function getQrCodeData(state: AppSliceState): { type?: string | null; diff --git a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap index 6318abd37570..e914c54fe4ca 100644 --- a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap +++ b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap @@ -29,6 +29,7 @@ exports[`AdvancedTab Component should match snapshot 1`] = ` > diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index 50aea4e0dc60..132b97f7caa9 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js @@ -40,9 +40,10 @@ export default class AdvancedTab extends PureComponent { setUseNonceField: PropTypes.func, useNonceField: PropTypes.bool, setHexDataFeatureFlag: PropTypes.func, - displayWarning: PropTypes.func, + displayErrorInSettings: PropTypes.func, + hideErrorInSettings: PropTypes.func, showResetAccountConfirmationModal: PropTypes.func, - warning: PropTypes.string, + errorInSettings: PropTypes.string, sendHexData: PropTypes.bool, showFiatInTestnets: PropTypes.bool, showTestNetworks: PropTypes.bool, @@ -80,7 +81,9 @@ export default class AdvancedTab extends PureComponent { componentDidMount() { const { t } = this.context; + const { hideErrorInSettings } = this.props; handleSettingsRefs(t, t('advanced'), this.settingsRefs); + hideErrorInSettings(); } async getTextFromFile(file) { @@ -112,7 +115,7 @@ export default class AdvancedTab extends PureComponent { renderStateLogs() { const { t } = this.context; - const { displayWarning } = this.props; + const { displayErrorInSettings } = this.props; return ( { - window.logStateString((err, result) => { + window.logStateString(async (err, result) => { if (err) { - displayWarning(t('stateLogError')); + displayErrorInSettings(t('stateLogError')); } else { - exportAsFile( - `${t('stateLogFileName')}.json`, - result, - ExportableContentType.JSON, - ); + try { + await exportAsFile( + `${t('stateLogFileName')}.json`, + result, + ExportableContentType.JSON, + ); + } catch (error) { + displayErrorInSettings(error.message); + } } }); }} @@ -576,11 +584,13 @@ export default class AdvancedTab extends PureComponent { } render() { - const { warning } = this.props; + const { errorInSettings } = this.props; // When adding/removing/editing the order of renders, double-check the order of the settingsRefs. This affects settings-search.js return (
- {warning ?
{warning}
: null} + {errorInSettings ? ( +
{errorInSettings}
+ ) : null} {this.renderStateLogs()} {this.renderResetAccount()} {this.renderToggleStxOptIn()} diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js index 2c64b79e4f4d..aa5dc11ace89 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.test.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.test.js @@ -1,15 +1,17 @@ import React from 'react'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import mockState from '../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; +import { exportAsFile } from '../../../helpers/utils/export-utils'; import AdvancedTab from '.'; const mockSetAutoLockTimeLimit = jest.fn().mockReturnValue({ type: 'TYPE' }); const mockSetShowTestNetworks = jest.fn(); const mockSetShowFiatConversionOnTestnetsPreference = jest.fn(); const mockSetStxPrefEnabled = jest.fn(); +const mockDisplayErrorInSettings = jest.fn(); jest.mock('../../../store/actions.ts', () => { return { @@ -21,6 +23,32 @@ jest.mock('../../../store/actions.ts', () => { }; }); +jest.mock('../../../ducks/app/app.ts', () => ({ + displayErrorInSettings: () => mockDisplayErrorInSettings, + hideErrorInSettings: () => jest.fn(), +})); + +jest.mock('../../../helpers/utils/export-utils', () => ({ + ...jest.requireActual('../../../helpers/utils/export-utils'), + exportAsFile: jest + .fn() + .mockResolvedValueOnce({}) + .mockImplementationOnce(new Error('state file error')), +})); + +jest.mock('webextension-polyfill', () => ({ + runtime: { + getPlatformInfo: jest.fn().mockResolvedValue('mac'), + }, +})); + +Object.defineProperty(window, 'stateHooks', { + value: { + getCleanAppState: () => mockState, + getLogs: () => [], + }, +}); + describe('AdvancedTab Component', () => { const mockStore = configureMockStore([thunk])(mockState); @@ -105,4 +133,33 @@ describe('AdvancedTab Component', () => { expect(mockSetStxPrefEnabled).toHaveBeenCalled(); }); }); + + describe('renderStateLogs', () => { + it('should render the toggle button for state log download', () => { + const { queryByTestId } = renderWithProvider(, mockStore); + const stateLogButton = queryByTestId('advanced-setting-state-logs'); + expect(stateLogButton).toBeInTheDocument(); + }); + + it('should call exportAsFile when the toggle button is clicked', async () => { + const { queryByTestId } = renderWithProvider(, mockStore); + const stateLogButton = queryByTestId( + 'advanced-setting-state-logs-button', + ); + fireEvent.click(stateLogButton); + await waitFor(() => { + expect(exportAsFile).toHaveBeenCalledTimes(1); + }); + }); + it('should call displayErrorInSettings when the state file download fails', async () => { + const { queryByTestId } = renderWithProvider(, mockStore); + const stateLogButton = queryByTestId( + 'advanced-setting-state-logs-button', + ); + fireEvent.click(stateLogButton); + await waitFor(() => { + expect(mockDisplayErrorInSettings).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js index f2ad894d1e8b..aaa094e0655c 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js @@ -5,7 +5,6 @@ import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../../shared/constants/prefe import { getPreferences } from '../../../selectors'; import { backupUserData, - displayWarning, setAutoLockTimeLimit, setDismissSeedBackUpReminder, setFeatureFlag, @@ -17,11 +16,15 @@ import { showModal, } from '../../../store/actions'; import { getSmartTransactionsPreferenceEnabled } from '../../../../shared/modules/selectors'; +import { + displayErrorInSettings, + hideErrorInSettings, +} from '../../../ducks/app/app'; import AdvancedTab from './advanced-tab.component'; export const mapStateToProps = (state) => { const { - appState: { warning }, + appState: { errorInSettings }, metamask, } = state; const { @@ -37,7 +40,7 @@ export const mapStateToProps = (state) => { } = getPreferences(state); return { - warning, + errorInSettings, sendHexData, showFiatInTestnets, showTestNetworks, @@ -54,7 +57,9 @@ export const mapDispatchToProps = (dispatch) => { backupUserData: () => backupUserData(), setHexDataFeatureFlag: (shouldShow) => dispatch(setFeatureFlag('sendHexData', shouldShow)), - displayWarning: (warning) => dispatch(displayWarning(warning)), + displayErrorInSettings: (errorInSettings) => + dispatch(displayErrorInSettings(errorInSettings)), + hideErrorInSettings: () => dispatch(hideErrorInSettings()), showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), setUseNonceField: (value) => dispatch(setUseNonceField(value)), diff --git a/ui/pages/settings/advanced-tab/advanced-tab.stories.js b/ui/pages/settings/advanced-tab/advanced-tab.stories.js index 46e7ee978f43..36c84cbba8c0 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.stories.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.stories.js @@ -22,7 +22,8 @@ export default { setDismissSeedBackUpReminder: { action: 'setDismissSeedBackUpReminder' }, setUseNonceField: { action: 'setUseNonceField' }, setHexDataFeatureFlag: { action: 'setHexDataFeatureFlag' }, - displayWarning: { action: 'displayWarning' }, + displayErrorInSettings: { action: 'displayErrorInSettings' }, + hideErrorInSettings: { action: 'hideErrorInSettings' }, history: { action: 'history' }, showResetAccountConfirmationModal: { action: 'showResetAccountConfirmationModal', diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 6f8080e516ae..2e31c6c7dd7f 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -49,6 +49,8 @@ export const LOCK_METAMASK = 'LOCK_METAMASK'; // error handling export const DISPLAY_WARNING = 'DISPLAY_WARNING'; export const HIDE_WARNING = 'HIDE_WARNING'; +export const SHOW_SETTINGS_PAGE_ERROR = 'SHOW_SETTINGS_PAGE_ERROR'; +export const HIDE_SETTINGS_PAGE_ERROR = 'HIDE_SETTINGS_PAGE_ERROR'; export const CAPTURE_SINGLE_EXCEPTION = 'CAPTURE_SINGLE_EXCEPTION'; // accounts screen export const SHOW_ACCOUNTS_PAGE = 'SHOW_ACCOUNTS_PAGE'; From 2de414e3f6bef968b4cd43ae2fba48ef79489590 Mon Sep 17 00:00:00 2001 From: George Marshall Date: Fri, 1 Nov 2024 11:18:00 -0700 Subject: [PATCH 004/111] chore: remove broken link in docs (#28232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR removes a broken link from the README that was originally intended to point to the `Box` component in Storybook. While the link works in Storybook, it does not function correctly when viewed directly on GitHub or in code editors, leading to potential confusion. To simplify access, we are removing this link; users can still locate the `Box` component by searching for it directly within Storybook. ## **Related issues** Fixes: N/A ## **Manual testing steps** 1. View the README file on GitHub. 2. Confirm that the broken link has been removed. 3. Check that references to the `Box` component remain understandable without the link. ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/d48cf851-163c-4169-a2f6-b0020ba79b8d ### **After** https://github.com/user-attachments/assets/46d1f0b1-a2fa-4130-b726-460524118969 No more link references in code base Screenshot 2024-10-31 at 4 00 15 PM ## **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 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/develop/.github/guidelines/LABELING_GUIDELINES.md)). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g., pulled and built the branch, reviewed the updated README). - [ ] 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. --- ui/components/component-library/README.md | 2 +- ui/components/component-library/text/README.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/components/component-library/README.md b/ui/components/component-library/README.md index ec5006d3491f..a1f865bfe95a 100644 --- a/ui/components/component-library/README.md +++ b/ui/components/component-library/README.md @@ -4,7 +4,7 @@ This folder contains design system components that are built 1:1 with the Figma ## Architecture -All components are built on top of the `Box` component and accept all `Box` [component props](/docs/components-componentlibrary-box--docs#props). +All components are built on top of the `Box` component and accept all `Box` component props. ### Layout diff --git a/ui/components/component-library/text/README.mdx b/ui/components/component-library/text/README.mdx index 5b275aa48e23..183fcbbca9f8 100644 --- a/ui/components/component-library/text/README.mdx +++ b/ui/components/component-library/text/README.mdx @@ -580,7 +580,7 @@ Values using the `TextAlign` object from `./ui/helpers/constants/design-system.j ### Box Props -Box props are now integrated with the `Text` component. Valid Box props: [Box](/docs/components-componentlibrary-box--docs#props) +Box props are now integrated with the `Text` component. You no longer need to pass these props as an object through `boxProps` From 59044a48787bdc4ffbb105219b8c74f59e60befa Mon Sep 17 00:00:00 2001 From: Victor Thomas <10986371+vthomas13@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:52:31 -0400 Subject: [PATCH 005/111] chore: Adding installType to Sentry Tags for easy filtering (#28084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** InstallType is a recently added flag to help quickly determine whether a Sentry issue is coming from a natural webstore install, or a developer environment. We want to be able to filter by this flag in the Sentry UI. Added the tag, but also simplified some previous logic from when I added extensionId to make adding extra attributes less tedious in the future. We can also use this pattern for tags. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28084?quickstart=1) ## **Related issues** Fixes: #27667 ## **Manual testing steps** 1. Open App 2. Use developer options to trigger a sentry error 3. Go into Sentry UI and verify that installType is a tag in addition to being in the extra properties. ## **Screenshots/Recordings** Screenshot 2024-10-24 at 1 04 59 PM ### **Before** ### **After** ## **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 - [ ] 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/develop/.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. --------- Co-authored-by: Harika <153644847+hjetpoluru@users.noreply.github.com> --- app/scripts/lib/setupSentry.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index 1b9e9f4ddbfc..354bb0bbb620 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -424,12 +424,17 @@ export function rewriteReport(report) { if (!report.extra) { report.extra = {}; } - - report.extra.appState = appState; - if (browser.runtime && browser.runtime.id) { - report.extra.extensionId = browser.runtime.id; + if (!report.tags) { + report.tags = {}; } - report.extra.installType = installType; + + Object.assign(report.extra, { + appState, + installType, + extensionId: browser.runtime?.id, + }); + + report.tags.installType = installType; } catch (err) { log('Error rewriting report', err); } From a94de6a93d583325b46166bcb7a7e2ac84f9f38a Mon Sep 17 00:00:00 2001 From: Niranjana Binoy <43930900+NiranjanaBinoy@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:13:10 -0400 Subject: [PATCH 006/111] fix: Removing `warning` prop from settings (#27990) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Even though `warning` property is still used in the setting-tab and security-tab, we are no longer using `displayWarning` to update the error from the settings. This makes the error displayed in the tabs irrelevant to the component. So with this PR we are removing the warning property from settings-tab and security-tab. We are removing the warning property from advance-tab in https://github.com/MetaMask/metamask-extension/pull/26999 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27990?quickstart=1) ## **Related issues** Related to https://github.com/MetaMask/metamask-extension/issues/25838 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** Screenshot 2024-11-01 at 9 52 31 AM Screenshot 2024-11-01 at 9 52 07 AM Screenshot 2024-11-01 at 9 52 19 AM ### **After** Screenshot 2024-11-01 at 10 20 47 AM Screenshot 2024-11-01 at 10 21 23 AM Screenshot 2024-11-01 at 10 21 14 AM ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. Co-authored-by: Harika <153644847+hjetpoluru@users.noreply.github.com> Co-authored-by: Danica Shen --- .../security-tab/__snapshots__/security-tab.test.js.snap | 5 ----- ui/pages/settings/security-tab/security-tab.component.js | 4 ---- ui/pages/settings/security-tab/security-tab.container.js | 6 +----- ui/pages/settings/security-tab/security-tab.test.js | 2 -- ui/pages/settings/settings-tab/settings-tab.component.js | 4 ---- ui/pages/settings/settings-tab/settings-tab.container.js | 6 +----- 6 files changed, 2 insertions(+), 25 deletions(-) diff --git a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap index dcec71767fe6..0927d04f89cb 100644 --- a/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap +++ b/ui/pages/settings/security-tab/__snapshots__/security-tab.test.js.snap @@ -83,11 +83,6 @@ exports[`Security Tab should match snapshot 1`] = `
-
- warning -
diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js index 1fae729d3f31..f9e854ff2465 100644 --- a/ui/pages/settings/security-tab/security-tab.component.js +++ b/ui/pages/settings/security-tab/security-tab.component.js @@ -64,7 +64,6 @@ export default class SecurityTab extends PureComponent { }; static propTypes = { - warning: PropTypes.string, history: PropTypes.object, openSeaEnabled: PropTypes.bool, setOpenSeaEnabled: PropTypes.func, @@ -1131,7 +1130,6 @@ export default class SecurityTab extends PureComponent { render() { const { - warning, petnamesEnabled, dataCollectionForMarketing, setDataCollectionForMarketing, @@ -1144,8 +1142,6 @@ export default class SecurityTab extends PureComponent { {showDataCollectionDisclaimer ? this.renderDataCollectionWarning() : null} - - {warning &&
{warning}
} {this.context.t('security')} diff --git a/ui/pages/settings/security-tab/security-tab.container.js b/ui/pages/settings/security-tab/security-tab.container.js index fa529c1ad3df..676a53097d4a 100644 --- a/ui/pages/settings/security-tab/security-tab.container.js +++ b/ui/pages/settings/security-tab/security-tab.container.js @@ -32,10 +32,7 @@ import { openBasicFunctionalityModal } from '../../../ducks/app/app'; import SecurityTab from './security-tab.component'; const mapStateToProps = (state) => { - const { - appState: { warning }, - metamask, - } = state; + const { metamask } = state; const petnamesEnabled = getPetnamesEnabled(state); @@ -60,7 +57,6 @@ const mapStateToProps = (state) => { const networkConfigurations = getNetworkConfigurationsByChainId(state); return { - warning, incomingTransactionsPreferences, networkConfigurations, participateInMetaMetrics, diff --git a/ui/pages/settings/security-tab/security-tab.test.js b/ui/pages/settings/security-tab/security-tab.test.js index 1685c5417151..ac93efc2e324 100644 --- a/ui/pages/settings/security-tab/security-tab.test.js +++ b/ui/pages/settings/security-tab/security-tab.test.js @@ -48,8 +48,6 @@ jest.mock('../../../ducks/app/app.ts', () => { }); describe('Security Tab', () => { - mockState.appState.warning = 'warning'; // This tests an otherwise untested render branch - const mockStore = configureMockStore([thunk])(mockState); function renderWithProviders(ui, store) { diff --git a/ui/pages/settings/settings-tab/settings-tab.component.js b/ui/pages/settings/settings-tab/settings-tab.component.js index 191bbbc78685..6d56cd9ae10b 100644 --- a/ui/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/pages/settings/settings-tab/settings-tab.component.js @@ -57,7 +57,6 @@ export default class SettingsTab extends PureComponent { static propTypes = { setUseBlockie: PropTypes.func, setCurrentCurrency: PropTypes.func, - warning: PropTypes.string, updateCurrentLocale: PropTypes.func, currentLocale: PropTypes.string, useBlockie: PropTypes.bool, @@ -429,11 +428,8 @@ export default class SettingsTab extends PureComponent { } render() { - const { warning } = this.props; - return (
- {warning ?
{warning}
: null} {this.renderCurrentConversion()} {this.renderShowNativeTokenAsMainBalance()} {this.renderCurrentLocale()} diff --git a/ui/pages/settings/settings-tab/settings-tab.container.js b/ui/pages/settings/settings-tab/settings-tab.container.js index 7de17ffefde4..e6ad25f0df92 100644 --- a/ui/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/pages/settings/settings-tab/settings-tab.container.js @@ -18,10 +18,7 @@ import { getProviderConfig } from '../../../ducks/metamask/metamask'; import SettingsTab from './settings-tab.component'; const mapStateToProps = (state) => { - const { - appState: { warning }, - metamask, - } = state; + const { metamask } = state; const { currentCurrency, useBlockie, currentLocale } = metamask; const { ticker: nativeCurrency } = getProviderConfig(state); const { address: selectedAddress } = getSelectedInternalAccount(state); @@ -31,7 +28,6 @@ const mapStateToProps = (state) => { const tokenList = getTokenList(state); return { - warning, currentLocale, currentCurrency, nativeCurrency, From db2e8be23acd9bbf09ef700f647c1b759b9f2aba Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 1 Nov 2024 17:42:49 -0230 Subject: [PATCH 007/111] chore: Remove obsolete preview build support (#27968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We had previously configured CI to support installing `@metamask`- scoped packages from the GitHub npm registry, as we used this registry for "preview builds" from the core repository in the past. This is no longer used; we publish preview builds to npm now instead. The obsolete CI changes have been removed. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27968?quickstart=1) ## **Related issues** * [Preview build instructions](https://github.com/MetaMask/core/blob/main/docs/contributing.md#testing-changes-to-packages-with-preview-builds) ([permalink](https://github.com/MetaMask/core/blob/56efd1d13a8873a5abb9bd9880d0576148b9d1e4/docs/contributing.md#testing-changes-to-packages-with-preview-builds)) * Preview build related extension PRs: #16547, #19970, #20096, #20312 ## **Manual testing steps** This is not anticipated to have any functional impact. If it does break something, it would be the install step, so we can test this by installing locally (and seeing that the install step succeeded on CI). ## **Screenshots/Recordings** N/A ## **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 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/develop/.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. --- .circleci/config.yml | 2 +- .circleci/scripts/install-dependencies.sh | 42 ----------------------- .yarnrc.yml | 8 ----- 3 files changed, 1 insertion(+), 51 deletions(-) delete mode 100755 .circleci/scripts/install-dependencies.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 3836d5e4048e..aa13dea93b75 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -444,7 +444,7 @@ jobs: - gh/install - run: name: Install dependencies - command: .circleci/scripts/install-dependencies.sh + command: yarn --immutable - save_cache: key: dependency-cache-{{ checksum "/tmp/YARN_VERSION" }}-{{ checksum "yarn.lock" }} paths: diff --git a/.circleci/scripts/install-dependencies.sh b/.circleci/scripts/install-dependencies.sh deleted file mode 100755 index 35b7a690fa0c..000000000000 --- a/.circleci/scripts/install-dependencies.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o pipefail - -IS_NON_FORK_DRAFT='false' - -if [[ -n $CIRCLE_PULL_REQUEST ]] && gh auth status -then - PR_NUMBER="${CIRCLE_PULL_REQUEST##*/}" - if [ -n "$PR_NUMBER" ] - then - IS_NON_FORK_DRAFT="$(gh pr view --json isDraft --jq '.isDraft' "$PR_NUMBER")" - fi -fi - -# Build query to see whether there are any "preview-like" packages in the manifest -# A "preview-like" package is a `@metamask`-scoped package with a prerelease version that has no period. -QUERY='.dependencies + .devDependencies' # Get list of all dependencies -QUERY+=' | with_entries( select(.key | startswith("@metamask") ) )' # filter to @metamask-scoped packages -QUERY+=' | to_entries[].value' # Get version ranges -QUERY+=' | select(test("^\\d+\\.\\d+\\.\\d+-[^.]+$"))' # Get pinned versions where the prerelease part has no "." - -# Use `-e` flag so that exit code indicates whether any matches were found -if jq -e "${QUERY}" < ./package.json -then - echo "Preview builds detected" - HAS_PREVIEW_BUILDS='true' -else - echo "No preview builds detected" - HAS_PREVIEW_BUILDS='false' -fi - -if [[ $IS_NON_FORK_DRAFT == 'true' && $HAS_PREVIEW_BUILDS == 'true' ]] -then - # Use GitHub registry on draft PRs, allowing the use of preview builds - echo "Installing with preview builds" - METAMASK_NPM_REGISTRY=https://npm.pkg.github.com yarn --immutable -else - echo "Installing without preview builds" - yarn --immutable -fi diff --git a/.yarnrc.yml b/.yarnrc.yml index cc0c959e2722..8e12d8037c6a 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -119,14 +119,6 @@ npmAuditIgnoreAdvisories: - 'react-beautiful-dnd (deprecation)' # New package name format for new versions: @ethereumjs/wallet. - 'ethereumjs-wallet (deprecation)' -npmRegistries: - 'https://npm.pkg.github.com': - npmAlwaysAuth: true - npmAuthToken: '${GITHUB_PACKAGE_READ_TOKEN-}' - -npmScopes: - metamask: - npmRegistryServer: '${METAMASK_NPM_REGISTRY:-https://registry.yarnpkg.com}' plugins: - path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs From d773dd695dd1585d2b1afaf8d3ddd21b0e54785a Mon Sep 17 00:00:00 2001 From: Bryan Fullam Date: Sat, 2 Nov 2024 09:18:07 +0100 Subject: [PATCH 008/111] feat: add token verification source count and link to block explorer (#27759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** In a previous redesign, the information about the number of sources a token has been verified on and the link to that token on the relevant block explorer was removed from the swap page. This returns that information. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27759?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to the swap page 2. Select swap assets and amount 3. See token verification info and block explorer link ## **Screenshots/Recordings** ### **Before** ![Screenshot 2024-10-10 at 14 30 01](https://github.com/user-attachments/assets/85a3281e-a7b2-4649-9a72-7f756b233214) ### **After** ![Screenshot 2024-10-10 at 14 30 21](https://github.com/user-attachments/assets/6f557ed5-a956-4ab8-b3bd-957ac4d9fc2a) ## **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 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/develop/.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. --- app/_locales/en/messages.json | 4 + shared/constants/common.ts | 54 ++++++++++++ shared/constants/swaps.ts | 29 ------- .../assets/nfts/nft-details/nft-details.tsx | 4 +- .../account-list/account-list.tsx | 4 +- ...nteractive-replacement-token-page.test.tsx | 4 +- .../interactive-replacement-token-page.tsx | 4 +- ui/pages/swaps/awaiting-swap/awaiting-swap.js | 4 +- .../prepare-swap-page/prepare-swap-page.js | 83 +++++++++++++------ .../prepare-swap-page.test.js | 12 +-- .../item-list/item-list.component.js | 4 +- .../smart-transaction-status.js | 4 +- 12 files changed, 136 insertions(+), 74 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 512eb3b0ee36..cc077810750b 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5876,6 +5876,10 @@ "swapTokenVerifiedOn1SourceTitle": { "message": "Potentially inauthentic token" }, + "swapTokenVerifiedSources": { + "message": "Confirmed by $1 sources. Verify on $2.", + "description": "$1 the number of sources that have verified the token, $2 points the user to a block explorer as a place they can verify information about the token." + }, "swapTooManyDecimalsError": { "message": "$1 allows up to $2 decimals", "description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token" diff --git a/shared/constants/common.ts b/shared/constants/common.ts index f45ec8abd7e4..96d2b0c65b55 100644 --- a/shared/constants/common.ts +++ b/shared/constants/common.ts @@ -1,5 +1,59 @@ +import { CHAIN_IDS } from './network'; + export enum EtherDenomination { ETH = 'ETH', GWEI = 'GWEI', WEI = 'WEI', } + +const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; +const BSC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'BscScan'; +const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; +const MAINNET_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Etherscan'; +const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/'; +const GOERLI_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Goerli Etherscan'; +const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/'; +const POLYGON_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'PolygonScan'; +const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/'; +const AVALANCHE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Snowtrace'; +const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/'; +const OPTIMISM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Optimism Explorer'; +const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/'; +const ARBITRUM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'ArbiScan'; +const ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL = 'https://explorer.zksync.io/'; +const ZKSYNC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'Zksync Explorer'; +const LINEA_DEFAULT_BLOCK_EXPLORER_URL = 'https://lineascan.build/'; +const LINEA_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'LineaScan'; +const BASE_DEFAULT_BLOCK_EXPLORER_URL = 'https://basescan.org/'; +const BASE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL = 'BaseScan'; + +type BlockExplorerUrlMap = { + [key: string]: string; +}; + +export const CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP: BlockExplorerUrlMap = { + [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_URL, + [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_URL, +} as const; + +export const CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP: BlockExplorerUrlMap = + { + [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL, + } as const; diff --git a/shared/constants/swaps.ts b/shared/constants/swaps.ts index 3868c7b6e2f0..8dfecccef6e6 100644 --- a/shared/constants/swaps.ts +++ b/shared/constants/swaps.ts @@ -49,10 +49,6 @@ export type SwapsTokenObject = { iconUrl: string; }; -type BlockExplorerUrlMap = { - [key: string]: string; -}; - export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { symbol: CURRENCY_SYMBOLS.ETH, name: 'Ether', @@ -174,17 +170,6 @@ export const TOKEN_API_BASE_URL = 'https://tokens.api.cx.metamask.io'; export const GAS_API_BASE_URL = 'https://gas.api.cx.metamask.io'; export const GAS_DEV_API_BASE_URL = 'https://gas.uat-api.cx.metamask.io'; -const BSC_DEFAULT_BLOCK_EXPLORER_URL = 'https://bscscan.com/'; -export const MAINNET_DEFAULT_BLOCK_EXPLORER_URL = 'https://etherscan.io/'; -const GOERLI_DEFAULT_BLOCK_EXPLORER_URL = 'https://goerli.etherscan.io/'; -const POLYGON_DEFAULT_BLOCK_EXPLORER_URL = 'https://polygonscan.com/'; -const AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL = 'https://snowtrace.io/'; -const OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL = 'https://optimistic.etherscan.io/'; -const ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL = 'https://arbiscan.io/'; -const ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL = 'https://explorer.zksync.io/'; -export const LINEA_DEFAULT_BLOCK_EXPLORER_URL = 'https://lineascan.build/'; -const BASE_DEFAULT_BLOCK_EXPLORER_URL = 'https://basescan.org/'; - export const ALLOWED_PROD_SWAPS_CHAIN_IDS = [ CHAIN_IDS.MAINNET, SWAPS_TESTNET_CHAIN_ID, @@ -298,20 +283,6 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, } as const; -export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP: BlockExplorerUrlMap = - { - [CHAIN_IDS.BSC]: BSC_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.MAINNET]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.POLYGON]: POLYGON_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.GOERLI]: GOERLI_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.AVALANCHE]: AVALANCHE_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.OPTIMISM]: OPTIMISM_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.ARBITRUM]: ARBITRUM_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.LINEA_MAINNET]: LINEA_DEFAULT_BLOCK_EXPLORER_URL, - [CHAIN_IDS.BASE]: BASE_DEFAULT_BLOCK_EXPLORER_URL, - } as const; - export const ETHEREUM = 'ethereum'; export const POLYGON = 'polygon'; export const BSC = 'bsc'; diff --git a/ui/components/app/assets/nfts/nft-details/nft-details.tsx b/ui/components/app/assets/nfts/nft-details/nft-details.tsx index 5ee8525cc98b..0dc9ec05250c 100644 --- a/ui/components/app/assets/nfts/nft-details/nft-details.tsx +++ b/ui/components/app/assets/nfts/nft-details/nft-details.tsx @@ -66,7 +66,7 @@ import { MetaMetricsContext } from '../../../../../contexts/metametrics'; import { Content, Footer, Page } from '../../../../multichain/pages/page'; import { formatCurrency } from '../../../../../helpers/utils/confirm-tx.util'; import { getShortDateFormatterV2 } from '../../../../../pages/asset/util'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../../shared/constants/common'; import { getConversionRate } from '../../../../../ducks/metamask/metamask'; import { Numeric } from '../../../../../../shared/modules/Numeric'; // TODO: Remove restricted import @@ -277,7 +277,7 @@ export default function NftDetails({ nft }: { nft: Nft }) { null as unknown as string, // no holderAddress { blockExplorerUrl: - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, }, ); }; diff --git a/ui/pages/institutional/account-list/account-list.tsx b/ui/pages/institutional/account-list/account-list.tsx index e710a0e6e93a..84431f867307 100644 --- a/ui/pages/institutional/account-list/account-list.tsx +++ b/ui/pages/institutional/account-list/account-list.tsx @@ -1,6 +1,6 @@ import React from 'react'; import CustodyLabels from '../../../components/institutional/custody-labels'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { shortenAddress } from '../../../helpers/utils/util'; import Tooltip from '../../../components/ui/tooltip'; @@ -41,7 +41,7 @@ type CustodyAccountListProps = { }; const getButtonLinkHref = (account: Account) => { - const url = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; + const url = CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; return `${url}address/${account.address}`; }; diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx index b2db09b4d06c..6fca1af16a8e 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx +++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.test.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom'; import thunk from 'redux-thunk'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; import mockState from '../../../../test/data/mock-state.json'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { shortenAddress } from '../../../helpers/utils/util'; import { getSelectedInternalAccountFromMockState } from '../../../../test/jest/mocks'; @@ -145,7 +145,7 @@ describe('Interactive Replacement Token Page', function () { it('should render all the accounts correctly', async () => { const expectedHref = `${ - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET] + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET] }address/${custodianAddress}`; await act(async () => { diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx index 5c00eb4ffa10..c12bb0aedf57 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx +++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx @@ -29,7 +29,7 @@ import { getMetaMaskAccounts } from '../../../selectors'; import { getInstitutionalConnectRequests } from '../../../ducks/institutional/institutional'; import { getSelectedInternalAccount } from '../../../selectors/accounts'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { mmiActionsFactory, @@ -43,7 +43,7 @@ import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { shortenAddress } from '../../../helpers/utils/util'; const getButtonLinkHref = ({ address }: { address: string }) => { - const url = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; + const url = CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; return `${url}address/${address}`; }; diff --git a/ui/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/pages/swaps/awaiting-swap/awaiting-swap.js index 7c410ca03ce5..111af726acfa 100644 --- a/ui/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/pages/swaps/awaiting-swap/awaiting-swap.js @@ -48,8 +48,8 @@ import { QUOTES_NOT_AVAILABLE_ERROR, CONTRACT_DATA_DISABLED_ERROR, OFFLINE_FOR_MAINTENANCE, - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils'; import PulseLoader from '../../../components/ui/pulse-loader'; @@ -143,7 +143,7 @@ export default function AwaitingSwap({ }; const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null; const blockExplorerUrl = getBlockExplorerLink( { hash: txHash, chainId }, diff --git a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js index 8a701289bebd..1e5eb5179e2c 100644 --- a/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js +++ b/ui/pages/swaps/prepare-swap-page/prepare-swap-page.js @@ -2,9 +2,9 @@ import React, { useContext, useEffect, useState, useCallback } from 'react'; import BigNumber from 'bignumber.js'; import PropTypes from 'prop-types'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; -import { uniqBy, isEqual } from 'lodash'; +import { uniqBy, isEqual, isEmpty } from 'lodash'; import { useHistory } from 'react-router-dom'; -import { getTokenTrackerLink } from '@metamask/etherscan-link'; +import { getAccountLink, getTokenTrackerLink } from '@metamask/etherscan-link'; import classnames from 'classnames'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -83,23 +83,23 @@ import { usePrevious } from '../../../hooks/usePrevious'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from '../../../../shared/modules/swaps.utils'; +import { isSwapsDefaultTokenAddress } from '../../../../shared/modules/swaps.utils'; import { MetaMetricsEventCategory, MetaMetricsEventLinkType, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, TokenBucketPriority, ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, QUOTES_EXPIRED_ERROR, MAX_ALLOWED_SLIPPAGE, } from '../../../../shared/constants/swaps'; +import { + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, + CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP, +} from '../../../../shared/constants/common'; import { resetSwapsPostFetchState, ignoreTokens, @@ -142,6 +142,7 @@ import SwapsBannerAlert from '../swaps-banner-alert/swaps-banner-alert'; import SwapsFooter from '../swaps-footer'; import SelectedToken from '../selected-token/selected-token'; import ListWithSearch from '../list-with-search/list-with-search'; +import { CHAIN_IDS } from '../../../../shared/constants/network'; import QuotesLoadingAnimation from './quotes-loading-animation'; import ReviewQuote from './review-quote'; @@ -228,8 +229,8 @@ export default function PrepareSwapPage({ const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); const isMarketingEnabled = useSelector(getDataCollectionForMarketing); - const fetchParamsFromToken = isSwapsDefaultTokenSymbol( - sourceTokenInfo?.symbol, + const fetchParamsFromToken = isSwapsDefaultTokenAddress( + sourceTokenInfo?.address, chainId, ) ? defaultSwapsToken @@ -241,7 +242,8 @@ export default function PrepareSwapPage({ // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that // the balance of the token can appear in the from token selection dropdown const fromTokenArray = - !isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance + !isSwapsDefaultTokenAddress(fromToken?.address, chainId) && + fromToken?.balance ? [fromToken] : []; const usersTokens = uniqBy( @@ -310,7 +312,10 @@ export default function PrepareSwapPage({ { showFiat: true }, true, ); - const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) + const swapFromFiatValue = isSwapsDefaultTokenAddress( + fromTokenAddress, + chainId, + ) ? swapFromEthFiatValue : swapFromTokenFiatValue; @@ -435,19 +440,27 @@ export default function PrepareSwapPage({ onInputChange(fromTokenInputValue, token.string, token.decimals); }; - const blockExplorerTokenLink = getTokenTrackerLink( - selectedToToken.address, - chainId, - null, // no networkId - null, // no holderAddress - { - blockExplorerUrl: - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, - }, - ); + const blockExplorerTokenLink = + chainId === CHAIN_IDS.ZKSYNC_ERA + ? // Use getAccountLink because zksync explorer uses a /address URL scheme instead of /token + getAccountLink(selectedToToken.address, chainId, { + blockExplorerUrl: + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, + }) + : getTokenTrackerLink( + selectedToToken.address, + chainId, + null, // no networkId + null, // no holderAddress + { + blockExplorerUrl: + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null, + }, + ); const blockExplorerLabel = rpcPrefs.blockExplorerUrl - ? getURLHostName(blockExplorerTokenLink) + ? CHAINID_DEFAULT_BLOCK_EXPLORER_HUMAN_READABLE_URL_MAP[chainId] ?? + t('etherscan') : t('etherscan'); const { address: toAddress } = toToken || {}; @@ -786,18 +799,23 @@ export default function PrepareSwapPage({ ); } - const isNonDefaultToken = !isSwapsDefaultTokenSymbol( - fromTokenSymbol, + const isNonDefaultFromToken = !isSwapsDefaultTokenAddress( + fromTokenAddress, chainId, ); const hasPositiveFromTokenBalance = rawFromTokenBalance > 0; const isTokenEligibleForMaxBalance = - isSmartTransaction || (!isSmartTransaction && isNonDefaultToken); + isSmartTransaction || (!isSmartTransaction && isNonDefaultFromToken); const showMaxBalanceLink = fromTokenSymbol && isTokenEligibleForMaxBalance && hasPositiveFromTokenBalance; + const isNonDefaultToToken = !isSwapsDefaultTokenAddress( + selectedToToken.address, + chainId, + ); + return (
@@ -1024,6 +1042,21 @@ export default function PrepareSwapPage({ {selectedToToken?.string && yourTokenToBalance}
+ +
+ {selectedToToken && + !isEmpty(selectedToToken) && + isNonDefaultToToken && + t('swapTokenVerifiedSources', [ + occurrences, + , + ])} +
+
{showCrossChainSwapsLink && ( { }, }); const props = createProps(); - const { getByText } = renderWithProvider( + const { getByText, getAllByText } = renderWithProvider( , store, ); @@ -128,7 +128,7 @@ describe('PrepareSwapPage', () => { expect( getByText('USDC is only verified on 1 source', { exact: false }), ).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); + expect(getAllByText('Etherscan')[0]).toBeInTheDocument(); expect(getByText('Continue swapping')).toBeInTheDocument(); }); @@ -143,7 +143,7 @@ describe('PrepareSwapPage', () => { }, }); const props = createProps(); - const { getByText } = renderWithProvider( + const { getByText, getAllByText } = renderWithProvider( , store, ); @@ -151,7 +151,7 @@ describe('PrepareSwapPage', () => { expect( getByText('Verify this token on', { exact: false }), ).toBeInTheDocument(); - expect(getByText('etherscan.io')).toBeInTheDocument(); + expect(getAllByText('Etherscan')[0]).toBeInTheDocument(); expect(getByText('Continue swapping')).toBeInTheDocument(); }); @@ -167,11 +167,11 @@ describe('PrepareSwapPage', () => { }, }); const props = createProps(); - const { getByText } = renderWithProvider( + const { getAllByText } = renderWithProvider( , store, ); - const blockExplorer = getByText('etherscan.io'); + const blockExplorer = getAllByText('Etherscan')[0]; expect(blockExplorer).toBeInTheDocument(); fireEvent.click(blockExplorer); expect(global.platform.openTab).toHaveBeenCalledWith({ diff --git a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js index 779d1edf58a8..bd1bb5aa5aaf 100644 --- a/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js +++ b/ui/pages/swaps/searchable-item-list/item-list/item-list.component.js @@ -13,7 +13,7 @@ import { getUseCurrencyRateCheck, } from '../../../../selectors'; import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../../shared/constants/common'; import { getURLHostName } from '../../../../helpers/utils/util'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; @@ -35,7 +35,7 @@ export default function ItemList({ const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const blockExplorerLink = rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null; const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const blockExplorerHostName = getURLHostName(blockExplorerLink); diff --git a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js index 7b8d5910c218..d3127c9a94f3 100644 --- a/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js +++ b/ui/pages/swaps/smart-transaction-status/smart-transaction-status.js @@ -23,7 +23,7 @@ import { getSmartTransactionsEnabled, getSmartTransactionsOptInStatusForMetrics, } from '../../../../shared/modules/selectors'; -import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; +import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/common'; import { DEFAULT_ROUTE, PREPARE_SWAP_ROUTE, @@ -87,7 +87,7 @@ export default function SmartTransactionStatusPage() { ); const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? - SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null; let smartTransactionStatus = SmartTransactionStatus.pending; From 5e28e36059dd50d6e6993f353a590a3886cc3a93 Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Sun, 3 Nov 2024 23:09:54 -0800 Subject: [PATCH 009/111] fix: margin on asset chart min/max indicators (#27916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Design feedback that the min/max indicators on the asset chart should not be edge to edge like the chart itself. They should have margin like the rest of the content on the page. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27916?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Click on a token in the token list 2. Click various date range buttons 3. Verify min/max indicators on the chart are in right place 4. Verify min/max indicators keep 16px margin from the edges ## **Screenshots/Recordings** ### **Before** ![image](https://github.com/user-attachments/assets/1b319f18-277d-47a9-b3b2-90b854a58864) ### **After** ![image](https://github.com/user-attachments/assets/90a12c91-16cf-46b4-be91-c287d7d93bf8) ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- .../asset/components/__snapshots__/asset-page.test.tsx.snap | 4 ++-- ui/pages/asset/components/chart/chart-tooltip.tsx | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap index 79400367de13..b5ebc0a83eb6 100644 --- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap +++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap @@ -764,7 +764,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` style="padding-right: 100%; direction: rtl;" >

$1.00

@@ -777,7 +777,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = ` style="padding-right: 100%; direction: rtl;" >

$1.00

diff --git a/ui/pages/asset/components/chart/chart-tooltip.tsx b/ui/pages/asset/components/chart/chart-tooltip.tsx index ac7fbc3c20d4..4fa4b83a85ff 100644 --- a/ui/pages/asset/components/chart/chart-tooltip.tsx +++ b/ui/pages/asset/components/chart/chart-tooltip.tsx @@ -42,6 +42,8 @@ const ChartTooltip = ({ }} > Date: Mon, 4 Nov 2024 09:50:59 +0000 Subject: [PATCH 010/111] refactor: remove global network usage from signatures (#28167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Remove global network usage from all signature components and hooks. Specifically: - Replace usages of the following selectors with the chain ID extracted from the signature request. - `getCurrentChainId` - `getNativeCurrency` - `getProviderConfig` - Add new selectors: - `selectNetworkConfigurationByChainId` - `selectDefaultRpcEndpointByChainId` [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28167?quickstart=1) ## **Related issues** Fixes: [#3375](https://github.com/MetaMask/MetaMask-planning/issues/3375) [#3500](https://github.com/MetaMask/MetaMask-planning/issues/3500) [#3459](https://github.com/MetaMask/MetaMask-planning/issues/3459) [#3374](https://github.com/MetaMask/MetaMask-planning/issues/3374) ## **Manual testing steps** Regression of all signature types including legacy and redesigned confirmations. Also verify Blockaid warnings. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- test/data/confirmations/personal_sign.ts | 4 + test/data/confirmations/typed_sign.ts | 11 +- .../confirmations/signatures/permit.test.tsx | 2 + .../signatures/personalSign.test.tsx | 2 + ui/hooks/useName.test.ts | 1 - ui/hooks/useNftCollectionsMetadata.test.ts | 4 - .../value-display/value-display.tsx | 1 + .../network-change-toast-legacy.tsx | 19 +- .../contract-details-modal.js | 10 +- .../blockaid-banner-alert.js | 6 +- .../signature-request-header.js | 32 ++- .../signature-request-header.stories.js | 17 ++ .../signature-request-header.test.js | 10 +- .../signature-request-original.stories.js | 19 +- .../signature-request-original.test.js | 7 + .../signature-request-siwe.test.js.snap | 2 +- .../signature-request-siwe.stories.js | 25 +- .../signature-request-siwe.test.js | 8 +- .../signature-request.test.js.snap | 41 --- .../signature-request/signature-request.js | 16 +- .../signature-request.stories.js | 8 +- .../signature-request.test.js | 269 ++++++++---------- .../confirm-signature-request/index.test.js | 13 +- .../hooks/alerts/useBlockaidAlerts.ts | 6 +- .../hooks/useConfirmationNetworkInfo.ts | 19 +- ...rackERC20WithoutDecimalInformation.test.ts | 3 +- .../useTrackERC20WithoutDecimalInformation.ts | 5 +- ui/selectors/selectors.js | 22 ++ 28 files changed, 307 insertions(+), 275 deletions(-) diff --git a/test/data/confirmations/personal_sign.ts b/test/data/confirmations/personal_sign.ts index b0f135efa53d..69c3ff9f75bc 100644 --- a/test/data/confirmations/personal_sign.ts +++ b/test/data/confirmations/personal_sign.ts @@ -1,3 +1,4 @@ +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { SignatureRequestType } from '../../../ui/pages/confirmations/types/confirm'; export const PERSONAL_SIGN_SENDER_ADDRESS = @@ -5,6 +6,7 @@ export const PERSONAL_SIGN_SENDER_ADDRESS = export const unapprovedPersonalSignMsg = { id: '0050d5b0-c023-11ee-a0cb-3390a510a0ab', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: new Date().getTime(), type: 'personal_sign', @@ -20,6 +22,7 @@ export const unapprovedPersonalSignMsg = { export const signatureRequestSIWE = { id: '210ca3b0-1ccb-11ef-b096-89c4d726ebb5', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -57,6 +60,7 @@ export const signatureRequestSIWE = { export const SignatureRequestSIWEWithResources = { id: '210ca3b0-1ccb-11ef-b096-89c4d726ebb5', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', diff --git a/test/data/confirmations/typed_sign.ts b/test/data/confirmations/typed_sign.ts index 7be24a1389c6..831d561f0cb2 100644 --- a/test/data/confirmations/typed_sign.ts +++ b/test/data/confirmations/typed_sign.ts @@ -1,9 +1,10 @@ -import { TransactionType } from '@metamask/transaction-controller'; +import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../shared/constants/app'; import { SignatureRequestType } from '../../../ui/pages/confirmations/types/confirm'; export const unapprovedTypedSignMsgV1 = { id: '82ab2400-e2c6-11ee-9627-73cc88f00492', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -60,6 +61,7 @@ const rawMessageV3 = { export const unapprovedTypedSignMsgV3 = { id: '17e41af0-e073-11ee-9eec-5fd284826685', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -129,6 +131,7 @@ export const rawMessageV4 = { export const unapprovedTypedSignMsgV4 = { id: '0050d5b0-c023-11ee-a0cb-3390a510a0ab', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: new Date().getTime(), chainid: '0x5', @@ -145,6 +148,7 @@ export const unapprovedTypedSignMsgV4 = { export const orderSignatureMsg = { id: 'e5249ae0-4b6b-11ef-831f-65b48eb489ec', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { result_type: 'loading', reason: 'validation_in_progress', @@ -165,6 +169,7 @@ export const orderSignatureMsg = { export const permitSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -185,6 +190,7 @@ export const permitSignatureMsg = { export const permitNFTSignatureMsg = { id: 'c5067710-87cf-11ef-916c-71f266571322', + chainId: CHAIN_IDS.GOERLI, status: 'unapproved', time: 1728651190529, type: 'eth_signTypedData', @@ -200,6 +206,7 @@ export const permitNFTSignatureMsg = { export const permitSignatureMsgWithNoDeadline = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -219,6 +226,7 @@ export const permitSignatureMsgWithNoDeadline = { export const permitBatchSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', @@ -239,6 +247,7 @@ export const permitBatchSignatureMsg = { export const permitSingleSignatureMsg = { id: '0b1787a0-1c44-11ef-b70d-e7064bd7b659', + chainId: CHAIN_IDS.GOERLI, securityAlertResponse: { reason: 'loading', result_type: 'validation_in_progress', diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx index 8e9c979562f2..5ff87bf7c533 100644 --- a/test/integration/confirmations/signatures/permit.test.tsx +++ b/test/integration/confirmations/signatures/permit.test.tsx @@ -1,6 +1,7 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import nock from 'nock'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, @@ -42,6 +43,7 @@ const getMetaMaskStateWithUnapprovedPermitSign = (accountAddress: string) => { unapprovedTypedMessages: { [pendingPermitId]: { id: pendingPermitId, + chainId: CHAIN_IDS.SEPOLIA, status: 'unapproved', time: pendingPermitTime, type: MESSAGE_TYPE.ETH_SIGN_TYPED_DATA, diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx index 690446caa533..5a9c311c9abd 100644 --- a/test/integration/confirmations/signatures/personalSign.test.tsx +++ b/test/integration/confirmations/signatures/personalSign.test.tsx @@ -1,5 +1,6 @@ import { ApprovalType } from '@metamask/controller-utils'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MetaMetricsEventCategory, @@ -34,6 +35,7 @@ const getMetaMaskStateWithUnapprovedPersonalSign = (accountAddress: string) => { unapprovedPersonalMsgs: { [pendingPersonalSignId]: { id: pendingPersonalSignId, + chainId: CHAIN_IDS.SEPOLIA, status: 'unapproved', time: pendingPersonalSignTime, type: MESSAGE_TYPE.PERSONAL_SIGN, diff --git a/ui/hooks/useName.test.ts b/ui/hooks/useName.test.ts index f746c4bb6267..b102e9dce7a0 100644 --- a/ui/hooks/useName.test.ts +++ b/ui/hooks/useName.test.ts @@ -15,7 +15,6 @@ jest.mock('react-redux', () => ({ })); jest.mock('../selectors', () => ({ - getCurrentChainId: jest.fn(), getNames: jest.fn(), })); diff --git a/ui/hooks/useNftCollectionsMetadata.test.ts b/ui/hooks/useNftCollectionsMetadata.test.ts index e1e2b6745ad1..cf7997cb518b 100644 --- a/ui/hooks/useNftCollectionsMetadata.test.ts +++ b/ui/hooks/useNftCollectionsMetadata.test.ts @@ -16,10 +16,6 @@ jest.mock('react-redux', () => ({ useSelector: (selector: any) => selector(), })); -jest.mock('../selectors', () => ({ - getCurrentChainId: jest.fn(), -})); - jest.mock('../store/actions', () => ({ getNFTContractInfo: jest.fn(), getTokenStandardAndDetails: jest.fn(), diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx index fa0c911d5eae..c7a9eae6a496 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/value-display/value-display.tsx @@ -59,6 +59,7 @@ const PermitSimulationValueDisplay: React.FC< const tokenDetails = useGetTokenStandardAndDetails(tokenContract); useTrackERC20WithoutDecimalInformation( + chainId, tokenContract, tokenDetails as TokenDetailsERC20, MetaMetricsEventLocation.SignatureConfirmation, diff --git a/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx index fca355a9b063..f8d6b87e50db 100644 --- a/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx +++ b/ui/pages/confirmations/components/confirm/network-change-toast/network-change-toast-legacy.tsx @@ -1,18 +1,14 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { Hex } from '@metamask/utils'; import { Box } from '../../../../../components/component-library'; import { Toast } from '../../../../../components/multichain'; import { getLastInteractedConfirmationInfo, setLastInteractedConfirmationInfo, } from '../../../../../store/actions'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../../../../../selectors'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; +import { selectNetworkConfigurationByChainId } from '../../../../../selectors'; const CHAIN_CHANGE_THRESHOLD_MILLISECONDS = 60 * 1000; // 1 Minute const TOAST_TIMEOUT_MILLISECONDS = 5 * 1000; // 5 Seconds @@ -22,12 +18,13 @@ const NetworkChangeToastLegacy = ({ }: { confirmation: { id: string; chainId: string }; }) => { - const chainId = useSelector(getCurrentChainId); - const newChainId = confirmation?.chainId ?? chainId; + const newChainId = confirmation?.chainId; const [toastVisible, setToastVisible] = useState(false); const t = useI18nContext(); - const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); - const network = networkConfigurations[newChainId as Hex]; + + const network = useSelector((state) => + selectNetworkConfigurationByChainId(state, newChainId), + ); const hideToast = useCallback(() => { setToastVisible(false); @@ -35,9 +32,11 @@ const NetworkChangeToastLegacy = ({ useEffect(() => { let isMounted = true; + if (!confirmation) { return undefined; } + (async () => { const lastInteractedConfirmationInfo = await getLastInteractedConfirmationInfo(); @@ -71,7 +70,7 @@ const NetworkChangeToastLegacy = ({ return () => { isMounted = false; }; - }, [confirmation?.id, chainId]); + }, [confirmation?.id]); if (!toastVisible) { return null; diff --git a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js index a0a16cc838c3..795401673691 100644 --- a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js +++ b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js @@ -39,7 +39,7 @@ export default function ContractDetailsModal({ tokenAddress, toAddress, chainId, - rpcPrefs, + blockExplorerUrl, tokenId, assetName, assetStandard, @@ -178,7 +178,7 @@ export default function ContractDetailsModal({ tokenAddress, chainId, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, null, ); @@ -287,7 +287,7 @@ export default function ContractDetailsModal({ toAddress, chainId, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, null, ); @@ -338,9 +338,9 @@ ContractDetailsModal.propTypes = { */ chainId: PropTypes.string, /** - * RPC prefs of the current network + * Block explorer URL of the current network */ - rpcPrefs: PropTypes.object, + blockExplorerUrl: PropTypes.string, /** * The token id of the NFT */ diff --git a/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js b/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js index 4115f06cd644..ff5adfea8b5d 100644 --- a/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js +++ b/ui/pages/confirmations/components/security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert.js @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { captureException } from '@sentry/browser'; import BlockaidPackage from '@blockaid/ppom_release/package.json'; -import { useSelector } from 'react-redux'; import { NETWORK_TO_NAME_MAP } from '../../../../../../shared/constants/network'; import { OverflowWrap } from '../../../../../helpers/constants/design-system'; import { I18nContext } from '../../../../../contexts/i18n'; @@ -20,7 +19,6 @@ import { useTransactionEventFragment } from '../../../hooks/useTransactionEventF import SecurityProviderBannerAlert from '../security-provider-banner-alert'; import LoadingIndicator from '../../../../../components/ui/loading-indicator'; -import { getCurrentChainId } from '../../../../../selectors'; import { getReportUrl } from './blockaid-banner-utils'; const zlib = require('zlib'); @@ -59,8 +57,6 @@ function BlockaidBannerAlert({ txData, ...props }) { const { securityAlertResponse, origin, msgParams, type, txParams, chainId } = txData; - const selectorChainId = useSelector(getCurrentChainId); - const t = useContext(I18nContext); const { updateTransactionEventFragment } = useTransactionEventFragment(); @@ -131,7 +127,7 @@ function BlockaidBannerAlert({ txData, ...props }) { const reportData = { blockNumber: block, blockaidVersion: BlockaidPackage.version, - chain: NETWORK_TO_NAME_MAP[chainId ?? selectorChainId], + chain: NETWORK_TO_NAME_MAP[chainId], classification: isFailedResultType ? 'error' : reason, domain: origin ?? msgParams?.origin ?? txParams?.origin, jsonRpcMethod: type, diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js index 9c91ca476ebb..61cbfe13e290 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.js @@ -1,16 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; +import { RpcEndpointType } from '@metamask/network-controller'; +import { NetworkType } from '@metamask/controller-utils'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { - getNativeCurrency, - getProviderConfig, -} from '../../../../ducks/metamask/metamask'; import { accountsWithSendEtherInfoSelector, - getCurrentChainId, getCurrentCurrency, + selectDefaultRpcEndpointByChainId, + selectNetworkConfigurationByChainId, } from '../../../../selectors'; import { formatCurrency } from '../../../../helpers/utils/confirm-tx.util'; import { @@ -26,22 +25,33 @@ import NetworkAccountBalanceHeader from '../../../../components/app/network-acco const SignatureRequestHeader = ({ txData }) => { const t = useI18nContext(); const { + chainId, msgParams: { from }, } = txData; const allAccounts = useSelector(accountsWithSendEtherInfoSelector); const fromAccount = getAccountByAddress(allAccounts, from); - const nativeCurrency = useSelector(getNativeCurrency); const currentCurrency = useSelector(getCurrentCurrency); - const currentChainId = useSelector(getCurrentChainId); - const providerConfig = useSelector(getProviderConfig); - const networkName = getNetworkNameFromProviderType(providerConfig.type); + const { nativeCurrency, name: networkNickname } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + + const defaultRpcEndpoint = useSelector((state) => + selectDefaultRpcEndpointByChainId(state, chainId), + ); + + const networkType = + defaultRpcEndpoint.type === RpcEndpointType.Custom + ? NetworkType.rpc + : defaultRpcEndpoint.networkClientId; + + const networkName = getNetworkNameFromProviderType(networkType); const conversionRate = null; // setting conversion rate to null by default to display balance in native const currentNetwork = networkName === '' - ? providerConfig.nickname || t('unknownNetwork') + ? networkNickname || t('unknownNetwork') : t(networkName); const balanceInBaseAsset = conversionRate @@ -71,7 +81,7 @@ const SignatureRequestHeader = ({ txData }) => { conversionRate ? currentCurrency?.toUpperCase() : nativeCurrency } accountAddress={fromAccount.address} - chainId={currentChainId} + chainId={chainId} /> ); }; diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js index 673b9a67e598..111be8932207 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.stories.js @@ -1,13 +1,30 @@ import React from 'react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { Provider } from 'react-redux'; +import configureStore from '../../../../store/store'; +import testData from '../../../../../.storybook/test-data'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestHeader from './signature-request-header'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + +const store = configureStore({ + ...testData, + metamask: { + ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}); + export default { title: 'Confirmations/Components/SignatureRequestHeader', + decorators: [(story) => {story()}], argTypes: { txData: { control: 'object' }, }, args: { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: JSON.stringify({ diff --git a/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js b/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js index a3ddd3136627..862339239643 100644 --- a/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js +++ b/ui/pages/confirmations/components/signature-request-header/signature-request-header.test.js @@ -1,12 +1,17 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestHeader from '.'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + const props = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', }, @@ -14,7 +19,10 @@ const props = { }; describe('SignatureRequestHeader', () => { - const store = configureMockStore()(mockState); + const store = configureMockStore()({ + ...mockState, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }); it('should match snapshot', () => { const { container } = renderWithProvider( diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js index 297978bb5f04..7343d5c34140 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.stories.js @@ -1,10 +1,16 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { Provider } from 'react-redux'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import testData from '../../../../../.storybook/test-data'; +import configureStore from '../../../../store/store'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import README from './README.mdx'; import SignatureRequestOriginal from './signature-request-original.component'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + const [MOCK_PRIMARY_ACCOUNT, MOCK_SECONDARY_ACCOUNT] = Object.values( testData.metamask.internalAccounts.accounts, ); @@ -41,9 +47,17 @@ const MOCK_SIGN_DATA = JSON.stringify({ }, }); +const store = configureStore({ + ...testData, + metamask: { + ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}); + export default { title: 'Confirmations/Components/SignatureRequestOriginal', - + decorators: [(story) => {story()}], component: SignatureRequestOriginal, parameters: { docs: { @@ -87,6 +101,7 @@ DefaultStory.storyName = 'personal_sign Type'; DefaultStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: MOCK_SIGN_DATA, @@ -102,6 +117,7 @@ ETHSignTypedStory.storyName = 'eth_signTypedData Type'; ETHSignTypedStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: [ @@ -128,6 +144,7 @@ AccountMismatchStory.storyName = 'Account Mismatch warning'; AccountMismatchStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', data: MOCK_SIGN_DATA, diff --git a/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js b/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js index 11577f44e312..b8d1429751e6 100644 --- a/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js +++ b/ui/pages/confirmations/components/signature-request-original/signature-request-original.test.js @@ -3,6 +3,7 @@ import configureMockStore from 'redux-mock-store'; import { fireEvent, screen } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { EthAccountType } from '@metamask/keyring-api'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { MESSAGE_TYPE } from '../../../../../shared/constants/app'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../../shared/constants/security-provider'; import mockState from '../../../../../test/data/mock-state.json'; @@ -11,6 +12,7 @@ import configureStore from '../../../../store/store'; import { rejectPendingApproval } from '../../../../store/actions'; import { shortenAddress } from '../../../../helpers/utils/util'; import { ETH_EOA_METHODS } from '../../../../../shared/constants/eth-methods'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestOriginal from '.'; jest.mock('../../../../store/actions', () => ({ @@ -21,12 +23,15 @@ jest.mock('../../../../store/actions', () => ({ setLastInteractedConfirmationInfo: jest.fn(), })); +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + const address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; const props = { signMessage: jest.fn(), cancelMessage: jest.fn(), txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: address, data: [ @@ -76,6 +81,7 @@ const render = ({ txData = props.txData, selectedAccount } = {}) => { const store = configureStore({ metamask: { ...mockState.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), internalAccounts, }, }); @@ -120,6 +126,7 @@ describe('SignatureRequestOriginal', () => { it('should escape RTL character in label or value', () => { const txData = { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', data: [ diff --git a/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap b/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap index a524cbcc1caf..fcf0bb30d8a3 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap +++ b/ui/pages/confirmations/components/signature-request-siwe/__snapshots__/signature-request-siwe.test.js.snap @@ -169,7 +169,7 @@ exports[`SignatureRequestSIWE (Sign in with Ethereum) should match snapshot 1`] > 966.987986 - ETH + GoerliETH
diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js index de29680421c6..7900cfe1828d 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.stories.js @@ -1,18 +1,37 @@ import React from 'react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { Provider } from 'react-redux'; import testData from '../../../../../.storybook/test-data'; +import configureStore from '../../../../store/store'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import README from './README.mdx'; import SignatureRequestSIWE from './signature-request-siwe'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + +const TRANSACTION_DATA_MOCK = { + chainId: CHAIN_ID_MOCK, +}; + const { internalAccounts: { accounts, selectedAccount }, } = testData.metamask; + const otherAccount = Object.values(accounts)[1]; const { address: selectedAddress } = accounts[selectedAccount]; +const store = configureStore({ + ...testData, + metamask: { + ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}); + export default { title: 'Confirmations/Components/SignatureRequestSIWE', - + decorators: [(story) => {story()}], component: SignatureRequestSIWE, parameters: { docs: { @@ -134,6 +153,7 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams, }, }; @@ -144,6 +164,7 @@ export const BadDomainStory = (args) => { BadDomainStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams: badDomainParams, }, }; @@ -154,6 +175,7 @@ export const BadAddressStory = (args) => { BadAddressStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams: badAddressParams, }, }; @@ -164,6 +186,7 @@ export const BadDomainAndAddressStory = (args) => { BadDomainAndAddressStory.args = { txData: { + ...TRANSACTION_DATA_MOCK, msgParams: badDomainAndAddressParams, }, }; diff --git a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js index c97be33e45b0..f8f1a67b8cbc 100644 --- a/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js +++ b/ui/pages/confirmations/components/signature-request-siwe/signature-request-siwe.test.js @@ -2,14 +2,16 @@ import React from 'react'; import { cloneDeep } from 'lodash'; import { fireEvent } from '@testing-library/react'; import { ApprovalType } from '@metamask/controller-utils'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import configureStore from '../../../../store/store'; -import { getCurrentChainId } from '../../../../selectors'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequestSIWE from '.'; const MOCK_ORIGIN = 'https://example-dapp.website'; const MOCK_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; const mockStoreInitialState = { metamask: { @@ -20,6 +22,7 @@ const mockStoreInitialState = { name: 'Example Test Dapp', }, }, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), }, }; @@ -37,6 +40,7 @@ const mockProps = { cancelPersonalMessage: jest.fn(), signPersonalMessage: jest.fn(), txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: MOCK_ADDRESS, data: '0x6c6f63616c686f73743a383038302077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078466232433135303034333433393034653566343038323537386334653865313131303563463765330a0a436c69636b20746f207369676e20696e20616e642061636365707420746865205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a20687474703a2f2f6c6f63616c686f73743a383038300a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2053544d74364b514d7777644f58453330360a4973737565642041743a20323032322d30332d31385432313a34303a34302e3832335a0a5265736f75726365733a0a2d20697066733a2f2f516d653773733341525667787636725871565069696b4d4a3875324e4c676d67737a673133705972444b456f69750a2d2068747470733a2f2f6578616d706c652e636f6d2f6d792d776562322d636c61696d2e6a736f6e', @@ -183,7 +187,7 @@ describe('SignatureRequestSIWE (Sign in with Ethereum)', () => { transactions: [ ...mockStoreInitialState.metamask.transactions, { - chainId: getCurrentChainId(mockStoreInitialState), + chainId: CHAIN_ID_MOCK, status: 'unapproved', }, ], diff --git a/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap b/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap index ba272fc26d89..943f8699b9aa 100644 --- a/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap +++ b/ui/pages/confirmations/components/signature-request/__snapshots__/signature-request.test.js.snap @@ -177,26 +177,6 @@ exports[`Signature Request Component render should match snapshot when we are us
-
- -
-

- To view and confirm your most recent request, you'll need to approve or reject existing requests first. -

-

-

-
@@ -962,7 +942,6 @@ exports[`Signature Request Component render should match snapshot when we want t > 0 - ETH
@@ -970,26 +949,6 @@ exports[`Signature Request Component render should match snapshot when we want t
-
- -
-

- To view and confirm your most recent request, you'll need to approve or reject existing requests first. -

-

-

-
diff --git a/ui/pages/confirmations/components/signature-request/signature-request.js b/ui/pages/confirmations/components/signature-request/signature-request.js index 3cf2a54327c9..2a7977da0c75 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.js @@ -18,16 +18,14 @@ import { doesAddressRequireLedgerHidConnection, getSubjectMetadata, getTotalUnapprovedMessagesCount, + selectNetworkConfigurationByChainId, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) accountsWithSendEtherInfoSelector, getSelectedAccount, getAccountType, ///: END:ONLY_INCLUDE_IF } from '../../../../selectors'; -import { - getProviderConfig, - isAddressLedger, -} from '../../../../ducks/metamask/metamask'; +import { isAddressLedger } from '../../../../ducks/metamask/metamask'; import { sanitizeMessage, ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) @@ -97,6 +95,7 @@ const SignatureRequest = ({ txData, warnings }) => { const { id, type, + chainId, msgParams: { from, data, origin, version }, } = txData; @@ -104,7 +103,12 @@ const SignatureRequest = ({ txData, warnings }) => { const hardwareWalletRequiresConnection = useSelector((state) => doesAddressRequireLedgerHidConnection(state, from), ); - const { chainId, rpcPrefs } = useSelector(getProviderConfig); + + const { blockExplorerUrls } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + + const blockExplorerUrl = blockExplorerUrls?.[0]; const unapprovedMessagesCount = useSelector(getTotalUnapprovedMessagesCount); const subjectMetadata = useSelector(getSubjectMetadata); const isLedgerWallet = useSelector((state) => isAddressLedger(state, from)); @@ -337,7 +341,7 @@ const SignatureRequest = ({ txData, warnings }) => { setShowContractDetails(false)} isContractRequestingSignature /> diff --git a/ui/pages/confirmations/components/signature-request/signature-request.stories.js b/ui/pages/confirmations/components/signature-request/signature-request.stories.js index b31032f8b1a7..cc5374e2becb 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.stories.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.stories.js @@ -1,21 +1,25 @@ import React from 'react'; import { Provider } from 'react-redux'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import configureStore from '../../../../store/store'; import testData from '../../../../../.storybook/test-data'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import README from './README.mdx'; import SignatureRequest from './signature-request'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + const store = configureStore({ ...testData, metamask: { ...testData.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), }, }); export default { title: 'Confirmations/Components/SignatureRequest', decorators: [(story) => {story()}], - component: SignatureRequest, parameters: { docs: { @@ -35,6 +39,7 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { txData: { + chainId: CHAIN_ID_MOCK, msgParams: { from: '0xb19ac54efa18cc3a14a5b821bfec73d284bf0c5e', data: JSON.stringify({ @@ -82,6 +87,7 @@ AccountMismatchStory.storyName = 'AccountMismatch'; AccountMismatchStory.args = { ...DefaultStory.args, txData: { + chainId: CHAIN_ID_MOCK, msgParams: { ...DefaultStory.args.txData.msgParams, from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4', diff --git a/ui/pages/confirmations/components/signature-request/signature-request.test.js b/ui/pages/confirmations/components/signature-request/signature-request.test.js index 9851cdbef454..2e10381c40cb 100644 --- a/ui/pages/confirmations/components/signature-request/signature-request.test.js +++ b/ui/pages/confirmations/components/signature-request/signature-request.test.js @@ -1,35 +1,25 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import configureMockStore from 'redux-mock-store'; import { EthAccountType } from '@metamask/keyring-api'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../../shared/constants/security-provider'; -import { - getNativeCurrency, - getProviderConfig, -} from '../../../../ducks/metamask/metamask'; -import { - accountsWithSendEtherInfoSelector, - conversionRateSelector, - getCurrentCurrency, - getMemoizedAddressBook, - getPreferences, - getSelectedAccount, - getTotalUnapprovedMessagesCount, - getInternalAccounts, - unconfirmedTransactionsHashSelector, - getAccountType, - getMemoizedMetaMaskInternalAccounts, - getSelectedInternalAccount, - pendingApprovalsSortedSelector, - getNetworkConfigurationsByChainId, -} from '../../../../selectors'; import { ETH_EOA_METHODS } from '../../../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../../../test/stub/networks'; import SignatureRequest from './signature-request'; +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => jest.fn(), +})); + +const CHAIN_ID_MOCK = '0x539'; + +const TRANSACTION_DATA_MOCK = { + chainId: CHAIN_ID_MOCK, +}; + const baseProps = { clearConfirmTransaction: () => jest.fn(), cancel: () => jest.fn(), @@ -37,8 +27,11 @@ const baseProps = { showRejectTransactionsConfirmationModal: () => jest.fn(), sign: () => jest.fn(), }; + const mockStore = { + ...mockState, metamask: { + ...mockState.metamask, ...mockNetworkState({ chainId: '0x539', nickname: 'Localhost 8545', @@ -61,12 +54,13 @@ const mockStore = { metadata: { name: 'John Doe', keyring: { - type: 'HD Key Tree', + type: 'Custody', }, }, options: {}, methods: ETH_EOA_METHODS, type: EthAccountType.Eoa, + balance: 0, }, }, selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', @@ -94,15 +88,6 @@ const mockStore = { }, }, }; -jest.mock('react-redux', () => { - const actual = jest.requireActual('react-redux'); - - return { - ...actual, - useSelector: jest.fn(), - useDispatch: () => jest.fn(), - }; -}); const mockCustodySignFn = jest.fn(); @@ -114,62 +99,13 @@ jest.mock('../../../../hooks/useMMICustodySignMessage', () => ({ jest.mock('@metamask-institutional/extension'); -const generateUseSelectorRouter = (opts) => (selector) => { - const mockSelectedInternalAccount = getSelectedInternalAccount(opts); - - switch (selector) { - case getProviderConfig: - return getProviderConfig(opts); - case getCurrentCurrency: - return opts.metamask.currentCurrency; - case getNativeCurrency: - return getProviderConfig(opts).ticker; - case getTotalUnapprovedMessagesCount: - return opts.metamask.unapprovedTypedMessagesCount; - case getPreferences: - return opts.metamask.preferences; - case conversionRateSelector: - return opts.metamask.currencyRates[getProviderConfig(opts).ticker] - ?.conversionRate; - case getSelectedAccount: - return mockSelectedInternalAccount; - case getInternalAccounts: - return Object.values(opts.metamask.internalAccounts.accounts); - case getMemoizedMetaMaskInternalAccounts: - return Object.values(opts.metamask.internalAccounts.accounts); - case getMemoizedAddressBook: - return []; - case accountsWithSendEtherInfoSelector: - return Object.values(opts.metamask.internalAccounts.accounts).map( - (internalAccount) => { - return { - ...internalAccount, - ...(opts.metamask.accounts[internalAccount.address] ?? {}), - balance: - opts.metamask.accounts[internalAccount.address]?.balance ?? 0, - }; - }, - ); - case getAccountType: - return 'custody'; - case unconfirmedTransactionsHashSelector: - return {}; - case pendingApprovalsSortedSelector: - return Object.values(opts.metamask.pendingApprovals); - case getNetworkConfigurationsByChainId: - return opts.metamask.networkConfigurationsByChainId; - default: - return undefined; - } -}; describe('Signature Request Component', () => { - const store = configureMockStore()(mockState); + const store = configureMockStore()(mockStore); describe('render', () => { let messageData; beforeEach(() => { - useSelector.mockImplementation(generateUseSelectorRouter(mockStore)); messageData = { domain: { chainId: 97, @@ -219,35 +155,39 @@ describe('Signature Request Component', () => { }); it('should match snapshot when we want to switch to fiat', () => { - useSelector.mockImplementation( - generateUseSelectorRouter({ - ...mockStore, - metamask: { - ...mockStore.metamask, - currencyRates: { - ...mockStore.metamask.currencyRates, - ETH: { - ...(mockStore.metamask.currencyRates.ETH || {}), - conversionRate: 231.06, - }, + const storeOverride = configureMockStore()({ + ...mockStore, + metamask: { + ...mockStore.metamask, + ...mockNetworkState({ + chainId: CHAIN_ID_MOCK, + }), + currencyRates: { + ...mockStore.metamask.currencyRates, + ETH: { + ...(mockStore.metamask.currencyRates.ETH || {}), + conversionRate: 231.06, }, }, - }), - ); + }, + }); + const msgParams = { from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', data: JSON.stringify(messageData), version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , - store, + storeOverride, ); expect(container).toMatchSnapshot(); @@ -260,10 +200,12 @@ describe('Signature Request Component', () => { version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , @@ -280,10 +222,12 @@ describe('Signature Request Component', () => { version: 'V4', origin: 'test', }; + const { queryByTestId } = renderWithProvider( , @@ -308,6 +252,7 @@ describe('Signature Request Component', () => { , @@ -321,29 +266,30 @@ describe('Signature Request Component', () => { }); it('should not render a reject multiple requests link if there is not multiple requests', () => { - useSelector.mockImplementation( - generateUseSelectorRouter({ - ...mockStore, - metamask: { - ...mockStore.metamask, - unapprovedTypedMessagesCount: 0, - }, - }), - ); + const storeOverride = configureMockStore()({ + ...mockStore, + metamask: { + ...mockStore.metamask, + unapprovedTypedMessagesCount: 0, + }, + }); + const msgParams = { from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', data: JSON.stringify(messageData), version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , - store, + storeOverride, ); expect( @@ -358,10 +304,12 @@ describe('Signature Request Component', () => { version: 'V4', origin: 'test', }; + const { container } = renderWithProvider( , @@ -384,6 +332,7 @@ describe('Signature Request Component', () => { , @@ -408,6 +357,7 @@ describe('Signature Request Component', () => { , @@ -429,6 +379,7 @@ describe('Signature Request Component', () => { { { }); it('should render a warning when the selected account is not the one being used to sign', () => { - const msgParams = { - from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', - data: JSON.stringify(messageData), - version: 'V4', - origin: 'test', - }; - - useSelector.mockImplementation( - generateUseSelectorRouter({ - ...mockStore, - metamask: { - ...mockStore.metamask, + const storeOverride = configureMockStore()({ + ...mockStore, + metamask: { + ...mockStore.metamask, + accounts: { + ...mockStore.metamask.accounts, + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + balance: '0x0', + name: 'Account 1', + }, + '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + balance: '0x0', + name: 'Account 2', + }, + }, + internalAccounts: { accounts: { - ...mockStore.metamask.accounts, - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1': { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - balance: '0x0', - name: 'Account 1', - }, - '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { - address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', - balance: '0x0', - name: 'Account 2', - }, - }, - internalAccounts: { - accounts: { - 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', - metadata: { - name: 'Account 1', - keyring: { - type: 'HD Key Tree', - }, + id: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', + metadata: { + name: 'Account 1', + keyring: { + type: 'HD Key Tree', }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, }, - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 2', - keyring: { - type: 'HD Key Tree', - }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, + }, + 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + metadata: { + name: 'Account 2', + keyring: { + type: 'HD Key Tree', }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, }, + options: {}, + methods: ETH_EOA_METHODS, + type: EthAccountType.Eoa, }, - selectedAccount: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', }, + selectedAccount: 'b7e813d6-e31c-4bad-8615-8d4eff9f44f1', }, - }), - ); + }, + }); + + const msgParams = { + from: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + data: JSON.stringify(messageData), + version: 'V4', + origin: 'test', + }; const { container } = renderWithProvider( , - store, + storeOverride, ); expect( @@ -571,6 +522,7 @@ describe('Signature Request Component', () => { {...baseProps} conversionRate={null} txData={{ + ...TRANSACTION_DATA_MOCK, msgParams, securityAlertResponse: { resultType: 'Malicious', @@ -602,6 +554,7 @@ describe('Signature Request Component', () => { , @@ -610,7 +563,9 @@ describe('Signature Request Component', () => { const rejectRequestsLink = getByTestId('page-container-footer-next'); fireEvent.click(rejectRequestsLink); - expect(mockCustodySignFn).toHaveBeenCalledWith({ msgParams }); + expect(mockCustodySignFn).toHaveBeenCalledWith( + expect.objectContaining({ msgParams }), + ); }); }); }); diff --git a/ui/pages/confirmations/confirm-signature-request/index.test.js b/ui/pages/confirmations/confirm-signature-request/index.test.js index e0c7c7c8bbeb..f634bf40845c 100644 --- a/ui/pages/confirmations/confirm-signature-request/index.test.js +++ b/ui/pages/confirmations/confirm-signature-request/index.test.js @@ -7,13 +7,21 @@ import { CHAIN_IDS } from '../../../../shared/constants/network'; import { mockNetworkState } from '../../../../test/stub/networks'; import ConfTx from '.'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + const mockState = { metamask: { + ...mockNetworkState({ + chainId: CHAIN_IDS.GOERLI, + nickname: 'Goerli test network', + ticker: undefined, + }), unapprovedPersonalMsgs: {}, unapprovedPersonalMsgCount: 0, unapprovedTypedMessages: { 267460284130106: { id: 267460284130106, + chainId: CHAIN_ID_MOCK, msgParams: { data: '{"domain":{"chainId":"5","name":"Ether Mail","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","version":"1"},"message":{"contents":"Hello, Bob!","from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}]},"primaryType":"Mail","types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Group":[{"name":"name","type":"string"},{"name":"members","type":"Person[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}]}}', from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', @@ -27,11 +35,6 @@ const mockState = { }, }, unapprovedTypedMessagesCount: 1, - ...mockNetworkState({ - chainId: CHAIN_IDS.GOERLI, - nickname: 'Goerli test network', - ticker: undefined, - }), currencyRates: {}, keyrings: [], subjectMetadata: {}, diff --git a/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts b/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts index 1b6412d66614..2f26fcefbee9 100644 --- a/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts +++ b/ui/pages/confirmations/hooks/alerts/useBlockaidAlerts.ts @@ -15,7 +15,6 @@ import { import { Alert } from '../../../../ducks/confirm-alerts/confirm-alerts'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { getCurrentChainId } from '../../../../selectors'; import { SIGNATURE_TRANSACTION_TYPES, REDESIGN_DEV_TRANSACTION_TYPES, @@ -51,7 +50,6 @@ type SecurityAlertResponsesState = { const useBlockaidAlerts = (): Alert[] => { const t = useI18nContext(); const { currentConfirmation } = useConfirmContext(); - const selectorChainId = useSelector(getCurrentChainId); const securityAlertId = ( currentConfirmation?.securityAlertResponse as SecurityAlertResponse @@ -99,9 +97,7 @@ const useBlockaidAlerts = (): Alert[] => { const reportData = { blockNumber: block, blockaidVersion: BlockaidPackage.version, - chain: (NETWORK_TO_NAME_MAP as Record)[ - chainId ?? selectorChainId - ], + chain: (NETWORK_TO_NAME_MAP as Record)[chainId], classification: isFailedResultType ? 'error' : reason, domain: origin ?? msgParams?.origin ?? origin, jsonRpcMethod: type, diff --git a/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts b/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts index 4c6dddfa7a5a..f0463287ef34 100644 --- a/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts +++ b/ui/pages/confirmations/hooks/useConfirmationNetworkInfo.ts @@ -6,30 +6,23 @@ import { NETWORK_TO_NAME_MAP, } from '../../../../shared/constants/network'; -import { - getCurrentChainId, - getNetworkConfigurationsByChainId, -} from '../../../selectors'; - import { useI18nContext } from '../../../hooks/useI18nContext'; import { useConfirmContext } from '../context/confirm'; +import { selectNetworkConfigurationByChainId } from '../../../selectors'; function useConfirmationNetworkInfo() { const t = useI18nContext(); const { currentConfirmation } = useConfirmContext(); - const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); - const currentChainId = useSelector(getCurrentChainId); + const chainId = currentConfirmation?.chainId as Hex; + + const networkConfiguration = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); let networkDisplayName = ''; let networkImageUrl = ''; if (currentConfirmation) { - // use the current confirmation chainId, else use the current network chainId - const chainId = - (currentConfirmation?.chainId as Hex | undefined) ?? currentChainId; - - const networkConfiguration = networkConfigurations[chainId]; - networkDisplayName = networkConfiguration?.name ?? NETWORK_TO_NAME_MAP[chainId as keyof typeof NETWORK_TO_NAME_MAP] ?? diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts index dff0103fbe21..9c98432918e2 100644 --- a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.test.ts @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useContext } from 'react'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { TokenStandard } from '../../../../shared/constants/transaction'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { TokenDetailsERC20 } from '../utils/token'; @@ -30,7 +31,7 @@ describe('useTrackERC20WithoutDecimalInformation', () => { }); renderHook(() => - useTrackERC20WithoutDecimalInformation('0x5', { + useTrackERC20WithoutDecimalInformation(CHAIN_IDS.MAINNET, '0x5', { standard: TokenStandard.ERC20, } as TokenDetailsERC20), ); diff --git a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts index fa6a5e620fc4..266048e11134 100644 --- a/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts +++ b/ui/pages/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts @@ -1,4 +1,3 @@ -import { useSelector } from 'react-redux'; import { useContext, useEffect } from 'react'; import { Hex } from '@metamask/utils'; @@ -10,23 +9,23 @@ import { } from '../../../../shared/constants/metametrics'; import { TokenStandard } from '../../../../shared/constants/transaction'; import { MetaMetricsContext } from '../../../contexts/metametrics'; -import { getCurrentChainId } from '../../../selectors'; import { parseTokenDetailDecimals, TokenDetailsERC20 } from '../utils/token'; /** * Track event that number of decimals in ERC20 is not obtained * + * @param chainId * @param tokenAddress * @param tokenDetails * @param metricLocation */ const useTrackERC20WithoutDecimalInformation = ( + chainId: Hex, tokenAddress: Hex | string | undefined, tokenDetails?: TokenDetailsERC20, metricLocation = MetaMetricsEventLocation.SignatureConfirmation, ) => { const trackEvent = useContext(MetaMetricsContext); - const chainId = useSelector(getCurrentChainId); useEffect(() => { if (chainId === undefined || tokenDetails === undefined) { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index bb7ef5f796d7..4ea9f20371ab 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -700,6 +700,28 @@ export const getNetworkConfigurationsByChainId = createDeepEqualSelector( (networkConfigurationsByChainId) => networkConfigurationsByChainId, ); +/** + * @type (state: any, chainId: string) => import('@metamask/network-controller').NetworkConfiguration + */ +export const selectNetworkConfigurationByChainId = createSelector( + getNetworkConfigurationsByChainId, + (_state, chainId) => chainId, + (networkConfigurationsByChainId, chainId) => + networkConfigurationsByChainId[chainId], +); + +export const selectDefaultRpcEndpointByChainId = createSelector( + selectNetworkConfigurationByChainId, + (networkConfiguration) => { + if (!networkConfiguration) { + return undefined; + } + + const { defaultRpcEndpointIndex, rpcEndpoints } = networkConfiguration; + return rpcEndpoints[defaultRpcEndpointIndex]; + }, +); + export function getRequestingNetworkInfo(state, chainIds) { // If chainIds is undefined, set it to an empty array let processedChainIds = chainIds === undefined ? [] : chainIds; From cdfaa42e0d4948a6c195d3e24d0caa9342cb35af Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:17:07 +0100 Subject: [PATCH 011/111] test: [POM] Migrate edit network rpc e2e tests and create related page class functions (#28161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Migrate `test/e2e/tests/network/multi-rpc.spec.ts` e2e tests to Page Object Model (POM) pattern. - Created edit network related page classes and functions - Removed unnecessary delay by correctly implementing page object functions - Objective of this PR is to improve test stability and maintainability, it also reduced flakiness. - update: it also fixes the flaky test Update Network [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28163 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --------- Co-authored-by: Harika <153644847+hjetpoluru@users.noreply.github.com> --- .../page-objects/pages/dialog/edit-network.ts | 54 ++++ .../pages/dialog/select-network.ts | 92 ++++-- test/e2e/page-objects/pages/homepage.ts | 86 ++++-- .../onboarding-privacy-settings-page.ts | 30 +- test/e2e/tests/network/multi-rpc.spec.ts | 283 ++++++------------ 5 files changed, 304 insertions(+), 241 deletions(-) create mode 100644 test/e2e/page-objects/pages/dialog/edit-network.ts diff --git a/test/e2e/page-objects/pages/dialog/edit-network.ts b/test/e2e/page-objects/pages/dialog/edit-network.ts new file mode 100644 index 000000000000..09a1dd70a5f9 --- /dev/null +++ b/test/e2e/page-objects/pages/dialog/edit-network.ts @@ -0,0 +1,54 @@ +import { Driver } from '../../../webdriver/driver'; + +class EditNetworkModal { + private driver: Driver; + + private readonly editModalNetworkNameInput = + '[data-testid="network-form-network-name"]'; + + private readonly editModalRpcDropDownButton = + '[data-testid="test-add-rpc-drop-down"]'; + + private readonly editModalSaveButton = { + text: 'Save', + tag: 'button', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForMultipleSelectors([ + this.editModalNetworkNameInput, + this.editModalRpcDropDownButton, + this.editModalSaveButton, + ]); + } catch (e) { + console.log( + 'Timeout while waiting for select network dialog to be loaded', + e, + ); + throw e; + } + console.log('Edit network dialog is loaded'); + } + + /** + * Selects an RPC from the dropdown in the edit network modal. + * + * @param rpcName - The name of the RPC to select. + */ + async selectRPCInEditNetworkModal(rpcName: string): Promise { + console.log(`Select RPC ${rpcName} in edit network modal`); + await this.driver.clickElement(this.editModalRpcDropDownButton); + await this.driver.clickElement({ + text: rpcName, + tag: 'button', + }); + await this.driver.clickElementAndWaitToDisappear(this.editModalSaveButton); + } +} + +export default EditNetworkModal; diff --git a/test/e2e/page-objects/pages/dialog/select-network.ts b/test/e2e/page-objects/pages/dialog/select-network.ts index 2c399a4118d8..bc20c42855ae 100644 --- a/test/e2e/page-objects/pages/dialog/select-network.ts +++ b/test/e2e/page-objects/pages/dialog/select-network.ts @@ -3,15 +3,15 @@ import { Driver } from '../../../webdriver/driver'; class SelectNetwork { private driver: Driver; - private networkName: string | undefined; - - private readonly addNetworkButton = { - tag: 'button', - text: 'Add a custom network', - }; + private readonly addNetworkButton = '[data-testid="test-add-button"]'; private readonly closeButton = 'button[aria-label="Close"]'; + private readonly editNetworkButton = + '[data-testid="network-list-item-options-edit"]'; + + private readonly rpcUrlItem = '.select-rpc-url__item'; + private readonly searchInput = '[data-testid="network-redesign-modal-search-input"]'; @@ -20,6 +20,11 @@ class SelectNetwork { tag: 'h4', }; + private readonly selectRpcMessage = { + text: 'Select RPC URL', + tag: 'h4', + }; + private readonly toggleButton = '.toggle-button > div'; constructor(driver: Driver) { @@ -42,15 +47,9 @@ class SelectNetwork { console.log('Select network dialog is loaded'); } - async selectNetworkName(networkName: string): Promise { - console.log(`Click ${networkName}`); - this.networkName = `[data-testid="${networkName}"]`; - await this.driver.clickElementAndWaitToDisappear(this.networkName); - } - - async addNewNetwork(): Promise { - console.log('Click Add network'); - await this.driver.clickElement(this.addNetworkButton); + async clickAddButton(): Promise { + console.log('Click Add Button'); + await this.driver.clickElementAndWaitToDisappear(this.addNetworkButton); } async clickCloseButton(): Promise { @@ -58,21 +57,68 @@ class SelectNetwork { await this.driver.clickElementAndWaitToDisappear(this.closeButton); } - async toggleShowTestNetwork(): Promise { - console.log('Toggle show test network in select network dialog'); - await this.driver.clickElement(this.toggleButton); - } - async fillNetworkSearchInput(networkName: string): Promise { console.log(`Fill network search input with ${networkName}`); await this.driver.fill(this.searchInput, networkName); } - async clickAddButton(): Promise { - console.log('Click Add Button'); + async openEditNetworkModal(): Promise { + console.log('Open edit network modal'); + await this.driver.clickElementAndWaitToDisappear(this.editNetworkButton); + } + + async openNetworkListOptions(chainId: string): Promise { + console.log(`Open network options for ${chainId} in network dialog`); + await this.driver.clickElement( + `[data-testid="network-list-item-options-button-${chainId}"]`, + ); + } + + async openNetworkRPC(chainId: string): Promise { + console.log(`Open network RPC ${chainId}`); await this.driver.clickElementAndWaitToDisappear( - '[data-testid="test-add-button"]', + `[data-testid="network-rpc-name-button-${chainId}"]`, + ); + await this.driver.waitForSelector(this.selectRpcMessage); + } + + async selectNetworkName(networkName: string): Promise { + console.log(`Click ${networkName}`); + const networkNameItem = `[data-testid="${networkName}"]`; + await this.driver.clickElementAndWaitToDisappear(networkNameItem); + } + + async selectRPC(rpcName: string): Promise { + console.log(`Select RPC ${rpcName} for network`); + await this.driver.waitForSelector(this.selectRpcMessage); + await this.driver.clickElementAndWaitToDisappear({ + text: rpcName, + tag: 'button', + }); + } + + async toggleShowTestNetwork(): Promise { + console.log('Toggle show test network in select network dialog'); + await this.driver.clickElement(this.toggleButton); + } + + async check_networkRPCNumber(expectedNumber: number): Promise { + console.log( + `Wait for ${expectedNumber} RPC URLs to be displayed in select network dialog`, ); + await this.driver.wait(async () => { + const rpcNumber = await this.driver.findElements(this.rpcUrlItem); + return rpcNumber.length === expectedNumber; + }, 10000); + console.log(`${expectedNumber} RPC URLs found in select network dialog`); + } + + async check_rpcIsSelected(rpcName: string): Promise { + console.log(`Check RPC ${rpcName} is selected in network dialog`); + await this.driver.waitForSelector({ + text: rpcName, + tag: 'button', + }); } } diff --git a/test/e2e/page-objects/pages/homepage.ts b/test/e2e/page-objects/pages/homepage.ts index 7c322b0f2cbb..101e6f9de83c 100644 --- a/test/e2e/page-objects/pages/homepage.ts +++ b/test/e2e/page-objects/pages/homepage.ts @@ -22,6 +22,11 @@ class HomePage { css: '.mm-banner-alert', }; + private readonly closeUseNetworkNotificationModalButton = { + text: 'Got it', + tag: 'h6', + }; + private readonly completedTransactions = '[data-testid="activity-list-item"]'; private readonly confirmedTransactions = { @@ -34,6 +39,8 @@ class HomePage { css: '.transaction-status-label--failed', }; + private readonly popoverBackground = '.popover-bg'; + private readonly sendButton = '[data-testid="eth-overview-send"]'; private readonly tokensTab = '[data-testid="account-overview__asset-tab"]'; @@ -60,8 +67,16 @@ class HomePage { console.log('Home page is loaded'); } - async startSendFlow(): Promise { - await this.driver.clickElement(this.sendButton); + async closeUseNetworkNotificationModal(): Promise { + // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#25788) + // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed + await this.driver.assertElementNotPresent(this.popoverBackground); + await this.driver.clickElementSafe( + this.closeUseNetworkNotificationModalButton, + ); + await this.driver.assertElementNotPresent( + this.closeUseNetworkNotificationModalButton, + ); } async goToActivityList(): Promise { @@ -69,13 +84,6 @@ class HomePage { await this.driver.clickElement(this.activityTab); } - async check_basicFunctionalityOffWarnigMessageIsDisplayed(): Promise { - console.log( - 'Check if basic functionality off warning message is displayed on homepage', - ); - await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); - } - async goToNFTList(): Promise { console.log(`Open NFT tab on homepage`); await this.driver.clickElement(this.nftTab); @@ -85,6 +93,10 @@ class HomePage { await this.driver.clickElement(this.nftIconOnActivityList); } + async startSendFlow(): Promise { + await this.driver.clickElement(this.sendButton); + } + /** * Checks if the toaster message for adding a network is displayed on the homepage. * @@ -100,6 +112,37 @@ class HomePage { }); } + async check_basicFunctionalityOffWarnigMessageIsDisplayed(): Promise { + console.log( + 'Check if basic functionality off warning message is displayed on homepage', + ); + await this.driver.waitForSelector(this.basicFunctionalityOffWarningMessage); + } + + /** + * This function checks the specified number of completed transactions are displayed in the activity list on the homepage. + * It waits up to 10 seconds for the expected number of completed transactions to be visible. + * + * @param expectedNumber - The number of completed transactions expected to be displayed in the activity list. Defaults to 1. + * @returns A promise that resolves if the expected number of completed transactions is displayed within the timeout period. + */ + async check_completedTxNumberDisplayedInActivity( + expectedNumber: number = 1, + ): Promise { + console.log( + `Wait for ${expectedNumber} completed transactions to be displayed in activity list`, + ); + await this.driver.wait(async () => { + const completedTxs = await this.driver.findElements( + this.completedTransactions, + ); + return completedTxs.length === expectedNumber; + }, 10000); + console.log( + `${expectedNumber} completed transactions found in activity list on homepage`, + ); + } + /** * This function checks if the specified number of confirmed transactions are displayed in the activity list on homepage. * It waits up to 10 seconds for the expected number of confirmed transactions to be visible. @@ -125,27 +168,20 @@ class HomePage { } /** - * This function checks the specified number of completed transactions are displayed in the activity list on the homepage. - * It waits up to 10 seconds for the expected number of completed transactions to be visible. + * Checks if the toaster message for editing a network is displayed on the homepage. * - * @param expectedNumber - The number of completed transactions expected to be displayed in the activity list. Defaults to 1. - * @returns A promise that resolves if the expected number of completed transactions is displayed within the timeout period. + * @param networkName - The name of the network that was edited. */ - async check_completedTxNumberDisplayedInActivity( - expectedNumber: number = 1, + async check_editNetworkMessageIsDisplayed( + networkName: string, ): Promise { console.log( - `Wait for ${expectedNumber} completed transactions to be displayed in activity list`, - ); - await this.driver.wait(async () => { - const completedTxs = await this.driver.findElements( - this.completedTransactions, - ); - return completedTxs.length === expectedNumber; - }, 10000); - console.log( - `${expectedNumber} completed transactions found in activity list on homepage`, + `Check the toaster message for editing network ${networkName} is displayed on homepage`, ); + await this.driver.waitForSelector({ + tag: 'h6', + text: `“${networkName}” was successfully edited!`, + }); } /** diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts index dac2ab447710..e8288edb98cb 100644 --- a/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts +++ b/test/e2e/page-objects/pages/onboarding/onboarding-privacy-settings-page.ts @@ -134,8 +134,7 @@ class OnboardingPrivacySettingsPage { this.confirmAddCustomNetworkButton, ); // Navigate back to default privacy settings - await this.driver.clickElement(this.categoryBackButton); - await this.driver.waitForElementToStopMoving(this.categoryBackButton); + await this.navigateBackToSettingsPage(); } /** @@ -152,6 +151,16 @@ class OnboardingPrivacySettingsPage { ); } + /** + * Navigate back to the onboarding privacy settings page. + */ + async navigateBackToSettingsPage(): Promise { + console.log('Navigate back to onboarding privacy settings page'); + // Wait until the onboarding carousel has stopped moving otherwise the click has no effect. + await this.driver.clickElement(this.categoryBackButton); + await this.driver.waitForElementToStopMoving(this.categoryBackButton); + } + async navigateToGeneralSettings(): Promise { console.log('Navigate to general settings'); await this.check_pageIsLoaded(); @@ -159,6 +168,17 @@ class OnboardingPrivacySettingsPage { await this.driver.waitForSelector(this.generalSettingsMessage); } + /** + * Open the edit network modal for a given network name. + * + * @param networkName - The name of the network to open the edit modal for. + */ + async openEditNetworkModal(networkName: string): Promise { + console.log(`Open edit network modal for ${networkName}`); + await this.driver.clickElement({ text: networkName, tag: 'p' }); + await this.driver.waitForSelector(this.addRpcUrlDropDown); + } + /** * Go to assets settings and toggle options, then navigate back. */ @@ -172,8 +192,7 @@ class OnboardingPrivacySettingsPage { await this.driver.findClickableElements(this.assetsPrivacyToggle) ).map((toggle) => toggle.click()), ); - await this.driver.clickElement(this.categoryBackButton); - await this.driver.waitForElementToStopMoving(this.categoryBackButton); + await this.navigateBackToSettingsPage(); } /** @@ -186,8 +205,7 @@ class OnboardingPrivacySettingsPage { await this.driver.waitForSelector(this.basicFunctionalityTurnOffMessage); await this.driver.clickElement(this.basicFunctionalityCheckbox); await this.driver.clickElement(this.basicFunctionalityTurnOffButton); - await this.driver.clickElement(this.categoryBackButton); - await this.driver.waitForElementToStopMoving(this.categoryBackButton); + await this.navigateBackToSettingsPage(); } } diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index 6fc7025f5dbc..362a4c3e29e4 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -1,20 +1,27 @@ import { strict as assert } from 'assert'; import { Suite } from 'mocha'; import FixtureBuilder from '../../fixture-builder'; -import { - defaultGanacheOptions, - importSRPOnboardingFlow, - regularDelayMs, - TEST_SEED_PHRASE, - unlockWallet, - withFixtures, -} from '../../helpers'; +import { defaultGanacheOptions, withFixtures } from '../../helpers'; import { Driver } from '../../webdriver/driver'; import { Mockttp } from '../../mock-e2e'; import { expectMockRequest, expectNoMockRequest, } from '../../helpers/mock-server'; +import EditNetworkModal from '../../page-objects/pages/dialog/edit-network'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import HomePage from '../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../page-objects/pages/onboarding/onboarding-complete-page'; +import OnboardingPrivacySettingsPage from '../../page-objects/pages/onboarding/onboarding-privacy-settings-page'; +import SelectNetwork from '../../page-objects/pages/dialog/select-network'; +import { + loginWithoutBalanceValidation, + loginWithBalanceValidation, +} from '../../page-objects/flows/login.flow'; +import { + completeImportSRPOnboardingFlow, + importSRPOnboardingFlow, +} from '../../page-objects/flows/onboarding.flow'; describe('MultiRpc:', function (this: Suite) { it('should migrate to multi rpc @no-mmi', async function () { @@ -73,36 +80,18 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver }: { driver: Driver }) => { - const password = 'password'; - - await driver.navigate(); - - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); + await completeImportSRPOnboardingFlow(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); - await driver.delay(regularDelayMs); - - // complete - await driver.clickElement('[data-testid="onboarding-complete-done"]'); - - // pin extension - await driver.clickElement('[data-testid="pin-extension-next"]'); - await driver.clickElement('[data-testid="pin-extension-done"]'); - - // pin extension walkthrough screen - await driver.findElement('[data-testid="account-menu-icon"]'); - - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - await driver.clickElement( - '[data-testid="network-rpc-name-button-0xa4b1"]', - ); - - const menuItems = await driver.findElements('.select-rpc-url__item'); + await new HeaderNavbar(driver).clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); // check rpc number - assert.equal(menuItems.length, 2); + await selectNetworkDialog.openNetworkRPC('0xa4b1'); + await selectNetworkDialog.check_networkRPCNumber(2); }, ); }); @@ -173,7 +162,9 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver, mockedEndpoint }) => { - await unlockWallet(driver); + await loginWithoutBalanceValidation(driver); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); const usedUrlBeforeSwitch = await mockedEndpoint[1].getSeenRequests(); @@ -189,28 +180,21 @@ describe('MultiRpc:', function (this: Suite) { // check that requests are sent on the background for the rpc https://responsive-rpc.test/ await expectNoMockRequest(driver, mockedEndpoint[0], { timeout: 3000 }); - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - // select second rpc - await driver.clickElement( - '[data-testid="network-rpc-name-button-0xa4b1"]', - ); - - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.openNetworkRPC('0xa4b1'); + await selectNetworkDialog.check_networkRPCNumber(2); - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); + // select second rpc for Arbitrum network in the network dialog + await selectNetworkDialog.selectRPC('Arbitrum mainnet 2'); + await homePage.check_pageIsLoaded(); + await headerNavbar.clickSwitchNetworkDropDown(); - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + // check that the second rpc is selected in the network dialog + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); const usedUrl = await mockedEndpoint[0].getSeenRequests(); // check the url first request send on the background to the mocked rpc after switch @@ -218,9 +202,6 @@ describe('MultiRpc:', function (this: Suite) { // check that requests are sent on the background for the url https://responsive-rpc.test/ await expectMockRequest(driver, mockedEndpoint[0], { timeout: 3000 }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); }, ); }); @@ -280,53 +261,33 @@ describe('MultiRpc:', function (this: Suite) { testSpecificMock: mockRPCURLAndChainId, }, - async ({ driver }: { driver: Driver }) => { - await unlockWallet(driver); - - // Avoid a stale element error - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - // Go to Edit Menu - await driver.clickElement( - '[data-testid="network-list-item-options-button-0xa4b1"]', - ); - await driver.clickElement( - '[data-testid="network-list-item-options-edit"]', + async ({ driver, ganacheServer }) => { + await loginWithBalanceValidation(driver, ganacheServer); + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + + // go to Edit Menu for Arbitrum network and select the second rpc + await selectNetworkDialog.openNetworkListOptions('0xa4b1'); + await selectNetworkDialog.openEditNetworkModal(); + + const editNetworkModal = new EditNetworkModal(driver); + await editNetworkModal.check_pageIsLoaded(); + await editNetworkModal.selectRPCInEditNetworkModal( + 'Arbitrum mainnet 2', ); - await driver.clickElement('[data-testid="test-add-rpc-drop-down"]'); - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); + // validate the network was successfully edited + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_editNetworkMessageIsDisplayed('Arbitrum One'); + await homePage.closeUseNetworkNotificationModal(); - await driver.clickElement({ - text: 'Save', - tag: 'button', - }); - - // Validate the network was edited - const networkEdited = await driver.isElementPresent({ - text: '“Arbitrum One” was successfully edited!', - }); - assert.equal( - networkEdited, - true, - '“Arbitrum One” was successfully edited!', - ); - - await driver.delay(regularDelayMs); - await driver.clickElement('[data-testid="network-display"]'); - - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); + // check that the second rpc is selected in the network dialog + await headerNavbar.clickSwitchNetworkDropDown(); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); }, ); }); @@ -387,93 +348,41 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver }: { driver: Driver }) => { - const password = 'password'; - - await driver.navigate(); - - await importSRPOnboardingFlow(driver, TEST_SEED_PHRASE, password); - - await driver.delay(regularDelayMs); - - // go to advanced settigns - await driver.clickElementAndWaitToDisappear({ - text: 'Manage default privacy settings', - }); - - await driver.clickElement({ - text: 'General', - }); - - // open edit modal - await driver.clickElement({ - text: 'arbitrum-mainnet.infura.io', - tag: 'p', - }); - - await driver.clickElement('[data-testid="test-add-rpc-drop-down"]'); - - await driver.delay(regularDelayMs); - await driver.clickElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - await driver.clickElementAndWaitToDisappear({ - text: 'Save', - tag: 'button', - }); - - await driver.clickElement('[data-testid="category-back-button"]'); - - await driver.clickElement( - '[data-testid="privacy-settings-back-button"]', + await importSRPOnboardingFlow(driver); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage( + driver, ); + await onboardingPrivacySettingsPage.check_pageIsLoaded(); + await onboardingPrivacySettingsPage.navigateToGeneralSettings(); - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - await driver.clickElement({ - text: 'Next', - tag: 'button', - }); - - await driver.clickElementAndWaitToDisappear({ - text: 'Done', - tag: 'button', - }); - - // Validate the network was edited - const networkEdited = await driver.isElementPresent({ - text: '“Arbitrum One” was successfully edited!', - }); - assert.equal( - networkEdited, - true, - '“Arbitrum One” was successfully edited!', + // open edit network modal during onboarding and select the second rpc + await onboardingPrivacySettingsPage.openEditNetworkModal( + 'Arbitrum One', ); - // Ensures popover backround doesn't kill test - await driver.assertElementNotPresent('.popover-bg'); - - // We need to use clickElementSafe + assertElementNotPresent as sometimes the network dialog doesn't appear, as per this issue (#27870) - // TODO: change the 2 actions for clickElementAndWaitToDisappear, once the issue is fixed - await driver.clickElementSafe({ tag: 'h6', text: 'Got it' }); - - await driver.assertElementNotPresent({ - tag: 'h6', - text: 'Got it', - }); - - await driver.clickElement('[data-testid="network-display"]'); - - const arbitrumRpcUsed = await driver.findElement({ - text: 'Arbitrum mainnet 2', - tag: 'button', - }); - - const existRpcUsed = arbitrumRpcUsed !== undefined; - assert.equal(existRpcUsed, true, 'Second Rpc is used'); + const editNetworkModal = new EditNetworkModal(driver); + await editNetworkModal.check_pageIsLoaded(); + await editNetworkModal.selectRPCInEditNetworkModal( + 'Arbitrum mainnet 2', + ); + await onboardingPrivacySettingsPage.navigateBackToSettingsPage(); + await onboardingPrivacySettingsPage.check_pageIsLoaded(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + + // finish onboarding and check the network successfully edited message is displayed + await onboardingCompletePage.completeOnboarding(); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_editNetworkMessageIsDisplayed('Arbitrum One'); + await homePage.closeUseNetworkNotificationModal(); + + // check that the second rpc is selected in the network dialog + await new HeaderNavbar(driver).clickSwitchNetworkDropDown(); + const selectNetworkDialog = new SelectNetwork(driver); + await selectNetworkDialog.check_pageIsLoaded(); + await selectNetworkDialog.check_rpcIsSelected('Arbitrum mainnet 2'); }, ); }); From 49e5e7861bb70571942e70c3f0c0ca29918d3e97 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 4 Nov 2024 10:39:12 +0000 Subject: [PATCH 012/111] fix: Prevent coercing symbols to zero in the edit spending cap modal (#28192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Since the `TextField` in the "edit spending cap" modal has a type `TextFieldType.Number`, it already blocks most symbols and letters. However, it does currently support `+`, `-` and `e` characters as they can be used to construe numbers. For example, when a `-` sign is introduced in the input field, the interim value is coerced to `''`, as there is no numerical equivalent to the sign by itself. The first part of this fix was to disable the "Save" button on such cases. If the user wants to revoke the spending cap, they can introduce `0`, but `''` is not a valid response. Furthermore, when a valid number is introduced but that uses scientific notation or `+`/`-` signs, the submission is disabled and a validation message is shown to the user: "Enter numbers only". [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28192?quickstart=1) ## **Related issues** Fixes: [#28096](https://github.com/MetaMask/metamask-extension/issues/28096) ## **Manual testing steps** 1. Deploy an erc20 token contract in the test DApp 2. Trigger an approve confirmation 3. Attempt to edit the spending cap with -1, 10e10, or any others. 4. You should be prevented from submitting and see the validation message. ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2024-10-30 at 17 46 48 ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- app/_locales/en/messages.json | 3 +++ .../edit-spending-cap-modal.tsx | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index cc077810750b..4b3c59b52297 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1824,6 +1824,9 @@ "editSpendingCapError": { "message": "The spending cap can’t exceed $1 decimal digits. Remove decimal digits to continue." }, + "editSpendingCapSpecialCharError": { + "message": "Enter numbers only" + }, "enable": { "message": "Enable" }, diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx index 2762e99652a5..f908333e4f25 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx @@ -124,6 +124,8 @@ export const EditSpendingCapModal = ({ decimals && parseInt(decimals, 10) < countDecimalDigits(customSpendingCapInputValue); + const showSpecialCharacterError = /[-+e]/u.test(customSpendingCapInputValue); + return ( )} + {showSpecialCharacterError && ( + + {t('editSpendingCapSpecialCharError')} + + )} From 7a8da64d2116074ce5837b28364513aa1211404c Mon Sep 17 00:00:00 2001 From: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:09:15 -0500 Subject: [PATCH 013/111] fix: Add different copy for tooltip when a snap is requesting a signature (#27492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? The tooltip content incorrectly says "site" when a snap is requesting a signature 2. What is the improvement/solution? Changing the copy to say "snap" for when a snap is making the signature request. ## **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 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/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --- app/_locales/en/messages.json | 3 + .../info/personal-sign/personal-sign.test.tsx | 96 +++++++++++++++++++ .../info/personal-sign/personal-sign.tsx | 12 ++- .../info/typed-sign-v1/typed-sign-v1.test.tsx | 62 ++++++++++++ .../info/typed-sign-v1/typed-sign-v1.tsx | 6 +- .../info/typed-sign/typed-sign.test.tsx | 62 ++++++++++++ .../confirm/info/typed-sign/typed-sign.tsx | 6 +- 7 files changed, 244 insertions(+), 3 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 4b3c59b52297..61171df6e922 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4438,6 +4438,9 @@ "requestFromInfo": { "message": "This is the site asking for your signature." }, + "requestFromInfoSnap": { + "message": "This is the Snap asking for your signature." + }, "requestFromTransactionDescription": { "message": "This is the site asking for your confirmation." }, diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx index 6fe06c81467a..e105493485ec 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.test.tsx @@ -10,6 +10,8 @@ import { } from '../../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; import { signatureRequestSIWE } from '../../../../../../../test/data/confirmations/personal_sign'; +import * as utils from '../../../../utils'; +import * as snapUtils from '../../../../../../helpers/utils/snaps'; import PersonalSignInfo from './personal-sign'; jest.mock( @@ -21,6 +23,29 @@ jest.mock( }), ); +jest.mock('../../../../utils', () => { + const originalUtils = jest.requireActual('../../../../utils'); + return { + ...originalUtils, + isSIWESignatureRequest: jest.fn().mockReturnValue(false), + }; +}); + +jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { + const originalUtils = jest.requireActual( + '../../../../../../../node_modules/@metamask/snaps-utils', + ); + return { + ...originalUtils, + stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), + getSnapPrefix: jest.fn().mockReturnValue('npm:'), + }; +}); + +jest.mock('../../../../../../helpers/utils/snaps', () => ({ + isSnapId: jest.fn(), +})); + describe('PersonalSignInfo', () => { it('renders correctly for personal sign request', () => { const state = getMockPersonalSignConfirmState(); @@ -62,6 +87,7 @@ describe('PersonalSignInfo', () => { }); it('display signing in from for SIWE request', () => { + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(true); const state = getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); const mockStore = configureMockStore([])(state); @@ -73,6 +99,7 @@ describe('PersonalSignInfo', () => { }); it('display simulation for SIWE request if preference useTransactionSimulations is enabled', () => { + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(true); const state = getMockPersonalSignConfirmStateForRequest( signatureRequestSIWE, { @@ -88,4 +115,73 @@ describe('PersonalSignInfo', () => { ); expect(getByText('Estimated changes')).toBeDefined(); }); + + it('does not display tooltip text when isSIWE is true', async () => { + const state = + getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); // isSIWE is true + + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(true); + const mockStore = configureMockStore([])(state); + const { queryByText, getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = getByText('Request from'); + await requestFromLabel.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + + expect( + queryByText('This is the site asking for your signature.'), + ).toBeNull(); + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeNull(); + }); + + it('displays "requestFromInfoSnap" tooltip when isSIWE is false and origin is a snap', async () => { + const state = + getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); + + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + + const mockStore = configureMockStore([])(state); + const { queryByText, getByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = getByText('Request from'); + await requestFromLabel.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeDefined(); + }); + + it('displays "requestFromInfo" tooltip when isSIWE is false and origin is not a snap', async () => { + const state = + getMockPersonalSignConfirmStateForRequest(signatureRequestSIWE); + (utils.isSIWESignatureRequest as jest.Mock).mockReturnValue(false); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + + const mockStore = configureMockStore([])(state); + const { getByText, queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = getByText('Request from'); + await requestFromLabel.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + + expect( + queryByText('This is the site asking for your signature.'), + ).toBeDefined(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx index 3199c3d108e0..5eb798439ca8 100644 --- a/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/personal-sign/personal-sign.tsx @@ -19,6 +19,7 @@ import { selectUseTransactionSimulations } from '../../../../selectors/preferenc import { isSIWESignatureRequest } from '../../../../utils'; import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row'; import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { SIWESignInfo } from './siwe-sign'; const PersonalSignInfo: React.FC = () => { @@ -39,6 +40,15 @@ const PersonalSignInfo: React.FC = () => { hexToText(currentConfirmation.msgParams?.data), ); + let toolTipMessage; + if (!isSIWE) { + if (isSnapId(currentConfirmation.msgParams.origin)) { + toolTipMessage = t('requestFromInfoSnap'); + } else { + toolTipMessage = t('requestFromInfo'); + } + } + return ( <> {isSIWE && useTransactionSimulations && ( @@ -56,7 +66,7 @@ const PersonalSignInfo: React.FC = () => { alertKey={RowAlertKey.RequestFrom} ownerId={currentConfirmation.id} label={t('requestFrom')} - tooltip={isSIWE ? undefined : t('requestFromInfo')} + tooltip={toolTipMessage} > diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx index c6801dd91314..2b1e6969ddd5 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.test.tsx @@ -5,6 +5,7 @@ import { TransactionType } from '@metamask/transaction-controller'; import { renderWithConfirmContextProvider } from '../../../../../../../test/lib/confirmations/render-helpers'; import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../test/data/confirmations/helper'; import { unapprovedTypedSignMsgV1 } from '../../../../../../../test/data/confirmations/typed_sign'; +import * as snapUtils from '../../../../../../helpers/utils/snaps'; import TypedSignInfoV1 from './typed-sign-v1'; jest.mock( @@ -16,6 +17,21 @@ jest.mock( }), ); +jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { + const originalUtils = jest.requireActual( + '../../../../../../../node_modules/@metamask/snaps-utils', + ); + return { + ...originalUtils, + stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), + getSnapPrefix: jest.fn().mockReturnValue('npm:'), + }; +}); + +jest.mock('../../../../../../helpers/utils/snaps', () => ({ + isSnapId: jest.fn(), +})); + describe('TypedSignInfo', () => { it('correctly renders typed sign data request', () => { const mockState = getMockTypedSignConfirmStateForRequest( @@ -42,4 +58,50 @@ describe('TypedSignInfo', () => { ); expect(container).toMatchInlineSnapshot(`
`); }); + + it('displays "requestFromInfoSnap" tooltip when origin is a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeDefined(); + }); + + it('displays "requestFromInfo" tooltip when origin is not a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the site asking for your signature.'), + ).toBeDefined(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx index b7bfdba16e8a..eb935d6bdef2 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/typed-sign-v1.tsx @@ -14,6 +14,7 @@ import { import { useConfirmContext } from '../../../../context/confirm'; import { ConfirmInfoRowTypedSignDataV1 } from '../../row/typed-sign-data-v1/typedSignDataV1'; import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; +import { isSnapId } from '../../../../../../helpers/utils/snaps'; const TypedSignV1Info: React.FC = () => { const t = useI18nContext(); @@ -23,6 +24,9 @@ const TypedSignV1Info: React.FC = () => { return null; } + const toolTipMessage = isSnapId(currentConfirmation.msgParams?.origin) + ? t('requestFromInfoSnap') + : t('requestFromInfo'); const chainId = currentConfirmation.chainId as string; return ( @@ -32,7 +36,7 @@ const TypedSignV1Info: React.FC = () => { alertKey={RowAlertKey.RequestFrom} ownerId={currentConfirmation.id} label={t('requestFrom')} - tooltip={t('requestFromInfo')} + tooltip={toolTipMessage} > { }; }); +jest.mock('../../../../../../../node_modules/@metamask/snaps-utils', () => { + const originalUtils = jest.requireActual( + '../../../../../../../node_modules/@metamask/snaps-utils', + ); + return { + ...originalUtils, + stripSnapPrefix: jest.fn().mockReturnValue('@metamask/examplesnap'), + getSnapPrefix: jest.fn().mockReturnValue('npm:'), + }; +}); + +jest.mock('../../../../../../helpers/utils/snaps', () => ({ + isSnapId: jest.fn(), +})); + describe('TypedSignInfo', () => { it('renders origin for typed sign data request', () => { const state = getMockTypedSignConfirmState(); @@ -127,4 +143,50 @@ describe('TypedSignInfo', () => { ); expect(container).toMatchSnapshot(); }); + + it('displays "requestFromInfoSnap" tooltip when origin is a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(true); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the Snap asking for your signature.'), + ).toBeDefined(); + }); + + it('displays "requestFromInfo" tooltip when origin is not a snap', async () => { + const mockState = getMockTypedSignConfirmStateForRequest({ + id: '123', + type: TransactionType.signTypedData, + chainId: '0x5', + }); + (snapUtils.isSnapId as jest.Mock).mockReturnValue(false); + const mockStore = configureMockStore([])(mockState); + const { queryByText } = renderWithConfirmContextProvider( + , + mockStore, + ); + + const requestFromLabel = queryByText('Request from'); + + await requestFromLabel?.dispatchEvent( + new MouseEvent('mouseenter', { bubbles: true }), + ); + expect( + queryByText('This is the site asking for your signature.'), + ).toBeDefined(); + }); }); diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx index c1830b05bd4a..fa5e61caef1f 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx @@ -22,6 +22,7 @@ import { fetchErc20Decimals } from '../../../../utils/token'; import { useConfirmContext } from '../../../../context/confirm'; import { selectUseTransactionSimulations } from '../../../../selectors/preferences'; import { ConfirmInfoRowTypedSignData } from '../../row/typed-sign-data/typedSignData'; +import { isSnapId } from '../../../../../../helpers/utils/snaps'; import { PermitSimulation } from './permit-simulation'; const TypedSignInfo: React.FC = () => { @@ -55,6 +56,9 @@ const TypedSignInfo: React.FC = () => { })(); }, [verifyingContract]); + const toolTipMessage = isSnapId(currentConfirmation.msgParams.origin) + ? t('requestFromInfoSnap') + : t('requestFromInfo'); const msgData = currentConfirmation.msgParams?.data as string; return ( @@ -73,7 +77,7 @@ const TypedSignInfo: React.FC = () => { alertKey={RowAlertKey.RequestFrom} ownerId={currentConfirmation.id} label={t('requestFrom')} - tooltip={t('requestFromInfo')} + tooltip={toolTipMessage} > From 77b77a83801bb6c621e0acd4e35b8d0b2e8a938e Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:43:59 -0500 Subject: [PATCH 014/111] refactor: move `getInternalAccounts` from `selectors.js` to `accounts.ts` (#27645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interactive-replacement-token-page.tsx | 2 ++ .../notifications-settings/notifications-settings.tsx | 2 +- ui/selectors/accounts.test.ts | 9 +++++++++ ui/selectors/accounts.ts | 5 ++++- ui/selectors/selectors.js | 6 +----- ui/selectors/selectors.test.js | 8 -------- ui/selectors/snaps/accounts.ts | 3 ++- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx index c12bb0aedf57..01ec70c93025 100644 --- a/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx +++ b/ui/pages/institutional/interactive-replacement-token-page/interactive-replacement-token-page.tsx @@ -154,6 +154,7 @@ const InteractiveReplacementTokenPage: React.FC = () => { const filteredAccounts = custodianAccounts.filter( (account: TokenAccount) => + // @ts-expect-error metaMaskAccounts isn't a real type metaMaskAccounts[account.address.toLowerCase()], ); @@ -163,6 +164,7 @@ const InteractiveReplacementTokenPage: React.FC = () => { name: account.name, labels: account.labels, balance: + // @ts-expect-error metaMaskAccounts isn't a real type metaMaskAccounts[account.address.toLowerCase()]?.balance || 0, }), ); diff --git a/ui/pages/notifications-settings/notifications-settings.tsx b/ui/pages/notifications-settings/notifications-settings.tsx index d929f048a793..0fafb468b733 100644 --- a/ui/pages/notifications-settings/notifications-settings.tsx +++ b/ui/pages/notifications-settings/notifications-settings.tsx @@ -57,7 +57,7 @@ export default function NotificationsSettings() { const isUpdatingMetamaskNotifications = useSelector( getIsUpdatingMetamaskNotifications, ); - const accounts: AccountType[] = useSelector(getInternalAccounts); + const accounts = useSelector(getInternalAccounts) as AccountType[]; // States const [loadingAllowNotifications, setLoadingAllowNotifications] = diff --git a/ui/selectors/accounts.test.ts b/ui/selectors/accounts.test.ts index 639da0185b72..033d88c30faa 100644 --- a/ui/selectors/accounts.test.ts +++ b/ui/selectors/accounts.test.ts @@ -15,6 +15,7 @@ import { hasCreatedBtcMainnetAccount, hasCreatedBtcTestnetAccount, getSelectedInternalAccount, + getInternalAccounts, } from './accounts'; const MOCK_STATE: AccountsState = { @@ -27,6 +28,14 @@ const MOCK_STATE: AccountsState = { }; describe('Accounts Selectors', () => { + describe('#getInternalAccounts', () => { + it('returns a list of internal accounts', () => { + expect(getInternalAccounts(mockState as AccountsState)).toStrictEqual( + Object.values(mockState.metamask.internalAccounts.accounts), + ); + }); + }); + describe('#getSelectedInternalAccount', () => { it('returns selected internalAccount', () => { expect( diff --git a/ui/selectors/accounts.ts b/ui/selectors/accounts.ts index d69cd130f9aa..af977b7511da 100644 --- a/ui/selectors/accounts.ts +++ b/ui/selectors/accounts.ts @@ -8,7 +8,6 @@ import { isBtcMainnetAddress, isBtcTestnetAddress, } from '../../shared/lib/multichain'; -import { getInternalAccounts } from './selectors'; export type AccountsState = { metamask: AccountsControllerState; @@ -20,6 +19,10 @@ function isBtcAccount(account: InternalAccount) { return Boolean(account && account.type === P2wpkh); } +export function getInternalAccounts(state: AccountsState) { + return Object.values(state.metamask.internalAccounts.accounts); +} + export function getSelectedInternalAccount(state: AccountsState) { const accountId = state.metamask.internalAccounts.selectedAccount; return state.metamask.internalAccounts.accounts[accountId]; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 4ea9f20371ab..a548a9f33dcc 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -117,7 +117,7 @@ import { getOrderedConnectedAccountsForConnectedDapp, getSubjectMetadata, } from './permissions'; -import { getSelectedInternalAccount } from './accounts'; +import { getSelectedInternalAccount, getInternalAccounts } from './accounts'; import { createDeepEqualSelector } from './util'; import { getMultichainBalances, getMultichainNetwork } from './multichain'; @@ -371,10 +371,6 @@ export function getSelectedInternalAccountWithBalance(state) { return selectedAccountWithBalance; } -export function getInternalAccounts(state) { - return Object.values(state.metamask.internalAccounts.accounts); -} - export function getInternalAccount(state, accountId) { return state.metamask.internalAccounts.accounts[accountId]; } diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 459864c1e1f3..d6656e481709 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -113,14 +113,6 @@ describe('Selectors', () => { }); }); - describe('#getInternalAccounts', () => { - it('returns a list of internal accounts', () => { - expect(selectors.getInternalAccounts(mockState)).toStrictEqual( - Object.values(mockState.metamask.internalAccounts.accounts), - ); - }); - }); - describe('#getInternalAccount', () => { it("returns undefined if the account doesn't exist", () => { expect( diff --git a/ui/selectors/snaps/accounts.ts b/ui/selectors/snaps/accounts.ts index b47f33726429..55a30f0c72eb 100644 --- a/ui/selectors/snaps/accounts.ts +++ b/ui/selectors/snaps/accounts.ts @@ -1,6 +1,7 @@ import { createSelector } from 'reselect'; import { AccountsControllerState } from '@metamask/accounts-controller'; -import { getAccountName, getInternalAccounts } from '../selectors'; +import { getAccountName } from '../selectors'; +import { getInternalAccounts } from '../accounts'; import { createDeepEqualSelector } from '../util'; /** From eab6233bff0e5bbc013248e2eb725a788572b1ba Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Mon, 4 Nov 2024 07:52:20 -0800 Subject: [PATCH 015/111] feat: multi chain polling for token prices (#28158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Multi chain polling for the token rates controller. This will fetch erc20 token prices across all evm chains. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28158?quickstart=1) ## **Related issues** ## **Manual testing steps** no visual changes, you should just see the network tab hitting price api across multiple chains, correct prices when switching chains, when adding new tokens, and `marketData` updated in state across chains ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --------- Co-authored-by: MetaMask Bot Co-authored-by: sahar-fehri --- ...s-controllers-npm-42.0.0-57b3d695bb.patch} | 0 app/scripts/metamask-controller.js | 28 +++++---- package.json | 2 +- ui/contexts/assetPolling.tsx | 13 +++++ ui/contexts/currencyRate.js | 13 ----- ui/hooks/useMultiPolling.ts | 57 +++++++++++++++++++ ui/hooks/useTokenRatesPolling.ts | 40 +++++++++++++ ui/pages/index.js | 6 +- ui/selectors/selectors.js | 16 ++++++ ui/store/actions.ts | 31 ++++++++++ yarn.lock | 18 +++--- 11 files changed, 183 insertions(+), 41 deletions(-) rename .yarn/patches/{@metamask-assets-controllers-npm-41.0.0-57b3d695bb.patch => @metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch} (100%) create mode 100644 ui/contexts/assetPolling.tsx delete mode 100644 ui/contexts/currencyRate.js create mode 100644 ui/hooks/useMultiPolling.ts create mode 100644 ui/hooks/useTokenRatesPolling.ts diff --git a/.yarn/patches/@metamask-assets-controllers-npm-41.0.0-57b3d695bb.patch b/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch similarity index 100% rename from .yarn/patches/@metamask-assets-controllers-npm-41.0.0-57b3d695bb.patch rename to .yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 009f87634caa..b43ef72cae5c 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1007,6 +1007,7 @@ export default class MetamaskController extends EventEmitter { state: initState.TokenRatesController, messenger: tokenRatesMessenger, tokenPricesService: new CodefiTokenPricesServiceV2(), + disabled: !this.preferencesController.state.useCurrencyRateCheck, }); this.controllerMessenger.subscribe( @@ -1015,9 +1016,9 @@ export default class MetamaskController extends EventEmitter { const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState; const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState; if (currUseCurrencyRateCheck && !prevUseCurrencyRateCheck) { - this.tokenRatesController.start(); + this.tokenRatesController.enable(); } else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) { - this.tokenRatesController.stop(); + this.tokenRatesController.disable(); } }, this.preferencesController.state), ); @@ -2590,12 +2591,6 @@ export default class MetamaskController extends EventEmitter { const preferencesControllerState = this.preferencesController.state; - const { useCurrencyRateCheck } = preferencesControllerState; - - if (useCurrencyRateCheck) { - this.tokenRatesController.start(); - } - if (this.#isTokenListPollingRequired(preferencesControllerState)) { this.tokenListController.start(); } @@ -2608,12 +2603,6 @@ export default class MetamaskController extends EventEmitter { const preferencesControllerState = this.preferencesController.state; - const { useCurrencyRateCheck } = preferencesControllerState; - - if (useCurrencyRateCheck) { - this.tokenRatesController.stop(); - } - if (this.#isTokenListPollingRequired(preferencesControllerState)) { this.tokenListController.stop(); } @@ -3250,6 +3239,7 @@ export default class MetamaskController extends EventEmitter { backup, approvalController, phishingController, + tokenRatesController, // Notification Controllers authenticationController, userStorageController, @@ -4016,6 +4006,13 @@ export default class MetamaskController extends EventEmitter { currencyRateController, ), + tokenRatesStartPolling: + tokenRatesController.startPolling.bind(tokenRatesController), + tokenRatesStopPollingByPollingToken: + tokenRatesController.stopPollingByPollingToken.bind( + tokenRatesController, + ), + // GasFeeController gasFeeStartPollingByNetworkClientId: gasFeeController.startPollingByNetworkClientId.bind(gasFeeController), @@ -6641,12 +6638,13 @@ export default class MetamaskController extends EventEmitter { /** * A method that is called by the background when all instances of metamask are closed. - * Currently used to stop polling in the gasFeeController. + * Currently used to stop controller polling. */ onClientClosed() { try { this.gasFeeController.stopAllPolling(); this.currencyRateController.stopAllPolling(); + this.tokenRatesController.stopAllPolling(); this.appStateController.clearPollingTokens(); } catch (error) { console.error(error); diff --git a/package.json b/package.json index 4b30ab0948c8..cad47b45c8f3 100644 --- a/package.json +++ b/package.json @@ -286,7 +286,7 @@ "@metamask/address-book-controller": "^6.0.0", "@metamask/announcement-controller": "^7.0.0", "@metamask/approval-controller": "^7.0.0", - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A41.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-41.0.0-57b3d695bb.patch", + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch", "@metamask/base-controller": "^7.0.0", "@metamask/bitcoin-wallet-snap": "^0.8.2", "@metamask/browser-passworder": "^4.3.0", diff --git a/ui/contexts/assetPolling.tsx b/ui/contexts/assetPolling.tsx new file mode 100644 index 000000000000..63cef9667fbd --- /dev/null +++ b/ui/contexts/assetPolling.tsx @@ -0,0 +1,13 @@ +import React, { ReactNode } from 'react'; +import useCurrencyRatePolling from '../hooks/useCurrencyRatePolling'; +import useTokenRatesPolling from '../hooks/useTokenRatesPolling'; + +// This provider is a step towards making controller polling fully UI based. +// Eventually, individual UI components will call the use*Polling hooks to +// poll and return particular data. This polls globally in the meantime. +export const AssetPollingProvider = ({ children }: { children: ReactNode }) => { + useCurrencyRatePolling(); + useTokenRatesPolling(); + + return <>{children}; +}; diff --git a/ui/contexts/currencyRate.js b/ui/contexts/currencyRate.js deleted file mode 100644 index 6739b730a882..000000000000 --- a/ui/contexts/currencyRate.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import useCurrencyRatePolling from '../hooks/useCurrencyRatePolling'; - -export const CurrencyRateProvider = ({ children }) => { - useCurrencyRatePolling(); - - return <>{children}; -}; - -CurrencyRateProvider.propTypes = { - children: PropTypes.node, -}; diff --git a/ui/hooks/useMultiPolling.ts b/ui/hooks/useMultiPolling.ts new file mode 100644 index 000000000000..f0b3ed33cdfc --- /dev/null +++ b/ui/hooks/useMultiPolling.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; + +type UseMultiPollingOptions = { + startPolling: (input: PollingInput) => Promise; + stopPollingByPollingToken: (pollingToken: string) => void; + input: PollingInput[]; +}; + +// A hook that manages multiple polling loops of a polling controller. +// Callers provide an array of inputs, and the hook manages starting +// and stopping polling loops for each input. +const useMultiPolling = ( + usePollingOptions: UseMultiPollingOptions, +) => { + const [polls, setPolls] = useState(new Map()); + + useEffect(() => { + // start new polls + for (const input of usePollingOptions.input) { + const key = JSON.stringify(input); + if (!polls.has(key)) { + usePollingOptions + .startPolling(input) + .then((token) => + setPolls((prevPolls) => new Map(prevPolls).set(key, token)), + ); + } + } + + // stop existing polls + for (const [inputKey, token] of polls.entries()) { + const exists = usePollingOptions.input.some( + (i) => inputKey === JSON.stringify(i), + ); + + if (!exists) { + usePollingOptions.stopPollingByPollingToken(token); + setPolls((prevPolls) => { + const newPolls = new Map(prevPolls); + newPolls.delete(inputKey); + return newPolls; + }); + } + } + }, [usePollingOptions.input && JSON.stringify(usePollingOptions.input)]); + + // stop all polling on dismount + useEffect(() => { + return () => { + for (const token of polls.values()) { + usePollingOptions.stopPollingByPollingToken(token); + } + }; + }, []); +}; + +export default useMultiPolling; diff --git a/ui/hooks/useTokenRatesPolling.ts b/ui/hooks/useTokenRatesPolling.ts new file mode 100644 index 000000000000..41c1c8793b97 --- /dev/null +++ b/ui/hooks/useTokenRatesPolling.ts @@ -0,0 +1,40 @@ +import { useSelector } from 'react-redux'; +import { + getMarketData, + getNetworkConfigurationsByChainId, + getTokenExchangeRates, + getTokensMarketData, + getUseCurrencyRateCheck, +} from '../selectors'; +import { + tokenRatesStartPolling, + tokenRatesStopPollingByPollingToken, +} from '../store/actions'; +import useMultiPolling from './useMultiPolling'; + +const useTokenRatesPolling = ({ chainIds }: { chainIds?: string[] } = {}) => { + // Selectors to determine polling input + const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); + const networkConfigurations = useSelector(getNetworkConfigurationsByChainId); + + // Selectors returning state updated by the polling + const tokenExchangeRates = useSelector(getTokenExchangeRates); + const tokensMarketData = useSelector(getTokensMarketData); + const marketData = useSelector(getMarketData); + + useMultiPolling({ + startPolling: tokenRatesStartPolling, + stopPollingByPollingToken: tokenRatesStopPollingByPollingToken, + input: useCurrencyRateCheck + ? chainIds ?? Object.keys(networkConfigurations) + : [], + }); + + return { + tokenExchangeRates, + tokensMarketData, + marketData, + }; +}; + +export default useTokenRatesPolling; diff --git a/ui/pages/index.js b/ui/pages/index.js index 0b1cdcef78cd..c30846fff1e6 100644 --- a/ui/pages/index.js +++ b/ui/pages/index.js @@ -10,7 +10,7 @@ import { LegacyMetaMetricsProvider, } from '../contexts/metametrics'; import { MetamaskNotificationsProvider } from '../contexts/metamask-notifications'; -import { CurrencyRateProvider } from '../contexts/currencyRate'; +import { AssetPollingProvider } from '../contexts/assetPolling'; import ErrorPage from './error'; import Routes from './routes'; @@ -49,11 +49,11 @@ class Index extends PureComponent { - + - + diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index a548a9f33dcc..70e970c4c9b3 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -578,11 +578,27 @@ export const getTokenExchangeRates = (state) => { ); }; +/** + * Get market data for tokens on the current chain + * + * @param state + * @returns {Record} + */ export const getTokensMarketData = (state) => { const chainId = getCurrentChainId(state); return state.metamask.marketData?.[chainId]; }; +/** + * Get market data for tokens across all chains + * + * @param state + * @returns {Record>} + */ +export const getMarketData = (state) => { + return state.metamask.marketData; +}; + export function getAddressBook(state) { const chainId = getCurrentChainId(state); if (!state.metamask.addressBook[chainId]) { diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 77189e9683af..886739d2d54f 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4555,6 +4555,37 @@ export async function currencyRateStopPollingByPollingToken( await removePollingTokenFromAppState(pollingToken); } +/** + * Informs the TokenRatesController that the UI requires + * token rate polling for the given chain id. + * + * @param chainId - The chain id to poll token rates on. + * @returns polling token that can be used to stop polling + */ +export async function tokenRatesStartPolling(chainId: string): Promise { + const pollingToken = await submitRequestToBackground( + 'tokenRatesStartPolling', + [{ chainId }], + ); + await addPollingTokenToAppState(pollingToken); + return pollingToken; +} + +/** + * Informs the TokenRatesController that the UI no longer + * requires token rate polling for the given chain id. + * + * @param pollingToken - + */ +export async function tokenRatesStopPollingByPollingToken( + pollingToken: string, +) { + await submitRequestToBackground('tokenRatesStopPollingByPollingToken', [ + pollingToken, + ]); + await removePollingTokenFromAppState(pollingToken); +} + /** * Informs the GasFeeController that the UI requires gas fee polling * diff --git a/yarn.lock b/yarn.lock index 7890bcaa7b3f..a1f4dfe4ea4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4772,9 +4772,9 @@ __metadata: languageName: node linkType: hard -"@metamask/assets-controllers@npm:41.0.0": - version: 41.0.0 - resolution: "@metamask/assets-controllers@npm:41.0.0" +"@metamask/assets-controllers@npm:42.0.0": + version: 42.0.0 + resolution: "@metamask/assets-controllers@npm:42.0.0" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4806,13 +4806,13 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/63f1a9605d692217889511ca161ee614d8e12d7f7233773afb34c4fb6323fad1c29b3a4ee920ef6f84e4b165ffb8764dfd105bdc9bad75084f52a7c876faa4f5 + checksum: 10/64d2bd43139ee5c19bd665b07212cd5d5dd41b457dedde3b5db31442292c4d064dc015011f5f001bb423683675fb20898ff652e91d2339ad1d21cc45fa93487a languageName: node linkType: hard -"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A41.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-41.0.0-57b3d695bb.patch": - version: 41.0.0 - resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A41.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-41.0.0-57b3d695bb.patch::version=41.0.0&hash=e14ff8" +"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch": + version: 42.0.0 + resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch::version=42.0.0&hash=e14ff8" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/address": "npm:^5.7.0" @@ -4844,7 +4844,7 @@ __metadata: "@metamask/keyring-controller": ^17.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/preferences-controller": ^13.0.0 - checksum: 10/f7d609be61f4e952abd78d996a44131941f1fcd476066d007bed5047d1c887d38e9e9cf117eeb963148674fd9ad6ae87c8384bc8a21d4281628aaab1b60ce7a8 + checksum: 10/9a6727b28f88fd2df3f4b1628dd5d8c2f3e73fd4b9cd090f22d175c2522faa6c6b7e9a93d0ec2b2d123a263c8f4116fbfe97f196b99401b28ac8597f522651eb languageName: node linkType: hard @@ -26397,7 +26397,7 @@ __metadata: "@metamask/announcement-controller": "npm:^7.0.0" "@metamask/api-specs": "npm:^0.9.3" "@metamask/approval-controller": "npm:^7.0.0" - "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A41.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-41.0.0-57b3d695bb.patch" + "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A42.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-42.0.0-57b3d695bb.patch" "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^7.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.8.2" From 08d1854986101fce1b8442a4fe00481ef443b74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B3nio=20Regadas?= Date: Mon, 4 Nov 2024 17:14:53 +0000 Subject: [PATCH 016/111] feat: adds the experimental toggle for Solana (#28190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR sdds a new experimental toggle for Solana accounts. Everything is code fenced for Flask build. ![Screenshot 2024-10-31 at 11 50 38](https://github.com/user-attachments/assets/8e6a290c-b22d-42a7-8602-ac61410ebdb4) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- app/_locales/en/messages.json | 9 ++++ .../preferences-controller.test.ts | 15 ++++++ .../controllers/preferences-controller.ts | 20 ++++++++ app/scripts/metamask-controller.js | 4 ++ shared/constants/metametrics.ts | 1 + .../experimental-tab.component.tsx | 47 +++++++++++++++++++ .../experimental-tab.container.ts | 12 +++++ .../experimental-tab/experimental-tab.test.js | 19 +++++++- ui/selectors/selectors.js | 12 +++++ ui/store/actions.ts | 10 ++++ 10 files changed, 148 insertions(+), 1 deletion(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 61171df6e922..08675837b34d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5224,6 +5224,15 @@ "message": "Contact the creators of $1 for further support.", "description": "This is shown when the insight snap throws an error. $1 is the snap name" }, + "solanaSupportSectionTitle": { + "message": "Solana" + }, + "solanaSupportToggleDescription": { + "message": "Turning on this feature will give you the option to add a Solana Account to your MetaMask Extension derived from your existing Secret Recovery Phrase. This is an experimental Beta feature, so you should use it at your own risk." + }, + "solanaSupportToggleTitle": { + "message": "Enable \"Add a new Solana account (Beta)\"" + }, "somethingDoesntLookRight": { "message": "Something doesn't look right? $1", "description": "A false positive message for users to contact support. $1 is a link to the support page." diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 74daf39e17ad..a4b91a8d3b1a 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -853,4 +853,19 @@ describe('preferences controller', () => { ); }); }); + + describe('setSolanaSupportEnabled', () => { + const { controller } = setupController({}); + it('has the default value as false', () => { + expect(controller.state.solanaSupportEnabled).toStrictEqual(false); + }); + + it('sets the solanaSupportEnabled property in state to true and then false', () => { + controller.setSolanaSupportEnabled(true); + expect(controller.state.solanaSupportEnabled).toStrictEqual(true); + + controller.setSolanaSupportEnabled(false); + expect(controller.state.solanaSupportEnabled).toStrictEqual(false); + }); + }); }); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index f6537952d651..e1cdb2e8a4f6 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -140,6 +140,7 @@ export type PreferencesControllerState = Omit< useRequestQueue: boolean; ///: BEGIN:ONLY_INCLUDE_IF(build-flask) watchEthereumAccountEnabled: boolean; + solanaSupportEnabled: boolean; ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; bitcoinTestnetSupportEnabled: boolean; @@ -184,6 +185,9 @@ export const getDefaultPreferencesControllerState = openSeaEnabled: true, securityAlertsEnabled: true, watchEthereumAccountEnabled: false, + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + solanaSupportEnabled: false, + ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: false, bitcoinTestnetSupportEnabled: false, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -340,6 +344,10 @@ const controllerMetadata = { persist: true, anonymous: false, }, + solanaSupportEnabled: { + persist: true, + anonymous: false, + }, bitcoinSupportEnabled: { persist: true, anonymous: false, @@ -669,6 +677,18 @@ export class PreferencesController extends BaseController< state.watchEthereumAccountEnabled = watchEthereumAccountEnabled; }); } + + /** + * Setter for the `solanaSupportEnabled` property. + * + * @param solanaSupportEnabled - Whether or not the user wants to + * enable the "Add a new Solana account" button. + */ + setSolanaSupportEnabled(solanaSupportEnabled: boolean): void { + this.update((state) => { + state.solanaSupportEnabled = solanaSupportEnabled; + }); + } ///: END:ONLY_INCLUDE_IF /** diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b43ef72cae5c..a85233ca5119 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3315,6 +3315,10 @@ export default class MetamaskController extends EventEmitter { preferencesController.setWatchEthereumAccountEnabled.bind( preferencesController, ), + setSolanaSupportEnabled: + preferencesController.setSolanaSupportEnabled.bind( + preferencesController, + ), ///: END:ONLY_INCLUDE_IF setBitcoinSupportEnabled: preferencesController.setBitcoinSupportEnabled.bind( diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 2d6a87b7d1c6..4a5c21ab5752 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -607,6 +607,7 @@ export enum MetaMetricsEventName { BridgeLinkClicked = 'Bridge Link Clicked', BitcoinSupportToggled = 'Bitcoin Support Toggled', BitcoinTestnetSupportToggled = 'Bitcoin Testnet Support Toggled', + SolanaSupportToggled = 'Solana Support Toggled', CurrentCurrency = 'Current Currency', DappViewed = 'Dapp Viewed', DecryptionApproved = 'Decryption Approved', diff --git a/ui/pages/settings/experimental-tab/experimental-tab.component.tsx b/ui/pages/settings/experimental-tab/experimental-tab.component.tsx index f1d1f610a7a4..0771428474d5 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.component.tsx +++ b/ui/pages/settings/experimental-tab/experimental-tab.component.tsx @@ -36,6 +36,10 @@ import { SurveyUrl } from '../../../../shared/constants/urls'; type ExperimentalTabProps = { watchAccountEnabled: boolean; setWatchAccountEnabled: (value: boolean) => void; + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + solanaSupportEnabled: boolean; + setSolanaSupportEnabled: (value: boolean) => void; + ///: END:ONLY_INCLUDE_IF bitcoinSupportEnabled: boolean; setBitcoinSupportEnabled: (value: boolean) => void; bitcoinTestnetSupportEnabled: boolean; @@ -369,6 +373,44 @@ export default class ExperimentalTab extends PureComponent ); } + + renderSolanaSupport() { + const { t, trackEvent } = this.context; + const { solanaSupportEnabled, setSolanaSupportEnabled } = this.props; + + return ( + <> + + {t('solanaSupportSectionTitle')} + + {this.renderToggleSection({ + title: t('solanaSupportToggleTitle'), + description: t('solanaSupportToggleDescription'), + toggleValue: solanaSupportEnabled, + toggleCallback: (value) => { + trackEvent({ + event: MetaMetricsEventName.SolanaSupportToggled, + category: MetaMetricsEventCategory.Settings, + properties: { + enabled: !value, + }, + }); + setSolanaSupportEnabled(!value); + }, + toggleContainerDataTestId: 'solana-support-toggle-div', + toggleDataTestId: 'solana-support-toggle', + toggleOffLabel: t('off'), + toggleOnLabel: t('on'), + })} + + ); + } ///: END:ONLY_INCLUDE_IF render() { @@ -398,6 +440,11 @@ export default class ExperimentalTab extends PureComponent this.renderBitcoinSupport() ///: END:ONLY_INCLUDE_IF } + { + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + this.renderSolanaSupport() + ///: END:ONLY_INCLUDE_IF + }
); } diff --git a/ui/pages/settings/experimental-tab/experimental-tab.container.ts b/ui/pages/settings/experimental-tab/experimental-tab.container.ts index f4d408565bfd..e3be762afd1c 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.container.ts +++ b/ui/pages/settings/experimental-tab/experimental-tab.container.ts @@ -13,8 +13,14 @@ import { setRedesignedConfirmationsEnabled, setRedesignedTransactionsEnabled, setWatchEthereumAccountEnabled, + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + setSolanaSupportEnabled, + ///: END:ONLY_INCLUDE_IF } from '../../../store/actions'; import { + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + getIsSolanaSupportEnabled, + ///: END:ONLY_INCLUDE_IF getIsBitcoinSupportEnabled, getIsBitcoinTestnetSupportEnabled, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) @@ -37,6 +43,9 @@ const mapStateToProps = (state: MetaMaskReduxState) => { const petnamesEnabled = getPetnamesEnabled(state); const featureNotificationsEnabled = getFeatureNotificationsEnabled(state); return { + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + solanaSupportEnabled: getIsSolanaSupportEnabled(state), + ///: END:ONLY_INCLUDE_IF watchAccountEnabled: getIsWatchEthereumAccountEnabled(state), bitcoinSupportEnabled: getIsBitcoinSupportEnabled(state), bitcoinTestnetSupportEnabled: getIsBitcoinTestnetSupportEnabled(state), @@ -55,6 +64,9 @@ const mapDispatchToProps = (dispatch: MetaMaskReduxDispatch) => { return { setWatchAccountEnabled: (value: boolean) => setWatchEthereumAccountEnabled(value), + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + setSolanaSupportEnabled: (value: boolean) => setSolanaSupportEnabled(value), + ///: END:ONLY_INCLUDE_IF setBitcoinSupportEnabled: (value: boolean) => setBitcoinSupportEnabled(value), setBitcoinTestnetSupportEnabled: (value: boolean) => diff --git a/ui/pages/settings/experimental-tab/experimental-tab.test.js b/ui/pages/settings/experimental-tab/experimental-tab.test.js index 784b2dc3b5d3..08d02bf94215 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.test.js +++ b/ui/pages/settings/experimental-tab/experimental-tab.test.js @@ -30,7 +30,7 @@ describe('ExperimentalTab', () => { const { getAllByRole } = render(); const toggle = getAllByRole('checkbox'); - expect(toggle).toHaveLength(8); + expect(toggle).toHaveLength(9); }); it('enables add account snap', async () => { @@ -108,4 +108,21 @@ describe('ExperimentalTab', () => { expect(setBitcoinSupportEnabled).toHaveBeenNthCalledWith(1, true); }); }); + + it('enables the experimental solana account feature', async () => { + const setSolanaSupportEnabled = jest.fn(); + const { getByTestId } = render( + {}, + { + setSolanaSupportEnabled, + solanaSupportEnabled: false, + }, + ); + const toggle = getByTestId('solana-support-toggle'); + + fireEvent.click(toggle); + await waitFor(() => { + expect(setSolanaSupportEnabled).toHaveBeenNthCalledWith(1, true); + }); + }); }); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 70e970c4c9b3..8b44dd715aba 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2364,6 +2364,18 @@ export function getIsBitcoinSupportEnabled(state) { return state.metamask.bitcoinSupportEnabled; } +///: BEGIN:ONLY_INCLUDE_IF(build-flask) +/** + * Get the state of the `solanaSupportEnabled` flag. + * + * @param {*} state + * @returns The state of the `solanaSupportEnabled` flag. + */ +export function getIsSolanaSupportEnabled(state) { + return state.metamask.solanaSupportEnabled; +} +///: END:ONLY_INCLUDE_IF + /** * Get the state of the `bitcoinTestnetSupportEnabled` flag. * diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 886739d2d54f..11df6a3f3e20 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -5002,6 +5002,16 @@ export async function setBitcoinTestnetSupportEnabled(value: boolean) { } } +///: BEGIN:ONLY_INCLUDE_IF(build-flask) +export async function setSolanaSupportEnabled(value: boolean) { + try { + await submitRequestToBackground('setSolanaSupportEnabled', [value]); + } catch (error) { + logErrorWithMessage(error); + } +} +///: END:ONLY_INCLUDE_IF + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) export async function setAddSnapAccountEnabled(value: boolean): Promise { try { From 63ea629b7892a32f8d3d8b943927610e2270a7cb Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 4 Nov 2024 11:24:11 -0600 Subject: [PATCH 017/111] fix: Fix alignment of long RPC labels in Networks menu (#28244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes alignment for long RPC labels in the networks list [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28244?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Add BNB Chain to MetaMask 2. Create a second RPC for it with the label `BNB Smart Chain (previously Binance Smart Chain Mainnet)` 3. See the label left-aligned ## **Screenshots/Recordings** ### **Before** ![image (2)](https://github.com/user-attachments/assets/eeb1a3e4-257b-42db-9130-abc79928553c) ### **After** SCR-20241104-iwaw ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- ui/components/multichain/network-list-item/index.scss | 4 ++++ .../multichain/network-list-item/network-list-item.tsx | 1 + 2 files changed, 5 insertions(+) diff --git a/ui/components/multichain/network-list-item/index.scss b/ui/components/multichain/network-list-item/index.scss index 6597487c9cf6..cfbe80f2ce48 100644 --- a/ui/components/multichain/network-list-item/index.scss +++ b/ui/components/multichain/network-list-item/index.scss @@ -37,4 +37,8 @@ &__delete { visibility: hidden; } + + &__rpc-endpoint { + max-width: 100%; + } } diff --git a/ui/components/multichain/network-list-item/network-list-item.tsx b/ui/components/multichain/network-list-item/network-list-item.tsx index f80572eb1e4a..204ad7a5861f 100644 --- a/ui/components/multichain/network-list-item/network-list-item.tsx +++ b/ui/components/multichain/network-list-item/network-list-item.tsx @@ -190,6 +190,7 @@ export const NetworkListItem = ({ as="button" variant={TextVariant.bodySmMedium} color={TextColor.textAlternative} + ellipsis > {rpcEndpoint.name ?? new URL(rpcEndpoint.url).host}
From 9d7798515fdafd0593a023aa64dbda4b2d33427a Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Mon, 4 Nov 2024 09:25:50 -0800 Subject: [PATCH 018/111] chore: use accounts api for token detection (#28254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Enables the flag to use accounts api for token detection [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28254?quickstart=1) ## **Related issues** ## **Manual testing steps** Onboard a new wallet and verify erc20 tokens are detected. In the background worker, a network request should hit accounts.api.cx.metamask.io ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- app/scripts/metamask-controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a85233ca5119..ac72ba85dd84 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1782,6 +1782,8 @@ export default class MetamaskController extends EventEmitter { trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), + useAccountsAPI: true, + platform: 'extension', }); const addressBookControllerMessenger = From 18e81ba8913831e2c3c10c2398f9ffcf6f26f40c Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Mon, 4 Nov 2024 17:29:49 +0000 Subject: [PATCH 019/111] fix: notification settings type (#28271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This fixes a poor type from converting a selectors file from JS to TS. Instead of using a custom type for the accounts type, we can just use inferred types. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28271?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/pull/27645#discussion_r1827945420 ## **Manual testing steps** N/A these are only type level changes ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- .../notifications-settings.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/ui/pages/notifications-settings/notifications-settings.tsx b/ui/pages/notifications-settings/notifications-settings.tsx index 0fafb468b733..7606fed99614 100644 --- a/ui/pages/notifications-settings/notifications-settings.tsx +++ b/ui/pages/notifications-settings/notifications-settings.tsx @@ -1,7 +1,6 @@ import React, { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; -import type { InternalAccount } from '@metamask/keyring-api'; import { useI18nContext } from '../../hooks/useI18nContext'; import { NOTIFICATIONS_ROUTE } from '../../helpers/constants/routes'; import { @@ -33,18 +32,6 @@ import { NotificationsSettingsAllowNotifications } from './notifications-setting import { NotificationsSettingsTypes } from './notifications-settings-types'; import { NotificationsSettingsPerAccount } from './notifications-settings-per-account'; -// Define KeyringType interface -type KeyringType = { - type: string; -}; - -// Define AccountType interface -type AccountType = InternalAccount & { - balance: string; - keyring: KeyringType; - label: string; -}; - export default function NotificationsSettings() { const history = useHistory(); const location = useLocation(); @@ -57,7 +44,7 @@ export default function NotificationsSettings() { const isUpdatingMetamaskNotifications = useSelector( getIsUpdatingMetamaskNotifications, ); - const accounts = useSelector(getInternalAccounts) as AccountType[]; + const accounts = useSelector(getInternalAccounts); // States const [loadingAllowNotifications, setLoadingAllowNotifications] = From aaee4e7ba905c2e8e8d383cdef22c12446ec7840 Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Mon, 4 Nov 2024 18:36:37 +0100 Subject: [PATCH 020/111] fix: ignore error when getTokenStandardAndDetails fails (#28030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes Sentry issue when we are not able to determine token contract standard. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28030?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/17287 ## **Manual testing steps** (I was not able to repro with my NFTs but to simulate, we can go to the core fct `getTokenStandardAndDetails` and add `throw new Error('Unable to determine contract standard');` at the start of the fct; 1. Go NFT page 2. Click on any NFT 3. Click Send 4. You should be able to send the NFT ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/2ef39b0e-106a-48a2-a150-82176adaf14c ### **After** https://github.com/user-attachments/assets/f0714310-410f-4f51-b7e2-6f0bc4011027 ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --------- Co-authored-by: David Walsh --- ui/ducks/send/send.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 512b621c38f4..30cbc6eeb5dd 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -2615,7 +2615,12 @@ export function updateSendAsset( let missingProperty = STANDARD_TO_REQUIRED_PROPERTIES[ providedDetails.standard - ]?.find((property) => providedDetails[property] === undefined); + ]?.find((property) => { + if (providedDetails.collection && property === 'symbol') { + return providedDetails.collection[property] === undefined; + } + return providedDetails[property] === undefined; + }); let details; @@ -2652,10 +2657,9 @@ export function updateSendAsset( providedDetails.address, sendingAddress, providedDetails.tokenId, - ).catch((error) => { + ).catch(() => { // prevent infinite stuck loading state dispatch(hideLoadingIndication()); - throw error; })), }; } From 9fb69f40168c78c8fa4205e8c3e68737af5916ac Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:37:03 +0700 Subject: [PATCH 021/111] fix: Permit message, dataTree value incorrectly using default ERC20 decimals for non-ERC20 token values (#28142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Removes bug where fetchErc20Decimals was used in `ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx` info component and passed to the message dataTree to be used. fetchErc20Decimals was incorrectly used as it cannot be assumed that the token contract is an ERC20 standard. Fixed by calling useGetTokenStandardAndDetails instead which will use the default 18 decimals digit if the standard is an ERC20 token with no decimals found in the details. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28142?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28118 ## **Manual testing steps** 1. Go to test-dapp 2. Test Permit button 3. Observe both the simulation and message value display "3,000" (contract is not an ERC20 standard so it does not apply default 18 decimals) ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- .../confirm/info/typed-sign/typed-sign.tsx | 20 ++++++------------- .../__snapshots__/confirm.test.tsx.snap | 12 +++++------ .../hooks/useGetTokenStandardAndDetails.ts | 6 +++++- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx index fa5e61caef1f..f14107c1fd8a 100644 --- a/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx +++ b/ui/pages/confirmations/components/confirm/info/typed-sign/typed-sign.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import { isValidAddress } from 'ethereumjs-util'; @@ -14,11 +14,11 @@ import { import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section'; import { useI18nContext } from '../../../../../../hooks/useI18nContext'; import { SignatureRequestType } from '../../../../types/confirm'; +import { useGetTokenStandardAndDetails } from '../../../../hooks/useGetTokenStandardAndDetails'; import { isOrderSignatureRequest, isPermitSignatureRequest, } from '../../../../utils'; -import { fetchErc20Decimals } from '../../../../utils/token'; import { useConfirmContext } from '../../../../context/confirm'; import { selectUseTransactionSimulations } from '../../../../selectors/preferences'; import { ConfirmInfoRowTypedSignData } from '../../row/typed-sign-data/typedSignData'; @@ -31,7 +31,6 @@ const TypedSignInfo: React.FC = () => { const useTransactionSimulations = useSelector( selectUseTransactionSimulations, ); - const [decimals, setDecimals] = useState(0); if (!currentConfirmation?.msgParams) { return null; @@ -44,17 +43,10 @@ const TypedSignInfo: React.FC = () => { const isPermit = isPermitSignatureRequest(currentConfirmation); const isOrder = isOrderSignatureRequest(currentConfirmation); - const chainId = currentConfirmation.chainId as string; + const tokenContract = isPermit || isOrder ? verifyingContract : undefined; + const { decimalsNumber } = useGetTokenStandardAndDetails(tokenContract); - useEffect(() => { - (async () => { - if (!isPermit && !isOrder) { - return; - } - const tokenDecimals = await fetchErc20Decimals(verifyingContract); - setDecimals(tokenDecimals); - })(); - }, [verifyingContract]); + const chainId = currentConfirmation.chainId as string; const toolTipMessage = isSnapId(currentConfirmation.msgParams.origin) ? t('requestFromInfoSnap') @@ -99,7 +91,7 @@ const TypedSignInfo: React.FC = () => { > diff --git a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap index e1e7633ac055..acc76e3c1bb3 100644 --- a/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap +++ b/ui/pages/confirmations/confirm/__snapshots__/confirm.test.tsx.snap @@ -349,7 +349,7 @@ exports[`Confirm should match snapshot for signature - typed sign - V4 - PermitB >
{ + if (!tokenAddress) { + return { decimalsNumber: undefined }; + } + const { value: details } = useAsyncResult( async () => (await memoizedGetTokenStandardAndDetails( From 7b8d2c6e35f75d98c427a1f1156d6ecf338619dd Mon Sep 17 00:00:00 2001 From: micaelae <100321200+micaelae@users.noreply.github.com> Date: Tue, 5 Nov 2024 06:20:33 -0800 Subject: [PATCH 022/111] chore: display bridge quotes (#28031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Changes: - display recommended quote in BridgeQuoteCard - add a placeholder modal for displaying alternative quotes [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28031?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMS-1448 ## **Manual testing steps** 1. Request bridge quotes 2. Click "Switch" button and verify that new quotes with updated params are requested/shown 3. Verify that bridge parameters are reset when extension is reopened ## **Screenshots/Recordings** ### **Before** N/A ### **After** https://github.com/user-attachments/assets/acb927af-b04e-46cb-9b93-e2cdeb219722 ## **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 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/develop/.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. --- app/_locales/en/messages.json | 28 ++ test/jest/mock-store.js | 3 + ui/ducks/bridge/selectors.test.ts | 2 +- ui/ducks/bridge/selectors.ts | 37 +- ui/hooks/bridge/useCountdownTimer.test.ts | 34 ++ ui/hooks/bridge/useCountdownTimer.ts | 37 ++ .../bridge/__snapshots__/index.test.tsx.snap | 2 +- ui/pages/bridge/index.scss | 2 + .../prepare-bridge-page.test.tsx.snap | 8 +- .../bridge/prepare/bridge-cta-button.test.tsx | 112 +++++- ui/pages/bridge/prepare/bridge-cta-button.tsx | 18 +- .../bridge/prepare/bridge-input-group.tsx | 16 +- ui/pages/bridge/prepare/index.scss | 52 +-- .../prepare/prepare-bridge-page.test.tsx | 11 +- .../bridge/prepare/prepare-bridge-page.tsx | 15 +- .../bridge-quote-card.test.tsx.snap | 357 ++++++++++++++++++ .../bridge-quotes-modal.test.tsx.snap | 139 +++++++ .../bridge/quotes/bridge-quote-card.test.tsx | 102 +++++ ui/pages/bridge/quotes/bridge-quote-card.tsx | 85 +++++ .../quotes/bridge-quotes-modal.test.tsx | 35 ++ .../bridge/quotes/bridge-quotes-modal.tsx | 65 ++++ ui/pages/bridge/quotes/index.scss | 83 ++++ ui/pages/bridge/quotes/quote-info-row.tsx | 51 +++ ui/pages/bridge/utils/quote.ts | 28 +- ui/pages/routes/utils.js | 11 + 25 files changed, 1291 insertions(+), 42 deletions(-) create mode 100644 ui/hooks/bridge/useCountdownTimer.test.ts create mode 100644 ui/hooks/bridge/useCountdownTimer.ts create mode 100644 ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap create mode 100644 ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap create mode 100644 ui/pages/bridge/quotes/bridge-quote-card.test.tsx create mode 100644 ui/pages/bridge/quotes/bridge-quote-card.tsx create mode 100644 ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx create mode 100644 ui/pages/bridge/quotes/bridge-quotes-modal.tsx create mode 100644 ui/pages/bridge/quotes/index.scss create mode 100644 ui/pages/bridge/quotes/quote-info-row.tsx diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 08675837b34d..409965f07ab9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -844,18 +844,40 @@ "bridge": { "message": "Bridge" }, + "bridgeCalculatingAmount": { + "message": "Calculating..." + }, "bridgeDontSend": { "message": "Bridge, don't send" }, + "bridgeEnterAmount": { + "message": "Enter amount" + }, "bridgeFrom": { "message": "Bridge from" }, + "bridgeOverallCost": { + "message": "Overall cost" + }, "bridgeSelectNetwork": { "message": "Select network" }, + "bridgeSelectTokenAndAmount": { + "message": "Select token and amount" + }, + "bridgeTimingMinutes": { + "message": "$1 minutes", + "description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase" + }, + "bridgeTimingTooltipText": { + "message": "This is the estimated time it will take for the bridging to be complete." + }, "bridgeTo": { "message": "Bridge to" }, + "bridgeTotalFeesTooltipText": { + "message": "This includes gas fees (paid to crypto miners) and relayer fees (paid to power complex services like bridging).\nFees are based on network traffic and transaction complexity. MetaMask does not profit from either fee." + }, "browserNotSupported": { "message": "Your browser is not supported..." }, @@ -1965,6 +1987,9 @@ "estimatedFeeTooltip": { "message": "Amount paid to process the transaction on network." }, + "estimatedTime": { + "message": "Estimated time" + }, "ethGasPriceFetchWarning": { "message": "Backup gas price is provided as the main gas estimation service is unavailable right now." }, @@ -6110,6 +6135,9 @@ "total": { "message": "Total" }, + "totalFees": { + "message": "Total fees" + }, "totalVolume": { "message": "Total volume" }, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 55ffa7f9ba1b..3fe524634075 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -3,6 +3,7 @@ import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../shared/constants/network'; import { KeyringType } from '../../shared/constants/keyring'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { mockNetworkState } from '../stub/networks'; +import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../app/scripts/controllers/bridge/constants'; export const createGetSmartTransactionFeesApiResponse = () => { return { @@ -726,6 +727,8 @@ export const createBridgeMockStore = ( destNetworkAllowlist: [], ...featureFlagOverrides, }, + quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, + quoteRequest: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest, ...bridgeStateOverrides, }, }, diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 6be67515e6e4..dbb49a20d66b 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -395,7 +395,7 @@ describe('Bridge selectors', () => { const state = createBridgeMockStore(); const result = getToAmount(state as never); - expect(result).toStrictEqual('0'); + expect(result).toStrictEqual(undefined); }); }); diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 568d62e7a2d4..a05640ded5c2 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -3,6 +3,7 @@ import { NetworkState, } from '@metamask/network-controller'; import { uniqBy } from 'lodash'; +import { createSelector } from 'reselect'; import { getNetworkConfigurationsByChainId, getIsBridgeEnabled, @@ -19,6 +20,10 @@ import { import { createDeepEqualSelector } from '../../selectors/util'; import { getProviderConfig } from '../metamask/metamask'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; +import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { BridgeState } from './bridge'; type BridgeAppState = { @@ -124,10 +129,38 @@ export const getToToken = ( export const getFromAmount = (state: BridgeAppState): string | null => state.bridge.fromTokenInputValue; -export const getToAmount = (_state: BridgeAppState) => { - return '0'; + +export const getBridgeQuotes = (state: BridgeAppState) => { + return { + quotes: state.metamask.bridgeState.quotes, + quotesLastFetchedMs: state.metamask.bridgeState.quotesLastFetched, + isLoading: + state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING, + }; }; +export const getRecommendedQuote = createSelector( + getBridgeQuotes, + ({ quotes }) => { + // TODO implement sorting + return quotes[0]; + }, +); + +export const getQuoteRequest = (state: BridgeAppState) => { + const { quoteRequest } = state.metamask.bridgeState; + return quoteRequest; +}; + +export const getToAmount = createSelector(getRecommendedQuote, (quote) => + quote + ? calcTokenAmount( + quote.quote.destTokenAmount, + quote.quote.destAsset.decimals, + ) + : undefined, +); + export const getIsBridgeTx = createDeepEqualSelector( getFromChain, getToChain, diff --git a/ui/hooks/bridge/useCountdownTimer.test.ts b/ui/hooks/bridge/useCountdownTimer.test.ts new file mode 100644 index 000000000000..14ac21d725fd --- /dev/null +++ b/ui/hooks/bridge/useCountdownTimer.test.ts @@ -0,0 +1,34 @@ +import { renderHookWithProvider } from '../../../test/lib/render-helpers'; +import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { flushPromises } from '../../../test/lib/timer-helpers'; +import { useCountdownTimer } from './useCountdownTimer'; + +jest.useFakeTimers(); +const renderUseCountdownTimer = (mockStoreState: object) => + renderHookWithProvider(() => useCountdownTimer(), mockStoreState); + +describe('useCountdownTimer', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + it('returns time remaining', async () => { + const quotesLastFetched = Date.now(); + const { result } = renderUseCountdownTimer( + createBridgeMockStore({}, {}, { quotesLastFetched }), + ); + + let i = 0; + while (i <= 30) { + const secondsLeft = Math.min(30, 30 - i + 1); + expect(result.current).toStrictEqual( + `0:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`, + ); + i += 10; + jest.advanceTimersByTime(10000); + await flushPromises(); + } + expect(result.current).toStrictEqual('0:00'); + }); +}); diff --git a/ui/hooks/bridge/useCountdownTimer.ts b/ui/hooks/bridge/useCountdownTimer.ts new file mode 100644 index 000000000000..112d35b33f6a --- /dev/null +++ b/ui/hooks/bridge/useCountdownTimer.ts @@ -0,0 +1,37 @@ +import { Duration } from 'luxon'; +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { getBridgeQuotes } from '../../ducks/bridge/selectors'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { REFRESH_INTERVAL_MS } from '../../../app/scripts/controllers/bridge/constants'; +import { SECOND } from '../../../shared/constants/time'; + +/** + * Custom hook that provides a countdown timer based on the last fetched quotes timestamp. + * + * This hook calculates the remaining time until the next refresh interval and updates every second. + * + * @returns The formatted remaining time in 'm:ss' format. + */ +export const useCountdownTimer = () => { + const [timeRemaining, setTimeRemaining] = useState(REFRESH_INTERVAL_MS); + const { quotesLastFetchedMs } = useSelector(getBridgeQuotes); + + useEffect(() => { + if (quotesLastFetchedMs) { + setTimeRemaining( + REFRESH_INTERVAL_MS - (Date.now() - quotesLastFetchedMs), + ); + } + }, [quotesLastFetchedMs]); + + useEffect(() => { + const interval = setInterval(() => { + setTimeRemaining(Math.max(0, timeRemaining - SECOND)); + }, SECOND); + return () => clearInterval(interval); + }, [timeRemaining]); + + return Duration.fromMillis(timeRemaining).toFormat('m:ss'); +}; diff --git a/ui/pages/bridge/__snapshots__/index.test.tsx.snap b/ui/pages/bridge/__snapshots__/index.test.tsx.snap index cebca14e93bb..b9a6a4c83797 100644 --- a/ui/pages/bridge/__snapshots__/index.test.tsx.snap +++ b/ui/pages/bridge/__snapshots__/index.test.tsx.snap @@ -61,7 +61,7 @@ exports[`Bridge renders the component with initial props 1`] = ` data-theme="light" disabled="" > - Select token + Select token and amount
diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss index 98a3a3ee5c34..bc96dfbbe825 100644 --- a/ui/pages/bridge/index.scss +++ b/ui/pages/bridge/index.scss @@ -1,6 +1,8 @@ @use "design-system"; @import 'prepare/index'; +@import 'quotes/index'; + .bridge { max-height: 100vh; diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap index 4284c1893d7c..26e25b8bd4cd 100644 --- a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap +++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap @@ -9,7 +9,7 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] = class="mm-box prepare-bridge-page__content" >
{ @@ -25,6 +29,52 @@ describe('BridgeCTAButton', () => { expect(getByRole('button')).toBeDisabled(); }); + it('should render the component when amount is missing', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: null, + fromToken: 'ETH', + toToken: 'ETH', + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Enter amount')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + + it('should render the component when amount and dest token is missing', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: null, + fromToken: 'ETH', + toToken: null, + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + {}, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Select token and amount')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + it('should render the component when tx is submittable', () => { const mockStore = createBridgeMockStore( { @@ -37,14 +87,72 @@ describe('BridgeCTAButton', () => { toToken: 'ETH', toChainId: CHAIN_IDS.LINEA_MAINNET, }, - {}, + { + quotes: mockBridgeQuotesNativeErc20, + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.FETCHED, + }, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Confirm')).toBeInTheDocument(); + expect(getByRole('button')).not.toBeDisabled(); + }); + + it('should disable the component when quotes are loading and there are no existing quotes', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: 'ETH', + toToken: 'ETH', + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + { + quotes: [], + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.LOADING, + }, + ); + const { getByText, getByRole } = renderWithProvider( + , + configureStore(mockStore), + ); + + expect(getByText('Fetching quotes...')).toBeInTheDocument(); + expect(getByRole('button')).toBeDisabled(); + }); + + it('should enable the component when quotes are loading and there are existing quotes', () => { + const mockStore = createBridgeMockStore( + { + srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM], + destNetworkAllowlist: [CHAIN_IDS.LINEA_MAINNET], + }, + { + fromTokenInputValue: 1, + fromToken: 'ETH', + toToken: 'ETH', + toChainId: CHAIN_IDS.LINEA_MAINNET, + }, + { + quotes: mockBridgeQuotesNativeErc20, + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.LOADING, + }, ); const { getByText, getByRole } = renderWithProvider( , configureStore(mockStore), ); - expect(getByText('Bridge')).toBeInTheDocument(); + expect(getByText('Confirm')).toBeInTheDocument(); expect(getByRole('button')).not.toBeDisabled(); }); }); diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index fedcf4d4606a..28a1a2c1fbd6 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { Button } from '../../../components/component-library'; import { + getBridgeQuotes, getFromAmount, getFromChain, getFromToken, @@ -22,16 +23,29 @@ export const BridgeCTAButton = () => { const fromAmount = useSelector(getFromAmount); const toAmount = useSelector(getToAmount); + const { isLoading } = useSelector(getBridgeQuotes); + const isTxSubmittable = fromToken && toToken && fromChain && toChain && fromAmount && toAmount; const label = useMemo(() => { + if (isLoading && !isTxSubmittable) { + return t('swapFetchingQuotes'); + } + + if (!fromAmount) { + if (!toToken) { + return t('bridgeSelectTokenAndAmount'); + } + return t('bridgeEnterAmount'); + } + if (isTxSubmittable) { - return t('bridge'); + return t('confirm'); } return t('swapSelectToken'); - }, [isTxSubmittable]); + }, [isLoading, fromAmount, toToken, isTxSubmittable]); return ( + + +
+
+
+`; + +exports[`BridgeQuoteCard should render the recommended quote while loading new quotes 1`] = ` +
+
+
+
+
+
+

+ Estimated time +

+
+
+ +
+
+
+
+
+

+

+

+ 1 minutes +

+
+
+
+
+

+ Quote rate +

+
+
+
+

+

+

+ 1 ETH = 2465.4630 USDC +

+
+
+
+
+

+ Total fees +

+
+
+ +
+
+
+
+
+

+ 0.01 ETH +

+
+

+ $0.01 +

+
+
+
+ +
+
+`; diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap new file mode 100644 index 000000000000..41d8a03d1ac1 --- /dev/null +++ b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap @@ -0,0 +1,139 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BridgeQuotesModal should render the modal 1`] = ` + +
+
+
+

cosmos1...6hdc0

@@ -141,6 +144,7 @@ exports[`SnapUIAddress renders Cosmos address with blockie 1`] = ` />

cosmos1...6hdc0

@@ -194,6 +198,7 @@ exports[`SnapUIAddress renders Ethereum address 1`] = `

0xab16a...Bfcdb

@@ -215,6 +220,7 @@ exports[`SnapUIAddress renders Ethereum address with blockie 1`] = ` />

0xab16a...Bfcdb

@@ -268,6 +274,7 @@ exports[`SnapUIAddress renders Hedera address 1`] = `

0.0.123...zbhlt

@@ -289,6 +296,7 @@ exports[`SnapUIAddress renders Hedera address with blockie 1`] = ` />

0.0.123...zbhlt

@@ -342,6 +350,7 @@ exports[`SnapUIAddress renders Polkadot address 1`] = `

5hmuyxw...egmfy

@@ -363,6 +372,7 @@ exports[`SnapUIAddress renders Polkadot address with blockie 1`] = ` />

5hmuyxw...egmfy

@@ -416,6 +426,7 @@ exports[`SnapUIAddress renders Starknet address 1`] = `

0x02dd1...0ab57

@@ -437,6 +448,7 @@ exports[`SnapUIAddress renders Starknet address with blockie 1`] = ` />

0x02dd1...0ab57

@@ -490,6 +502,7 @@ exports[`SnapUIAddress renders legacy Ethereum address 1`] = `

0xab16a...Bfcdb

diff --git a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx index c75b172a616e..d39cb5adf321 100644 --- a/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx +++ b/ui/components/app/snaps/snap-ui-address/snap-ui-address.tsx @@ -9,6 +9,7 @@ import { AlignItems, Display, TextColor, + TextVariant, } from '../../../../helpers/constants/design-system'; import { shortenAddress } from '../../../../helpers/utils/util'; import { toChecksumHexAddress } from '../../../../../shared/modules/hexstring-utils'; @@ -20,11 +21,17 @@ export type SnapUIAddressProps = { address: string; // This is not currently exposed to Snaps. avatarSize?: 'xs' | 'sm' | 'md' | 'lg'; + truncate?: boolean; + displayName?: boolean; + avatar?: boolean; }; export const SnapUIAddress: React.FunctionComponent = ({ address, avatarSize = 'md', + truncate = true, + displayName = false, + avatar = true, }) => { const caipIdentifier = useMemo(() => { if (isHexString(address)) { @@ -41,15 +48,17 @@ export const SnapUIAddress: React.FunctionComponent = ({ [caipIdentifier], ); - const displayName = useDisplayName(parsed); + const name = useDisplayName(parsed); - const value = - displayName ?? - shortenAddress( - parsed.chain.namespace === 'eip155' - ? toChecksumHexAddress(parsed.address) - : parsed.address, - ); + // For EVM addresses, we make sure they are checksummed. + const transformedAddress = + parsed.chain.namespace === 'eip155' + ? toChecksumHexAddress(parsed.address) + : parsed.address; + + const formattedAddress = truncate + ? shortenAddress(transformedAddress) + : address; return ( = ({ alignItems={AlignItems.center} gap={2} > - - {value} + {avatar && } + + {displayName && name ? name : formattedAddress} + ); }; diff --git a/ui/components/app/snaps/snap-ui-renderer/components/address.ts b/ui/components/app/snaps/snap-ui-renderer/components/address.ts index 1e39966df760..908890ecef9f 100644 --- a/ui/components/app/snaps/snap-ui-renderer/components/address.ts +++ b/ui/components/app/snaps/snap-ui-renderer/components/address.ts @@ -6,5 +6,8 @@ export const address: UIComponentFactory = ({ element }) => ({ props: { address: element.props.address, avatarSize: 'xs', + truncate: element.props.truncate, + displayName: element.props.displayName, + avatar: element.props.avatar, }, }); diff --git a/yarn.lock b/yarn.lock index a1f4dfe4ea4c..9ac1b7409679 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4760,15 +4760,15 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^7.0.0, @metamask/approval-controller@npm:^7.0.2": - version: 7.0.2 - resolution: "@metamask/approval-controller@npm:7.0.2" +"@metamask/approval-controller@npm:^7.0.0, @metamask/approval-controller@npm:^7.1.1": + version: 7.1.1 + resolution: "@metamask/approval-controller@npm:7.1.1" dependencies: - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/utils": "npm:^9.1.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/utils": "npm:^10.0.0" nanoid: "npm:^3.1.31" - checksum: 10/0ce1f607f11b5c8c9d6a462e89935388187f87d5627814882c8ce808b2e84bd727028f92708ac99c59c638578aadd5e91cb2799d8c8e4be497ee646f39821ea6 + checksum: 10/10155e8c10be80a65bd99cc1aa83baf93900955aac35eb1cfc88c4ab8beff91de9db1168dd831b82378f26756e6f961296834d1703f9a727dd47402c735ee815 languageName: node linkType: hard @@ -5523,7 +5523,7 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2": +"@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.2": version: 9.0.3 resolution: "@metamask/json-rpc-engine@npm:9.0.3" dependencies: @@ -5534,15 +5534,15 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-middleware-stream@npm:^8.0.1, @metamask/json-rpc-middleware-stream@npm:^8.0.2, @metamask/json-rpc-middleware-stream@npm:^8.0.4": - version: 8.0.4 - resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.4" +"@metamask/json-rpc-middleware-stream@npm:^8.0.4, @metamask/json-rpc-middleware-stream@npm:^8.0.5": + version: 8.0.5 + resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.5" dependencies: - "@metamask/json-rpc-engine": "npm:^10.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/utils": "npm:^10.0.0" readable-stream: "npm:^3.6.2" - checksum: 10/93c842e1ac8e624c65d888cb3539b38ade5b8415ea45f649d78dad91e7139f11fa96bbf89136998d21def7711b3f710939f8e4498ce31a6cf461892e3f4ba176 + checksum: 10/486a4c64d445dc7ac7927ac5b9d01818ecef3fbb23d17eadada4748ed6cae9e259741e3c9380829b04a5c141d0972384647aedfde906dc83501b9d7f700ed621 languageName: node linkType: hard @@ -5831,22 +5831,22 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-controller@npm:^11.0.0": - version: 11.0.0 - resolution: "@metamask/permission-controller@npm:11.0.0" +"@metamask/permission-controller@npm:^11.0.0, @metamask/permission-controller@npm:^11.0.3": + version: 11.0.3 + resolution: "@metamask/permission-controller@npm:11.0.3" dependencies: - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/controller-utils": "npm:^11.0.2" - "@metamask/json-rpc-engine": "npm:^9.0.2" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/utils": "npm:^9.1.0" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/utils": "npm:^10.0.0" "@types/deep-freeze-strict": "npm:^1.1.0" deep-freeze-strict: "npm:^1.1.1" immer: "npm:^9.0.6" nanoid: "npm:^3.1.31" peerDependencies: "@metamask/approval-controller": ^7.0.0 - checksum: 10/3bd957b72ac4ed307566b650b5531e739732b9e6a414ec630bd43fc86c7c99b446eb5666f744abfb30c043824fe1b5a13681df8bd7c2244640b8996eec8e927a + checksum: 10/e90411ae34410176945e79c8e863ff2d78a12c01e98837a7298dc94d4815c65fec2cd338d4ae0026f91899acfe21bfe8b857a3b2f12c3d96719e5afb68df0e68 languageName: node linkType: hard @@ -6031,16 +6031,16 @@ __metadata: languageName: node linkType: hard -"@metamask/providers@npm:^17.1.2": - version: 17.2.0 - resolution: "@metamask/providers@npm:17.2.0" +"@metamask/providers@npm:^18.1.1": + version: 18.1.1 + resolution: "@metamask/providers@npm:18.1.1" dependencies: - "@metamask/json-rpc-engine": "npm:^9.0.1" - "@metamask/json-rpc-middleware-stream": "npm:^8.0.1" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.5" "@metamask/object-multiplex": "npm:^2.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/safe-event-emitter": "npm:^3.1.1" - "@metamask/utils": "npm:^9.0.0" + "@metamask/utils": "npm:^10.0.0" detect-browser: "npm:^5.2.0" extension-port-stream: "npm:^4.1.0" fast-deep-equal: "npm:^3.1.3" @@ -6048,7 +6048,7 @@ __metadata: readable-stream: "npm:^3.6.2" peerDependencies: webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/b2fc93cdc059528bfeb14a61d6153f9a5f2679e5c6640648c16cd4e5067f758a67c2c6abab962615e878e6b9d7f1bbcd3632584ad7e57ec9df8c16f47b13e608 + checksum: 10/dca428d84e490343d85921d4fb09216a0b64be59a036d7b4f7b5ca4e2581c29a4106d58ff9dfe0650dc2b9387dd2adad508fc61073a9fda8ebde8ee3a5137abe languageName: node linkType: hard @@ -6189,24 +6189,24 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.11.1": - version: 9.11.1 - resolution: "@metamask/snaps-controllers@npm:9.11.1" +"@metamask/snaps-controllers@npm:^9.10.0, @metamask/snaps-controllers@npm:^9.12.0": + version: 9.12.0 + resolution: "@metamask/snaps-controllers@npm:9.12.0" dependencies: - "@metamask/approval-controller": "npm:^7.0.2" - "@metamask/base-controller": "npm:^6.0.2" - "@metamask/json-rpc-engine": "npm:^9.0.2" - "@metamask/json-rpc-middleware-stream": "npm:^8.0.2" + "@metamask/approval-controller": "npm:^7.1.1" + "@metamask/base-controller": "npm:^7.0.2" + "@metamask/json-rpc-engine": "npm:^10.0.1" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.5" "@metamask/object-multiplex": "npm:^2.0.0" - "@metamask/permission-controller": "npm:^11.0.0" + "@metamask/permission-controller": "npm:^11.0.3" "@metamask/phishing-controller": "npm:^12.0.2" "@metamask/post-message-stream": "npm:^8.1.1" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-rpc-methods": "npm:^11.5.0" - "@metamask/snaps-sdk": "npm:^6.9.0" - "@metamask/snaps-utils": "npm:^8.4.1" - "@metamask/utils": "npm:^9.2.1" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/snaps-registry": "npm:^3.2.2" + "@metamask/snaps-rpc-methods": "npm:^11.5.1" + "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-utils": "npm:^8.5.0" + "@metamask/utils": "npm:^10.0.0" "@xstate/fsm": "npm:^2.0.0" browserify-zlib: "npm:^0.2.0" concat-stream: "npm:^2.0.0" @@ -6216,73 +6216,74 @@ __metadata: nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" readable-web-to-node-stream: "npm:^3.0.2" + semver: "npm:^7.5.4" tar-stream: "npm:^3.1.7" peerDependencies: - "@metamask/snaps-execution-environments": ^6.9.1 + "@metamask/snaps-execution-environments": ^6.9.2 peerDependenciesMeta: "@metamask/snaps-execution-environments": optional: true - checksum: 10/e9d47b62c39cf331d26a9e35dcf5c0452aff70980db31b42b56b11165d8d1dc7e3b5ad6b495644baa0276b18a7d9681bfb059388c4f2fb1b07c6bbc8b8da799b + checksum: 10/8d411ff2cfd43e62fe780092e935a1d977379488407b56cca1390edfa9408871cbaf3599f6e6ee999340d46fd3650f225a3270ceec9492c6f2dc4d93538c25ae languageName: node linkType: hard -"@metamask/snaps-execution-environments@npm:^6.9.1": - version: 6.9.1 - resolution: "@metamask/snaps-execution-environments@npm:6.9.1" +"@metamask/snaps-execution-environments@npm:^6.9.2": + version: 6.9.2 + resolution: "@metamask/snaps-execution-environments@npm:6.9.2" dependencies: - "@metamask/json-rpc-engine": "npm:^9.0.2" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/object-multiplex": "npm:^2.0.0" "@metamask/post-message-stream": "npm:^8.1.1" - "@metamask/providers": "npm:^17.1.2" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.8.0" - "@metamask/snaps-utils": "npm:^8.4.0" + "@metamask/providers": "npm:^18.1.1" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-utils": "npm:^8.5.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" + "@metamask/utils": "npm:^10.0.0" nanoid: "npm:^3.1.31" readable-stream: "npm:^3.6.2" - checksum: 10/87fb63e89780ebeb9083c93988167e671ceb3d1c77980a2cd32801f83d285669859bfd248197d3a2d683119b87554f1f835965549ad04587c8c2fa2f01fa1f18 + checksum: 10/f81dd3728417dc63ed16b102504cdf6c815bffef7b1dad9e7b0e064618b008e1f0fe6d05c225bcafeee09fb4bc473599ee710e1a26a6f3604e965f656fce8e36 languageName: node linkType: hard -"@metamask/snaps-registry@npm:^3.2.1": - version: 3.2.1 - resolution: "@metamask/snaps-registry@npm:3.2.1" +"@metamask/snaps-registry@npm:^3.2.1, @metamask/snaps-registry@npm:^3.2.2": + version: 3.2.2 + resolution: "@metamask/snaps-registry@npm:3.2.2" dependencies: "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.0.0" + "@metamask/utils": "npm:^10.0.0" "@noble/curves": "npm:^1.2.0" "@noble/hashes": "npm:^1.3.2" - checksum: 10/b2a413f27db9b5701d3773017035ee1e153734a25363e3877f44be4a70f51c48d77ad0ac8f1e96a7d732d2079a4b259896f361b3cba1ae0bf0bbc1075406f178 + checksum: 10/ca8239e838bbb913435e166136bbc9bd7222c4bd87b1525fa7ae3cdf2e0b868b5d4d90a67d1ed49633d566bdef9243abdbf5f5937b85a85d24184087f555813e languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.5.0": - version: 11.5.0 - resolution: "@metamask/snaps-rpc-methods@npm:11.5.0" +"@metamask/snaps-rpc-methods@npm:^11.5.1": + version: 11.5.1 + resolution: "@metamask/snaps-rpc-methods@npm:11.5.1" dependencies: "@metamask/key-tree": "npm:^9.1.2" - "@metamask/permission-controller": "npm:^11.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/snaps-sdk": "npm:^6.9.0" - "@metamask/snaps-utils": "npm:^8.4.1" + "@metamask/permission-controller": "npm:^11.0.3" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-utils": "npm:^8.5.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" + "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" - checksum: 10/a89b79926d5204a70369cd70e5174290805e8f9ede8057a49e347bd0e680d88de40ddfc25b3e54f53a16c3080a736ab73b50ffe50623264564af13f8709a23d3 + checksum: 10/0f999a5dd64f1b1123366f448ae833f0e95a415791600bb535959ba67d2269fbe3c4504d47f04db71bafa79a9a87d6b832fb2e2b5ef29567078c95bce2638f35 languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.9.0": - version: 6.9.0 - resolution: "@metamask/snaps-sdk@npm:6.9.0" +"@metamask/snaps-sdk@npm:^6.10.0": + version: 6.10.0 + resolution: "@metamask/snaps-sdk@npm:6.10.0" dependencies: "@metamask/key-tree": "npm:^9.1.2" - "@metamask/providers": "npm:^17.1.2" - "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/providers": "npm:^18.1.1" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" - checksum: 10/ea2c34c4451f671acc6c3c0ad0d46e770e8b7d0741c1d78a30bc36b883f09a10e9a428b8b564ecd0171da95fdf78bb8ac0de261423a1b35de5d22852300a24ee + "@metamask/utils": "npm:^10.0.0" + checksum: 10/02f04536328a64ff1e9e48fb6b109698d6d83f42af5666a9758ccb1e7a1e67c0c2e296ef2fef419dd3d1c8f26bbf30b9f31911a1baa66f044f21cd0ecb7a11a7 languageName: node linkType: hard @@ -6317,21 +6318,21 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.4.0, @metamask/snaps-utils@npm:^8.4.1": - version: 8.4.1 - resolution: "@metamask/snaps-utils@npm:8.4.1" +"@metamask/snaps-utils@npm:^8.1.1, @metamask/snaps-utils@npm:^8.3.0, @metamask/snaps-utils@npm:^8.5.0, @metamask/snaps-utils@npm:^8.5.1": + version: 8.5.1 + resolution: "@metamask/snaps-utils@npm:8.5.1" dependencies: "@babel/core": "npm:^7.23.2" "@babel/types": "npm:^7.23.0" - "@metamask/base-controller": "npm:^6.0.2" + "@metamask/base-controller": "npm:^7.0.2" "@metamask/key-tree": "npm:^9.1.2" - "@metamask/permission-controller": "npm:^11.0.0" - "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/permission-controller": "npm:^11.0.3" + "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/slip44": "npm:^4.0.0" - "@metamask/snaps-registry": "npm:^3.2.1" - "@metamask/snaps-sdk": "npm:^6.9.0" + "@metamask/snaps-registry": "npm:^3.2.2" + "@metamask/snaps-sdk": "npm:^6.10.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^9.2.1" + "@metamask/utils": "npm:^10.0.0" "@noble/hashes": "npm:^1.3.1" "@scure/base": "npm:^1.1.1" chalk: "npm:^4.1.2" @@ -6344,7 +6345,7 @@ __metadata: semver: "npm:^7.5.4" ses: "npm:^1.1.0" validate-npm-package-name: "npm:^5.0.0" - checksum: 10/c68a2fe69dc835c2b996d621fd4698435475d419a85aa557aa000aae0ab7ebb68d2a52f0b28bbab94fff895ece9a94077e3910a21b16d904cff3b9419ca575b6 + checksum: 10/38c8098c7dfa82bf907d31f77e2d5ffe6a82dca9bf5d633d10ac024f153d94b8124d7ddfb2e3e35befb7af619ebff1900e81476f889011eebdd80b5e12328c30 languageName: node linkType: hard @@ -26463,11 +26464,11 @@ __metadata: "@metamask/selected-network-controller": "npm:^18.0.2" "@metamask/signature-controller": "npm:^21.0.0" "@metamask/smart-transactions-controller": "npm:^13.0.0" - "@metamask/snaps-controllers": "npm:^9.11.1" - "@metamask/snaps-execution-environments": "npm:^6.9.1" - "@metamask/snaps-rpc-methods": "npm:^11.5.0" - "@metamask/snaps-sdk": "npm:^6.9.0" - "@metamask/snaps-utils": "npm:^8.4.1" + "@metamask/snaps-controllers": "npm:^9.12.0" + "@metamask/snaps-execution-environments": "npm:^6.9.2" + "@metamask/snaps-rpc-methods": "npm:^11.5.1" + "@metamask/snaps-sdk": "npm:^6.10.0" + "@metamask/snaps-utils": "npm:^8.5.1" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.7.0" "@metamask/transaction-controller": "npm:^38.1.0" From d970c2591823e54d821725fd5d0912f1bf3231b0 Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:50:20 -0800 Subject: [PATCH 028/111] chore: Add gravity logo and image mappings (#28306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds Gravity logo and chain mappings. Internal PR of https://github.com/MetaMask/metamask-extension/pull/27747 that wasn't passing CI for some reason. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28306?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/pull/27747 ## **Manual testing steps** ## **Screenshots/Recordings** Screenshot 2024-11-05 at 12 49 17 PM ## **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 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/develop/.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. --- app/images/gravity.svg | 10 ++++++++++ shared/constants/network.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 app/images/gravity.svg diff --git a/app/images/gravity.svg b/app/images/gravity.svg new file mode 100644 index 000000000000..a94d44f8d285 --- /dev/null +++ b/app/images/gravity.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 64d330b73b2c..4844e7c2e981 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -153,6 +153,8 @@ export const CHAIN_IDS = { ARBITRUM_SEPOLIA: '0x66eee', NEAR: '0x18d', NEAR_TESTNET: '0x18e', + GRAVITY_ALPHA_MAINNET: '0x659', + GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', } as const; export const CHAINLIST_CHAIN_IDS_MAP = { @@ -209,6 +211,8 @@ export const CHAINLIST_CHAIN_IDS_MAP = { FILECOIN: '0x13a', NUMBERS: '0x290b', APE: '0x8173', + GRAVITY_ALPHA_MAINNET: '0x659', + GRAVITY_ALPHA_TESTNET_SEPOLIA: '0x34c1', } as const; // To add a deprecation warning to a network, add it to the array @@ -454,6 +458,8 @@ export const NUMBERS_TOKEN_IMAGE_URL = './images/numbers-token.png'; export const SEI_IMAGE_URL = './images/sei.svg'; export const NEAR_IMAGE_URL = './images/near.svg'; export const APE_IMAGE_URL = './images/ape.svg'; +export const GRAVITY_ALPHA_MAINNET_IMAGE_URL = './images/gravity.svg'; +export const GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL = './images/gravity.svg'; export const INFURA_PROVIDER_TYPES = [ NETWORK_TYPES.MAINNET, @@ -792,6 +798,10 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [CHAINLIST_CHAIN_IDS_MAP.BASE]: BASE_TOKEN_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.NUMBERS]: NUMBERS_MAINNET_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.SEI]: SEI_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.GRAVITY_ALPHA_MAINNET]: + GRAVITY_ALPHA_MAINNET_IMAGE_URL, + [CHAINLIST_CHAIN_IDS_MAP.GRAVITY_ALPHA_TESTNET_SEPOLIA]: + GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL, } as const; export const CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP = { @@ -824,6 +834,9 @@ export const CHAIN_ID_TOKEN_IMAGE_MAP = { [CHAIN_IDS.MOONBEAM]: MOONBEAM_TOKEN_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.IOTEX_MAINNET]: IOTEX_TOKEN_IMAGE_URL, [CHAINLIST_CHAIN_IDS_MAP.APE_MAINNET]: APE_TOKEN_IMAGE_URL, + [CHAIN_IDS.GRAVITY_ALPHA_MAINNET]: GRAVITY_ALPHA_MAINNET_IMAGE_URL, + [CHAIN_IDS.GRAVITY_ALPHA_TESTNET_SEPOLIA]: + GRAVITY_ALPHA_TESTNET_SEPOLIA_IMAGE_URL, } as const; export const INFURA_BLOCKED_KEY = 'countryBlocked'; From 63fb3ac6fc30a20cdc112585839e81d013826768 Mon Sep 17 00:00:00 2001 From: Vinicius Stevam <45455812+vinistevam@users.noreply.github.com> Date: Wed, 6 Nov 2024 08:12:57 +0000 Subject: [PATCH 029/111] fix: remove scroll-to-bottom requirement in redesigned transaction confirmations (#27910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR addresses the removal of the scroll-to-bottom requirement in the redesigned transaction confirmation screens. It eliminates the need for users to scroll to the bottom in order to enable the confirm button, streamlining the confirmation process. The scroll-to-bottom arrow is also removed, ensuring a smoother user experience without unnecessary interaction barriers. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27910?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3495 ## **Manual testing steps** 1. Go to test dapp 2. Have 2+ transaction insights snaps installed 3. Click Create Token 4. See the confirm button disabled until you scroll to bottom ## **Screenshots/Recordings** [deploy.webm](https://github.com/user-attachments/assets/79716a68-e70f-456a-b962-ccec8732935b) ### **Before** ### **After** ## **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 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/develop/.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. --- .../scroll-to-bottom/scroll-to-bottom.test.tsx | 15 ++++++++++++++- .../scroll-to-bottom/scroll-to-bottom.tsx | 18 ++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.test.tsx b/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.test.tsx index d5954e56609b..6bf31166b6e5 100644 --- a/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.test.tsx +++ b/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.test.tsx @@ -2,7 +2,10 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import { unapprovedTypedSignMsgV4 } from '../../../../../../test/data/confirmations/typed_sign'; -import { getMockPersonalSignConfirmState } from '../../../../../../test/data/confirmations/helper'; +import { + getMockContractInteractionConfirmState, + getMockPersonalSignConfirmState, +} from '../../../../../../test/data/confirmations/helper'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import * as usePreviousHooks from '../../../../../hooks/usePrevious'; import ScrollToBottom from './scroll-to-bottom'; @@ -116,6 +119,16 @@ describe('ScrollToBottom', () => { expect(mockSetHasScrolledToBottom).toHaveBeenCalledWith(false); }); + it('does not render the scroll button when the confirmation is transaction redesigned', () => { + const mockStateTransaction = getMockContractInteractionConfirmState(); + const { container } = renderWithConfirmContextProvider( + foobar, + configureMockStore([])(mockStateTransaction), + ); + + expect(container.querySelector(buttonSelector)).not.toBeInTheDocument(); + }); + describe('when user has scrolled to the bottom', () => { beforeEach(() => { mockedUseScrollRequiredResult.isScrolledToBottom = true; diff --git a/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx b/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx index f42997de114e..c61053818923 100644 --- a/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx +++ b/ui/pages/confirmations/components/confirm/scroll-to-bottom/scroll-to-bottom.tsx @@ -1,5 +1,6 @@ import React, { useContext, useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { TransactionType } from '@metamask/transaction-controller'; import { Box, ButtonIcon, @@ -20,6 +21,7 @@ import { usePrevious } from '../../../../../hooks/usePrevious'; import { useScrollRequired } from '../../../../../hooks/useScrollRequired'; import { useConfirmContext } from '../../../context/confirm'; import { selectConfirmationAdvancedDetailsOpen } from '../../../selectors/preferences'; +import { REDESIGN_DEV_TRANSACTION_TYPES } from '../../../utils'; type ContentProps = { /** @@ -49,6 +51,13 @@ const ScrollToBottom = ({ children }: ContentProps) => { offsetPxFromBottom: 0, }); + const isTransactionRedesign = REDESIGN_DEV_TRANSACTION_TYPES.includes( + currentConfirmation?.type as TransactionType, + ); + + const showScrollToBottom = + isScrollable && !isScrolledToBottom && !isTransactionRedesign; + /** * Scroll to the top of the page when the confirmation changes. This happens * when we navigate through different confirmations. Also, resets hasScrolledToBottom @@ -71,8 +80,13 @@ const ScrollToBottom = ({ children }: ContentProps) => { }, [currentConfirmation?.id, previousId, ref?.current]); useEffect(() => { + if (isTransactionRedesign) { + setIsScrollToBottomCompleted(true); + return; + } + setIsScrollToBottomCompleted(!isScrollable || hasScrolledToBottom); - }, [isScrollable, hasScrolledToBottom]); + }, [isScrollable, hasScrolledToBottom, isTransactionRedesign]); return ( { > {children} - {isScrollable && !isScrolledToBottom && ( + {showScrollToBottom && ( Date: Wed, 6 Nov 2024 17:23:06 +0800 Subject: [PATCH 030/111] feat: btc e2e tests (#27986) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds e2e tests for the btc send flow. ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --------- Co-authored-by: Charly Chevalier --- test/e2e/constants.ts | 13 ++ test/e2e/flask/btc/btc-send.spec.ts | 153 +++++++++++++++++ test/e2e/flask/btc/common-btc.ts | 158 +++++++++++++++++- .../app/wallet-overview/coin-buttons.tsx | 26 ++- 4 files changed, 337 insertions(+), 13 deletions(-) create mode 100644 test/e2e/flask/btc/btc-send.spec.ts diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 8bf39d261bcb..2e24bf4042ba 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -50,3 +50,16 @@ export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252'; /* Default (mocked) BTC balance used by the Bitcoin RPC provider */ export const DEFAULT_BTC_BALANCE = 1; // BTC + +/* Default BTC fees rate */ +export const DEFAULT_BTC_FEES_RATE = 0.00001; // BTC + +/* Default BTC conversion rate to USD */ +export const DEFAULT_BTC_CONVERSION_RATE = 62000; // USD + +/* Default BTC transaction ID */ +export const DEFAULT_BTC_TRANSACTION_ID = + 'e4111a707317da67d49a71af4cbcf6c0546f900ca32c3842d2254e315d1fca18'; + +/* Number of sats in 1 BTC */ +export const SATS_IN_1_BTC = 100000000; // sats diff --git a/test/e2e/flask/btc/btc-send.spec.ts b/test/e2e/flask/btc/btc-send.spec.ts new file mode 100644 index 000000000000..c1a956cd00b7 --- /dev/null +++ b/test/e2e/flask/btc/btc-send.spec.ts @@ -0,0 +1,153 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { Driver } from '../../webdriver/driver'; +import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; +import { + getTransactionRequest, + SendFlowPlaceHolders, + withBtcAccountSnap, +} from './common-btc'; + +export async function startSendFlow(driver: Driver, recipient?: string) { + // Wait a bit so the MultichainRatesController is able to fetch BTC -> USD rates. + await driver.delay(1000); + + // Start the send flow. + const sendButton = await driver.waitForSelector({ + text: 'Send', + tag: 'button', + css: '[data-testid="coin-overview-send"]', + }); + await sendButton.click(); + + // See the review button is disabled by default. + await driver.waitForSelector({ + text: 'Review', + tag: 'button', + css: '[disabled]', + }); + + if (recipient) { + // Set the recipient address (if any). + await driver.pasteIntoField( + `input[placeholder="${SendFlowPlaceHolders.RECIPIENT}"]`, + recipient, + ); + } +} + +describe('BTC Account - Send', function (this: Suite) { + it('can send complete the send flow', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver, mockServer) => { + await startSendFlow(driver, DEFAULT_BTC_ACCOUNT); + + // Set the amount to send. + const mockAmountToSend = '0.5'; + await driver.pasteIntoField( + `input[placeholder="${SendFlowPlaceHolders.AMOUNT}"]`, + mockAmountToSend, + ); + + // From here, the "summary panel" should have some information about the fees and total. + await driver.waitForSelector({ + text: 'Total', + tag: 'p', + }); + + // The review button will become available. + const snapReviewButton = await driver.findClickableElement({ + text: 'Review', + tag: 'button', + css: '.snap-ui-renderer__footer-button', + }); + assert.equal(await snapReviewButton.isEnabled(), true); + await snapReviewButton.click(); + + // TODO: There isn't any check for the fees and total amount. This requires calculating the vbytes used in a transaction dynamically. + // We already have unit tests for these calculations on the Snap. + + // ------------------------------------------------------------------------------ + // From here, we have moved to the confirmation screen (second part of the flow). + + // We should be able to send the transaction right away. + const snapSendButton = await driver.waitForSelector({ + text: 'Send', + tag: 'button', + css: '.snap-ui-renderer__footer-button', + }); + assert.equal(await snapSendButton.isEnabled(), true); + await snapSendButton.click(); + + // Check that we are selecting the "Activity tab" right after the send. + await driver.waitForSelector({ + tag: 'div', + text: 'Bitcoin activity is not supported', + }); + + const transaction = await getTransactionRequest(mockServer); + assert(transaction !== undefined); + }, + ); + }); + + it('can send the max amount', async function () { + await withBtcAccountSnap( + { title: this.test?.fullTitle() }, + async (driver, mockServer) => { + await startSendFlow(driver, DEFAULT_BTC_ACCOUNT); + + // Use the max spendable amount of that account. + await driver.clickElement({ + text: 'Max', + tag: 'button', + }); + + // From here, the "summary panel" should have some information about the fees and total. + await driver.waitForSelector({ + text: 'Total', + tag: 'p', + }); + + await driver.waitForSelector({ + text: `${DEFAULT_BTC_BALANCE} BTC`, + tag: 'p', + }); + + // The review button will become available. + const snapReviewButton = await driver.findClickableElement({ + text: 'Review', + tag: 'button', + css: '.snap-ui-renderer__footer-button', + }); + assert.equal(await snapReviewButton.isEnabled(), true); + await snapReviewButton.click(); + + // TODO: There isn't any check for the fees and total amount. This requires calculating the vbytes used in a transaction dynamically. + // We already have unit tests for these calculations on the snap. + + // ------------------------------------------------------------------------------ + // From here, we have moved to the confirmation screen (second part of the flow). + + // We should be able to send the transaction right away. + const snapSendButton = await driver.waitForSelector({ + text: 'Send', + tag: 'button', + css: '.snap-ui-renderer__footer-button', + }); + assert.equal(await snapSendButton.isEnabled(), true); + await snapSendButton.click(); + + // Check that we are selecting the "Activity tab" right after the send. + await driver.waitForSelector({ + tag: 'div', + text: 'Bitcoin activity is not supported', + }); + + const transaction = await getTransactionRequest(mockServer); + assert(transaction !== undefined); + }, + ); + }); +}); diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index 6891b3bfd60e..452f9ad44f6b 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -1,11 +1,26 @@ import { Mockttp } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { withFixtures, unlockWallet } from '../../helpers'; -import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; +import { + DEFAULT_BTC_ACCOUNT, + DEFAULT_BTC_BALANCE, + DEFAULT_BTC_FEES_RATE, + DEFAULT_BTC_TRANSACTION_ID, + DEFAULT_BTC_CONVERSION_RATE, + SATS_IN_1_BTC, +} from '../../constants'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { Driver } from '../../webdriver/driver'; import messages from '../../../../app/_locales/en/messages.json'; +const QUICKNODE_URL_REGEX = /^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u; + +export enum SendFlowPlaceHolders { + AMOUNT = 'Enter amount to send', + RECIPIENT = 'Enter receiving address', + LOADING = 'Preparing transaction', +} + export async function createBtcAccount(driver: Driver) { await driver.clickElement('[data-testid="account-menu-icon"]'); await driver.clickElement( @@ -27,12 +42,17 @@ export async function createBtcAccount(driver: Driver) { ); } +export function btcToSats(btc: number): number { + // Watchout, we're not using BigNumber(s) here (but that's ok for test purposes) + return btc * SATS_IN_1_BTC; +} + export async function mockBtcBalanceQuote( mockServer: Mockttp, address: string = DEFAULT_BTC_ACCOUNT, ) { return await mockServer - .forPost(/^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u) + .forPost(QUICKNODE_URL_REGEX) .withJsonBodyIncluding({ method: 'bb_getaddress', }) @@ -42,7 +62,7 @@ export async function mockBtcBalanceQuote( json: { result: { address, - balance: (DEFAULT_BTC_BALANCE * 1e8).toString(), // Converts from BTC to sats + balance: btcToSats(DEFAULT_BTC_BALANCE).toString(), // Converts from BTC to sats totalReceived: '0', totalSent: '0', unconfirmedBalance: '0', @@ -54,6 +74,105 @@ export async function mockBtcBalanceQuote( }); } +export async function mockBtcFeeCallQuote(mockServer: Mockttp) { + return await mockServer + .forPost(QUICKNODE_URL_REGEX) + .withJsonBodyIncluding({ + method: 'estimatesmartfee', + }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: { + blocks: 1, + feerate: DEFAULT_BTC_FEES_RATE, // sats + }, + }, + }; + }); +} + +export async function mockMempoolInfo(mockServer: Mockttp) { + return await mockServer + .forPost(QUICKNODE_URL_REGEX) + .withJsonBodyIncluding({ + method: 'getmempoolinfo', + }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: { + loaded: true, + size: 165194, + bytes: 93042828, + usage: 550175264, + total_fee: 1.60127931, + maxmempool: 2048000000, + mempoolminfee: DEFAULT_BTC_FEES_RATE, + minrelaytxfee: DEFAULT_BTC_FEES_RATE, + incrementalrelayfee: 0.00001, + unbroadcastcount: 0, + fullrbf: true, + }, + }, + }; + }); +} + +export async function mockGetUTXO(mockServer: Mockttp) { + return await mockServer + .forPost(QUICKNODE_URL_REGEX) + .withJsonBodyIncluding({ + method: 'bb_getutxos', + }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: [ + { + txid: DEFAULT_BTC_TRANSACTION_ID, + vout: 0, + value: btcToSats(DEFAULT_BTC_BALANCE).toString(), + height: 101100110, + confirmations: 6, + }, + ], + }, + }; + }); +} + +export async function mockSendTransaction(mockServer: Mockttp) { + return await mockServer + .forPost(QUICKNODE_URL_REGEX) + .withJsonBodyIncluding({ + method: 'sendrawtransaction', + }) + .thenCallback(() => { + return { + statusCode: 200, + json: { + result: DEFAULT_BTC_TRANSACTION_ID, + }, + }; + }); +} + +export async function mockRatesCall(mockServer: Mockttp) { + return await mockServer + .forGet('https://min-api.cryptocompare.com/data/pricemulti') + .withQuery({ fsyms: 'btc', tsyms: 'usd,USD' }) + .thenCallback(() => { + return { + statusCode: 200, + json: { BTC: { USD: DEFAULT_BTC_CONVERSION_RATE } }, + }; + }); +} + export async function mockRampsDynamicFeatureFlag( mockServer: Mockttp, subDomain: string, @@ -87,7 +206,7 @@ export async function withBtcAccountSnap( title, bitcoinSupportEnabled, }: { title?: string; bitcoinSupportEnabled?: boolean }, - test: (driver: Driver) => Promise, + test: (driver: Driver, mockServer: Mockttp) => Promise, ) { await withFixtures( { @@ -99,17 +218,44 @@ export async function withBtcAccountSnap( title, dapp: true, testSpecificMock: async (mockServer: Mockttp) => [ + await mockRatesCall(mockServer), await mockBtcBalanceQuote(mockServer), // See: PROD_RAMP_API_BASE_URL await mockRampsDynamicFeatureFlag(mockServer, 'api'), // See: UAT_RAMP_API_BASE_URL await mockRampsDynamicFeatureFlag(mockServer, 'uat-api'), + await mockMempoolInfo(mockServer), + await mockBtcFeeCallQuote(mockServer), + await mockGetUTXO(mockServer), + await mockSendTransaction(mockServer), ], }, - async ({ driver }: { driver: Driver }) => { + async ({ driver, mockServer }: { driver: Driver; mockServer: Mockttp }) => { await unlockWallet(driver); await createBtcAccount(driver); - await test(driver); + await test(driver, mockServer); + }, + ); +} + +export async function getQuickNodeSeenRequests(mockServer: Mockttp) { + const seenRequests = await Promise.all( + ( + await mockServer.getMockedEndpoints() + ).map((mockedEndpoint) => mockedEndpoint.getSeenRequests()), + ); + return seenRequests + .flat() + .filter((request) => request.url.match(QUICKNODE_URL_REGEX)); +} + +export async function getTransactionRequest(mockServer: Mockttp) { + // Check that the transaction has been sent. + const transactionRequest = (await getQuickNodeSeenRequests(mockServer)).find( + async (request) => { + const body = (await request.body.getJson()) as { method: string }; + return body.method === 'sendrawtransaction'; }, ); + return transactionRequest; } diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index bac7872c79e3..f49d7bf31801 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -125,6 +125,12 @@ const CoinButtons = ({ const account = useSelector(getSelectedAccount); const { address: selectedAddress } = account; const history = useHistory(); + ///: BEGIN:ONLY_INCLUDE_IF(build-flask) + const currentActivityTabName = useSelector( + // @ts-expect-error TODO: fix state type + (state) => state.metamask.defaultHomeActiveTabName, + ); + ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const location = useLocation(); const keyring = useSelector(getCurrentKeyring); @@ -279,14 +285,20 @@ const CoinButtons = ({ switch (account.type) { ///: BEGIN:ONLY_INCLUDE_IF(build-flask) case BtcAccountType.P2wpkh: { - await sendMultichainTransaction( - BITCOIN_WALLET_SNAP_ID, - account.id, - chainId as CaipChainId, - ); + try { + // FIXME: We switch the tab before starting the send flow (we + // faced some inconsistencies when changing it after). + await dispatch(setDefaultHomeActiveTabName('activity')); + await sendMultichainTransaction( + BITCOIN_WALLET_SNAP_ID, + account.id, + chainId as CaipChainId, + ); + } catch { + // Restore the previous tab in case of any error (see FIXME comment above). + await dispatch(setDefaultHomeActiveTabName(currentActivityTabName)); + } - // We automatically switch to the activity tab once the transaction has been sent. - dispatch(setDefaultHomeActiveTabName('activity')); break; } ///: END:ONLY_INCLUDE_IF From 4dd5413eceba9c32e01b2fb9fb9b73a2e4b2ab4c Mon Sep 17 00:00:00 2001 From: chloeYue <105063779+chloeYue@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:29:11 +0100 Subject: [PATCH 031/111] test: [POM] Refactor e2e tests to use onboarding flows defined in Page Object Models (#28202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** - Refactor e2e tests to use onboarding flows defined in Page Object Models, as the new onboarding functions are more stable, and have removed unnecessary delays - Deprecate old onboarding functions - Correct the logic in test `test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts` to make it more clear, we only need to toggle the basic functionality off during onboarding, it will also toggle off sync account option. - Removed ganache server in e2e tests when the ganache is not necessary [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27155?quickstart=1) ## **Related issues** Fixes: #28226 ## **Manual testing steps** Check code readability, make sure tests pass. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --------- Co-authored-by: Danica Shen --- test/e2e/helpers.js | 39 ++++++-- .../e2e/page-objects/flows/onboarding.flow.ts | 75 +++++++++++++--- .../onboarding/onboarding-password-page.ts | 16 ++-- .../pages/onboarding/onboarding-srp-page.ts | 6 +- test/e2e/tests/network/multi-rpc.spec.ts | 8 +- .../importing-private-key-account.spec.ts | 34 +++---- .../account-syncing/new-user-sync.spec.ts | 32 +++---- .../onboarding-with-opt-out.spec.ts | 88 +++++++++++-------- .../sync-after-adding-account.spec.ts | 60 +++++++------ .../sync-after-modifying-account-name.spec.ts | 34 +++---- .../sync-after-onboarding.spec.ts | 21 +++-- test/e2e/tests/onboarding/onboarding.spec.ts | 10 ++- .../onboarding-infura-call-privacy.spec.ts | 2 +- 13 files changed, 258 insertions(+), 167 deletions(-) diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 5eaf14b8360b..1cb9e47f1f80 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -382,6 +382,12 @@ const getWindowHandles = async (driver, handlesCount) => { return { extension, dapp, popup }; }; +/** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. + * @param driver + * @param seedPhrase + * @param password + */ const importSRPOnboardingFlow = async (driver, seedPhrase, password) => { // agree to terms of use await driver.clickElement('[data-testid="onboarding-terms-checkbox"]'); @@ -408,6 +414,12 @@ const importSRPOnboardingFlow = async (driver, seedPhrase, password) => { await driver.assertElementNotPresent('.loading-overlay'); }; +/** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. + * @param driver + * @param seedPhrase + * @param password + */ const completeImportSRPOnboardingFlow = async ( driver, seedPhrase, @@ -423,6 +435,12 @@ const completeImportSRPOnboardingFlow = async ( await driver.clickElement('[data-testid="pin-extension-done"]'); }; +/** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. + * @param driver + * @param seedPhrase + * @param password + */ const completeImportSRPOnboardingFlowWordByWord = async ( driver, seedPhrase, @@ -466,8 +484,8 @@ const completeImportSRPOnboardingFlowWordByWord = async ( }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Begin the create new wallet flow on onboarding screen. - * * @param {WebDriver} driver */ const onboardingBeginCreateNewWallet = async (driver) => { @@ -479,8 +497,8 @@ const onboardingBeginCreateNewWallet = async (driver) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Choose either "I Agree" or "No Thanks" on the MetaMetrics onboarding screen - * * @param {WebDriver} driver * @param {boolean} option - true to opt into metrics, default is false */ @@ -491,8 +509,8 @@ const onboardingChooseMetametricsOption = async (driver, option = false) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Set a password for MetaMask during onboarding - * * @param {WebDriver} driver * @param {string} password - Password to set */ @@ -505,9 +523,9 @@ const onboardingCreatePassword = async (driver, password) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Choose to secure wallet, and then get recovery phrase and confirm the SRP * during onboarding flow. - * * @param {WebDriver} driver */ const onboardingRevealAndConfirmSRP = async (driver) => { @@ -543,9 +561,9 @@ const onboardingRevealAndConfirmSRP = async (driver) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Complete the onboarding flow by confirming completion. Final step before the * reminder to pin the extension. - * * @param {WebDriver} driver */ const onboardingCompleteWalletCreation = async (driver) => { @@ -555,8 +573,8 @@ const onboardingCompleteWalletCreation = async (driver) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Move through the steps of pinning extension after successful onboarding - * * @param {WebDriver} driver */ const onboardingPinExtension = async (driver) => { @@ -566,12 +584,12 @@ const onboardingPinExtension = async (driver) => { }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Completes the onboarding flow with optional opt-out settings for wallet creation. * * This function navigates through the onboarding process, allowing for opt-out of certain features. * It waits for the appropriate heading to appear, then proceeds to opt-out of third-party API * integration for general and assets sections if specified in the optOutOptions. - * * @param {WebDriver} driver - The Selenium WebDriver instance. * @param {object} optOutOptions - Optional. An object specifying which features to opt-out of. * @param {boolean} optOutOptions.basicFunctionality - Optional. Defaults to true. Opt-out of basic functionality. @@ -663,11 +681,11 @@ const onboardingCompleteWalletCreationWithOptOut = async ( }; /** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. * Completes the onboarding flow for creating a new wallet with opt-out options. * * This function guides the user through the onboarding process of creating a new wallet, * including opting out of certain features as specified by the `optOutOptions` parameter. - * * @param {object} driver - The Selenium driver instance. * @param {string} password - The password to use for the new wallet. * @param {object} optOutOptions - An object specifying the features to opt out of. @@ -688,6 +706,11 @@ const completeCreateNewWalletOnboardingFlowWithOptOut = async ( await onboardingCompleteWalletCreationWithOptOut(driver, optOutOptions); }; +/** + * @deprecated Please use page object functions in `onboarding.flow.ts` and in `pages/onboarding/*`. + * @param driver + * @param password + */ const completeCreateNewWalletOnboardingFlow = async (driver, password) => { await onboardingBeginCreateNewWallet(driver); await onboardingChooseMetametricsOption(driver, false); diff --git a/test/e2e/page-objects/flows/onboarding.flow.ts b/test/e2e/page-objects/flows/onboarding.flow.ts index b5fda9e0c276..adf591330798 100644 --- a/test/e2e/page-objects/flows/onboarding.flow.ts +++ b/test/e2e/page-objects/flows/onboarding.flow.ts @@ -5,8 +5,19 @@ import OnboardingSrpPage from '../pages/onboarding/onboarding-srp-page'; import StartOnboardingPage from '../pages/onboarding/start-onboarding-page'; import SecureWalletPage from '../pages/onboarding/secure-wallet-page'; import OnboardingCompletePage from '../pages/onboarding/onboarding-complete-page'; +import { WALLET_PASSWORD } from '../../helpers'; +import { E2E_SRP } from '../../default-fixture'; -export const createNewWalletOnboardingFlow = async (driver: Driver) => { +/** + * Create new wallet onboarding flow + * + * @param driver - The WebDriver instance. + * @param password - The password to create. Defaults to WALLET_PASSWORD. + */ +export const createNewWalletOnboardingFlow = async ( + driver: Driver, + password: string = WALLET_PASSWORD, +) => { console.log('Starting the creation of a new wallet onboarding flow'); await driver.navigate(); const startOnboardingPage = new StartOnboardingPage(driver); @@ -20,16 +31,33 @@ export const createNewWalletOnboardingFlow = async (driver: Driver) => { const onboardingPasswordPage = new OnboardingPasswordPage(driver); await onboardingPasswordPage.check_pageIsLoaded(); - await onboardingPasswordPage.createWalletPassword(); + await onboardingPasswordPage.createWalletPassword(password); const secureWalletPage = new SecureWalletPage(driver); await secureWalletPage.check_pageIsLoaded(); await secureWalletPage.revealAndConfirmSRP(); }; -export const importSRPOnboardingFlow = async (driver: Driver) => { +/** + * Import SRP onboarding flow + * + * @param options - The options object. + * @param options.driver - The WebDriver instance. + * @param [options.seedPhrase] - The seed phrase to import. + * @param [options.password] - The password to use. + */ +export const importSRPOnboardingFlow = async ({ + driver, + seedPhrase = E2E_SRP, + password = WALLET_PASSWORD, +}: { + driver: Driver; + seedPhrase?: string; + password?: string; +}): Promise => { console.log('Starting the import of SRP onboarding flow'); await driver.navigate(); + const startOnboardingPage = new StartOnboardingPage(driver); await startOnboardingPage.check_pageIsLoaded(); await startOnboardingPage.checkTermsCheckbox(); @@ -41,26 +69,53 @@ export const importSRPOnboardingFlow = async (driver: Driver) => { const onboardingSrpPage = new OnboardingSrpPage(driver); await onboardingSrpPage.check_pageIsLoaded(); - await onboardingSrpPage.fillSrp(); + await onboardingSrpPage.fillSrp(seedPhrase); await onboardingSrpPage.clickConfirmButton(); const onboardingPasswordPage = new OnboardingPasswordPage(driver); await onboardingPasswordPage.check_pageIsLoaded(); - await onboardingPasswordPage.createImportedWalletPassword(); + await onboardingPasswordPage.createImportedWalletPassword(password); }; -export const completeCreateNewWalletOnboardingFlow = async (driver: Driver) => { +/** + * Complete create new wallet onboarding flow + * + * @param driver - The WebDriver instance. + * @param password - The password to use. Defaults to WALLET_PASSWORD. + */ +export const completeCreateNewWalletOnboardingFlow = async ( + driver: Driver, + password: string = WALLET_PASSWORD, +) => { console.log('start to complete create new wallet onboarding flow '); - await createNewWalletOnboardingFlow(driver); + await createNewWalletOnboardingFlow(driver, password); const onboardingCompletePage = new OnboardingCompletePage(driver); await onboardingCompletePage.check_pageIsLoaded(); await onboardingCompletePage.check_congratulationsMessageIsDisplayed(); await onboardingCompletePage.completeOnboarding(); }; -export const completeImportSRPOnboardingFlow = async (driver: Driver) => { - console.log('start to complete import srp onboarding flow '); - await importSRPOnboardingFlow(driver); +/** + * Complete import SRP onboarding flow + * + * @param options - The options object. + * @param options.driver - The WebDriver instance. + * @param [options.seedPhrase] - The seed phrase to import. Defaults to E2E_SRP. + * @param [options.password] - The password to use. Defaults to WALLET_PASSWORD. + * @returns A promise that resolves when the onboarding flow is complete. + */ +export const completeImportSRPOnboardingFlow = async ({ + driver, + seedPhrase = E2E_SRP, + password = WALLET_PASSWORD, +}: { + driver: Driver; + seedPhrase?: string; + password?: string; +}): Promise => { + console.log('Starting to complete import SRP onboarding flow'); + await importSRPOnboardingFlow({ driver, seedPhrase, password }); + const onboardingCompletePage = new OnboardingCompletePage(driver); await onboardingCompletePage.check_pageIsLoaded(); await onboardingCompletePage.check_walletReadyMessageIsDisplayed(); diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts index 81c2d21aceb6..ac8dd4f923f2 100644 --- a/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts +++ b/test/e2e/page-objects/pages/onboarding/onboarding-password-page.ts @@ -52,30 +52,26 @@ class OnboardingPasswordPage { /** * Create a password for new imported wallet * - * @param newPassword - The new password to create. Defaults to WALLET_PASSWORD. - * @param confirmPassword - The confirm password to create. Defaults to WALLET_PASSWORD. + * @param password - The password to create. Defaults to WALLET_PASSWORD. */ async createImportedWalletPassword( - newPassword: string = WALLET_PASSWORD, - confirmPassword: string = WALLET_PASSWORD, + password: string = WALLET_PASSWORD, ): Promise { console.log('Create password for new imported wallet'); - await this.fillWalletPassword(newPassword, confirmPassword); + await this.fillWalletPassword(password, password); await this.driver.clickElementAndWaitToDisappear(this.importWalletButton); } /** * Create a password for new created wallet * - * @param newPassword - The new password to create. Defaults to WALLET_PASSWORD. - * @param confirmPassword - The confirm password to create. Defaults to WALLET_PASSWORD. + * @param password - The new password to create. Defaults to WALLET_PASSWORD. */ async createWalletPassword( - newPassword: string = WALLET_PASSWORD, - confirmPassword: string = WALLET_PASSWORD, + password: string = WALLET_PASSWORD, ): Promise { console.log('Create password for new created wallet'); - await this.fillWalletPassword(newPassword, confirmPassword); + await this.fillWalletPassword(password, password); await this.driver.clickElementAndWaitToDisappear(this.createWalletButton); } diff --git a/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts index da3e74153c67..b61279c8fe8c 100644 --- a/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts +++ b/test/e2e/page-objects/pages/onboarding/onboarding-srp-page.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'assert'; import { Driver } from '../../../webdriver/driver'; -import { TEST_SEED_PHRASE } from '../../../helpers'; +import { E2E_SRP } from '../../../default-fixture'; class OnboardingSrpPage { private driver: Driver; @@ -53,9 +53,9 @@ class OnboardingSrpPage { /** * Fill the SRP words with the provided seed phrase * - * @param seedPhrase - The seed phrase to fill. Defaults to TEST_SEED_PHRASE. + * @param seedPhrase - The seed phrase to fill. Defaults to E2E_SRP. */ - async fillSrp(seedPhrase: string = TEST_SEED_PHRASE): Promise { + async fillSrp(seedPhrase: string = E2E_SRP): Promise { await this.driver.pasteIntoField(this.srpWord0, seedPhrase); } diff --git a/test/e2e/tests/network/multi-rpc.spec.ts b/test/e2e/tests/network/multi-rpc.spec.ts index 362a4c3e29e4..ac693361435d 100644 --- a/test/e2e/tests/network/multi-rpc.spec.ts +++ b/test/e2e/tests/network/multi-rpc.spec.ts @@ -79,11 +79,11 @@ describe('MultiRpc:', function (this: Suite) { testSpecificMock: mockRPCURLAndChainId, }, - async ({ driver }: { driver: Driver }) => { - await completeImportSRPOnboardingFlow(driver); + async ({ driver, ganacheServer }) => { + await completeImportSRPOnboardingFlow({ driver }); const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); - await homePage.check_expectedBalanceIsDisplayed(); + await homePage.check_ganacheBalanceIsDisplayed(ganacheServer); await new HeaderNavbar(driver).clickSwitchNetworkDropDown(); const selectNetworkDialog = new SelectNetwork(driver); @@ -348,7 +348,7 @@ describe('MultiRpc:', function (this: Suite) { }, async ({ driver }: { driver: Driver }) => { - await importSRPOnboardingFlow(driver); + await importSRPOnboardingFlow({ driver }); const onboardingCompletePage = new OnboardingCompletePage(driver); await onboardingCompletePage.check_pageIsLoaded(); await onboardingCompletePage.navigateToDefaultPrivacySettings(); diff --git a/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts b/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts index 1494d8b2ceb3..7b9e2378b058 100644 --- a/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/importing-private-key-account.spec.ts @@ -1,9 +1,5 @@ import { Mockttp } from 'mockttp'; -import { - withFixtures, - defaultGanacheOptions, - completeImportSRPOnboardingFlow, -} from '../../../helpers'; +import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; import { @@ -14,6 +10,8 @@ import { import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; import { accountsSyncMockResponse } from './mockData'; import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; @@ -28,7 +26,6 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server, { @@ -42,12 +39,14 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -74,7 +73,6 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server); @@ -85,12 +83,14 @@ describe('Account syncing - Import With Private Key @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts b/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts index eb0c2c7b65e8..992027dd7840 100644 --- a/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/new-user-sync.spec.ts @@ -1,16 +1,16 @@ import { Mockttp } from 'mockttp'; -import { - withFixtures, - defaultGanacheOptions, - completeImportSRPOnboardingFlow, - completeCreateNewWalletOnboardingFlow, -} from '../../../helpers'; +import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; import { NOTIFICATIONS_TEAM_PASSWORD } from '../constants'; import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { + completeCreateNewWalletOnboardingFlow, + completeImportSRPOnboardingFlow, +} from '../../../page-objects/flows/onboarding.flow'; import { getSRP, IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; describe('Account syncing - New User @no-mmi', function () { @@ -26,7 +26,6 @@ describe('Account syncing - New User @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server); @@ -38,13 +37,14 @@ describe('Account syncing - New User @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - // Create a new wallet await completeCreateNewWalletOnboardingFlow( driver, NOTIFICATIONS_TEAM_PASSWORD, ); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); // Open account menu and validate 1 account is shown const header = new HeaderNavbar(driver); @@ -75,7 +75,6 @@ describe('Account syncing - New User @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server); @@ -86,14 +85,15 @@ describe('Account syncing - New User @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - // Onboard with import flow using SRP from new account created above - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - walletSrp, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: walletSrp, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); // Open account menu and validate the 2 accounts have been retrieved const header = new HeaderNavbar(driver); diff --git a/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts b/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts index 5cf0bb3c4d19..f9574a27cb10 100644 --- a/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/onboarding-with-opt-out.spec.ts @@ -1,12 +1,5 @@ import { Mockttp } from 'mockttp'; -import { - withFixtures, - defaultGanacheOptions, - completeImportSRPOnboardingFlow, - importSRPOnboardingFlow, - onboardingCompleteWalletCreationWithOptOut, - completeCreateNewWalletOnboardingFlowWithOptOut, -} from '../../../helpers'; +import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; import { @@ -14,10 +7,18 @@ import { NOTIFICATIONS_TEAM_SEED_PHRASE, } from '../constants'; import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; -import HeaderNavbar from '../../../page-objects/pages/header-navbar'; import AccountListPage from '../../../page-objects/pages/account-list-page'; -import { accountsSyncMockResponse } from './mockData'; +import HeaderNavbar from '../../../page-objects/pages/header-navbar'; +import HomePage from '../../../page-objects/pages/homepage'; +import OnboardingCompletePage from '../../../page-objects/pages/onboarding/onboarding-complete-page'; +import OnboardingPrivacySettingsPage from '../../../page-objects/pages/onboarding/onboarding-privacy-settings-page'; +import { + createNewWalletOnboardingFlow, + importSRPOnboardingFlow, + completeImportSRPOnboardingFlow, +} from '../../../page-objects/flows/onboarding.flow'; import { getSRP, IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; +import { accountsSyncMockResponse } from './mockData'; describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { if (!IS_ACCOUNT_SYNCING_ENABLED) { @@ -31,7 +32,6 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { // Mocks are still set up to ensure that requests are not matched @@ -45,19 +45,25 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await importSRPOnboardingFlow( + await importSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); - - await onboardingCompleteWalletCreationWithOptOut(driver, { - isNewWallet: false, - basicFunctionality: false, - profileSync: true, - assets: false, + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, }); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = + new OnboardingPrivacySettingsPage(driver); + await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -85,7 +91,6 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { // Mocks are still set up to ensure that requests are not matched @@ -97,17 +102,24 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeCreateNewWalletOnboardingFlowWithOptOut( + await createNewWalletOnboardingFlow( driver, NOTIFICATIONS_TEAM_PASSWORD, - { - isNewWallet: true, - basicFunctionality: false, - profileSync: true, - assets: false, - }, ); + const onboardingCompletePage = new OnboardingCompletePage(driver); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.navigateToDefaultPrivacySettings(); + + const onboardingPrivacySettingsPage = + new OnboardingPrivacySettingsPage(driver); + await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings(); + await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage(); + await onboardingCompletePage.check_pageIsLoaded(); + await onboardingCompletePage.completeOnboarding(); + + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -120,7 +132,6 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { 'Account 1', ); await accountListPage.addNewAccountWithCustomLabel('New Account'); - // Set SRP to use for retreival walletSrp = await getSRP(driver, NOTIFICATIONS_TEAM_PASSWORD); if (!walletSrp) { @@ -132,7 +143,6 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { // Mocks are still set up to ensure that requests are not matched @@ -144,12 +154,14 @@ describe('Account syncing - Opt-out Profile Sync @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - walletSrp, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: walletSrp, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts index d6c0dc373f69..31f92520f13e 100644 --- a/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/sync-after-adding-account.spec.ts @@ -1,9 +1,5 @@ import { Mockttp } from 'mockttp'; -import { - withFixtures, - defaultGanacheOptions, - completeImportSRPOnboardingFlow, -} from '../../../helpers'; +import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; import { @@ -13,6 +9,8 @@ import { import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; import { accountsSyncMockResponse } from './mockData'; import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; @@ -27,7 +25,6 @@ describe('Account syncing - Add Account @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server, { @@ -41,12 +38,14 @@ describe('Account syncing - Add Account @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -72,7 +71,6 @@ describe('Account syncing - Add Account @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server); @@ -83,12 +81,14 @@ describe('Account syncing - Add Account @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -122,7 +122,6 @@ describe('Account syncing - Add Account @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server, { @@ -136,12 +135,14 @@ describe('Account syncing - Add Account @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -165,7 +166,6 @@ describe('Account syncing - Add Account @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server); @@ -176,12 +176,14 @@ describe('Account syncing - Add Account @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts index 418962b370de..45ee3ab23a85 100644 --- a/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/sync-after-modifying-account-name.spec.ts @@ -1,9 +1,5 @@ import { Mockttp } from 'mockttp'; -import { - withFixtures, - defaultGanacheOptions, - completeImportSRPOnboardingFlow, -} from '../../../helpers'; +import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; import { @@ -13,6 +9,8 @@ import { import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; import { accountsSyncMockResponse } from './mockData'; import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; @@ -27,7 +25,6 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server, { @@ -41,12 +38,14 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); @@ -71,7 +70,6 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server); @@ -82,12 +80,14 @@ describe('Account syncing - Rename Accounts @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts b/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts index 4ec904256525..5bebe7220e49 100644 --- a/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts +++ b/test/e2e/tests/notifications/account-syncing/sync-after-onboarding.spec.ts @@ -1,9 +1,5 @@ import { Mockttp } from 'mockttp'; -import { - withFixtures, - defaultGanacheOptions, - completeImportSRPOnboardingFlow, -} from '../../../helpers'; +import { withFixtures } from '../../../helpers'; import FixtureBuilder from '../../../fixture-builder'; import { mockNotificationServices } from '../mocks'; import { @@ -13,6 +9,8 @@ import { import { UserStorageMockttpController } from '../../../helpers/user-storage/userStorageMockttpController'; import HeaderNavbar from '../../../page-objects/pages/header-navbar'; import AccountListPage from '../../../page-objects/pages/account-list-page'; +import HomePage from '../../../page-objects/pages/homepage'; +import { completeImportSRPOnboardingFlow } from '../../../page-objects/flows/onboarding.flow'; import { accountsSyncMockResponse } from './mockData'; import { IS_ACCOUNT_SYNCING_ENABLED } from './helpers'; @@ -27,7 +25,6 @@ describe('Account syncing - Onboarding @no-mmi', function () { await withFixtures( { fixtures: new FixtureBuilder({ onboarding: true }).build(), - ganacheOptions: defaultGanacheOptions, title: this.test?.fullTitle(), testSpecificMock: (server: Mockttp) => { userStorageMockttpController.setupPath('accounts', server, { @@ -40,12 +37,14 @@ describe('Account syncing - Onboarding @no-mmi', function () { }, }, async ({ driver }) => { - await driver.navigate(); - await completeImportSRPOnboardingFlow( + await completeImportSRPOnboardingFlow({ driver, - NOTIFICATIONS_TEAM_SEED_PHRASE, - NOTIFICATIONS_TEAM_PASSWORD, - ); + seedPhrase: NOTIFICATIONS_TEAM_SEED_PHRASE, + password: NOTIFICATIONS_TEAM_PASSWORD, + }); + const homePage = new HomePage(driver); + await homePage.check_pageIsLoaded(); + await homePage.check_expectedBalanceIsDisplayed(); const header = new HeaderNavbar(driver); await header.check_pageIsLoaded(); diff --git a/test/e2e/tests/onboarding/onboarding.spec.ts b/test/e2e/tests/onboarding/onboarding.spec.ts index aa04ca151f86..9ea81f040998 100644 --- a/test/e2e/tests/onboarding/onboarding.spec.ts +++ b/test/e2e/tests/onboarding/onboarding.spec.ts @@ -1,5 +1,6 @@ import { convertToHexValue, + TEST_SEED_PHRASE, WALLET_PASSWORD, withFixtures, } from '../../helpers'; @@ -55,7 +56,7 @@ describe('MetaMask onboarding @no-mmi', function () { title: this.test?.fullTitle(), }, async ({ driver }: { driver: Driver }) => { - await completeImportSRPOnboardingFlow(driver); + await completeImportSRPOnboardingFlow({ driver }); const homePage = new HomePage(driver); await homePage.check_pageIsLoaded(); await homePage.check_expectedBalanceIsDisplayed(); @@ -164,7 +165,10 @@ describe('MetaMask onboarding @no-mmi', function () { title: this.test?.fullTitle(), }, async ({ driver, secondaryGanacheServer }) => { - await importSRPOnboardingFlow(driver); + await importSRPOnboardingFlow({ + driver, + seedPhrase: TEST_SEED_PHRASE, + }); const onboardingCompletePage = new OnboardingCompletePage(driver); await onboardingCompletePage.check_pageIsLoaded(); @@ -209,7 +213,7 @@ describe('MetaMask onboarding @no-mmi', function () { title: this.test?.fullTitle(), }, async ({ driver }) => { - await importSRPOnboardingFlow(driver); + await importSRPOnboardingFlow({ driver }); const onboardingCompletePage = new OnboardingCompletePage(driver); await onboardingCompletePage.check_pageIsLoaded(); diff --git a/test/e2e/tests/privacy/onboarding-infura-call-privacy.spec.ts b/test/e2e/tests/privacy/onboarding-infura-call-privacy.spec.ts index b18d713d9474..1644cb068a3a 100644 --- a/test/e2e/tests/privacy/onboarding-infura-call-privacy.spec.ts +++ b/test/e2e/tests/privacy/onboarding-infura-call-privacy.spec.ts @@ -146,7 +146,7 @@ describe('MetaMask onboarding @no-mmi', function () { testSpecificMock: mockInfura, }, async ({ driver, mockedEndpoint: mockedEndpoints }) => { - await importSRPOnboardingFlow(driver); + await importSRPOnboardingFlow({ driver }); // Check no requests before completing onboarding // Intended delay to ensure we cover at least 1 polling loop of time for the network request From 04e4542ea5cb434ccb5d596b326e4e899335a662 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:35:59 +0100 Subject: [PATCH 032/111] chore: add the gas_included prop into Quotes Requested event (#28295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adds the gas_included prop into Quotes Requested event in swaps. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28295?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to Swaps on Ethereum mainnet 2. Fill in the form with max ETH -> ERC20 3. You will see in a network request that the new prop is there for the Quotes Requested event ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- ui/ducks/swaps/swaps.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index 8dd7336d7a62..d23c0ce69381 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -858,6 +858,7 @@ export const fetchQuotesAndSetQuoteState = ( stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: getSmartTransactionsOptInStatusForMetrics(state), + gas_included: newSelectedQuote.isGasIncludedTrade, anonymizedData: true, }, }); From 74e163fc106f96f4eea44cc78f180f2e706a5b16 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:51:28 +0100 Subject: [PATCH 033/111] fix: flaky test `Phishing Detection Via Iframe should redirect users to the the MetaMask Phishing Detection page when an iframe domain is on the phishing blocklist` (#28293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** There is a race condition in our Extension e2e tests, where we click the continue to unsafe link before the event listener has been attached. This causes the click to not take any effect and the test fail sometimes. Here is an example of the failure: https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/103699/workflows/b2ef7c35-f54a-4ea8-bd8b-15e57585d6fe/jobs/3865722/artifacts See how after clicking the anchor element, the redirect never happens, because the click was done before the event listener was added. ![image](https://github.com/user-attachments/assets/92663874-9175-4aa0-af86-0c4859e087bf) [The fix has been done in the phishing page level](https://github.com/MetaMask/phishing-warning/pull/173), where I added a new test id once the event listener click has been added, so we can now wait for that selector id to be in the page, before clicking. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28293?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27661 ## **Manual testing steps** 1. Check ci ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- .../phishing-detection.spec.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/e2e/tests/phishing-controller/phishing-detection.spec.js b/test/e2e/tests/phishing-controller/phishing-detection.spec.js index 67fb82f8fa55..ad199cea1e70 100644 --- a/test/e2e/tests/phishing-controller/phishing-detection.spec.js +++ b/test/e2e/tests/phishing-controller/phishing-detection.spec.js @@ -58,6 +58,12 @@ describe('Phishing Detection', function () { await unlockWallet(driver); await openDapp(driver); await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); + + // we need to wait for this selector to mitigate a race condition on the phishing page site + // see more here https://github.com/MetaMask/phishing-warning/pull/173 + await driver.waitForSelector({ + testId: 'unsafe-continue-loaded', + }); await driver.clickElement({ text: 'Proceed anyway', }); @@ -103,10 +109,15 @@ describe('Phishing Detection', function () { } await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); + + // we need to wait for this selector to mitigate a race condition on the phishing page site + // see more here https://github.com/MetaMask/phishing-warning/pull/173 + await driver.waitForSelector({ + testId: 'unsafe-continue-loaded', + }); await driver.clickElement({ text: 'Proceed anyway', }); - await driver.wait(until.titleIs(WINDOW_TITLES.TestDApp), 10000); }; } @@ -169,6 +180,12 @@ describe('Phishing Detection', function () { text: 'Open this warning in a new tab', }); await driver.switchToWindowWithTitle('MetaMask Phishing Detection'); + + // we need to wait for this selector to mitigate a race condition on the phishing page site + // see more here https://github.com/MetaMask/phishing-warning/pull/173 + await driver.waitForSelector({ + testId: 'unsafe-continue-loaded', + }); await driver.clickElement({ text: 'Proceed anyway', }); From f117f7c864526ac9c326912b8050bc1456cb7697 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 6 Nov 2024 16:25:57 +0530 Subject: [PATCH 034/111] fix: use transaction address to get lock for custom nonce (#28272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Use transaction from address to get custom nonce value. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28014 ## **Manual testing steps** 1. Enable an account connected to the dApp 2. Switch to non-enabled account in the MetaMask 3. Initiate a transaction from the dApp 4. Ensure correct nonce is displayed ## **Screenshots/Recordings** TODO ## **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 if applicable - [ ] 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/develop/.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. --- ui/hooks/useMMICustodySendTransaction.ts | 6 +++--- .../approve-static-simulation.tsx | 5 ++--- .../components/confirm/info/approve/approve.tsx | 5 ++--- .../edit-spending-cap-modal.tsx | 5 ++--- .../revoke-static-simulation.tsx | 5 ++--- .../info/approve/spending-cap/spending-cap.tsx | 10 ++++------ .../shared/advanced-details/advanced-details.tsx | 14 ++++++++++++-- .../confirm-approve/confirm-approve.js | 4 ++-- .../confirm-transaction-base.component.js | 9 ++++++--- .../confirm-transaction-base.container.js | 2 +- .../token-allowance/token-allowance.js | 8 ++++---- ui/store/actions.ts | 11 ++++------- 12 files changed, 44 insertions(+), 40 deletions(-) diff --git a/ui/hooks/useMMICustodySendTransaction.ts b/ui/hooks/useMMICustodySendTransaction.ts index 0c05d9e16f96..49634fbf0174 100644 --- a/ui/hooks/useMMICustodySendTransaction.ts +++ b/ui/hooks/useMMICustodySendTransaction.ts @@ -34,9 +34,9 @@ export function useMMICustodySendTransaction() { const accountType = useSelector(getAccountType); const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); - const { currentConfirmation } = useConfirmContext() as unknown as { - currentConfirmation: TransactionMeta | undefined; - }; + const { currentConfirmation } = useConfirmContext< + TransactionMeta | undefined + >(); const { from } = getConfirmationSender(currentConfirmation); const fromChecksumHexAddress = toChecksumHexAddress(from || ''); diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx index f1117e2a22b2..ad8a3565f619 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve-static-simulation/approve-static-simulation.tsx @@ -25,9 +25,8 @@ import { useIsNFT } from '../hooks/use-is-nft'; export const ApproveStaticSimulation = () => { const t = useI18nContext(); - const { currentConfirmation: transactionMeta } = useConfirmContext() as { - currentConfirmation: TransactionMeta; - }; + const { currentConfirmation: transactionMeta } = + useConfirmContext(); const { decimals: initialDecimals } = useAssetDetails( transactionMeta?.txParams?.to, diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx index fed03a75e17e..fa1766a8aa72 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -18,9 +18,8 @@ import { RevokeStaticSimulation } from './revoke-static-simulation/revoke-static import { SpendingCap } from './spending-cap/spending-cap'; const ApproveInfo = () => { - const { currentConfirmation: transactionMeta } = useConfirmContext() as { - currentConfirmation: TransactionMeta; - }; + const { currentConfirmation: transactionMeta } = + useConfirmContext(); const { isNFT } = useIsNFT(transactionMeta); diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx index f908333e4f25..961e63ae8f92 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx @@ -47,9 +47,8 @@ export const EditSpendingCapModal = ({ const dispatch = useDispatch(); - const { currentConfirmation: transactionMeta } = useConfirmContext() as { - currentConfirmation: TransactionMeta; - }; + const { currentConfirmation: transactionMeta } = + useConfirmContext(); const { userBalance, tokenSymbol, decimals } = useAssetDetails( transactionMeta.txParams.to, diff --git a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx index 77cee271d667..e121e01b6320 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/revoke-static-simulation/revoke-static-simulation.tsx @@ -12,9 +12,8 @@ import StaticSimulation from '../../shared/static-simulation/static-simulation'; export const RevokeStaticSimulation = () => { const t = useI18nContext(); - const { currentConfirmation: transactionMeta } = useConfirmContext() as { - currentConfirmation: TransactionMeta; - }; + const { currentConfirmation: transactionMeta } = + useConfirmContext(); const { chainId } = transactionMeta; diff --git a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx index 2ebd9d8e7e4e..638ff638844c 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx @@ -28,9 +28,8 @@ const SpendingCapGroup = ({ }) => { const t = useI18nContext(); - const { currentConfirmation: transactionMeta } = useConfirmContext() as { - currentConfirmation: TransactionMeta; - }; + const { currentConfirmation: transactionMeta } = + useConfirmContext(); const { spendingCap, formattedSpendingCap, value } = useApproveTokenSimulation(transactionMeta, decimals); @@ -81,9 +80,8 @@ export const SpendingCap = ({ }) => { const t = useI18nContext(); - const { currentConfirmation: transactionMeta } = useConfirmContext() as { - currentConfirmation: TransactionMeta; - }; + const { currentConfirmation: transactionMeta } = + useConfirmContext(); const { userBalance, tokenSymbol, decimals } = useAssetDetails( transactionMeta.txParams.to, diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx index ebb0f69d75c1..d0a6d72d11e9 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.tsx @@ -1,5 +1,7 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { TransactionMeta } from '@metamask/transaction-controller'; + import { ConfirmInfoRow, ConfirmInfoRowText, @@ -17,15 +19,23 @@ import { updateCustomNonce, } from '../../../../../../../store/actions'; import { selectConfirmationAdvancedDetailsOpen } from '../../../../../selectors/preferences'; +import { useConfirmContext } from '../../../../../context/confirm'; +import { isSignatureTransactionType } from '../../../../../utils'; import { TransactionData } from '../transaction-data/transaction-data'; const NonceDetails = () => { + const { currentConfirmation } = useConfirmContext(); const t = useI18nContext(); const dispatch = useDispatch(); useEffect(() => { - dispatch(getNextNonce()); - }, [dispatch]); + if ( + currentConfirmation && + !isSignatureTransactionType(currentConfirmation) + ) { + dispatch(getNextNonce(currentConfirmation.txParams.from)); + } + }, [currentConfirmation, dispatch]); const enableCustomNonce = useSelector(getUseNonceField); const nextNonce = useSelector(getNextSuggestedNonce); diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js index a5dcaeb6202d..1b604ec43e6b 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js @@ -66,7 +66,7 @@ export default function ConfirmApprove({ isSetApproveForAll, }) { const dispatch = useDispatch(); - const { txParams: { data: transactionData } = {} } = transaction; + const { txParams: { data: transactionData, from } = {} } = transaction; const currentCurrency = useSelector(getCurrentCurrency); const nativeCurrency = useSelector(getNativeCurrency); @@ -266,7 +266,7 @@ export default function ConfirmApprove({ updateCustomNonce={(value) => { dispatch(updateCustomNonce(value)); }} - getNextNonce={() => dispatch(getNextNonce())} + getNextNonce={() => dispatch(getNextNonce(from))} showCustomizeNonceModal={({ /* eslint-disable no-shadow */ useNonceField, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index c07b8a263f8a..25d99e8a9f16 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -217,6 +217,7 @@ export default class ConfirmTransactionBase extends Component { mostRecentOverviewPage, txData, getNextNonce, + fromAddress, } = this.props; const { @@ -236,7 +237,7 @@ export default class ConfirmTransactionBase extends Component { transactionStatus === TransactionStatus.confirmed; if (txData.id !== prevTxData.id) { - getNextNonce(); + getNextNonce(fromAddress); } if ( @@ -413,6 +414,7 @@ export default class ConfirmTransactionBase extends Component { tokenSymbol, isUsingPaymaster, isSigningOrSubmitting, + fromAddress, } = this.props; const { t } = this.context; @@ -447,7 +449,7 @@ export default class ConfirmTransactionBase extends Component { updateCustomNonce(inputValue); - getNextNonce(); + getNextNonce(fromAddress); }; const renderTotalMaxAmount = ({ @@ -1012,6 +1014,7 @@ export default class ConfirmTransactionBase extends Component { this._isMounted = true; const { toAddress, + fromAddress, txData: { origin, chainId: txChainId } = {}, getNextNonce, tryReverseResolveAddress, @@ -1041,7 +1044,7 @@ export default class ConfirmTransactionBase extends Component { }, }); - getNextNonce(); + getNextNonce(fromAddress); if (toAddress) { tryReverseResolveAddress(toAddress); } diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index e06090f48e75..795dfae0c63a 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -429,7 +429,7 @@ export const mapDispatchToProps = (dispatch) => { fetchSmartTransactionsLiveness: () => { dispatch(fetchSmartTransactionsLiveness()); }, - getNextNonce: () => dispatch(getNextNonce()), + getNextNonce: (address) => dispatch(getNextNonce(address)), setNextNonce: (val) => dispatch(setNextNonce(val)), setDefaultHomeActiveTabName: (tabName) => dispatch(setDefaultHomeActiveTabName(tabName)), diff --git a/ui/pages/confirmations/token-allowance/token-allowance.js b/ui/pages/confirmations/token-allowance/token-allowance.js index 8ccd19ecb566..61bf5e0974df 100644 --- a/ui/pages/confirmations/token-allowance/token-allowance.js +++ b/ui/pages/confirmations/token-allowance/token-allowance.js @@ -347,12 +347,12 @@ export default function TokenAllowance({ }; const handleNextNonce = useCallback(() => { - dispatch(getNextNonce()); - }, [getNextNonce, dispatch]); + dispatch(getNextNonce(txData.txParams.from)); + }, [dispatch, txData.txParams.from]); useEffect(() => { - dispatch(getNextNonce()); - }, [getNextNonce, dispatch]); + dispatch(getNextNonce(txData.txParams.from)); + }, [dispatch, txData.txParams.from]); const handleUpdateCustomNonce = (value) => { dispatch(updateCustomNonce(value)); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 11df6a3f3e20..8018595371f4 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4374,16 +4374,13 @@ export function setNextNonce(nextNonce: string): PayloadAction { * accidental usage of a stale nonce as the call to getNextNonce only works for * the currently selected address. * + * @param address - address for which nonce lock shouuld be obtained. * @returns */ -export function getNextNonce(): ThunkAction< - Promise, - MetaMaskReduxState, - unknown, - AnyAction -> { +export function getNextNonce( + address, +): ThunkAction, MetaMaskReduxState, unknown, AnyAction> { return async (dispatch, getState) => { - const { address } = getSelectedInternalAccount(getState()); const networkClientId = getSelectedNetworkClientId(getState()); let nextNonce; try { From e1f09aa52ff0019221396010abd03a96ed62b2b8 Mon Sep 17 00:00:00 2001 From: digiwand <20778143+digiwand@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:44:13 +0700 Subject: [PATCH 035/111] fix: GasDetailItem invalid paddingStart prop (#28281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes console error that appears no ui/ux changes [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28281?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Use native send tx confirmation 2. Set "Max" fee 3. Click continue 4. notice error ## **Screenshots/Recordings** ### **Before** ![CleanShot 2024-11-01 at 01 27 44@2x](https://github.com/user-attachments/assets/e639850f-7999-4eba-83aa-780c28778e02) ### **After** error no longer occurs ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- .../__snapshots__/confirm-gas-display.test.js.snap | 7 +++---- .../components/gas-details-item/gas-details-item.js | 8 ++++---- .../__snapshots__/confirm-send-ether.test.js.snap | 7 +++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap b/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap index e35c865829b8..aa39688362e6 100644 --- a/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap +++ b/ui/pages/confirmations/components/confirm-gas-display/__snapshots__/confirm-gas-display.test.js.snap @@ -93,10 +93,10 @@ exports[`ConfirmGasDisplay should match snapshot 1`] = ` class="mm-box mm-text transaction-detail-item__row-subText mm-text--body-sm mm-text--text-align-end mm-box--color-text-alternative" >

-
-
+ } diff --git a/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap b/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap index 0da1e036c9f0..9ec3f2ac08d2 100644 --- a/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap +++ b/ui/pages/confirmations/confirm-send-ether/__snapshots__/confirm-send-ether.test.js.snap @@ -503,10 +503,10 @@ exports[`ConfirmSendEther should render correct information for for confirm send class="mm-box mm-text transaction-detail-item__row-subText mm-text--body-sm mm-text--text-align-end mm-box--color-text-alternative" >

Date: Wed, 6 Nov 2024 19:34:35 +0700 Subject: [PATCH 036/111] feat: Enable simulation metrics for redesign transactions (#28280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28280?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28292 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- .../confirm/info/base-transaction-info/base-transaction-info.tsx | 1 + .../components/confirm/info/native-transfer/native-transfer.tsx | 1 + .../confirm/info/nft-token-transfer/nft-token-transfer.tsx | 1 + .../components/confirm/info/token-transfer/token-transfer.tsx | 1 + 4 files changed, 4 insertions(+) diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx b/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx index 08329536b524..23629ee5096c 100644 --- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx +++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx @@ -22,6 +22,7 @@ const BaseTransactionInfo = () => { diff --git a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx index a2dd3ceaaa05..c098936989dd 100644 --- a/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx +++ b/ui/pages/confirmations/components/confirm/info/native-transfer/native-transfer.tsx @@ -24,6 +24,7 @@ const NativeTransferInfo = () => { )} diff --git a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/nft-token-transfer.tsx b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/nft-token-transfer.tsx index b5d579994ef9..113920a6aec2 100644 --- a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/nft-token-transfer.tsx +++ b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/nft-token-transfer.tsx @@ -24,6 +24,7 @@ const NFTTokenTransferInfo = () => { )} diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx index df1136ec5213..50e9d85936f0 100644 --- a/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx +++ b/ui/pages/confirmations/components/confirm/info/token-transfer/token-transfer.tsx @@ -24,6 +24,7 @@ const TokenTransferInfo = () => { )} From e5b415f97a4de7eab023eb689357f467c0e6f5c5 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 6 Nov 2024 18:09:35 +0530 Subject: [PATCH 037/111] chore: adding e2e tests for NFT permit (#28004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Adding e2e tests for NFT permit. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/27908 ## **Manual testing steps** NA ## **Screenshots/Recordings** NA ## **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 if applicable - [ ] 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/develop/.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. --- package.json | 2 +- privacy-snapshot.json | 1 + .../signatures/nft-permit.spec.ts | 187 ++++++++++++++++++ .../signatures/signature-helpers.ts | 14 +- .../tests/tokens/add-multiple-tokens.spec.js | 11 +- yarn.lock | 10 +- 6 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 test/e2e/tests/confirmations/signatures/nft-permit.spec.ts diff --git a/package.json b/package.json index 1672e6845873..921472a1c27e 100644 --- a/package.json +++ b/package.json @@ -466,7 +466,7 @@ "@metamask/phishing-warning": "^4.1.0", "@metamask/preferences-controller": "^13.0.2", "@metamask/test-bundler": "^1.0.0", - "@metamask/test-dapp": "8.7.0", + "@metamask/test-dapp": "8.13.0", "@octokit/core": "^3.6.0", "@open-rpc/meta-schema": "^1.14.6", "@open-rpc/mock-server": "^1.7.5", diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 589504ea2cc7..6e041ea3d71b 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -32,6 +32,7 @@ "localhost:8000", "localhost:8545", "mainnet.infura.io", + "metamask-sdk.api.cx.metamask.io", "metamask.eth", "metamask.github.io", "metametrics.metamask.test", diff --git a/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts new file mode 100644 index 000000000000..383a3bd6b924 --- /dev/null +++ b/test/e2e/tests/confirmations/signatures/nft-permit.spec.ts @@ -0,0 +1,187 @@ +import { strict as assert } from 'assert'; +import { TransactionEnvelopeType } from '@metamask/transaction-controller'; +import { Suite } from 'mocha'; +import { MockedEndpoint } from 'mockttp'; +import { DAPP_HOST_ADDRESS, WINDOW_TITLES } from '../../../helpers'; +import { Ganache } from '../../../seeder/ganache'; +import { Driver } from '../../../webdriver/driver'; +import { + mockSignatureApproved, + mockSignatureRejected, + scrollAndConfirmAndAssertConfirm, + withRedesignConfirmationFixtures, +} from '../helpers'; +import { TestSuiteArguments } from '../transactions/shared'; +import { + assertAccountDetailsMetrics, + assertPastedAddress, + assertSignatureConfirmedMetrics, + assertSignatureRejectedMetrics, + clickHeaderInfoBtn, + copyAddressAndPasteWalletAddress, + openDappAndTriggerDeploy, + SignatureType, + triggerSignature, +} from './signature-helpers'; + +describe('Confirmation Signature - NFT Permit @no-mmi', function (this: Suite) { + it('initiates and confirms and emits the correct events', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ + driver, + ganacheServer, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + const addresses = await (ganacheServer as Ganache).getAccounts(); + const publicAddress = addresses?.[0] as string; + + await openDappAndTriggerDeploy(driver); + await driver.delay(1000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement('[data-testid="confirm-footer-button"]'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.delay(1000); + await triggerSignature(driver, SignatureType.NFTPermit); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await clickHeaderInfoBtn(driver); + await copyAddressAndPasteWalletAddress(driver); + await assertPastedAddress(driver); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + + await assertInfoValues(driver); + await scrollAndConfirmAndAssertConfirm(driver); + await driver.delay(1000); + + await assertAccountDetailsMetrics( + driver, + mockedEndpoints as MockedEndpoint[], + 'eth_signTypedData_v4', + ); + + await assertSignatureConfirmedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Permit', + uiCustomizations: ['redesigned_confirmation', 'permit'], + }); + + await assertVerifiedResults(driver, publicAddress); + }, + mockSignatureApproved, + ); + }); + + it('initiates and rejects and emits the correct events', async function () { + await withRedesignConfirmationFixtures( + this.test?.fullTitle(), + TransactionEnvelopeType.legacy, + async ({ + driver, + mockedEndpoint: mockedEndpoints, + }: TestSuiteArguments) => { + await openDappAndTriggerDeploy(driver); + await driver.delay(1000); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); + await driver.clickElement('[data-testid="confirm-footer-button"]'); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.delay(1000); + await triggerSignature(driver, SignatureType.NFTPermit); + + await driver.clickElementAndWaitForWindowToClose( + '[data-testid="confirm-footer-cancel-button"]', + ); + + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + + await driver.waitForSelector({ + tag: 'span', + text: 'Error: User rejected the request.', + }); + + await assertSignatureRejectedMetrics({ + driver, + mockedEndpoints: mockedEndpoints as MockedEndpoint[], + signatureType: 'eth_signTypedData_v4', + primaryType: 'Permit', + uiCustomizations: ['redesigned_confirmation', 'permit'], + location: 'confirmation', + }); + }, + mockSignatureRejected, + ); + }); +}); + +async function assertInfoValues(driver: Driver) { + await driver.clickElement('[data-testid="sectionCollapseButton"]'); + const origin = driver.findElement({ text: DAPP_HOST_ADDRESS }); + const contractPetName = driver.findElement({ + css: '.name__value', + text: '0x581c3...45947', + }); + + const title = driver.findElement({ text: 'Withdrawal request' }); + const description = driver.findElement({ + text: 'This site wants permission to withdraw your NFTs', + }); + const primaryType = driver.findElement({ text: 'Permit' }); + const spender = driver.findElement({ + css: '.name__value', + text: '0x581c3...45947', + }); + const tokenId = driver.findElement({ text: '3606393' }); + const nonce = driver.findElement({ text: '0' }); + const deadline = driver.findElement({ text: '23 December 2024, 23:03' }); + + assert.ok(await origin, 'origin'); + assert.ok(await contractPetName, 'contractPetName'); + assert.ok(await title, 'title'); + assert.ok(await description, 'description'); + assert.ok(await primaryType, 'primaryType'); + assert.ok(await spender, 'spender'); + assert.ok(await tokenId, 'tokenId'); + assert.ok(await nonce, 'nonce'); + assert.ok(await deadline, 'deadline'); +} + +async function assertVerifiedResults(driver: Driver, publicAddress: string) { + await driver.waitUntilXWindowHandles(2); + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement('#sign721PermitVerify'); + + await driver.waitForSelector({ + css: '#sign721PermitVerifyResult', + text: publicAddress, + }); + + await driver.waitForSelector({ + css: '#sign721PermitResult', + text: '0x572bc6300f6aa669e85e0a7792bc0b0803fb70c3c492226b30007ff7030b03600e390ef295a5a525d19f444943ae82697f0e5b5b0d77cc382cb2ea9486ec27801c', + }); + + await driver.waitForSelector({ + css: '#sign721PermitResultR', + text: 'r: 0x572bc6300f6aa669e85e0a7792bc0b0803fb70c3c492226b30007ff7030b0360', + }); + + await driver.waitForSelector({ + css: '#sign721PermitResultS', + text: 's: 0x0e390ef295a5a525d19f444943ae82697f0e5b5b0d77cc382cb2ea9486ec2780', + }); + + await driver.waitForSelector({ + css: '#sign721PermitResultV', + text: 'v: 28', + }); + + await driver.waitForSelector({ + css: '#sign721PermitVerifyResult', + text: publicAddress, + }); +} diff --git a/test/e2e/tests/confirmations/signatures/signature-helpers.ts b/test/e2e/tests/confirmations/signatures/signature-helpers.ts index 9b87e5b4e9cc..5242be3f3c20 100644 --- a/test/e2e/tests/confirmations/signatures/signature-helpers.ts +++ b/test/e2e/tests/confirmations/signatures/signature-helpers.ts @@ -14,6 +14,7 @@ export const WALLET_ETH_BALANCE = '25'; export enum SignatureType { PersonalSign = '#personalSign', Permit = '#signPermit', + NFTPermit = '#sign721Permit', SignTypedDataV3 = '#signTypedDataV3', SignTypedDataV4 = '#signTypedDataV4', SignTypedData = '#signTypedData', @@ -240,12 +241,23 @@ export async function assertPastedAddress(driver: Driver) { assert.equal(await formFieldEl.getAttribute('value'), WALLET_ADDRESS); } +export async function triggerSignature(driver: Driver, type: string) { + await driver.clickElement(type); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); +} + export async function openDappAndTriggerSignature( driver: Driver, type: string, ) { await unlockWallet(driver); await openDapp(driver); - await driver.clickElement(type); + await triggerSignature(driver, type); +} + +export async function openDappAndTriggerDeploy(driver: Driver) { + await unlockWallet(driver); + await openDapp(driver); + await driver.clickElement('#deployNFTsButton'); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); } diff --git a/test/e2e/tests/tokens/add-multiple-tokens.spec.js b/test/e2e/tests/tokens/add-multiple-tokens.spec.js index 490460e770cb..8be2a430b622 100644 --- a/test/e2e/tests/tokens/add-multiple-tokens.spec.js +++ b/test/e2e/tests/tokens/add-multiple-tokens.spec.js @@ -28,6 +28,11 @@ describe('Multiple ERC20 Watch Asset', function () { await openDapp(driver, undefined, DAPP_URL); // Create Token 1 + const createToken = await driver.findElement({ + text: 'Create Token', + tag: 'button', + }); + await driver.scrollToElement(createToken); await driver.clickElement({ text: 'Create Token', tag: 'button' }); await switchToNotificationWindow(driver); await driver.findClickableElement({ text: 'Confirm', tag: 'button' }); @@ -37,7 +42,7 @@ describe('Multiple ERC20 Watch Asset', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.wait(async () => { const tokenAddressesElement = await driver.findElement( - '#tokenAddresses', + '#erc20TokenAddresses', ); const tokenAddresses = await tokenAddressesElement.getText(); return tokenAddresses !== ''; @@ -53,7 +58,7 @@ describe('Multiple ERC20 Watch Asset', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.wait(async () => { const tokenAddressesElement = await driver.findElement( - '#tokenAddresses', + '#erc20TokenAddresses', ); const tokenAddresses = await tokenAddressesElement.getText(); return tokenAddresses.split(',').length === 2; @@ -69,7 +74,7 @@ describe('Multiple ERC20 Watch Asset', function () { await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); await driver.wait(async () => { const tokenAddressesElement = await driver.findElement( - '#tokenAddresses', + '#erc20TokenAddresses', ); const tokenAddresses = await tokenAddressesElement.getText(); return tokenAddresses.split(',').length === 3; diff --git a/yarn.lock b/yarn.lock index 9ac1b7409679..4c3305e98b13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6386,10 +6386,10 @@ __metadata: languageName: node linkType: hard -"@metamask/test-dapp@npm:8.7.0": - version: 8.7.0 - resolution: "@metamask/test-dapp@npm:8.7.0" - checksum: 10/c2559179d3372e5fc8d67a60c1e4056fad9809486eaff6a2aa9c351a2a613eeecc15885a5fd9b71b8f4139058fe168abeac06bd6bdb6d4a47fe0b9b4146923ab +"@metamask/test-dapp@npm:8.13.0": + version: 8.13.0 + resolution: "@metamask/test-dapp@npm:8.13.0" + checksum: 10/b588e562ce81d94e22e8c19b6b160b6dc1c5f68314edaeeb3c886a0676d4bd21205c741d039a1a8ad2dfc1e87109239d18da17c322fbaa99f527b543b0032786 languageName: node linkType: hard @@ -26470,7 +26470,7 @@ __metadata: "@metamask/snaps-sdk": "npm:^6.10.0" "@metamask/snaps-utils": "npm:^8.5.1" "@metamask/test-bundler": "npm:^1.0.0" - "@metamask/test-dapp": "npm:8.7.0" + "@metamask/test-dapp": "npm:8.13.0" "@metamask/transaction-controller": "npm:^38.1.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^9.3.0" From 10102d0f025df131254dccf1a90731214ba5e589 Mon Sep 17 00:00:00 2001 From: seaona <54408225+seaona@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:33:33 +0100 Subject: [PATCH 038/111] chore: e2e quality gate enhancement (#28206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** With the label: ![Screenshot from 2024-10-31 12-02-14](https://github.com/user-attachments/assets/c112d2b3-4099-4bbd-bde2-1c97df04f9a4) When removing the label: ![Screenshot from 2024-10-31 12-11-56](https://github.com/user-attachments/assets/b8591786-b91d-4a11-9f4c-ba09c9f407d5) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28206?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. See here, were we skip the git diff as it had the label https://app.circleci.com/pipelines/github/MetaMask/metamask-extension/108789/workflows/01710cd9-1a1b-4c47-8f66-3796d57ea732/jobs/4069000 ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- .circleci/scripts/git-diff-develop.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.circleci/scripts/git-diff-develop.ts b/.circleci/scripts/git-diff-develop.ts index 43435db17418..f4437d6154db 100644 --- a/.circleci/scripts/git-diff-develop.ts +++ b/.circleci/scripts/git-diff-develop.ts @@ -20,6 +20,7 @@ type PRInfo = { ref: string; }; body: string; + labels: { name: string }[]; }; /** @@ -123,7 +124,7 @@ async function storeGitDiffOutputAndPrBody() { fs.mkdirSync(CHANGED_FILES_DIR, { recursive: true }); console.log( - `Determining whether this run is for a PR targeting ${MAIN_BRANCH}`, + `Determining whether to run git diff...`, ); if (!PR_NUMBER) { console.log('Not a PR, skipping git diff'); @@ -140,6 +141,9 @@ async function storeGitDiffOutputAndPrBody() { console.log(`This is for a PR targeting '${baseRef}', skipping git diff`); writePrBodyToFile(prInfo.body); return; + } else if (prInfo.labels.some(label => label.name === 'skip-e2e-quality-gate')) { + console.log('PR has the skip-e2e-quality-gate label, skipping git diff'); + return; } console.log('Attempting to get git diff...'); From 6a54c99301f019ab5ba7e7af5cba5390ed094651 Mon Sep 17 00:00:00 2001 From: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:56:51 -0300 Subject: [PATCH 039/111] chore: revert commit `3da34f4` (feat: btc e2e tests (#27986)) (#28323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3da34f448c9777722799d15389bdc0c83dfd28f9. ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28323?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- test/e2e/constants.ts | 13 -- test/e2e/flask/btc/btc-send.spec.ts | 153 ----------------- test/e2e/flask/btc/common-btc.ts | 158 +----------------- .../app/wallet-overview/coin-buttons.tsx | 26 +-- 4 files changed, 13 insertions(+), 337 deletions(-) delete mode 100644 test/e2e/flask/btc/btc-send.spec.ts diff --git a/test/e2e/constants.ts b/test/e2e/constants.ts index 2e24bf4042ba..8bf39d261bcb 100644 --- a/test/e2e/constants.ts +++ b/test/e2e/constants.ts @@ -50,16 +50,3 @@ export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252'; /* Default (mocked) BTC balance used by the Bitcoin RPC provider */ export const DEFAULT_BTC_BALANCE = 1; // BTC - -/* Default BTC fees rate */ -export const DEFAULT_BTC_FEES_RATE = 0.00001; // BTC - -/* Default BTC conversion rate to USD */ -export const DEFAULT_BTC_CONVERSION_RATE = 62000; // USD - -/* Default BTC transaction ID */ -export const DEFAULT_BTC_TRANSACTION_ID = - 'e4111a707317da67d49a71af4cbcf6c0546f900ca32c3842d2254e315d1fca18'; - -/* Number of sats in 1 BTC */ -export const SATS_IN_1_BTC = 100000000; // sats diff --git a/test/e2e/flask/btc/btc-send.spec.ts b/test/e2e/flask/btc/btc-send.spec.ts deleted file mode 100644 index c1a956cd00b7..000000000000 --- a/test/e2e/flask/btc/btc-send.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { strict as assert } from 'assert'; -import { Suite } from 'mocha'; -import { Driver } from '../../webdriver/driver'; -import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; -import { - getTransactionRequest, - SendFlowPlaceHolders, - withBtcAccountSnap, -} from './common-btc'; - -export async function startSendFlow(driver: Driver, recipient?: string) { - // Wait a bit so the MultichainRatesController is able to fetch BTC -> USD rates. - await driver.delay(1000); - - // Start the send flow. - const sendButton = await driver.waitForSelector({ - text: 'Send', - tag: 'button', - css: '[data-testid="coin-overview-send"]', - }); - await sendButton.click(); - - // See the review button is disabled by default. - await driver.waitForSelector({ - text: 'Review', - tag: 'button', - css: '[disabled]', - }); - - if (recipient) { - // Set the recipient address (if any). - await driver.pasteIntoField( - `input[placeholder="${SendFlowPlaceHolders.RECIPIENT}"]`, - recipient, - ); - } -} - -describe('BTC Account - Send', function (this: Suite) { - it('can send complete the send flow', async function () { - await withBtcAccountSnap( - { title: this.test?.fullTitle() }, - async (driver, mockServer) => { - await startSendFlow(driver, DEFAULT_BTC_ACCOUNT); - - // Set the amount to send. - const mockAmountToSend = '0.5'; - await driver.pasteIntoField( - `input[placeholder="${SendFlowPlaceHolders.AMOUNT}"]`, - mockAmountToSend, - ); - - // From here, the "summary panel" should have some information about the fees and total. - await driver.waitForSelector({ - text: 'Total', - tag: 'p', - }); - - // The review button will become available. - const snapReviewButton = await driver.findClickableElement({ - text: 'Review', - tag: 'button', - css: '.snap-ui-renderer__footer-button', - }); - assert.equal(await snapReviewButton.isEnabled(), true); - await snapReviewButton.click(); - - // TODO: There isn't any check for the fees and total amount. This requires calculating the vbytes used in a transaction dynamically. - // We already have unit tests for these calculations on the Snap. - - // ------------------------------------------------------------------------------ - // From here, we have moved to the confirmation screen (second part of the flow). - - // We should be able to send the transaction right away. - const snapSendButton = await driver.waitForSelector({ - text: 'Send', - tag: 'button', - css: '.snap-ui-renderer__footer-button', - }); - assert.equal(await snapSendButton.isEnabled(), true); - await snapSendButton.click(); - - // Check that we are selecting the "Activity tab" right after the send. - await driver.waitForSelector({ - tag: 'div', - text: 'Bitcoin activity is not supported', - }); - - const transaction = await getTransactionRequest(mockServer); - assert(transaction !== undefined); - }, - ); - }); - - it('can send the max amount', async function () { - await withBtcAccountSnap( - { title: this.test?.fullTitle() }, - async (driver, mockServer) => { - await startSendFlow(driver, DEFAULT_BTC_ACCOUNT); - - // Use the max spendable amount of that account. - await driver.clickElement({ - text: 'Max', - tag: 'button', - }); - - // From here, the "summary panel" should have some information about the fees and total. - await driver.waitForSelector({ - text: 'Total', - tag: 'p', - }); - - await driver.waitForSelector({ - text: `${DEFAULT_BTC_BALANCE} BTC`, - tag: 'p', - }); - - // The review button will become available. - const snapReviewButton = await driver.findClickableElement({ - text: 'Review', - tag: 'button', - css: '.snap-ui-renderer__footer-button', - }); - assert.equal(await snapReviewButton.isEnabled(), true); - await snapReviewButton.click(); - - // TODO: There isn't any check for the fees and total amount. This requires calculating the vbytes used in a transaction dynamically. - // We already have unit tests for these calculations on the snap. - - // ------------------------------------------------------------------------------ - // From here, we have moved to the confirmation screen (second part of the flow). - - // We should be able to send the transaction right away. - const snapSendButton = await driver.waitForSelector({ - text: 'Send', - tag: 'button', - css: '.snap-ui-renderer__footer-button', - }); - assert.equal(await snapSendButton.isEnabled(), true); - await snapSendButton.click(); - - // Check that we are selecting the "Activity tab" right after the send. - await driver.waitForSelector({ - tag: 'div', - text: 'Bitcoin activity is not supported', - }); - - const transaction = await getTransactionRequest(mockServer); - assert(transaction !== undefined); - }, - ); - }); -}); diff --git a/test/e2e/flask/btc/common-btc.ts b/test/e2e/flask/btc/common-btc.ts index 452f9ad44f6b..6891b3bfd60e 100644 --- a/test/e2e/flask/btc/common-btc.ts +++ b/test/e2e/flask/btc/common-btc.ts @@ -1,26 +1,11 @@ import { Mockttp } from 'mockttp'; import FixtureBuilder from '../../fixture-builder'; import { withFixtures, unlockWallet } from '../../helpers'; -import { - DEFAULT_BTC_ACCOUNT, - DEFAULT_BTC_BALANCE, - DEFAULT_BTC_FEES_RATE, - DEFAULT_BTC_TRANSACTION_ID, - DEFAULT_BTC_CONVERSION_RATE, - SATS_IN_1_BTC, -} from '../../constants'; +import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants'; import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; import { Driver } from '../../webdriver/driver'; import messages from '../../../../app/_locales/en/messages.json'; -const QUICKNODE_URL_REGEX = /^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u; - -export enum SendFlowPlaceHolders { - AMOUNT = 'Enter amount to send', - RECIPIENT = 'Enter receiving address', - LOADING = 'Preparing transaction', -} - export async function createBtcAccount(driver: Driver) { await driver.clickElement('[data-testid="account-menu-icon"]'); await driver.clickElement( @@ -42,17 +27,12 @@ export async function createBtcAccount(driver: Driver) { ); } -export function btcToSats(btc: number): number { - // Watchout, we're not using BigNumber(s) here (but that's ok for test purposes) - return btc * SATS_IN_1_BTC; -} - export async function mockBtcBalanceQuote( mockServer: Mockttp, address: string = DEFAULT_BTC_ACCOUNT, ) { return await mockServer - .forPost(QUICKNODE_URL_REGEX) + .forPost(/^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u) .withJsonBodyIncluding({ method: 'bb_getaddress', }) @@ -62,7 +42,7 @@ export async function mockBtcBalanceQuote( json: { result: { address, - balance: btcToSats(DEFAULT_BTC_BALANCE).toString(), // Converts from BTC to sats + balance: (DEFAULT_BTC_BALANCE * 1e8).toString(), // Converts from BTC to sats totalReceived: '0', totalSent: '0', unconfirmedBalance: '0', @@ -74,105 +54,6 @@ export async function mockBtcBalanceQuote( }); } -export async function mockBtcFeeCallQuote(mockServer: Mockttp) { - return await mockServer - .forPost(QUICKNODE_URL_REGEX) - .withJsonBodyIncluding({ - method: 'estimatesmartfee', - }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - result: { - blocks: 1, - feerate: DEFAULT_BTC_FEES_RATE, // sats - }, - }, - }; - }); -} - -export async function mockMempoolInfo(mockServer: Mockttp) { - return await mockServer - .forPost(QUICKNODE_URL_REGEX) - .withJsonBodyIncluding({ - method: 'getmempoolinfo', - }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - result: { - loaded: true, - size: 165194, - bytes: 93042828, - usage: 550175264, - total_fee: 1.60127931, - maxmempool: 2048000000, - mempoolminfee: DEFAULT_BTC_FEES_RATE, - minrelaytxfee: DEFAULT_BTC_FEES_RATE, - incrementalrelayfee: 0.00001, - unbroadcastcount: 0, - fullrbf: true, - }, - }, - }; - }); -} - -export async function mockGetUTXO(mockServer: Mockttp) { - return await mockServer - .forPost(QUICKNODE_URL_REGEX) - .withJsonBodyIncluding({ - method: 'bb_getutxos', - }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - result: [ - { - txid: DEFAULT_BTC_TRANSACTION_ID, - vout: 0, - value: btcToSats(DEFAULT_BTC_BALANCE).toString(), - height: 101100110, - confirmations: 6, - }, - ], - }, - }; - }); -} - -export async function mockSendTransaction(mockServer: Mockttp) { - return await mockServer - .forPost(QUICKNODE_URL_REGEX) - .withJsonBodyIncluding({ - method: 'sendrawtransaction', - }) - .thenCallback(() => { - return { - statusCode: 200, - json: { - result: DEFAULT_BTC_TRANSACTION_ID, - }, - }; - }); -} - -export async function mockRatesCall(mockServer: Mockttp) { - return await mockServer - .forGet('https://min-api.cryptocompare.com/data/pricemulti') - .withQuery({ fsyms: 'btc', tsyms: 'usd,USD' }) - .thenCallback(() => { - return { - statusCode: 200, - json: { BTC: { USD: DEFAULT_BTC_CONVERSION_RATE } }, - }; - }); -} - export async function mockRampsDynamicFeatureFlag( mockServer: Mockttp, subDomain: string, @@ -206,7 +87,7 @@ export async function withBtcAccountSnap( title, bitcoinSupportEnabled, }: { title?: string; bitcoinSupportEnabled?: boolean }, - test: (driver: Driver, mockServer: Mockttp) => Promise, + test: (driver: Driver) => Promise, ) { await withFixtures( { @@ -218,44 +99,17 @@ export async function withBtcAccountSnap( title, dapp: true, testSpecificMock: async (mockServer: Mockttp) => [ - await mockRatesCall(mockServer), await mockBtcBalanceQuote(mockServer), // See: PROD_RAMP_API_BASE_URL await mockRampsDynamicFeatureFlag(mockServer, 'api'), // See: UAT_RAMP_API_BASE_URL await mockRampsDynamicFeatureFlag(mockServer, 'uat-api'), - await mockMempoolInfo(mockServer), - await mockBtcFeeCallQuote(mockServer), - await mockGetUTXO(mockServer), - await mockSendTransaction(mockServer), ], }, - async ({ driver, mockServer }: { driver: Driver; mockServer: Mockttp }) => { + async ({ driver }: { driver: Driver }) => { await unlockWallet(driver); await createBtcAccount(driver); - await test(driver, mockServer); - }, - ); -} - -export async function getQuickNodeSeenRequests(mockServer: Mockttp) { - const seenRequests = await Promise.all( - ( - await mockServer.getMockedEndpoints() - ).map((mockedEndpoint) => mockedEndpoint.getSeenRequests()), - ); - return seenRequests - .flat() - .filter((request) => request.url.match(QUICKNODE_URL_REGEX)); -} - -export async function getTransactionRequest(mockServer: Mockttp) { - // Check that the transaction has been sent. - const transactionRequest = (await getQuickNodeSeenRequests(mockServer)).find( - async (request) => { - const body = (await request.body.getJson()) as { method: string }; - return body.method === 'sendrawtransaction'; + await test(driver); }, ); - return transactionRequest; } diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx index f49d7bf31801..bac7872c79e3 100644 --- a/ui/components/app/wallet-overview/coin-buttons.tsx +++ b/ui/components/app/wallet-overview/coin-buttons.tsx @@ -125,12 +125,6 @@ const CoinButtons = ({ const account = useSelector(getSelectedAccount); const { address: selectedAddress } = account; const history = useHistory(); - ///: BEGIN:ONLY_INCLUDE_IF(build-flask) - const currentActivityTabName = useSelector( - // @ts-expect-error TODO: fix state type - (state) => state.metamask.defaultHomeActiveTabName, - ); - ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const location = useLocation(); const keyring = useSelector(getCurrentKeyring); @@ -285,20 +279,14 @@ const CoinButtons = ({ switch (account.type) { ///: BEGIN:ONLY_INCLUDE_IF(build-flask) case BtcAccountType.P2wpkh: { - try { - // FIXME: We switch the tab before starting the send flow (we - // faced some inconsistencies when changing it after). - await dispatch(setDefaultHomeActiveTabName('activity')); - await sendMultichainTransaction( - BITCOIN_WALLET_SNAP_ID, - account.id, - chainId as CaipChainId, - ); - } catch { - // Restore the previous tab in case of any error (see FIXME comment above). - await dispatch(setDefaultHomeActiveTabName(currentActivityTabName)); - } + await sendMultichainTransaction( + BITCOIN_WALLET_SNAP_ID, + account.id, + chainId as CaipChainId, + ); + // We automatically switch to the activity tab once the transaction has been sent. + dispatch(setDefaultHomeActiveTabName('activity')); break; } ///: END:ONLY_INCLUDE_IF From e5ae877fa63300d9cac9492be89e977bfa2f4418 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:03:37 +0100 Subject: [PATCH 040/111] chore: Remove STX opt in modal (#28291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [This recent PR](https://github.com/MetaMask/metamask-extension/pull/27885) enabled STX by default for new users and only hid the STX opt in modal. The purpose of this PR is to clean up unused code for the STX opt in modal. ## **Related issues** Fixes: ## **Manual testing steps** 1. Install the extension from scratch 2. Be on Ethereum mainnet and have some funds there 3. You will not see any STX opt in modal ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- app/_locales/de/messages.json | 22 -- app/_locales/el/messages.json | 22 -- app/_locales/en/messages.json | 22 -- app/_locales/en_GB/messages.json | 22 -- app/_locales/es/messages.json | 22 -- app/_locales/fr/messages.json | 22 -- app/_locales/hi/messages.json | 22 -- app/_locales/id/messages.json | 22 -- app/_locales/ja/messages.json | 22 -- app/_locales/ko/messages.json | 22 -- app/_locales/pt/messages.json | 22 -- app/_locales/ru/messages.json | 22 -- app/_locales/tl/messages.json | 22 -- app/_locales/tr/messages.json | 22 -- app/_locales/vi/messages.json | 22 -- app/_locales/zh_CN/messages.json | 22 -- .../preferences-controller.test.ts | 4 +- .../controllers/preferences-controller.ts | 4 +- app/scripts/lib/backup.test.js | 2 +- shared/modules/selectors/index.test.ts | 124 +--------- .../modules/selectors/smart-transactions.ts | 41 +--- test/data/mock-state.json | 2 +- test/e2e/default-fixture.js | 2 +- test/e2e/fixture-builder.js | 4 +- test/e2e/restore/MetaMaskUserData.json | 2 +- ...rs-after-init-opt-in-background-state.json | 2 +- .../errors-after-init-opt-in-ui-state.json | 2 +- ...s-before-init-opt-in-background-state.json | 2 +- .../errors-before-init-opt-in-ui-state.json | 2 +- .../data/integration-init-state.json | 2 +- .../data/onboarding-completion-route.json | 2 +- ui/ducks/metamask/metamask.js | 2 +- ui/pages/home/home.component.js | 21 +- ui/pages/home/home.container.js | 4 - .../advanced-tab.component.test.js.snap | 12 +- ...rt-transactions-opt-in-modal.test.tsx.snap | 3 - .../smart-transactions/components/index.scss | 26 --- .../smart-transactions-opt-in-modal.test.tsx | 70 ------ .../smart-transactions-opt-in-modal.tsx | 219 ------------------ ui/pages/smart-transactions/index.scss | 1 - 40 files changed, 31 insertions(+), 876 deletions(-) delete mode 100644 ui/pages/smart-transactions/components/__snapshots__/smart-transactions-opt-in-modal.test.tsx.snap delete mode 100644 ui/pages/smart-transactions/components/index.scss delete mode 100644 ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.test.tsx delete mode 100644 ui/pages/smart-transactions/components/smart-transactions-opt-in-modal.tsx diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 9af24022bcb3..771deef4c28c 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Beschleunigung der Gasgebühr bearbeiten" }, - "enable": { - "message": "Aktivieren" - }, "enableAutoDetect": { "message": " Automatische Erkennung aktivieren" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Smart Transactions" }, - "smartTransactionsBenefit1": { - "message": "Erfolgsrate: 99,5 %" - }, - "smartTransactionsBenefit2": { - "message": "Spart Ihnen Geld" - }, - "smartTransactionsBenefit3": { - "message": "Updates in Echtzeit" - }, - "smartTransactionsDescription": { - "message": "Erzielen Sie mit Smart Transactions höhere Erfolgsraten, einen Frontrunning-Schutz und eine bessere Transparenz." - }, - "smartTransactionsDescription2": { - "message": "Nur auf Ethereum verfügbar. Sie können diese Funktion jederzeit in den Einstellungen aktivieren oder deaktivieren. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Verbesserter Transaktionsschutz" - }, "snapAccountCreated": { "message": "Konto erstellt" }, diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index 308099b1c2b1..7ae594c8b9b8 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Επεξεργασία τελών επίσπευσης συναλλαγής" }, - "enable": { - "message": "Ενεργοποίηση" - }, "enableAutoDetect": { "message": " Ενεργοποίηση αυτόματου εντοπισμού" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Έξυπνες συναλλαγές" }, - "smartTransactionsBenefit1": { - "message": "Ποσοστό επιτυχίας 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Σας εξοικονομεί χρήματα" - }, - "smartTransactionsBenefit3": { - "message": "Ενημερώσεις σε πραγματικό χρόνο" - }, - "smartTransactionsDescription": { - "message": "Ξεκλειδώστε υψηλότερα ποσοστά επιτυχίας, προστασία σε \"προπορευόμενες συναλλαγές\" και καλύτερη ορατότητα με τις Έξυπνες Συναλλαγές." - }, - "smartTransactionsDescription2": { - "message": "Διατίθεται μόνο στο Ethereum. Ενεργοποιήστε ή απενεργοποιήστε το ανά πάσα στιγμή στις ρυθμίσεις. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Ενισχυμένη Προστασία Συναλλαγών" - }, "snapAccountCreated": { "message": "Ο λογαριασμός δημιουργήθηκε" }, diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 409965f07ab9..ab6b06411731 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1849,9 +1849,6 @@ "editSpendingCapSpecialCharError": { "message": "Enter numbers only" }, - "enable": { - "message": "Enable" - }, "enableAutoDetect": { "message": " Enable autodetect" }, @@ -5064,25 +5061,6 @@ "smartTransactions": { "message": "Smart Transactions" }, - "smartTransactionsBenefit1": { - "message": "99.5% success rate" - }, - "smartTransactionsBenefit2": { - "message": "Saves you money" - }, - "smartTransactionsBenefit3": { - "message": "Real-time updates" - }, - "smartTransactionsDescription": { - "message": "Unlock higher success rates, frontrunning protection, and better visibility with Smart Transactions." - }, - "smartTransactionsDescription2": { - "message": "Only available on Ethereum. Enable or disable any time in settings. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Enhanced Transaction Protection" - }, "snapAccountCreated": { "message": "Account created" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index fc635e33a708..e8eb1e58ea71 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1707,9 +1707,6 @@ "effortlesslyNavigateYourDigitalAssets": { "message": "Effortlessly navigate your digital assets" }, - "enable": { - "message": "Enable" - }, "enableAutoDetect": { "message": " Enable autodetect" }, @@ -4815,25 +4812,6 @@ "smartTransactions": { "message": "Smart Transactions" }, - "smartTransactionsBenefit1": { - "message": "99.5% success rate" - }, - "smartTransactionsBenefit2": { - "message": "Saves you money" - }, - "smartTransactionsBenefit3": { - "message": "Real-time updates" - }, - "smartTransactionsDescription": { - "message": "Unlock higher success rates, frontrunning protection, and better visibility with Smart Transactions." - }, - "smartTransactionsDescription2": { - "message": "Only available on Ethereum. Enable or disable any time in settings. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Enhanced Transaction Protection" - }, "snapAccountCreated": { "message": "Account created" }, diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index ada162b9a12b..0f774d8f6ba2 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1617,9 +1617,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Editar la tarifa de aceleración de gas" }, - "enable": { - "message": "Habilitar" - }, "enableAutoDetect": { "message": " Activar autodetección" }, @@ -4634,25 +4631,6 @@ "smartTransactions": { "message": "Transacciones inteligentes" }, - "smartTransactionsBenefit1": { - "message": "Índice de éxito del 99.5%" - }, - "smartTransactionsBenefit2": { - "message": "Le permite ahorrar dinero" - }, - "smartTransactionsBenefit3": { - "message": "Actualizaciones en tiempo real" - }, - "smartTransactionsDescription": { - "message": "Desbloquee índices de éxito más altos, protección contra frontrunning y mejor visibilidad con transacciones inteligentes." - }, - "smartTransactionsDescription2": { - "message": "Solo disponible en Ethereum. Active o desactive en cualquier momento en la configuración. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Protección mejorada de transacciones" - }, "snapAccountCreated": { "message": "Cuenta creada" }, diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 856638ba2b8a..985dfd44c9dd 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Modifier les gas fees d’accélération" }, - "enable": { - "message": "Activer" - }, "enableAutoDetect": { "message": " Activer la détection automatique" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Transactions intelligentes" }, - "smartTransactionsBenefit1": { - "message": "Taux de réussite de 99,5 %" - }, - "smartTransactionsBenefit2": { - "message": "Cela vous permet d’économiser de l’argent" - }, - "smartTransactionsBenefit3": { - "message": "Mises à jour en temps réel" - }, - "smartTransactionsDescription": { - "message": "Bénéficiez de taux de réussite plus élevés, d’une protection contre le « front running » et d’une meilleure visibilité grâce aux transactions intelligentes." - }, - "smartTransactionsDescription2": { - "message": "Disponible uniquement sur Ethereum. Vous pouvez activer ou désactiver cette option à tout moment dans les paramètres. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Protection renforcée des transactions" - }, "snapAccountCreated": { "message": "Le compte a été créé" }, diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 45e64a972e17..540023a75fac 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "गैस फ़ीस स्पीड अप को बदलें" }, - "enable": { - "message": "चालू करें" - }, "enableAutoDetect": { "message": " ऑटो डिटेक्ट इनेबल करें" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "स्मार्ट ट्रांसेक्शन" }, - "smartTransactionsBenefit1": { - "message": "99.5% सफलता दर" - }, - "smartTransactionsBenefit2": { - "message": "आपका पैसा बचाता है" - }, - "smartTransactionsBenefit3": { - "message": "रियल-टाइम अपडेट" - }, - "smartTransactionsDescription": { - "message": "स्मार्ट ट्रांसेक्शन के साथ उच्च सफलता दर, फ्रंटरनिंग सुरक्षा और बेहतर दृश्यता अनलॉक करें।" - }, - "smartTransactionsDescription2": { - "message": "केवल Ethereum पर उपलब्ध है। सेटिंग्स में किसी भी समय चालू करें या बंद करें। $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "एनहांस्ड ट्रांसेक्शन प्रोटेक्शन" - }, "snapAccountCreated": { "message": "अकाउंट बनाया गया" }, diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 6314d9ed3468..81f7d2a9c633 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Edit biaya gas percepatan" }, - "enable": { - "message": "Aktifkan" - }, "enableAutoDetect": { "message": " Aktifkan deteksi otomatis" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Transaksi Pintar" }, - "smartTransactionsBenefit1": { - "message": "Tingkat keberhasilan 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Menghemat uang Anda" - }, - "smartTransactionsBenefit3": { - "message": "Pembaruan waktu nyata" - }, - "smartTransactionsDescription": { - "message": "Raih tingkat keberhasilan yang lebih tinggi, perlindungan frontrunning, dan visibilitas yang lebih baik dengan Transaksi Pintar." - }, - "smartTransactionsDescription2": { - "message": "Hanya tersedia di Ethereum. Aktifkan atau nonaktifkan kapan saja di pengaturan. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Peningkatan Perlindungan Transaksi" - }, "snapAccountCreated": { "message": "Akun dibuat" }, diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 61730b2bc325..5787bb88c397 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "高速化用のガス代を編集" }, - "enable": { - "message": "有効にする" - }, "enableAutoDetect": { "message": " 自動検出を有効にする" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "スマートトランザクション" }, - "smartTransactionsBenefit1": { - "message": "99.5%の成功率" - }, - "smartTransactionsBenefit2": { - "message": "お金を節約できます" - }, - "smartTransactionsBenefit3": { - "message": "リアルタイムの最新情報" - }, - "smartTransactionsDescription": { - "message": "スマートトランザクションで、成功率を上げ、フロントランニングを防ぎ、可視性を高めましょう。" - }, - "smartTransactionsDescription2": { - "message": "イーサリアムでのみご利用いただけ、いつでも設定で有効・無効を切り替えられます。$1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "強化されたトランザクション保護" - }, "snapAccountCreated": { "message": "アカウントが作成されました" }, diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 05c04fbd17a9..32d7bd4399b5 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "가스비 가속 편집" }, - "enable": { - "message": "활성화" - }, "enableAutoDetect": { "message": " 자동 감지 활성화" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "스마트 트랜잭션" }, - "smartTransactionsBenefit1": { - "message": "99.5% 성공률" - }, - "smartTransactionsBenefit2": { - "message": "비용 절감" - }, - "smartTransactionsBenefit3": { - "message": "실시간 업데이트" - }, - "smartTransactionsDescription": { - "message": "스마트 트랜잭션으로 선행거래를 방지하고 더 높은 성공률과 가시성을 확보하세요." - }, - "smartTransactionsDescription2": { - "message": "이더리움에서만 사용할 수 있습니다. 설정에서 언제든지 활성화하거나 비활성화할 수 있습니다. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "트랜잭션 보호 강화" - }, "snapAccountCreated": { "message": "계정 생성됨" }, diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 4c02a9dc223e..4eecb941d36f 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Editar taxa de gás para aceleração" }, - "enable": { - "message": "Ativar" - }, "enableAutoDetect": { "message": " Ativar detecção automática" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Transações inteligentes" }, - "smartTransactionsBenefit1": { - "message": "99,5% de taxa de sucesso" - }, - "smartTransactionsBenefit2": { - "message": "Faz você economizar dinheiro" - }, - "smartTransactionsBenefit3": { - "message": "Atualizações em tempo real" - }, - "smartTransactionsDescription": { - "message": "Desbloqueie taxas de sucesso maiores, proteção contra front running e melhor visibilidade com as transações inteligentes." - }, - "smartTransactionsDescription2": { - "message": "Disponível somente na Ethereum. Ative ou desative a qualquer momento nas configurações. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Proteção de transações aprimorada" - }, "snapAccountCreated": { "message": "Conta criada" }, diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index f1e5d27589c5..ce53cc239de5 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Изменить плату за газ за ускорение" }, - "enable": { - "message": "Включить" - }, "enableAutoDetect": { "message": " Включить автоопределение" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Умные транзакции" }, - "smartTransactionsBenefit1": { - "message": "Коэффициент успеха 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Экономит вам деньги" - }, - "smartTransactionsBenefit3": { - "message": "Обновления в реальном времени" - }, - "smartTransactionsDescription": { - "message": "Откройте для себя более высокие коэффициенты успеха, передовую защиту и лучшую прозрачность с помощью умных транзакций." - }, - "smartTransactionsDescription2": { - "message": "Доступно только на Ethereum. Включайте или отключайте в любое время в настройках. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Улучшенная защита транзакций" - }, "snapAccountCreated": { "message": "Счет создан" }, diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index 76e91829fc2c..8909ac662e34 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "I-edit ang pagpapabilis ng bayad sa gas" }, - "enable": { - "message": "Payagan" - }, "enableAutoDetect": { "message": " Paganahin ang autodetect" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Mga Smart Transaction" }, - "smartTransactionsBenefit1": { - "message": "99.5% tiyansa ng tagumpay" - }, - "smartTransactionsBenefit2": { - "message": "Makatitipid ng pera" - }, - "smartTransactionsBenefit3": { - "message": "Mga real-time na update" - }, - "smartTransactionsDescription": { - "message": "Mag-unlock na mas mataas na tiyansa ng tagumpay, proteksyon sa frontrunning, at mas mahusay na visibility sa mga Smart Transaction." - }, - "smartTransactionsDescription2": { - "message": "Available lamang sa Ethereum. I-enable o i-disable anumang oras sa mga setting. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Pinahusay na Proteksyon sa Transaksyon" - }, "snapAccountCreated": { "message": "Nagawa ang account" }, diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 3b1899614d70..8c4e44d3192e 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Hızlandırma gaz ücretini düzenle" }, - "enable": { - "message": "Etkinleştir" - }, "enableAutoDetect": { "message": " Otomatik algılamayı etkinleştir" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Akıllı İşlemler" }, - "smartTransactionsBenefit1": { - "message": "%99,5 başarı oranı" - }, - "smartTransactionsBenefit2": { - "message": "Paradan tasarruf sağlar" - }, - "smartTransactionsBenefit3": { - "message": "Gerçek zamanlı güncellemeler" - }, - "smartTransactionsDescription": { - "message": "Akıllı İşlemler ile daha yüksek başarı oranlarının, arkadan çalıştırma korumasının ve daha iyi görünürlüğün kilidini açın." - }, - "smartTransactionsDescription2": { - "message": "Sadece Ethereum'da mevcuttur. Dilediğiniz zaman ayarlar kısmında etkinleştirin veya devre dışı bırakın. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "İyileştirilmiş İşlem Koruması" - }, "snapAccountCreated": { "message": "Hesap oluşturuldu" }, diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 4bfcba6dac1f..b741fe6eb536 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "Chỉnh sửa phí gas tăng tốc" }, - "enable": { - "message": "Bật" - }, "enableAutoDetect": { "message": " Bật tự động phát hiện" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "Giao dịch thông minh" }, - "smartTransactionsBenefit1": { - "message": "Tỷ lệ thành công 99,5%" - }, - "smartTransactionsBenefit2": { - "message": "Tiết kiệm tiền của bạn" - }, - "smartTransactionsBenefit3": { - "message": "Cập nhật theo thời gian thực" - }, - "smartTransactionsDescription": { - "message": "Đạt tỷ lệ thành công cao hơn, bảo vệ chống hành vi lợi dụng thông tin biết trước và khả năng hiển thị tốt hơn với Giao dịch thông minh." - }, - "smartTransactionsDescription2": { - "message": "Chỉ có sẵn trên Ethereum. Có thể bật/tắt bất cứ lúc nào trong phần Cài đặt. $1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "Tăng cường bảo vệ giao dịch" - }, "snapAccountCreated": { "message": "Tài khoản đã được tạo" }, diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index 80a31d532482..e5695cdfaecf 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1620,9 +1620,6 @@ "editSpeedUpEditGasFeeModalTitle": { "message": "编辑加速燃料费用" }, - "enable": { - "message": "启用" - }, "enableAutoDetect": { "message": " 启用自动检测" }, @@ -4637,25 +4634,6 @@ "smartTransactions": { "message": "智能交易" }, - "smartTransactionsBenefit1": { - "message": "99.5%的成功率" - }, - "smartTransactionsBenefit2": { - "message": "为您省钱" - }, - "smartTransactionsBenefit3": { - "message": "实时更新" - }, - "smartTransactionsDescription": { - "message": "通过智能交易解锁更高的成功率、抢先交易保护和更高的透明度。" - }, - "smartTransactionsDescription2": { - "message": "仅适用于以太坊。可随时在设置中启用或禁用。$1", - "description": "$1 is an external link to learn more about Smart Transactions" - }, - "smartTransactionsOptItModalTitle": { - "message": "增强型交易保护" - }, "snapAccountCreated": { "message": "账户已创建" }, diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index a4b91a8d3b1a..25010cdd3a0f 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -733,7 +733,7 @@ describe('preferences controller', () => { privacyMode: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: null, + smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, @@ -762,7 +762,7 @@ describe('preferences controller', () => { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: null, + smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, petnamesEnabled: true, diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index e1cdb2e8a4f6..bc7d03155f75 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -103,7 +103,7 @@ export type Preferences = { showExtensionInFullSizeView: boolean; showFiatInTestnets: boolean; showTestNetworks: boolean; - smartTransactionsOptInStatus: boolean | null; + smartTransactionsOptInStatus: boolean; showNativeTokenAsMainBalance: boolean; useNativeCurrencyAsPrimaryCurrency: boolean; hideZeroBalanceTokens: boolean; @@ -209,7 +209,7 @@ export const getDefaultPreferencesControllerState = showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: null, // null means we will show the Smart Transactions opt-in modal to a user if they are eligible + smartTransactionsOptInStatus: true, showNativeTokenAsMainBalance: false, useNativeCurrencyAsPrimaryCurrency: true, hideZeroBalanceTokens: false, diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index 7a322148c847..b3a7f176c2e6 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -165,7 +165,7 @@ const jsonData = JSON.stringify({ showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: true, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, useNativeCurrencyAsPrimaryCurrency: true, showMultiRpcModal: false, }, diff --git a/shared/modules/selectors/index.test.ts b/shared/modules/selectors/index.test.ts index 9f0b1b201a5c..2e40d47db102 100644 --- a/shared/modules/selectors/index.test.ts +++ b/shared/modules/selectors/index.test.ts @@ -9,7 +9,6 @@ import { getCurrentChainSupportsSmartTransactions, getSmartTransactionsEnabled, getIsSmartTransaction, - getIsSmartTransactionsOptInModalAvailable, getSmartTransactionsPreferenceEnabled, } from '.'; @@ -70,7 +69,7 @@ describe('Selectors', () => { }; describe('getSmartTransactionsOptInStatusForMetrics and getSmartTransactionsPreferenceEnabled', () => { - const createMockOptInStatusState = (status: boolean | null) => { + const createMockOptInStatusState = (status: boolean) => { return { metamask: { preferences: { @@ -89,7 +88,6 @@ describe('Selectors', () => { jestIt.each([ { status: true, expected: true }, { status: false, expected: false }, - { status: null, expected: null }, ])( 'should return $expected if the smart transactions opt-in status is $status', ({ status, expected }) => { @@ -113,7 +111,6 @@ describe('Selectors', () => { jestIt.each([ { status: true, expected: true }, { status: false, expected: false }, - { status: null, expected: true }, ])( 'should return $expected if the smart transactions opt-in status is $status', ({ status, expected }) => { @@ -316,123 +313,4 @@ describe('Selectors', () => { expect(result).toBe(false); }); }); - - describe('getIsSmartTransactionsOptInModalAvailable', () => { - jestIt( - 'returns true for Ethereum Mainnet + supported RPC URL + null opt-in status and non-zero balance', - () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(true); - }, - ); - - jestIt( - 'returns false for Polygon Mainnet + supported RPC URL + null opt-in status and non-zero balance', - () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }, - ); - - jestIt( - 'returns false for Ethereum Mainnet + unsupported RPC URL + null opt-in status and non-zero balance', - () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, - rpcUrl: 'https://mainnet.quiknode.pro/', - }), - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }, - ); - - jestIt( - 'returns false for Ethereum Mainnet + supported RPC URL + true opt-in status and non-zero balance', - () => { - const state = createMockState(); - expect(getIsSmartTransactionsOptInModalAvailable(state)).toBe(false); - }, - ); - - jestIt( - 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x0)', - () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - accounts: { - ...state.metamask.accounts, - '0x123': { - address: '0x123', - balance: '0x0', - }, - }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }, - ); - - jestIt( - 'returns false for Ethereum Mainnet + supported RPC URL + null opt-in status and zero balance (0x00)', - () => { - const state = createMockState(); - const newState = { - ...state, - metamask: { - ...state.metamask, - preferences: { - ...state.metamask.preferences, - smartTransactionsOptInStatus: null, - }, - accounts: { - ...state.metamask.accounts, - '0x123': { - address: '0x123', - balance: '0x00', - }, - }, - }, - }; - expect(getIsSmartTransactionsOptInModalAvailable(newState)).toBe(false); - }, - ); - }); }); diff --git a/shared/modules/selectors/smart-transactions.ts b/shared/modules/selectors/smart-transactions.ts index a02fe63692b3..4fb6d56fc87d 100644 --- a/shared/modules/selectors/smart-transactions.ts +++ b/shared/modules/selectors/smart-transactions.ts @@ -7,21 +7,16 @@ import { getCurrentChainId, getCurrentNetwork, accountSupportsSmartTx, - getSelectedAccount, getPreferences, // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths } from '../../../ui/selectors/selectors'; // TODO: Migrate shared selectors to this file. import { isProduction } from '../environment'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { MultichainState } from '../../../ui/selectors/multichain'; - type SmartTransactionsMetaMaskState = { metamask: { preferences: { - smartTransactionsOptInStatus?: boolean | null; + smartTransactionsOptInStatus?: boolean; }; internalAccounts: { selectedAccount: string; @@ -72,10 +67,8 @@ type SmartTransactionsMetaMaskState = { */ export const getSmartTransactionsOptInStatusInternal = createSelector( getPreferences, - (preferences: { - smartTransactionsOptInStatus?: boolean | null; - }): boolean | null => { - return preferences?.smartTransactionsOptInStatus ?? null; + (preferences: { smartTransactionsOptInStatus?: boolean }): boolean => { + return preferences?.smartTransactionsOptInStatus ?? true; }, ); @@ -93,7 +86,7 @@ export const getSmartTransactionsOptInStatusInternal = createSelector( */ export const getSmartTransactionsOptInStatusForMetrics = createSelector( getSmartTransactionsOptInStatusInternal, - (optInStatus: boolean | null): boolean | null => optInStatus, + (optInStatus: boolean): boolean => optInStatus, ); /** @@ -105,7 +98,7 @@ export const getSmartTransactionsOptInStatusForMetrics = createSelector( */ export const getSmartTransactionsPreferenceEnabled = createSelector( getSmartTransactionsOptInStatusInternal, - (optInStatus: boolean | null): boolean => { + (optInStatus: boolean): boolean => { // In the absence of an explicit opt-in or opt-out, // the Smart Transactions toggle is enabled. const DEFAULT_SMART_TRANSACTIONS_ENABLED = true; @@ -137,30 +130,6 @@ const getIsAllowedRpcUrlForSmartTransactions = ( return rpcUrl?.hostname?.endsWith('.infura.io'); }; -/** - * Checks if the selected account has a non-zero balance. - * - * @param state - The state object containing account information. - * @returns true if the selected account has a non-zero balance, otherwise false. - */ -const hasNonZeroBalance = (state: SmartTransactionsMetaMaskState) => { - const selectedAccount = getSelectedAccount( - state as unknown as MultichainState, - ); - return BigInt(selectedAccount?.balance || '0x0') > 0n; -}; - -export const getIsSmartTransactionsOptInModalAvailable = ( - state: SmartTransactionsMetaMaskState, -) => { - return ( - getCurrentChainSupportsSmartTransactions(state) && - getIsAllowedRpcUrlForSmartTransactions(state) && - getSmartTransactionsOptInStatusInternal(state) === null && - hasNonZeroBalance(state) - ); -}; - export const getSmartTransactionsEnabled = ( state: SmartTransactionsMetaMaskState, ): boolean => { diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 2865478912f3..184787b07836 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -372,7 +372,7 @@ "showFiatInTestnets": false, "showNativeTokenAsMainBalance": true, "showTestNetworks": true, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "tokenSortConfig": { "key": "tokenFiatAmount", "order": "dsc", diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 95f35bf1694c..2d3d2999ed43 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -212,7 +212,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 4d7e1873bff4..334e2f74ceca 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -77,7 +77,7 @@ function onboardingFixture() { showFiatInTestnets: false, privacyMode: false, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, showNativeTokenAsMainBalance: true, petnamesEnabled: true, showMultiRpcModal: false, @@ -124,7 +124,7 @@ function onboardingFixture() { [ETHERSCAN_SUPPORTED_CHAIN_IDS.GNOSIS]: true, }, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, }, QueuedRequestController: { queuedRequestCount: 0, diff --git a/test/e2e/restore/MetaMaskUserData.json b/test/e2e/restore/MetaMaskUserData.json index 846acc8164cd..7a687ec254c0 100644 --- a/test/e2e/restore/MetaMaskUserData.json +++ b/test/e2e/restore/MetaMaskUserData.json @@ -36,7 +36,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false + "smartTransactionsOptInStatus": true }, "theme": "light", "useBlockie": false, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 6d77cd3ae351..1a871780591f 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -224,7 +224,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showMultiRpcModal": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index e577bb71a6be..e8f8f81a6293 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -30,7 +30,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "showMultiRpcModal": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 89b1b29100bb..1a51023a2ca1 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -128,7 +128,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "isRedesignedConfirmationsDeveloperEnabled": "boolean", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index f13d3e078c64..bf7f87a16134 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -128,7 +128,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "showNativeTokenAsMainBalance": true, "petnamesEnabled": true, "isRedesignedConfirmationsDeveloperEnabled": "boolean", diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index 7949e19cfa51..a0ae3a8fb146 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -781,7 +781,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": true, - "smartTransactionsOptInStatus": false, + "smartTransactionsOptInStatus": true, "petnamesEnabled": false, "showConfirmationAdvancedDetails": false, "showMultiRpcModal": false diff --git a/test/integration/data/onboarding-completion-route.json b/test/integration/data/onboarding-completion-route.json index e47d1379b2eb..b2c19536a138 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -223,7 +223,7 @@ "showExtensionInFullSizeView": false, "showFiatInTestnets": false, "showTestNetworks": false, - "smartTransactionsOptInStatus": null, + "smartTransactionsOptInStatus": true, "hideZeroBalanceTokens": false, "petnamesEnabled": true, "redesignedConfirmationsEnabled": true, diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index 9627608eb709..63ff92a11ccc 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -47,7 +47,7 @@ const initialState = { showExtensionInFullSizeView: false, showFiatInTestnets: false, showTestNetworks: false, - smartTransactionsOptInStatus: false, + smartTransactionsOptInStatus: true, petnamesEnabled: true, featureNotificationsEnabled: false, privacyMode: false, diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 37c147427ac5..9ac71dd6a766 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -13,7 +13,6 @@ import TermsOfUsePopup from '../../components/app/terms-of-use-popup'; import RecoveryPhraseReminder from '../../components/app/recovery-phrase-reminder'; import WhatsNewPopup from '../../components/app/whats-new-popup'; import { FirstTimeFlowType } from '../../../shared/constants/onboarding'; -import SmartTransactionsOptInModal from '../smart-transactions/components/smart-transactions-opt-in-modal'; ///: END:ONLY_INCLUDE_IF import HomeNotification from '../../components/app/home-notification'; import MultipleNotifications from '../../components/app/multiple-notifications'; @@ -155,7 +154,6 @@ export default class Home extends PureComponent { hideWhatsNewPopup: PropTypes.func.isRequired, announcementsToShow: PropTypes.bool.isRequired, onboardedInThisUISession: PropTypes.bool, - isSmartTransactionsOptInModalAvailable: PropTypes.bool.isRequired, showMultiRpcModal: PropTypes.bool.isRequired, ///: END:ONLY_INCLUDE_IF newNetworkAddedConfigurationId: PropTypes.string, @@ -937,7 +935,6 @@ export default class Home extends PureComponent { announcementsToShow, firstTimeFlowType, newNetworkAddedConfigurationId, - isSmartTransactionsOptInModalAvailable, showMultiRpcModal, ///: END:ONLY_INCLUDE_IF } = this.props; @@ -956,20 +953,11 @@ export default class Home extends PureComponent { !process.env.IN_TEST && !newNetworkAddedConfigurationId; - const showSmartTransactionsOptInModal = - canSeeModals && isSmartTransactionsOptInModalAvailable; - const showWhatsNew = - canSeeModals && - announcementsToShow && - showWhatsNewPopup && - !showSmartTransactionsOptInModal; + canSeeModals && announcementsToShow && showWhatsNewPopup; const showMultiRpcEditModal = - canSeeModals && - showMultiRpcModal && - !showSmartTransactionsOptInModal && - !showWhatsNew; + canSeeModals && showMultiRpcModal && !showWhatsNew; const showTermsOfUse = completedOnboarding && !onboardedInThisUISession && showTermsOfUsePopup; @@ -991,11 +979,6 @@ export default class Home extends PureComponent { { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) } - - {showMultiRpcEditModal && } {showWhatsNew ? : null} {!showWhatsNew && showRecoveryPhraseReminder ? ( diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index dfeb1a5e7cdb..9d4511021529 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -222,10 +222,6 @@ const mapStateToProps = (state) => { custodianDeepLink: getCustodianDeepLink(state), accountType: getAccountType(state), ///: END:ONLY_INCLUDE_IF - - // Set to false to prevent the opt-in modal from showing. - // TODO(dbrans): Remove opt-in modal once default opt-in is stable. - isSmartTransactionsOptInModalAvailable: false, showMultiRpcModal: state.metamask.preferences.showMultiRpcModal, }; }; diff --git a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap index e914c54fe4ca..1bbfe6d81743 100644 --- a/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap +++ b/ui/pages/settings/advanced-tab/__snapshots__/advanced-tab.component.test.js.snap @@ -99,34 +99,34 @@ exports[`AdvancedTab Component should match snapshot 1`] = ` class="settings-page__content-item-col" >
Date: Thu, 7 Nov 2024 14:40:43 +0000 Subject: [PATCH 050/111] feat: Convert mmi controller to a non-controller (#27983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** We want to bring MMIController up to date with our latest controller patterns. After review, it turns out that MMIController does not have any state and therefore should not inherit from BaseController (or anything, for that matter). [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27983?quickstart=1) ## **Related issues** Fixes: #25926 ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- .../controllers/mmi-controller.test.ts | 91 ++++++++++------ app/scripts/controllers/mmi-controller.ts | 100 ++++++++++-------- app/scripts/metamask-controller.js | 6 +- shared/constants/mmi-controller.ts | 56 ++++++++-- 4 files changed, 165 insertions(+), 88 deletions(-) diff --git a/app/scripts/controllers/mmi-controller.test.ts b/app/scripts/controllers/mmi-controller.test.ts index 64bc46132724..479c4cb0f14d 100644 --- a/app/scripts/controllers/mmi-controller.test.ts +++ b/app/scripts/controllers/mmi-controller.test.ts @@ -17,13 +17,14 @@ import { NETWORK_TYPES, TEST_NETWORK_TICKER_MAP, } from '../../../shared/constants/network'; -import MMIController from './mmi-controller'; +import { MMIController, AllowedActions } from './mmi-controller'; import { AppStateController } from './app-state-controller'; import { ControllerMessenger } from '@metamask/base-controller'; import { mmiKeyringBuilderFactory } from '../mmi-keyring-builder-factory'; import MetaMetricsController from './metametrics'; import { ETH_EOA_METHODS } from '../../../shared/constants/eth-methods'; import { mockNetworkState } from '../../../test/stub/networks'; +import { InfuraNetworkType } from '@metamask/controller-utils'; import { API_REQUEST_LOG_EVENT } from '@metamask-institutional/sdk'; jest.mock('@metamask-institutional/portfolio-dashboard', () => ({ @@ -39,6 +40,21 @@ jest.mock('./permissions', () => ({ }), })); +export const createMockNetworkConfiguration = ( + override?: Partial, +): NetworkConfiguration => { + return { + chainId: CHAIN_IDS.SEPOLIA, + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + name: 'Mock Network', + nativeCurrency: 'MOCK TOKEN', + rpcEndpoints: [], + defaultBlockExplorerUrlIndex: 0, + ...override, + }; +}; + const mockAccount = { address: '0x758b8178a9A4B7206d1f648c4a77C515Cbac7001', id: 'mock-id', @@ -73,10 +89,10 @@ describe('MMIController', function () { mmiConfigurationController, controllerMessenger, accountsController, - networkController, keyringController, metaMetricsController, - custodyController; + custodyController, + mmiControllerMessenger; beforeEach(async function () { const mockMessenger = { @@ -89,22 +105,10 @@ describe('MMIController', function () { subscribe: jest.fn(), }; - networkController = new NetworkController({ - messenger: new ControllerMessenger().getRestricted({ - name: 'NetworkController', - allowedEvents: [ - 'NetworkController:stateChange', - 'NetworkController:networkWillChange', - 'NetworkController:networkDidChange', - 'NetworkController:infuraIsBlocked', - 'NetworkController:infuraIsUnblocked', - ], - }), - state: mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), - infuraProjectId: 'mock-infura-project-id', - }); - - controllerMessenger = new ControllerMessenger(); + const controllerMessenger = new ControllerMessenger< + AllowedActions, + never + >(); accountsController = new AccountsController({ messenger: controllerMessenger.getRestricted({ @@ -212,7 +216,31 @@ describe('MMIController', function () { onNetworkDidChange: jest.fn(), }); - const mmiControllerMessenger = controllerMessenger.getRestricted({ + controllerMessenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue(mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA })), + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + InfuraNetworkType['sepolia'], + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkClientById', + jest.fn().mockReturnValue({ + configuration: { + chainId: CHAIN_IDS.SEPOLIA, + } + }), + ); + + controllerMessenger.registerActionHandler( + 'NetworkController:getNetworkConfigurationByChainId', + jest.fn().mockReturnValue(createMockNetworkConfiguration()), + ); + + mmiControllerMessenger = controllerMessenger.getRestricted({ name: 'MMIController', allowedActions: [ 'AccountsController:getAccountByAddress', @@ -220,6 +248,10 @@ describe('MMIController', function () { 'AccountsController:listAccounts', 'AccountsController:getSelectedAccount', 'AccountsController:setSelectedAccount', + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', + 'NetworkController:getNetworkClientById', + 'NetworkController:getNetworkConfigurationByChainId' ], }); @@ -253,7 +285,6 @@ describe('MMIController', function () { }) } }), - networkController, permissionController, custodyController, metaMetricsController, @@ -507,9 +538,7 @@ describe('MMIController', function () { CUSTODIAN_TYPES['CUSTODIAN-TYPE'] = { keyringClass: { type: 'mock-keyring-class' }, }; - mmiController.messenger.call = jest - .fn() - .mockReturnValue({ address: '0x1' }); + jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue({ address: '0x1' }); mmiController.custodyController.getCustodyTypeByAddress = jest .fn() .mockReturnValue('custodian-type'); @@ -628,9 +657,7 @@ describe('MMIController', function () { mmiController.custodyController.getAccountDetails = jest .fn() .mockReturnValue({}); - mmiController.messenger.call = jest - .fn() - .mockReturnValue([mockAccount, mockAccount2]); + jest.spyOn(mmiControllerMessenger, 'call').mockReturnValue([mockAccount, mockAccount2]); mmiController.mmiConfigurationController.store.getState = jest .fn() .mockReturnValue({ @@ -701,7 +728,7 @@ describe('MMIController', function () { describe('handleMmiCheckIfTokenIsPresent', () => { it('should check if a token is present', async () => { - mmiController.messenger.call = jest + mmiController.messagingSystem.call = jest .fn() .mockReturnValue({ address: '0x1' }); mmiController.custodyController.getCustodyTypeByAddress = jest @@ -733,7 +760,7 @@ describe('MMIController', function () { describe('handleMmiDashboardData', () => { it('should return internalAccounts as identities', async () => { - const controllerMessengerSpy = jest.spyOn(controllerMessenger, 'call'); + const controllerMessengerSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.handleMmiDashboardData(); expect(controllerMessengerSpy).toHaveBeenCalledWith( @@ -811,7 +838,7 @@ describe('MMIController', function () { describe('setAccountAndNetwork', () => { it('should set a new selected account if the selectedAddress and the address from the arguments is different', async () => { - const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call'); + const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.setAccountAndNetwork( 'mock-origin', mockAccount2.address, @@ -829,14 +856,14 @@ describe('MMIController', function () { }); it('should not set a new selected account the accounts are the same', async () => { - const selectedAccountSpy = jest.spyOn(controllerMessenger, 'call'); + const selectedAccountSpy = jest.spyOn(mmiControllerMessenger, 'call'); await mmiController.setAccountAndNetwork( 'mock-origin', mockAccount.address, '0x1', ); - expect(selectedAccountSpy).toHaveBeenCalledTimes(1); + expect(selectedAccountSpy).toHaveBeenCalledTimes(4); const selectedAccount = accountsController.getSelectedAccount(); expect(selectedAccount.id).toBe(mockAccount.id); }); diff --git a/app/scripts/controllers/mmi-controller.ts b/app/scripts/controllers/mmi-controller.ts index 65cdac69ba0b..ac8de8b76656 100644 --- a/app/scripts/controllers/mmi-controller.ts +++ b/app/scripts/controllers/mmi-controller.ts @@ -1,4 +1,3 @@ -import EventEmitter from 'events'; import log from 'loglevel'; import { captureException } from '@sentry/browser'; import { @@ -21,13 +20,13 @@ import { IApiCallLogEntry } from '@metamask-institutional/types'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { TransactionMeta } from '@metamask/transaction-controller'; import { KeyringTypes } from '@metamask/keyring-controller'; +import { NetworkState } from '@metamask/network-controller'; import { MessageParamsPersonal, MessageParamsTyped, SignatureController, } from '@metamask/signature-controller'; import { OriginalRequest } from '@metamask/message-manager'; -import { NetworkController } from '@metamask/network-controller'; import { InternalAccount } from '@metamask/keyring-api'; import { toHex } from '@metamask/controller-utils'; import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils'; @@ -41,15 +40,12 @@ import { Label, Signature, ConnectionRequest, + MMIControllerMessenger, } from '../../../shared/constants/mmi-controller'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { getCurrentChainId } from '../../../ui/selectors'; import MetaMetricsController from './metametrics'; import { getPermissionBackgroundApiMethods } from './permissions'; import AccountTrackerController from './account-tracker-controller'; import { AppStateController } from './app-state-controller'; -import { PreferencesController } from './preferences-controller'; type UpdateCustodianTransactionsParameters = { keyring: CustodyKeyring; @@ -64,7 +60,7 @@ type UpdateCustodianTransactionsParameters = { setTxHash: (txId: string, txHash: string) => void; }; -export default class MMIController extends EventEmitter { +export class MMIController { public opts: MMIControllerOptions; public mmiConfigurationController: MmiConfigurationController; @@ -73,8 +69,6 @@ export default class MMIController extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any public keyringController: any; - public preferencesController: PreferencesController; - public appStateController: AppStateController; public transactionUpdateController: TransactionUpdateController; @@ -93,7 +87,7 @@ export default class MMIController extends EventEmitter { private metaMetricsController: MetaMetricsController; - private networkController: NetworkController; + #networkControllerState: NetworkState; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -101,9 +95,7 @@ export default class MMIController extends EventEmitter { private signatureController: SignatureController; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private messenger: any; + private messagingSystem: MMIControllerMessenger; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -139,13 +131,10 @@ export default class MMIController extends EventEmitter { }; constructor(opts: MMIControllerOptions) { - super(); - this.opts = opts; - this.messenger = opts.messenger; + this.messagingSystem = opts.messenger; this.mmiConfigurationController = opts.mmiConfigurationController; this.keyringController = opts.keyringController; - this.preferencesController = opts.preferencesController; this.appStateController = opts.appStateController; this.transactionUpdateController = opts.transactionUpdateController; this.custodyController = opts.custodyController; @@ -153,7 +142,6 @@ export default class MMIController extends EventEmitter { this.getPendingNonce = opts.getPendingNonce; this.accountTrackerController = opts.accountTrackerController; this.metaMetricsController = opts.metaMetricsController; - this.networkController = opts.networkController; this.permissionController = opts.permissionController; this.signatureController = opts.signatureController; this.platform = opts.platform; @@ -214,6 +202,10 @@ export default class MMIController extends EventEmitter { this.setConnectionRequest(payload); }, ); + + this.#networkControllerState = this.messagingSystem.call( + 'NetworkController:getState', + ); } // End of constructor async persistKeyringsAfterRefreshTokenChange() { @@ -402,7 +394,10 @@ export default class MMIController extends EventEmitter { // Check if any address is already added if ( newAccounts.some((address) => - this.messenger.call('AccountsController:getAccountByAddress', address), + this.messagingSystem.call( + 'AccountsController:getAccountByAddress', + address, + ), ) ) { throw new Error('Cannot import duplicate accounts'); @@ -502,15 +497,17 @@ export default class MMIController extends EventEmitter { // If the label is defined if (label) { // Set the label for the address - const account = this.messenger.call( + const account = this.messagingSystem.call( 'AccountsController:getAccountByAddress', address, ); - this.messenger.call( - 'AccountsController:setAccountName', - account.id, - label, - ); + if (account) { + this.messagingSystem.call( + 'AccountsController:setAccountName', + account.id, + label, + ); + } } } }); @@ -552,7 +549,7 @@ export default class MMIController extends EventEmitter { ) { let currentCustodyType: string = ''; if (!custodianType) { - const { address } = this.messenger.call( + const { address } = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); currentCustodyType = this.custodyController.getCustodyTypeByAddress( @@ -637,7 +634,7 @@ export default class MMIController extends EventEmitter { // Based on a custodian name, get all the tokens associated with that custodian async getCustodianJWTList(custodianEnvName: string) { - const internalAccounts = this.messenger.call( + const internalAccounts = this.messagingSystem.call( 'AccountsController:listAccounts', ); @@ -736,7 +733,8 @@ export default class MMIController extends EventEmitter { const currentAddress = address || - this.messenger.call('AccountsController:getSelectedAccount').address; + this.messagingSystem.call('AccountsController:getSelectedAccount') + .address; const currentCustodyType = this.custodyController.getCustodyTypeByAddress( toChecksumHexAddress(currentAddress), ); @@ -761,7 +759,7 @@ export default class MMIController extends EventEmitter { // TEMP: Convert internal accounts to match identities format // TODO: Convert handleMmiPortfolio to use internal accounts - const internalAccounts = this.messenger + const internalAccounts = this.messagingSystem .call('AccountsController:listAccounts') .map((internalAccount: InternalAccount) => { return { @@ -774,9 +772,10 @@ export default class MMIController extends EventEmitter { this.custodyController.getAccountDetails(address); const extensionId = this.extension.runtime.id; - const networks = Object.values( - this.networkController.state.networkConfigurationsByChainId, + const { networkConfigurationsByChainId } = this.messagingSystem.call( + 'NetworkController:getState', ); + const networks = Object.values(networkConfigurationsByChainId); return handleMmiPortfolio({ keyringAccounts, @@ -848,30 +847,38 @@ export default class MMIController extends EventEmitter { async setAccountAndNetwork(origin: string, address: string, chainId: number) { await this.appStateController.getUnlockPromise(true); const addressToLowerCase = address.toLowerCase(); - const { address: selectedAddress } = this.messenger.call( + const { address: selectedAddress } = this.messagingSystem.call( 'AccountsController:getSelectedAccount', ); if (selectedAddress.toLowerCase() !== addressToLowerCase) { - const internalAccount = this.messenger.call( + const internalAccount = this.messagingSystem.call( 'AccountsController:getAccountByAddress', addressToLowerCase, ); - this.messenger.call( - 'AccountsController:setSelectedAccount', - internalAccount.id, - ); + if (internalAccount) { + this.messagingSystem.call( + 'AccountsController:setSelectedAccount', + internalAccount.id, + ); + } } - const selectedChainId = getCurrentChainId({ - metamask: this.networkController.state, - }); + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + const { + configuration: { chainId: selectedChainId }, + } = this.messagingSystem.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ); if (selectedChainId !== toHex(chainId)) { - const networkConfiguration = - this.networkController.state.networkConfigurationsByChainId[ - toHex(chainId) - ]; + const networkConfiguration = this.messagingSystem.call( + 'NetworkController:getNetworkConfigurationByChainId', + toHex(chainId), + ); const { networkClientId } = networkConfiguration?.rpcEndpoints?.[ @@ -879,7 +886,10 @@ export default class MMIController extends EventEmitter { ] ?? {}; if (networkClientId) { - await this.networkController.setActiveNetwork(networkClientId); + await this.messagingSystem.call( + 'NetworkController:setActiveNetwork', + networkClientId, + ); } } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 89d91f134ae2..b11ffac6d2a8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -282,7 +282,7 @@ import { checkForMultipleVersionsRunning, } from './detect-multiple-instances'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) -import MMIController from './controllers/mmi-controller'; +import { MMIController } from './controllers/mmi-controller'; import { mmiKeyringBuilderFactory } from './mmi-keyring-builder-factory'; ///: END:ONLY_INCLUDE_IF import ComposableObservableStore from './lib/ComposableObservableStore'; @@ -2026,6 +2026,8 @@ export default class MetamaskController extends EventEmitter { 'AccountsController:listAccounts', 'AccountsController:getSelectedAccount', 'AccountsController:setSelectedAccount', + 'NetworkController:getState', + 'NetworkController:setActiveNetwork', ], }); @@ -2033,7 +2035,6 @@ export default class MetamaskController extends EventEmitter { messenger: mmiControllerMessenger, mmiConfigurationController: this.mmiConfigurationController, keyringController: this.keyringController, - preferencesController: this.preferencesController, appStateController: this.appStateController, transactionUpdateController: this.transactionUpdateController, custodyController: this.custodyController, @@ -2041,7 +2042,6 @@ export default class MetamaskController extends EventEmitter { getPendingNonce: this.getPendingNonce.bind(this), accountTrackerController: this.accountTrackerController, metaMetricsController: this.metaMetricsController, - networkController: this.networkController, permissionController: this.permissionController, signatureController: this.signatureController, platform: this.platform, diff --git a/shared/constants/mmi-controller.ts b/shared/constants/mmi-controller.ts index 67be9f72cee6..83998fe6e7b9 100644 --- a/shared/constants/mmi-controller.ts +++ b/shared/constants/mmi-controller.ts @@ -3,10 +3,20 @@ import { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionUpdateController } from '@metamask-institutional/transaction-update'; import { CustodyController } from '@metamask-institutional/custody-controller'; import { SignatureController } from '@metamask/signature-controller'; -import { NetworkController } from '@metamask/network-controller'; -// TODO: Remove restricted import -// eslint-disable-next-line import/no-restricted-paths -import { PreferencesController } from '../../app/scripts/controllers/preferences-controller'; +import { + NetworkController, + NetworkControllerGetNetworkClientByIdAction, + NetworkControllerGetStateAction, + NetworkControllerSetActiveNetworkAction, +} from '@metamask/network-controller'; +import { + AccountsControllerGetAccountByAddressAction, + AccountsControllerSetAccountNameAction, + AccountsControllerListAccountsAction, + AccountsControllerGetSelectedAccountAction, + AccountsControllerSetSelectedAccountAction, +} from '@metamask/accounts-controller'; +import { RestrictedControllerMessenger } from '@metamask/base-controller'; // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { AppStateController } from '../../app/scripts/controllers/app-state-controller'; @@ -17,18 +27,48 @@ import AccountTrackerController from '../../app/scripts/controllers/account-trac // eslint-disable-next-line import/no-restricted-paths import MetaMetricsController from '../../app/scripts/controllers/metametrics'; +// Unique name for the controller +const controllerName = 'MMIController'; + +type NetworkControllerGetNetworkConfigurationByChainId = { + type: `NetworkController:getNetworkConfigurationByChainId`; + handler: NetworkController['getNetworkConfigurationByChainId']; +}; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | AccountsControllerGetAccountByAddressAction + | AccountsControllerSetAccountNameAction + | AccountsControllerListAccountsAction + | AccountsControllerGetSelectedAccountAction + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetStateAction + | NetworkControllerSetActiveNetworkAction + | NetworkControllerGetNetworkClientByIdAction + | NetworkControllerGetNetworkConfigurationByChainId; + +/** + * Messenger type for the {@link MMIController}. + */ +export type MMIControllerMessenger = RestrictedControllerMessenger< + typeof controllerName, + AllowedActions, + never, + AllowedActions['type'], + never +>; + export type MMIControllerOptions = { mmiConfigurationController: MmiConfigurationController; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any keyringController: any; - preferencesController: PreferencesController; appStateController: AppStateController; transactionUpdateController: TransactionUpdateController; custodyController: CustodyController; - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - messenger: any; + messenger: MMIControllerMessenger; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any getState: () => any; From afa736569ce1bfbd5292dc28b9694ae13c7d8941 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 7 Nov 2024 17:06:24 +0100 Subject: [PATCH 051/111] fix: disable account syncing (#28359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR disables account syncing. The QA team found a critical bug that prevents us from being confident enough to release this feature now. We'll continue investigating and we'll re-enable when we're 100% confident about this. ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28359?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. No testing steps. Account syncing is disabled ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --- app/scripts/metamask-controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index b11ffac6d2a8..caac30573406 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -156,6 +156,7 @@ import { NotificationServicesPushController, NotificationServicesController, } from '@metamask/notification-services-controller'; +import { isProduction } from '../../shared/modules/environment'; import { methodsRequiringNetworkSwitch, methodsThatCanSwitchNetworkWithoutApproval, @@ -1572,7 +1573,7 @@ export default class MetamaskController extends EventEmitter { }, }, env: { - isAccountSyncingEnabled: isManifestV3, + isAccountSyncingEnabled: !isProduction() && isManifestV3, }, messenger: this.controllerMessenger.getRestricted({ name: 'UserStorageController', From 1614632ab7bd8a73ed7e6e9c1e44ef26e00583b1 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Thu, 7 Nov 2024 11:01:21 -0600 Subject: [PATCH 052/111] fix: Revert "fix: Negate privacy mode in Send screen" (#28360) Reverts MetaMask/metamask-extension#28248 Need to revert this before https://github.com/MetaMask/metamask-extension/pull/28021 --- ui/components/app/currency-input/currency-input.js | 1 - .../user-preferenced-currency-display.component.d.ts | 1 - .../user-preferenced-currency-display.component.js | 3 --- .../asset-picker-amount/asset-balance/asset-balance-text.tsx | 3 --- .../ui/currency-display/currency-display.component.d.ts | 1 - .../ui/currency-display/currency-display.component.js | 4 +--- ui/pages/asset/components/asset-page.tsx | 3 --- 7 files changed, 1 insertion(+), 15 deletions(-) diff --git a/ui/components/app/currency-input/currency-input.js b/ui/components/app/currency-input/currency-input.js index da69f3cbbe70..43da00ad3ab0 100644 --- a/ui/components/app/currency-input/currency-input.js +++ b/ui/components/app/currency-input/currency-input.js @@ -222,7 +222,6 @@ export default function CurrencyInput({ suffix={suffix} className="currency-input__conversion-component" displayValue={displayValue} - privacyModeExempt /> ); }; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index ba465f1c5f08..4db61d568f4a 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -16,7 +16,6 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< showCurrencySuffix?: boolean; shouldCheckShowNativeToken?: boolean; isAggregatedFiatOverviewBalance?: boolean; - privacyModeExempt?: boolean; } >; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 77157e4b5c95..613b731d0a16 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -28,7 +28,6 @@ export default function UserPreferencedCurrencyDisplay({ showNative, showCurrencySuffix, shouldCheckShowNativeToken, - privacyModeExempt, ...restProps }) { // NOTE: When displaying currencies, we need the actual account to detect whether we're in a @@ -84,7 +83,6 @@ export default function UserPreferencedCurrencyDisplay({ numberOfDecimals={numberOfDecimals} prefixComponent={prefixComponent} suffix={showCurrencySuffix && !showEthLogo && currency} - privacyModeExempt={privacyModeExempt} /> ); } @@ -128,7 +126,6 @@ const UserPreferencedCurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, shouldCheckShowNativeToken: PropTypes.bool, - privacyModeExempt: PropTypes.bool, }; UserPreferencedCurrencyDisplay.propTypes = diff --git a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx index fbf608deedd8..67def7ee82b1 100644 --- a/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-balance/asset-balance-text.tsx @@ -105,7 +105,6 @@ export function AssetBalanceText({ currency={secondaryCurrency} numberOfDecimals={2} displayValue={`${formattedFiat}${errorText}`} - privacyModeExempt /> ); } @@ -117,7 +116,6 @@ export function AssetBalanceText({ {...commonProps} value={asset.balance} type={PRIMARY} - privacyModeExempt /> {errorText ? ( ); } diff --git a/ui/components/ui/currency-display/currency-display.component.d.ts b/ui/components/ui/currency-display/currency-display.component.d.ts index fd642cd4e6cd..d8e1d6b60e1e 100644 --- a/ui/components/ui/currency-display/currency-display.component.d.ts +++ b/ui/components/ui/currency-display/currency-display.component.d.ts @@ -25,7 +25,6 @@ export type CurrencyDisplayProps = OverridingUnion< // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any suffixProps?: Record; - privacyModeExempt?: boolean; } >; diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index a1b413dd6a7f..a0bb114409f6 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -35,7 +35,6 @@ export default function CurrencyDisplay({ textProps = {}, suffixProps = {}, isAggregatedFiatOverviewBalance = false, - privacyModeExempt, ...props }) { const { privacyMode } = useSelector(getPreferences); @@ -77,7 +76,7 @@ export default function CurrencyDisplay({ className="currency-display-component__text" ellipsis variant={TextVariant.inherit} - isHidden={!privacyModeExempt && privacyMode} + isHidden={privacyMode} data-testid="account-value-and-suffix" {...textProps} > @@ -126,7 +125,6 @@ const CurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, isAggregatedFiatOverviewBalance: PropTypes.bool, - privacyModeExempt: PropTypes.bool, }; CurrencyDisplay.propTypes = CurrencyDisplayPropTypes; diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx index 4fb294b17294..c70b60169edb 100644 --- a/ui/pages/asset/components/asset-page.tsx +++ b/ui/pages/asset/components/asset-page.tsx @@ -8,7 +8,6 @@ import { getCurrentCurrency, getIsBridgeChain, getIsSwapsChain, - getPreferences, getSelectedInternalAccount, getSwapsDefaultToken, getTokensMarketData, @@ -103,7 +102,6 @@ const AssetPage = ({ const conversionRate = useSelector(getConversionRate); const allMarketData = useSelector(getTokensMarketData); const isBridgeChain = useSelector(getIsBridgeChain); - const { privacyMode } = useSelector(getPreferences); const isBuyableChain = useSelector(getIsNativeTokenBuyable); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); const account = useSelector(getSelectedInternalAccount, isEqual); @@ -198,7 +196,6 @@ const AssetPage = ({ tokenImage={image} isOriginalTokenSymbol={asset.isOriginalNativeSymbol} isNativeCurrency={true} - privacyMode={privacyMode} /> ) : ( Date: Thu, 7 Nov 2024 11:29:03 -0600 Subject: [PATCH 053/111] fix: bump `@metamask/queued-request-controller` with patch fix (#28355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Bumps version of QueuedRequestController, with a patch that fixes an issue where `QueuedRequestController.state.queuedRequestCount` is not updated after flushing requests for an origin ## References - https://github.com/MetaMask/core/pull/4899 - https://github.com/MetaMask/core/pull/4846 - https://github.com/MetaMask/metamask-extension/pull/28090 ## Fixes Fixes #28358 [Slack discussion in v12.7.0 RC Thread](https://consensys.slack.com/archives/C029JG63136/p1730918073046389?thread_ts=1729246801.516029&cid=C029JG63136) ## Before https://drive.google.com/file/d/1ujdQgVLlT8KlwRwO-Cc3XvRHPrkpxIg_/view?usp=drive_link ## After https://github.com/user-attachments/assets/e77928e5-165b-441a-b4da-0e10471c0529 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28355?quickstart=1) ## **Manual testing steps** On a dapp permissioned for chain A and B, on chain A, queue up one send transaction, then use wallet_switchEthereumChain to switch to chain B, then queue up several more send transactions. Reject/approve the first transaction. Afterwards, you should see chain B as the active chain for the dapp, and all subsequent approvals cleared/rejected automatically. - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index b570cc6f9245..2fd832a3ce71 100644 --- a/package.json +++ b/package.json @@ -333,7 +333,7 @@ "@metamask/preinstalled-example-snap": "^0.2.0", "@metamask/profile-sync-controller": "^0.9.7", "@metamask/providers": "^14.0.2", - "@metamask/queued-request-controller": "^7.0.0", + "@metamask/queued-request-controller": "^7.0.1", "@metamask/rate-limit-controller": "^6.0.0", "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.1.1", diff --git a/yarn.lock b/yarn.lock index aa975513b90a..f22f36057099 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6069,12 +6069,12 @@ __metadata: languageName: node linkType: hard -"@metamask/queued-request-controller@npm:^7.0.0": - version: 7.0.0 - resolution: "@metamask/queued-request-controller@npm:7.0.0" +"@metamask/queued-request-controller@npm:^7.0.1": + version: 7.0.1 + resolution: "@metamask/queued-request-controller@npm:7.0.1" dependencies: "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/swappable-obj-proxy": "npm:^2.2.0" @@ -6082,7 +6082,7 @@ __metadata: peerDependencies: "@metamask/network-controller": ^22.0.0 "@metamask/selected-network-controller": ^19.0.0 - checksum: 10/69118c11e3faecdbec7c9f02f4ecec4734ce0950115bfac0cdd4338309898690ae3187bcef1cc4f75f54c5c02eff07d80286d3ef29088a665039c13cb50bef88 + checksum: 10/e5b16b3dc2fa0dcf74a81b5046abb65bc05da3802ee891b5a59a80b980301c790cf949d72adba00ead6f5b3d2eaac40694308297f7dc08eb5e5f05b5a68bbf57 languageName: node linkType: hard @@ -26565,7 +26565,7 @@ __metadata: "@metamask/preinstalled-example-snap": "npm:^0.2.0" "@metamask/profile-sync-controller": "npm:^0.9.7" "@metamask/providers": "npm:^14.0.2" - "@metamask/queued-request-controller": "npm:^7.0.0" + "@metamask/queued-request-controller": "npm:^7.0.1" "@metamask/rate-limit-controller": "npm:^6.0.0" "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.1.1" From 2484e864219220a5088e831b41c0144c8a717f10 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Thu, 7 Nov 2024 12:57:17 -0500 Subject: [PATCH 054/111] perf: ensure `setupLocale` doesn't fetch `_locales/en/messages.json` twice (#26553) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We always load the english version of `messages.json`, but we also always load the user's locale's `messages.json`. These can be the same thing but our locale loader didn't take that into consideration. This PR updates the function to only load the user's local if it differs from our default locale, otherwise it just uses the same `messages.json` between the two. Our locale function has a side effect: it loads locale-related Internationalization features as well. So this PR also updates those side-effects in a similar manner to avoid doing the work twice. --- shared/lib/error-utils.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/shared/lib/error-utils.js b/shared/lib/error-utils.js index 89caef28026d..66efd6fb61fb 100644 --- a/shared/lib/error-utils.js +++ b/shared/lib/error-utils.js @@ -5,17 +5,27 @@ import getFirstPreferredLangCode from '../../app/scripts/lib/get-first-preferred import { fetchLocale, loadRelativeTimeFormatLocaleData } from '../modules/i18n'; import switchDirection from './switch-direction'; +const defaultLocale = 'en'; const _setupLocale = async (currentLocale) => { - const currentLocaleMessages = currentLocale - ? await fetchLocale(currentLocale) - : {}; - const enLocaleMessages = await fetchLocale('en'); + const enRelativeTime = loadRelativeTimeFormatLocaleData(defaultLocale); + const enLocale = fetchLocale(defaultLocale); - await loadRelativeTimeFormatLocaleData('en'); - if (currentLocale) { - await loadRelativeTimeFormatLocaleData(currentLocale); + const promises = [enRelativeTime, enLocale]; + if (currentLocale === defaultLocale) { + // enLocaleMessages and currentLocaleMessages are the same; reuse enLocale + promises.push(enLocale); // currentLocaleMessages + } else if (currentLocale) { + // currentLocale does not match enLocaleMessages + promises.push(fetchLocale(currentLocale)); // currentLocaleMessages + promises.push(loadRelativeTimeFormatLocaleData(currentLocale)); + } else { + // currentLocale is not set + promises.push(Promise.resolve({})); // currentLocaleMessages } + const [, enLocaleMessages, currentLocaleMessages] = await Promise.all( + promises, + ); return { currentLocaleMessages, enLocaleMessages }; }; From 54c563ec980699cb28af5df143de6fa4cacd2b7e Mon Sep 17 00:00:00 2001 From: Norbert Elter <72046715+itsyoboieltr@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:19:14 +0400 Subject: [PATCH 055/111] fix: mv2 firefox csp header (#27770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27770?quickstart=1) This PR implements a workaround for a long-standing Firefox MV2 bug where the content-security-policy header is not bypassed, triggering an error. The solution is simple: we check if the extension is MV2 running in Firefox. If yes, we override the header to prevent the error from raising. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/3133, https://github.com/MetaMask/MetaMask-planning/issues/3342 ## **Manual testing steps** 1. Opening github.com should not trigger the CSP error ## **Screenshots/Recordings** ### **Before** csp-toggle-off reprod ### **After** csp-toggle-on fixed ## **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 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/develop/.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. --------- Co-authored-by: David Murdoch <187813+davidmurdoch@users.noreply.github.com> --- app/_locales/en/messages.json | 6 ++ app/scripts/background.js | 41 ++++++++ app/scripts/constants/sentry-state.ts | 1 + .../preferences-controller.test.ts | 17 ++++ .../controllers/preferences-controller.ts | 20 ++++ app/scripts/lib/backup.test.js | 1 + app/scripts/metamask-controller.js | 4 + development/build/utils.js | 2 +- development/create-static-server.js | 13 ++- development/static-server.js | 2 +- .../test/plugins.SelfInjectPlugin.test.ts | 4 +- .../utils/plugins/SelfInjectPlugin/index.ts | 21 +++- .../utils/plugins/SelfInjectPlugin/types.ts | 15 ++- package.json | 1 + shared/modules/add-nonce-to-csp.test.ts | 98 +++++++++++++++++++ shared/modules/add-nonce-to-csp.ts | 38 +++++++ shared/modules/provider-injection.js | 94 +++++++++++------- shared/modules/provider-injection.test.ts | 6 +- test/e2e/default-fixture.js | 1 + test/e2e/fixture-builder.js | 1 + test/e2e/helpers.js | 5 +- test/e2e/phishing-warning-page-server.js | 2 +- .../index.html | 9 ++ .../content-security-policy.spec.ts | 46 +++++++++ ...rs-after-init-opt-in-background-state.json | 1 + .../errors-after-init-opt-in-ui-state.json | 1 + ...s-before-init-opt-in-background-state.json | 1 + .../errors-before-init-opt-in-ui-state.json | 1 + .../synchronous-injection.spec.js | 2 +- ui/helpers/constants/settings.js | 15 +++ ui/helpers/utils/settings-search.js | 6 +- ui/helpers/utils/settings-search.test.js | 9 +- .../advanced-tab/advanced-tab.component.js | 45 +++++++++ .../advanced-tab/advanced-tab.container.js | 6 ++ .../advanced-tab/advanced-tab.stories.js | 18 ++++ ui/store/actions.ts | 12 +++ yarn.lock | 10 ++ 37 files changed, 515 insertions(+), 60 deletions(-) create mode 100644 shared/modules/add-nonce-to-csp.test.ts create mode 100644 shared/modules/add-nonce-to-csp.ts create mode 100644 test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html create mode 100644 test/e2e/tests/content-security-policy/content-security-policy.spec.ts diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 62dda6c29ef0..202dc04c2fa8 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -3789,6 +3789,12 @@ "outdatedBrowserNotification": { "message": "Your browser is out of date. If you don't update your browser, you won't be able to get security patches and new features from MetaMask." }, + "overrideContentSecurityPolicyHeader": { + "message": "Override Content-Security-Policy header" + }, + "overrideContentSecurityPolicyHeaderDescription": { + "message": "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility." + }, "padlock": { "message": "Padlock" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index bacb6adddf9f..90a52b6c0d19 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -56,6 +56,8 @@ import { // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import { getCurrentChainId } from '../../ui/selectors'; +import { addNonceToCsp } from '../../shared/modules/add-nonce-to-csp'; +import { checkURLForProviderInjection } from '../../shared/modules/provider-injection'; import migrations from './migrations'; import Migrator from './lib/migrator'; import ExtensionPlatform from './platforms/extension'; @@ -333,6 +335,40 @@ function maybeDetectPhishing(theController) { ); } +/** + * Overrides the Content-Security-Policy (CSP) header by adding a nonce to the `script-src` directive. + * This is a workaround for [Bug #1446231](https://bugzilla.mozilla.org/show_bug.cgi?id=1446231), + * which involves overriding the page CSP for inline script nodes injected by extension content scripts. + */ +function overrideContentSecurityPolicyHeader() { + // The extension url is unique per install on Firefox, so we can safely add it as a nonce to the CSP header + const nonce = btoa(browser.runtime.getURL('/')); + browser.webRequest.onHeadersReceived.addListener( + ({ responseHeaders, url }) => { + // Check whether inpage.js is going to be injected into the page or not. + // There is no reason to modify the headers if we are not injecting inpage.js. + const isInjected = checkURLForProviderInjection(new URL(url)); + + // Check if the user has enabled the overrideContentSecurityPolicyHeader preference + const isEnabled = + controller.preferencesController.state + .overrideContentSecurityPolicyHeader; + + if (isInjected && isEnabled) { + for (const header of responseHeaders) { + if (header.name.toLowerCase() === 'content-security-policy') { + header.value = addNonceToCsp(header.value, nonce); + } + } + } + + return { responseHeaders }; + }, + { types: ['main_frame', 'sub_frame'], urls: ['http://*/*', 'https://*/*'] }, + ['blocking', 'responseHeaders'], + ); +} + // These are set after initialization let connectRemote; let connectExternalExtension; @@ -479,6 +515,11 @@ async function initialize() { if (!isManifestV3) { await loadPhishingWarningPage(); + // Workaround for Bug #1446231 to override page CSP for inline script nodes injected by extension content scripts + // https://bugzilla.mozilla.org/show_bug.cgi?id=1446231 + if (getPlatform() === PLATFORM_FIREFOX) { + overrideContentSecurityPolicyHeader(); + } } await sendReadyMessageToTabs(); log.info('MetaMask initialization complete.'); diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 3125016ea0b5..655851590441 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -223,6 +223,7 @@ export const SENTRY_BACKGROUND_STATE = { advancedGasFee: true, currentLocale: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: true, forgottenPassword: true, identities: false, diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index 25010cdd3a0f..39a2d49648b2 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -837,6 +837,23 @@ describe('preferences controller', () => { }); }); + describe('overrideContentSecurityPolicyHeader', () => { + it('defaults overrideContentSecurityPolicyHeader to true', () => { + const { controller } = setupController({}); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(true); + }); + + it('set overrideContentSecurityPolicyHeader to false', () => { + const { controller } = setupController({}); + controller.setOverrideContentSecurityPolicyHeader(false); + expect( + controller.state.overrideContentSecurityPolicyHeader, + ).toStrictEqual(false); + }); + }); + describe('snapsAddSnapAccountModalDismissed', () => { it('defaults snapsAddSnapAccountModalDismissed to false', () => { const { controller } = setupController({}); diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index b4ce3ca71e64..dce2ef3d0512 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -133,6 +133,7 @@ export type PreferencesControllerState = Omit< useNonceField: boolean; usePhishDetect: boolean; dismissSeedBackUpReminder: boolean; + overrideContentSecurityPolicyHeader: boolean; useMultiAccountBalanceChecker: boolean; useSafeChainsListValidation: boolean; use4ByteResolution: boolean; @@ -175,6 +176,7 @@ export const getDefaultPreferencesControllerState = useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useMultiAccountBalanceChecker: true, useSafeChainsListValidation: true, // set to true means the dynamic list from the API is being used @@ -306,6 +308,10 @@ const controllerMetadata = { persist: true, anonymous: true, }, + overrideContentSecurityPolicyHeader: { + persist: true, + anonymous: true, + }, useMultiAccountBalanceChecker: { persist: true, anonymous: true, @@ -1009,6 +1015,20 @@ export class PreferencesController extends BaseController< }); } + /** + * A setter for the user preference to override the Content-Security-Policy header + * + * @param overrideContentSecurityPolicyHeader - User preference for overriding the Content-Security-Policy header. + */ + setOverrideContentSecurityPolicyHeader( + overrideContentSecurityPolicyHeader: boolean, + ): void { + this.update((state) => { + state.overrideContentSecurityPolicyHeader = + overrideContentSecurityPolicyHeader; + }); + } + /** * A setter for the incomingTransactions in preference to be updated * diff --git a/app/scripts/lib/backup.test.js b/app/scripts/lib/backup.test.js index b3a7f176c2e6..826aa04018d9 100644 --- a/app/scripts/lib/backup.test.js +++ b/app/scripts/lib/backup.test.js @@ -150,6 +150,7 @@ const jsonData = JSON.stringify({ useNonceField: false, usePhishDetect: true, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, useTokenDetection: false, useCollectibleDetection: false, openSeaEnabled: false, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index caac30573406..d43c12ff24a1 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -3487,6 +3487,10 @@ export default class MetamaskController extends EventEmitter { preferencesController.setDismissSeedBackUpReminder.bind( preferencesController, ), + setOverrideContentSecurityPolicyHeader: + preferencesController.setOverrideContentSecurityPolicyHeader.bind( + preferencesController, + ), setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind( preferencesController, ), diff --git a/development/build/utils.js b/development/build/utils.js index 525815d2520a..626aacd588c7 100644 --- a/development/build/utils.js +++ b/development/build/utils.js @@ -293,7 +293,7 @@ function getBuildName({ function makeSelfInjecting(filePath) { const fileContents = readFileSync(filePath, 'utf8'); const textContent = JSON.stringify(fileContents); - const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};d.documentElement.appendChild(s).remove();}`; + const js = `{let d=document,s=d.createElement('script');s.textContent=${textContent};s.nonce=btoa((globalThis.browser||chrome).runtime.getURL('/'));d.documentElement.appendChild(s).remove();}`; writeFileSync(filePath, js, 'utf8'); } diff --git a/development/create-static-server.js b/development/create-static-server.js index a8d5e28b0088..8e55fa54ca13 100755 --- a/development/create-static-server.js +++ b/development/create-static-server.js @@ -4,10 +4,17 @@ const path = require('path'); const serveHandler = require('serve-handler'); -const createStaticServer = (rootDirectory) => { +/** + * Creates an HTTP server that serves static files from a directory using serve-handler. + * If a request URL starts with `/node_modules/`, it rewrites the URL and serves files from the `node_modules` directory. + * + * @param { NonNullable[2]> } options - Configuration options for serve-handler. Documentation can be found here: https://github.com/vercel/serve-handler + * @returns {http.Server} An instance of an HTTP server configured with the specified options. + */ +const createStaticServer = (options) => { return http.createServer((request, response) => { if (request.url.startsWith('/node_modules/')) { - request.url = request.url.substr(14); + request.url = request.url.slice(14); return serveHandler(request, response, { directoryListing: false, public: path.resolve('./node_modules'), @@ -15,7 +22,7 @@ const createStaticServer = (rootDirectory) => { } return serveHandler(request, response, { directoryListing: false, - public: rootDirectory, + ...options, }); }); }; diff --git a/development/static-server.js b/development/static-server.js index bb15133d6fdd..ec3a51a512f0 100755 --- a/development/static-server.js +++ b/development/static-server.js @@ -31,7 +31,7 @@ const onRequest = (request, response) => { }; const startServer = ({ port, rootDirectory }) => { - const server = createStaticServer(rootDirectory); + const server = createStaticServer({ public: rootDirectory }); server.on('request', onRequest); diff --git a/development/webpack/test/plugins.SelfInjectPlugin.test.ts b/development/webpack/test/plugins.SelfInjectPlugin.test.ts index 3a3ef729eacf..b6390654a4bb 100644 --- a/development/webpack/test/plugins.SelfInjectPlugin.test.ts +++ b/development/webpack/test/plugins.SelfInjectPlugin.test.ts @@ -55,7 +55,7 @@ describe('SelfInjectPlugin', () => { // reference the `sourceMappingURL` assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="${source}\\n//# sourceMappingURL=${filename}.map"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } else { // the new source should NOT reference the new sourcemap, since it's @@ -66,7 +66,7 @@ describe('SelfInjectPlugin', () => { // console. assert.strictEqual( newSource, - `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;d.documentElement.appendChild(s).remove()}`, + `{let d=document,s=d.createElement('script');s.textContent="console.log(3);"+\`\\n//# sourceURL=\${(globalThis.browser||chrome).runtime.getURL("${filename}")};\`;s.nonce=btoa((globalThis.browser||chrome).runtime.getURL("/"));d.documentElement.appendChild(s).remove()}`, ); } diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts index b80f6102ab75..18d3624310ae 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/index.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/index.ts @@ -6,6 +6,19 @@ import type { SelfInjectPluginOptions, Source, Compiler } from './types'; export { type SelfInjectPluginOptions } from './types'; +/** + * Generates a runtime URL expression for a given path. + * + * This function constructs a URL string using the `runtime.getURL` method + * from either the `globalThis.browser` or `chrome` object, depending on + * which one is available in the global scope. + * + * @param path - The path of the runtime URL. + * @returns The constructed runtime URL string. + */ +const getRuntimeURLExpression = (path: string) => + `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(path)})`; + /** * Default options for the SelfInjectPlugin. */ @@ -13,8 +26,11 @@ const defaultOptions = { // The default `sourceUrlExpression` is configured for browser extensions. // It generates the absolute url of the given file as an extension url. // e.g., `chrome-extension:///scripts/inpage.js` - sourceUrlExpression: (filename: string) => - `(globalThis.browser||chrome).runtime.getURL(${JSON.stringify(filename)})`, + sourceUrlExpression: getRuntimeURLExpression, + // The default `nonceExpression` is configured for browser extensions. + // It generates the absolute url of a path as an extension url in base64. + // e.g., `Y2hyb21lLWV4dGVuc2lvbjovLzxleHRlbnNpb24taWQ+Lw==` + nonceExpression: (path: string) => `btoa(${getRuntimeURLExpression(path)})`, } satisfies SelfInjectPluginOptions; /** @@ -142,6 +158,7 @@ export class SelfInjectPlugin { `\`\\n//# sourceURL=\${${this.options.sourceUrlExpression(file)}};\``, ); newSource.add(`;`); + newSource.add(`s.nonce=${this.options.nonceExpression('/')};`); // add and immediately remove the script to avoid modifying the DOM. newSource.add(`d.documentElement.appendChild(s).remove()`); newSource.add(`}`); diff --git a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts index 2240227bd76a..e70467cd270b 100644 --- a/development/webpack/utils/plugins/SelfInjectPlugin/types.ts +++ b/development/webpack/utils/plugins/SelfInjectPlugin/types.ts @@ -23,7 +23,7 @@ export type SelfInjectPluginOptions = { * will be injected into matched file to provide a sourceURL for the self * injected script. * - * Defaults to `(filename: string) => (globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * Defaults to `(filename: string) => (globalThis.browser||chrome).runtime.getURL("${filename}")` * * @example Custom * ```js @@ -39,11 +39,22 @@ export type SelfInjectPluginOptions = { * * ```js * { - * sourceUrlExpression: (filename) => `(globalThis.browser||globalThis.chrome).runtime.getURL("${filename}")` + * sourceUrlExpression: (filename) => `(globalThis.browser||chrome).runtime.getURL("${filename}")` * } * ``` * @param filename - the chunk's relative filename as it will exist in the output directory * @returns */ sourceUrlExpression?: (filename: string) => string; + /** + * A function that returns a JavaScript expression escaped as a string which + * will be injected into matched file to set a nonce for the self + * injected script. + * + * Defaults to `(path: string) => btoa((globalThis.browser||chrome).runtime.getURL("${path}"))` + * + * @param path - the path to be encoded as a nonce + * @returns + */ + nonceExpression?: (path: string) => string; }; diff --git a/package.json b/package.json index 2fd832a3ce71..c612dd76fb20 100644 --- a/package.json +++ b/package.json @@ -528,6 +528,7 @@ "@types/redux-mock-store": "1.0.6", "@types/remote-redux-devtools": "^0.5.5", "@types/selenium-webdriver": "^4.1.19", + "@types/serve-handler": "^6.1.4", "@types/sinon": "^10.0.13", "@types/sprintf-js": "^1", "@types/w3c-web-hid": "^1.0.3", diff --git a/shared/modules/add-nonce-to-csp.test.ts b/shared/modules/add-nonce-to-csp.test.ts new file mode 100644 index 000000000000..dc80bfd9a92a --- /dev/null +++ b/shared/modules/add-nonce-to-csp.test.ts @@ -0,0 +1,98 @@ +import { addNonceToCsp } from './add-nonce-to-csp'; + +describe('addNonceToCsp', () => { + it('empty string', () => { + const input = ''; + const expected = ''; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one empty directive', () => { + const input = 'script-src'; + const expected = `script-src 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, one value', () => { + const input = 'script-src default.example'; + const expected = `script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('one directive, two values', () => { + const input = "script-src 'self' default.example"; + const expected = `script-src 'self' default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('multiple directives', () => { + const input = + "default-src 'self'; script-src 'unsafe-eval' scripts.example; object-src; style-src styles.example"; + const expected = `default-src 'self'; script-src 'unsafe-eval' scripts.example 'nonce-test'; object-src; style-src styles.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('no applicable directive', () => { + const input = 'img-src https://example.com'; + const expected = `img-src https://example.com`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('non-ASCII directives', () => { + const input = 'script-src default.example;\u0080;style-src style.example'; + const expected = `script-src default.example 'nonce-test';\u0080;style-src style.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('uppercase directive names', () => { + const input = 'SCRIPT-SRC DEFAULT.EXAMPLE'; + const expected = `SCRIPT-SRC DEFAULT.EXAMPLE 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('duplicate directive names', () => { + const input = + 'default-src default.example; script-src script.example; script-src script.example'; + const expected = `default-src default.example; script-src script.example 'nonce-test'; script-src script.example`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('nonce value contains script-src', () => { + const input = + "default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com"; + const expected = `default-src 'self' 'nonce-script-src'; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('url value contains script-src', () => { + const input = + "default-src 'self' https://script-src.com; script-src 'self' https://example.com"; + const expected = `default-src 'self' https://script-src.com; script-src 'self' https://example.com 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('fallback to default-src', () => { + const input = `default-src 'none'`; + const expected = `default-src 'none' 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); + + it('keep ascii whitespace characters', () => { + const input = ' script-src default.example '; + const expected = ` script-src default.example 'nonce-test'`; + const output = addNonceToCsp(input, 'test'); + expect(output).toBe(expected); + }); +}); diff --git a/shared/modules/add-nonce-to-csp.ts b/shared/modules/add-nonce-to-csp.ts new file mode 100644 index 000000000000..a8b7fe333089 --- /dev/null +++ b/shared/modules/add-nonce-to-csp.ts @@ -0,0 +1,38 @@ +// ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE. +// See . +const ASCII_WHITESPACE_CHARS = ['\t', '\n', '\f', '\r', ' '].join(''); + +const matchDirective = (directive: string) => + /* eslint-disable require-unicode-regexp */ + new RegExp( + `^([${ASCII_WHITESPACE_CHARS}]*${directive}[${ASCII_WHITESPACE_CHARS}]*)`, // Match the directive and surrounding ASCII whitespace + 'is', // Case-insensitive, including newlines + ); +const matchScript = matchDirective('script-src'); +const matchDefault = matchDirective('default-src'); + +/** + * Adds a nonce to a Content Security Policy (CSP) string. + * + * @param text - The Content Security Policy (CSP) string to add the nonce to. + * @param nonce - The nonce to add to the Content Security Policy (CSP) string. + * @returns The updated Content Security Policy (CSP) string. + */ +export const addNonceToCsp = (text: string, nonce: string) => { + const formattedNonce = ` 'nonce-${nonce}'`; + const directives = text.split(';'); + const scriptIndex = directives.findIndex((directive) => + matchScript.test(directive), + ); + if (scriptIndex >= 0) { + directives[scriptIndex] += formattedNonce; + } else { + const defaultIndex = directives.findIndex((directive) => + matchDefault.test(directive), + ); + if (defaultIndex >= 0) { + directives[defaultIndex] += formattedNonce; + } + } + return directives.join(';'); +}; diff --git a/shared/modules/provider-injection.js b/shared/modules/provider-injection.js index 25a316e93440..b96df88c29e2 100644 --- a/shared/modules/provider-injection.js +++ b/shared/modules/provider-injection.js @@ -5,40 +5,36 @@ */ export default function shouldInjectProvider() { return ( - doctypeCheck() && - suffixCheck() && - documentElementCheck() && - !blockedDomainCheck() + checkURLForProviderInjection(new URL(window.location)) && + checkDocumentForProviderInjection() ); } /** - * Checks the doctype of the current document if it exists + * Checks if a given URL is eligible for provider injection. * - * @returns {boolean} {@code true} if the doctype is html or if none exists + * This function determines if a URL passes the suffix check and is not part of the blocked domains. + * + * @param {URL} url - The URL to be checked for injection. + * @returns {boolean} Returns `true` if the URL passes the suffix check and is not blocked, otherwise `false`. */ -function doctypeCheck() { - const { doctype } = window.document; - if (doctype) { - return doctype.name === 'html'; - } - return true; +export function checkURLForProviderInjection(url) { + return suffixCheck(url) && !blockedDomainCheck(url); } /** - * Returns whether or not the extension (suffix) of the current document is prohibited + * Returns whether or not the extension (suffix) of the given URL's pathname is prohibited * - * This checks {@code window.location.pathname} against a set of file extensions - * that we should not inject the provider into. This check is indifferent of - * query parameters in the location. + * This checks the provided URL's pathname against a set of file extensions + * that we should not inject the provider into. * - * @returns {boolean} whether or not the extension of the current document is prohibited + * @param {URL} url - The URL to check + * @returns {boolean} whether or not the extension of the given URL's pathname is prohibited */ -function suffixCheck() { +function suffixCheck({ pathname }) { const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]; - const currentUrl = window.location.pathname; for (let i = 0; i < prohibitedTypes.length; i++) { - if (prohibitedTypes[i].test(currentUrl)) { + if (prohibitedTypes[i].test(pathname)) { return false; } } @@ -46,24 +42,12 @@ function suffixCheck() { } /** - * Checks the documentElement of the current document + * Checks if the given domain is blocked * - * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + * @param {URL} url - The URL to check + * @returns {boolean} {@code true} if the given domain is blocked */ -function documentElementCheck() { - const documentElement = document.documentElement.nodeName; - if (documentElement) { - return documentElement.toLowerCase() === 'html'; - } - return true; -} - -/** - * Checks if the current domain is blocked - * - * @returns {boolean} {@code true} if the current domain is blocked - */ -function blockedDomainCheck() { +function blockedDomainCheck(url) { // If making any changes, please also update the same list found in the MetaMask-Mobile & SDK repositories const blockedDomains = [ 'execution.consensys.io', @@ -85,8 +69,7 @@ function blockedDomainCheck() { 'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html', ]; - const { hostname: currentHostname, pathname: currentPathname } = - window.location; + const { hostname: currentHostname, pathname: currentPathname } = url; const trimTrailingSlash = (str) => str.endsWith('/') ? str.slice(0, -1) : str; @@ -104,3 +87,38 @@ function blockedDomainCheck() { ) ); } + +/** + * Checks if the document is suitable for provider injection by verifying the doctype and document element. + * + * @returns {boolean} `true` if the document passes both the doctype and document element checks, otherwise `false`. + */ +export function checkDocumentForProviderInjection() { + return doctypeCheck() && documentElementCheck(); +} + +/** + * Checks the doctype of the current document if it exists + * + * @returns {boolean} {@code true} if the doctype is html or if none exists + */ +function doctypeCheck() { + const { doctype } = window.document; + if (doctype) { + return doctype.name === 'html'; + } + return true; +} + +/** + * Checks the documentElement of the current document + * + * @returns {boolean} {@code true} if the documentElement is an html node or if none exists + */ +function documentElementCheck() { + const documentElement = document.documentElement.nodeName; + if (documentElement) { + return documentElement.toLowerCase() === 'html'; + } + return true; +} diff --git a/shared/modules/provider-injection.test.ts b/shared/modules/provider-injection.test.ts index 1742e31b4db6..c7da24b46642 100644 --- a/shared/modules/provider-injection.test.ts +++ b/shared/modules/provider-injection.test.ts @@ -8,11 +8,7 @@ describe('shouldInjectProvider', () => { const urlObj = new URL(urlString); mockedWindow.mockImplementation(() => ({ - location: { - hostname: urlObj.hostname, - origin: urlObj.origin, - pathname: urlObj.pathname, - }, + location: urlObj, document: { doctype: { name: 'html', diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index 2d3d2999ed43..fd2d5be42891 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -193,6 +193,7 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { currentLocale: 'en', useExternalServices: true, dismissSeedBackUpReminder: true, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: { diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 334e2f74ceca..bea9e9bad77f 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -63,6 +63,7 @@ function onboardingFixture() { advancedGasFee: {}, currentLocale: 'en', dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, featureFlags: {}, forgottenPassword: false, identities: {}, diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index 1cb9e47f1f80..c3705c1ebf6c 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -65,6 +65,7 @@ async function withFixtures(options, testSuite) { smartContract, driverOptions, dappOptions, + staticServerOptions, title, ignoredConsoleErrors = [], dappPath = undefined, @@ -159,7 +160,9 @@ async function withFixtures(options, testSuite) { 'dist', ); } - dappServer.push(createStaticServer(dappDirectory)); + dappServer.push( + createStaticServer({ public: dappDirectory, ...staticServerOptions }), + ); dappServer[i].listen(`${dappBasePort + i}`); await new Promise((resolve, reject) => { dappServer[i].on('listening', resolve); diff --git a/test/e2e/phishing-warning-page-server.js b/test/e2e/phishing-warning-page-server.js index 2f6099e1a3d0..a66c9211ed83 100644 --- a/test/e2e/phishing-warning-page-server.js +++ b/test/e2e/phishing-warning-page-server.js @@ -13,7 +13,7 @@ const phishingWarningDirectory = path.resolve( class PhishingWarningPageServer { constructor() { - this._server = createStaticServer(phishingWarningDirectory); + this._server = createStaticServer({ public: phishingWarningDirectory }); } async start({ port = 9999 } = {}) { diff --git a/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html new file mode 100644 index 000000000000..dae42d094890 --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy-mock-page/index.html @@ -0,0 +1,9 @@ + + + + Mock CSP header testing + + +
Mock Page for Content-Security-Policy header testing
+ + diff --git a/test/e2e/tests/content-security-policy/content-security-policy.spec.ts b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts new file mode 100644 index 000000000000..996c64343c5b --- /dev/null +++ b/test/e2e/tests/content-security-policy/content-security-policy.spec.ts @@ -0,0 +1,46 @@ +import { strict as assert } from 'assert'; +import { Suite } from 'mocha'; +import { + defaultGanacheOptions, + openDapp, + unlockWallet, + withFixtures, +} from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; + +describe('Content-Security-Policy', function (this: Suite) { + it('opening a restricted website should still load the extension', async function () { + await withFixtures( + { + dapp: true, + dappPaths: [ + './tests/content-security-policy/content-security-policy-mock-page', + ], + staticServerOptions: { + headers: [ + { + source: 'index.html', + headers: [ + { + key: 'Content-Security-Policy', + value: `default-src 'none'`, + }, + ], + }, + ], + }, + fixtures: new FixtureBuilder().build(), + ganacheOptions: defaultGanacheOptions, + title: this.test?.fullTitle(), + }, + async ({ driver }) => { + await unlockWallet(driver); + await openDapp(driver); + const isExtensionLoaded: boolean = await driver.executeScript( + 'return typeof window.ethereum !== "undefined"', + ); + assert.equal(isExtensionLoaded, true); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 1a871780591f..cebacab2b1d9 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -198,6 +198,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": "boolean", "useTokenDetection": true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index e8f8f81a6293..97399d34c508 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -115,6 +115,7 @@ "useNonceField": false, "usePhishDetect": true, "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "useMultiAccountBalanceChecker": true, "useSafeChainsListValidation": true, "useTokenDetection": true, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 1a51023a2ca1..7622ad15937c 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index bf7f87a16134..b3fa8d117beb 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -115,6 +115,7 @@ "currentLocale": "en", "useExternalServices": "boolean", "dismissSeedBackUpReminder": true, + "overrideContentSecurityPolicyHeader": true, "featureFlags": {}, "forgottenPassword": false, "identities": "object", diff --git a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js index 917e289f320f..8e4254fb8b34 100644 --- a/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js +++ b/test/e2e/tests/synchronous-injection/synchronous-injection.spec.js @@ -7,7 +7,7 @@ const dappPort = 8080; describe('The provider', function () { it('can be injected synchronously and successfully used by a dapp', async function () { - const dappServer = createStaticServer(__dirname); + const dappServer = createStaticServer({ public: __dirname }); dappServer.listen(dappPort); await new Promise((resolve, reject) => { dappServer.on('listening', resolve); diff --git a/ui/helpers/constants/settings.js b/ui/helpers/constants/settings.js index c22b0cbcf183..232bcdae5aff 100644 --- a/ui/helpers/constants/settings.js +++ b/ui/helpers/constants/settings.js @@ -1,4 +1,8 @@ /* eslint-disable @metamask/design-tokens/color-no-hex*/ +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getPlatform } from '../../../app/scripts/lib/util'; +import { PLATFORM_FIREFOX } from '../../../shared/constants/app'; import { IconName } from '../../components/component-library'; import { ADVANCED_ROUTE, @@ -19,6 +23,7 @@ import { * # @param {string} route tab route with appended arbitrary, unique anchor tag / hash route * # @param {string} iconName * # @param {string} featureFlag ENV variable name. If the ENV value exists, the route will be searchable; else, route will not be searchable. + * # @param {boolean} hidden If true, the route will not be searchable. */ /** @type {SettingRouteConfig[]} */ @@ -154,6 +159,16 @@ const SETTINGS_CONSTANTS = [ route: `${ADVANCED_ROUTE}#export-data`, icon: 'fas fa-download', }, + // advanced settingsRefs[11] + { + tabMessage: (t) => t('advanced'), + sectionMessage: (t) => t('overrideContentSecurityPolicyHeader'), + descriptionMessage: (t) => + t('overrideContentSecurityPolicyHeaderDescription'), + route: `${ADVANCED_ROUTE}#override-content-security-policy-header`, + icon: 'fas fa-sliders-h', + hidden: getPlatform() !== PLATFORM_FIREFOX, + }, { tabMessage: (t) => t('contacts'), sectionMessage: (t) => t('contacts'), diff --git a/ui/helpers/utils/settings-search.js b/ui/helpers/utils/settings-search.js index 07b4501c0208..8c11ad8fad52 100644 --- a/ui/helpers/utils/settings-search.js +++ b/ui/helpers/utils/settings-search.js @@ -8,8 +8,10 @@ export function getSettingsRoutes() { if (settingsRoutes) { return settingsRoutes; } - settingsRoutes = SETTINGS_CONSTANTS.filter((routeObject) => - routeObject.featureFlag ? process.env[routeObject.featureFlag] : true, + settingsRoutes = SETTINGS_CONSTANTS.filter( + (routeObject) => + (routeObject.featureFlag ? process.env[routeObject.featureFlag] : true) && + !routeObject.hidden, ); return settingsRoutes; } diff --git a/ui/helpers/utils/settings-search.test.js b/ui/helpers/utils/settings-search.test.js index cc7b875d8c5e..30af3ee6b9da 100644 --- a/ui/helpers/utils/settings-search.test.js +++ b/ui/helpers/utils/settings-search.test.js @@ -68,6 +68,10 @@ const t = (key) => { return 'Dismiss Secret Recovery Phrase backup reminder'; case 'dismissReminderDescriptionField': return 'Turn this on to dismiss the Secret Recovery Phrase backup reminder message. We highly recommend that you back up your Secret Recovery Phrase to avoid loss of funds'; + case 'overrideContentSecurityPolicyHeader': + return 'Override Content-Security-Policy header'; + case 'overrideContentSecurityPolicyHeaderDescription': + return "This option is a workaround for a known issue in Firefox, where a dapp's Content-Security-Policy header may prevent the extension from loading properly. Disabling this option is not recommended unless required for specific web page compatibility."; case 'Contacts': return 'Contacts'; case 'securityAndPrivacy': @@ -147,9 +151,12 @@ describe('Settings Search Utils', () => { describe('getSettingsRoutes', () => { it('should be an array of settings routes objects', () => { const NUM_OF_ENV_FEATURE_FLAG_SETTINGS = 4; + const NUM_OF_HIDDEN_SETTINGS = 1; expect(getSettingsRoutes()).toHaveLength( - SETTINGS_CONSTANTS.length - NUM_OF_ENV_FEATURE_FLAG_SETTINGS, + SETTINGS_CONSTANTS.length - + NUM_OF_ENV_FEATURE_FLAG_SETTINGS - + NUM_OF_HIDDEN_SETTINGS, ); }); }); diff --git a/ui/pages/settings/advanced-tab/advanced-tab.component.js b/ui/pages/settings/advanced-tab/advanced-tab.component.js index 132b97f7caa9..6be7bcd52004 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.component.js @@ -29,6 +29,10 @@ import { getNumberOfSettingRoutesInTab, handleSettingsRefs, } from '../../../helpers/utils/settings-search'; +// TODO: Remove restricted import +// eslint-disable-next-line import/no-restricted-paths +import { getPlatform } from '../../../../app/scripts/lib/util'; +import { PLATFORM_FIREFOX } from '../../../../shared/constants/app'; export default class AdvancedTab extends PureComponent { static contextTypes = { @@ -58,6 +62,8 @@ export default class AdvancedTab extends PureComponent { backupUserData: PropTypes.func.isRequired, showExtensionInFullSizeView: PropTypes.bool, setShowExtensionInFullSizeView: PropTypes.func.isRequired, + overrideContentSecurityPolicyHeader: PropTypes.bool, + setOverrideContentSecurityPolicyHeader: PropTypes.func.isRequired, }; state = { @@ -583,6 +589,42 @@ export default class AdvancedTab extends PureComponent { ); } + renderOverrideContentSecurityPolicyHeader() { + const { t } = this.context; + const { + overrideContentSecurityPolicyHeader, + setOverrideContentSecurityPolicyHeader, + } = this.props; + + return ( + +
+ {t('overrideContentSecurityPolicyHeader')} +
+ {t('overrideContentSecurityPolicyHeaderDescription')} +
+
+ +
+ setOverrideContentSecurityPolicyHeader(!value)} + offLabel={t('off')} + onLabel={t('on')} + /> +
+
+ ); + } + render() { const { errorInSettings } = this.props; // When adding/removing/editing the order of renders, double-check the order of the settingsRefs. This affects settings-search.js @@ -602,6 +644,9 @@ export default class AdvancedTab extends PureComponent { {this.renderAutoLockTimeLimit()} {this.renderUserDataBackup()} {this.renderDismissSeedBackupReminderControl()} + {getPlatform() === PLATFORM_FIREFOX + ? this.renderOverrideContentSecurityPolicyHeader() + : null}
); } diff --git a/ui/pages/settings/advanced-tab/advanced-tab.container.js b/ui/pages/settings/advanced-tab/advanced-tab.container.js index aaa094e0655c..0be61499c1af 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.container.js @@ -7,6 +7,7 @@ import { backupUserData, setAutoLockTimeLimit, setDismissSeedBackUpReminder, + setOverrideContentSecurityPolicyHeader, setFeatureFlag, setShowExtensionInFullSizeView, setShowFiatConversionOnTestnetsPreference, @@ -31,6 +32,7 @@ export const mapStateToProps = (state) => { featureFlags: { sendHexData } = {}, useNonceField, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, } = metamask; const { showFiatInTestnets, @@ -49,6 +51,7 @@ export const mapStateToProps = (state) => { autoLockTimeLimit, useNonceField, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, }; }; @@ -81,6 +84,9 @@ export const mapDispatchToProps = (dispatch) => { setDismissSeedBackUpReminder: (value) => { return dispatch(setDismissSeedBackUpReminder(value)); }, + setOverrideContentSecurityPolicyHeader: (value) => { + return dispatch(setOverrideContentSecurityPolicyHeader(value)); + }, }; }; diff --git a/ui/pages/settings/advanced-tab/advanced-tab.stories.js b/ui/pages/settings/advanced-tab/advanced-tab.stories.js index 36c84cbba8c0..855bdd8aa86a 100644 --- a/ui/pages/settings/advanced-tab/advanced-tab.stories.js +++ b/ui/pages/settings/advanced-tab/advanced-tab.stories.js @@ -12,6 +12,7 @@ export default { showFiatInTestnets: { control: 'boolean' }, useLedgerLive: { control: 'boolean' }, dismissSeedBackUpReminder: { control: 'boolean' }, + overrideContentSecurityPolicyHeader: { control: 'boolean' }, setAutoLockTimeLimit: { action: 'setAutoLockTimeLimit' }, setShowFiatConversionOnTestnetsPreference: { action: 'setShowFiatConversionOnTestnetsPreference', @@ -20,6 +21,9 @@ export default { setIpfsGateway: { action: 'setIpfsGateway' }, setIsIpfsGatewayEnabled: { action: 'setIsIpfsGatewayEnabled' }, setDismissSeedBackUpReminder: { action: 'setDismissSeedBackUpReminder' }, + setOverrideContentSecurityPolicyHeader: { + action: 'setOverrideContentSecurityPolicyHeader', + }, setUseNonceField: { action: 'setUseNonceField' }, setHexDataFeatureFlag: { action: 'setHexDataFeatureFlag' }, displayErrorInSettings: { action: 'displayErrorInSettings' }, @@ -38,6 +42,7 @@ export const DefaultStory = (args) => { sendHexData, showFiatInTestnets, dismissSeedBackUpReminder, + overrideContentSecurityPolicyHeader, }, updateArgs, ] = useArgs(); @@ -65,6 +70,12 @@ export const DefaultStory = (args) => { dismissSeedBackUpReminder: !dismissSeedBackUpReminder, }); }; + + const handleOverrideContentSecurityPolicyHeader = () => { + updateArgs({ + overrideContentSecurityPolicyHeader: !overrideContentSecurityPolicyHeader, + }); + }; return (
{ setShowFiatConversionOnTestnetsPreference={handleShowFiatInTestnets} dismissSeedBackUpReminder={dismissSeedBackUpReminder} setDismissSeedBackUpReminder={handleDismissSeedBackUpReminder} + overrideContentSecurityPolicyHeader={ + overrideContentSecurityPolicyHeader + } + setOverrideContentSecurityPolicyHeader={ + handleOverrideContentSecurityPolicyHeader + } ipfsGateway="ipfs-gateway" />
@@ -91,4 +108,5 @@ DefaultStory.args = { showFiatInTestnets: false, useLedgerLive: false, dismissSeedBackUpReminder: false, + overrideContentSecurityPolicyHeader: true, }; diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 4f66067a5759..67de497817c8 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4218,6 +4218,18 @@ export function setDismissSeedBackUpReminder( }; } +export function setOverrideContentSecurityPolicyHeader( + value: boolean, +): ThunkAction { + return async (dispatch: MetaMaskReduxDispatch) => { + dispatch(showLoadingIndication()); + await submitRequestToBackground('setOverrideContentSecurityPolicyHeader', [ + value, + ]); + dispatch(hideLoadingIndication()); + }; +} + export function getRpcMethodPreferences(): ThunkAction< void, MetaMaskReduxState, diff --git a/yarn.lock b/yarn.lock index f22f36057099..4352ef21767a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11134,6 +11134,15 @@ __metadata: languageName: node linkType: hard +"@types/serve-handler@npm:^6.1.4": + version: 6.1.4 + resolution: "@types/serve-handler@npm:6.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/c92ae204605659b37202af97cfcc7690be43b9290692c1d6c3c93805b399044fd67573af4eb2e7b1fd975451db6d0d5c6cd2f09b20997209fa3341f345f661e4 + languageName: node + linkType: hard + "@types/serve-index@npm:^1.9.4": version: 1.9.4 resolution: "@types/serve-index@npm:1.9.4" @@ -26654,6 +26663,7 @@ __metadata: "@types/redux-mock-store": "npm:1.0.6" "@types/remote-redux-devtools": "npm:^0.5.5" "@types/selenium-webdriver": "npm:^4.1.19" + "@types/serve-handler": "npm:^6.1.4" "@types/sinon": "npm:^10.0.13" "@types/sprintf-js": "npm:^1" "@types/w3c-web-hid": "npm:^1.0.3" From 82fdd64f5787ac4da20d2da6ddcd55787d136163 Mon Sep 17 00:00:00 2001 From: Nick Gambino <35090461+gambinish@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:16:31 -0800 Subject: [PATCH 056/111] fix: Bug 28347 - Privacy mode tweaks (#28367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Privacy Mode should only effect PortfolioView and main account picker popover. It should not impact other areas of the App like Send/Swap/Gas because the toggle only exists on PortfolioView. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28367?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/metamask-extension/issues/28347 ## **Manual testing steps** You can toggle privacyMode with eyeball on main PortfolioView Should respect privacyMode: 1. Go to PortfolioView, toggling eyeball should show/hide balances for tokens as well as main balance 2. Go to AccountPicker from main Portfolio View, balances should hide/show 3. Go to AccountPicker from asset detail view, balances should hide/show Should _not_ respect privacyMode: 1. Go to AssetDetails, token balance should show on main page 2. Should not be respected on send/swap/gas screens 3. Balances should not be impacted elsewhere in the app. Please try to verify this while reviewing. ## **Screenshots/Recordings** https://github.com/user-attachments/assets/695fa68a-c9bb-4871-b03c-8c41c88b1344 ## **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 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/develop/.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. --- ui/components/app/assets/token-cell/token-cell.tsx | 5 +++-- ui/components/app/assets/token-list/token-list.tsx | 4 +++- .../user-preferenced-currency-display.component.d.ts | 1 + .../user-preferenced-currency-display.component.js | 3 +++ ui/components/app/wallet-overview/coin-overview.tsx | 6 +++--- .../multichain/account-list-item/account-list-item.js | 7 +++++++ .../multichain/account-list-menu/account-list-menu.tsx | 3 +++ .../ui/currency-display/currency-display.component.js | 5 ++--- ui/pages/routes/routes.component.js | 7 ++++++- ui/pages/routes/routes.container.js | 3 ++- 10 files changed, 33 insertions(+), 11 deletions(-) diff --git a/ui/components/app/assets/token-cell/token-cell.tsx b/ui/components/app/assets/token-cell/token-cell.tsx index 3a042de1ebb8..31bb388aa65b 100644 --- a/ui/components/app/assets/token-cell/token-cell.tsx +++ b/ui/components/app/assets/token-cell/token-cell.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; -import { getTokenList, getPreferences } from '../../../../selectors'; +import { getTokenList } from '../../../../selectors'; import { useTokenFiatAmount } from '../../../../hooks/useTokenFiatAmount'; import { TokenListItem } from '../../../multichain'; import { isEqualCaseInsensitive } from '../../../../../shared/modules/string-utils'; @@ -12,6 +12,7 @@ type TokenCellProps = { symbol: string; string?: string; image: string; + privacyMode?: boolean; onClick?: (arg: string) => void; }; @@ -20,10 +21,10 @@ export default function TokenCell({ image, symbol, string, + privacyMode = false, onClick, }: TokenCellProps) { const tokenList = useSelector(getTokenList); - const { privacyMode } = useSelector(getPreferences); const tokenData = Object.values(tokenList).find( (token) => isEqualCaseInsensitive(token.symbol, symbol) && diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index 11190c68f267..f0b17d686026 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -30,7 +30,8 @@ export default function TokenList({ nativeToken, }: TokenListProps) { const t = useI18nContext(); - const { tokenSortConfig, tokenNetworkFilter } = useSelector(getPreferences); + const { tokenSortConfig, tokenNetworkFilter, privacyMode } = + useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const conversionRate = useSelector(getConversionRate); const nativeTokenWithBalance = useNativeTokenBalance(); @@ -88,6 +89,7 @@ export default function TokenList({ ); diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts index 4db61d568f4a..779309858a18 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.d.ts @@ -16,6 +16,7 @@ export type UserPrefrencedCurrencyDisplayProps = OverridingUnion< showCurrencySuffix?: boolean; shouldCheckShowNativeToken?: boolean; isAggregatedFiatOverviewBalance?: boolean; + privacyMode?: boolean; } >; diff --git a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index 613b731d0a16..a466f7813672 100644 --- a/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -28,6 +28,7 @@ export default function UserPreferencedCurrencyDisplay({ showNative, showCurrencySuffix, shouldCheckShowNativeToken, + privacyMode = false, ...restProps }) { // NOTE: When displaying currencies, we need the actual account to detect whether we're in a @@ -83,6 +84,7 @@ export default function UserPreferencedCurrencyDisplay({ numberOfDecimals={numberOfDecimals} prefixComponent={prefixComponent} suffix={showCurrencySuffix && !showEthLogo && currency} + privacyMode={privacyMode} /> ); } @@ -126,6 +128,7 @@ const UserPreferencedCurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, shouldCheckShowNativeToken: PropTypes.bool, + privacyMode: PropTypes.bool, }; UserPreferencedCurrencyDisplay.propTypes = diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx index 9f267c96a53d..93d9e1061428 100644 --- a/ui/components/app/wallet-overview/coin-overview.tsx +++ b/ui/components/app/wallet-overview/coin-overview.tsx @@ -132,7 +132,8 @@ export const CoinOverview = ({ const shouldShowPopover = useSelector(getShouldShowAggregatedBalancePopover); const isTestnet = useSelector(getIsTestnet); - const { showFiatInTestnets, privacyMode } = useSelector(getPreferences); + const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } = + useSelector(getPreferences); const selectedAccount = useSelector(getSelectedAccount); const shouldHideZeroBalanceTokens = useSelector( @@ -143,8 +144,6 @@ export const CoinOverview = ({ shouldHideZeroBalanceTokens, ); - const { showNativeTokenAsMainBalance } = useSelector(getPreferences); - const isEvm = useSelector(getMultichainIsEvm); const isNotAggregatedFiatBalance = showNativeTokenAsMainBalance || isTestnet || !isEvm; @@ -281,6 +280,7 @@ export const CoinOverview = ({ isAggregatedFiatOverviewBalance={ !showNativeTokenAsMainBalance && !isTestnet } + privacyMode={privacyMode} /> { const t = useI18nContext(); const [accountOptionsMenuOpen, setAccountOptionsMenuOpen] = useState(false); @@ -313,6 +314,7 @@ const AccountListItem = ({ type={PRIMARY} showFiat={showFiat} data-testid="first-currency-display" + privacyMode={privacyMode} /> @@ -360,6 +362,7 @@ const AccountListItem = ({ type={SECONDARY} showNative data-testid="second-currency-display" + privacyMode={privacyMode} /> @@ -507,6 +510,10 @@ AccountListItem.propTypes = { * Determines if list item should be scrolled to when selected */ shouldScrollToWhenSelected: PropTypes.bool, + /** + * Determines if list balance should be obfuscated + */ + privacyMode: PropTypes.bool, }; AccountListItem.displayName = 'AccountListItem'; diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index eff0d3cb8868..cfb49d246ca6 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -188,6 +188,7 @@ export const mergeAccounts = ( type AccountListMenuProps = { onClose: () => void; + privacyMode?: boolean; showAccountCreation?: boolean; accountListItemProps?: object; allowedAccountTypes?: KeyringAccountType[]; @@ -195,6 +196,7 @@ type AccountListMenuProps = { export const AccountListMenu = ({ onClose, + privacyMode = false, showAccountCreation = true, accountListItemProps, allowedAccountTypes = [ @@ -644,6 +646,7 @@ export const AccountListMenu = ({ isHidden={Boolean(account.hidden)} currentTabOrigin={currentTabOrigin} isActive={Boolean(account.active)} + privacyMode={privacyMode} {...accountListItemProps} /> diff --git a/ui/components/ui/currency-display/currency-display.component.js b/ui/components/ui/currency-display/currency-display.component.js index a0bb114409f6..7e2569ffaee3 100644 --- a/ui/components/ui/currency-display/currency-display.component.js +++ b/ui/components/ui/currency-display/currency-display.component.js @@ -1,10 +1,8 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'; import { EtherDenomination } from '../../../../shared/constants/common'; -import { getPreferences } from '../../../selectors'; import { SensitiveText, Box } from '../../component-library'; import { AlignItems, @@ -35,9 +33,9 @@ export default function CurrencyDisplay({ textProps = {}, suffixProps = {}, isAggregatedFiatOverviewBalance = false, + privacyMode = false, ...props }) { - const { privacyMode } = useSelector(getPreferences); const [title, parts] = useCurrencyDisplay(value, { account, displayValue, @@ -125,6 +123,7 @@ const CurrencyDisplayPropTypes = { textProps: PropTypes.object, suffixProps: PropTypes.object, isAggregatedFiatOverviewBalance: PropTypes.bool, + privacyMode: PropTypes.bool, }; CurrencyDisplay.propTypes = CurrencyDisplayPropTypes; diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index e26e17be9e23..83e707c30f85 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -138,6 +138,7 @@ export default class Routes extends Component { history: PropTypes.object, location: PropTypes.object, autoLockTimeLimit: PropTypes.number, + privacyMode: PropTypes.bool, pageChanged: PropTypes.func.isRequired, browserEnvironmentOs: PropTypes.string, browserEnvironmentBrowser: PropTypes.string, @@ -417,6 +418,7 @@ export default class Routes extends Component { switchedNetworkDetails, clearSwitchedNetworkDetails, clearEditedNetwork, + privacyMode, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal, hideShowKeyringSnapRemovalResultModal, @@ -494,7 +496,10 @@ export default class Routes extends Component { ///: END:ONLY_INCLUDE_IF } {isAccountMenuOpen ? ( - toggleAccountMenu()} /> + toggleAccountMenu()} + privacyMode={privacyMode} + /> ) : null} {isNetworkMenuOpen ? ( Date: Thu, 7 Nov 2024 17:34:47 -0800 Subject: [PATCH 057/111] build: update yarn to v4.5.1 (#28365) ## **Description** Updates yarn to the latest v4.5.1, and adjusts LavaMoat policies to accommodate. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28365?quickstart=1) --- .../generate-attributions/package.json | 2 +- lavamoat/build-system/policy.json | 20 ++----------------- package.json | 2 +- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/development/generate-attributions/package.json b/development/generate-attributions/package.json index 92bf1a5153c2..5778cbd0c634 100644 --- a/development/generate-attributions/package.json +++ b/development/generate-attributions/package.json @@ -9,7 +9,7 @@ }, "engines": { "node": ">= 20", - "yarn": "^4.4.1" + "yarn": "^4.5.1" }, "lavamoat": { "allowScripts": { diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 5a607452526c..2d6b7ce3ad14 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -3004,7 +3004,7 @@ "eslint-plugin-prettier": true, "eslint-plugin-react": true, "eslint-plugin-react-hooks": true, - "eslint>@eslint/eslintrc>ajv": true, + "eslint>ajv": true, "eslint>globals": true, "eslint>ignore": true, "eslint>minimatch": true, @@ -3012,17 +3012,6 @@ "nock>debug": true } }, - "eslint>@eslint/eslintrc>ajv": { - "globals": { - "console": true - }, - "packages": { - "@metamask/snaps-utils>fast-json-stable-stringify": true, - "eslint>@eslint/eslintrc>ajv>json-schema-traverse": true, - "eslint>fast-deep-equal": true, - "uri-js": true - } - }, "eslint>@eslint/eslintrc>import-fresh": { "builtin": { "path.dirname": true @@ -8491,17 +8480,12 @@ "process.stdout.write": true }, "packages": { + "eslint>ajv": true, "lodash": true, - "stylelint>table>ajv": true, "stylelint>table>slice-ansi": true, "stylelint>table>string-width": true } }, - "stylelint>table>ajv": { - "packages": { - "eslint>fast-deep-equal": true - } - }, "stylelint>table>slice-ansi": { "packages": { "stylelint>table>slice-ansi>ansi-styles": true, diff --git a/package.json b/package.json index c612dd76fb20..338e3eaa2068 100644 --- a/package.json +++ b/package.json @@ -751,5 +751,5 @@ "jest-preview": false } }, - "packageManager": "yarn@4.4.1" + "packageManager": "yarn@4.5.1" } From 04dd7d318ec039640960f1fcd4ba26acedb7940e Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 8 Nov 2024 10:17:15 +0000 Subject: [PATCH 058/111] fix: gas limit estimation (#28327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Upgrade transaction controller to fix gas limit estimation on specific networks. ## **Related issues** Fixes: #28307 #28175 ## **Manual testing steps** See issue. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- package.json | 2 +- test/e2e/flask/user-operations.spec.ts | 6 +++-- .../tests/transaction/edit-gas-fee.spec.js | 16 +++++------ .../transaction/multiple-transactions.spec.js | 27 ++++++++----------- yarn.lock | 12 ++++----- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 338e3eaa2068..13bddaa69287 100644 --- a/package.json +++ b/package.json @@ -347,7 +347,7 @@ "@metamask/snaps-sdk": "^6.10.0", "@metamask/snaps-utils": "^8.5.1", "@metamask/solana-wallet-snap": "^0.1.9", - "@metamask/transaction-controller": "^38.1.0", + "@metamask/transaction-controller": "^38.3.0", "@metamask/user-operation-controller": "^13.0.0", "@metamask/utils": "^10.0.1", "@ngraveio/bc-ur": "^1.1.12", diff --git a/test/e2e/flask/user-operations.spec.ts b/test/e2e/flask/user-operations.spec.ts index be7141444c97..7512e0b563c9 100644 --- a/test/e2e/flask/user-operations.spec.ts +++ b/test/e2e/flask/user-operations.spec.ts @@ -256,7 +256,8 @@ describe('User Operations', function () { from: ERC_4337_ACCOUNT, to: GANACHE_ACCOUNT, value: convertETHToHexGwei(1), - data: '0x', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', }); await confirmTransaction(driver); @@ -294,7 +295,8 @@ describe('User Operations', function () { from: ERC_4337_ACCOUNT, to: GANACHE_ACCOUNT, value: convertETHToHexGwei(1), - data: '0x', + maxFeePerGas: '0x0', + maxPriorityFeePerGas: '0x0', }); await confirmTransaction(driver); diff --git a/test/e2e/tests/transaction/edit-gas-fee.spec.js b/test/e2e/tests/transaction/edit-gas-fee.spec.js index 85ae4da3a31f..918831f8f3ad 100644 --- a/test/e2e/tests/transaction/edit-gas-fee.spec.js +++ b/test/e2e/tests/transaction/edit-gas-fee.spec.js @@ -1,11 +1,11 @@ const { strict: assert } = require('assert'); const { createInternalTransaction, + createDappTransaction, } = require('../../page-objects/flows/transaction'); const { withFixtures, - openDapp, unlockWallet, generateGanacheOptions, WINDOW_TITLES, @@ -172,11 +172,9 @@ describe('Editing Confirm Transaction', function () { // login to extension await unlockWallet(driver); - // open dapp and connect - await openDapp(driver); - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', + await createDappTransaction(driver, { + maxFeePerGas: '0x2000000000', + maxPriorityFeePerGas: '0x1000000000', }); // check transaction in extension popup @@ -198,12 +196,12 @@ describe('Editing Confirm Transaction', function () { '.currency-display-component__text', ); const transactionAmount = transactionAmounts[0]; - assert.equal(await transactionAmount.getText(), '0'); + assert.equal(await transactionAmount.getText(), '0.001'); // has correct updated value on the confirm screen the transaction await driver.waitForSelector({ css: '.currency-display-component__text', - text: '0.00021', + text: '0.00185144', }); // confirms the transaction @@ -227,7 +225,7 @@ describe('Editing Confirm Transaction', function () { '[data-testid="transaction-list-item-primary-currency"]', ); assert.equal(txValues.length, 1); - assert.ok(/-0\s*ETH/u.test(await txValues[0].getText())); + assert.ok(/-0.001\s*ETH/u.test(await txValues[0].getText())); }, ); }); diff --git a/test/e2e/tests/transaction/multiple-transactions.spec.js b/test/e2e/tests/transaction/multiple-transactions.spec.js index 8f1318c31e3b..4d913cb07edb 100644 --- a/test/e2e/tests/transaction/multiple-transactions.spec.js +++ b/test/e2e/tests/transaction/multiple-transactions.spec.js @@ -26,19 +26,13 @@ describe('Multiple transactions', function () { // initiates a transaction from the dapp await openDapp(driver); // creates first transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // creates second transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // confirms second transaction @@ -94,19 +88,13 @@ describe('Multiple transactions', function () { // initiates a transaction from the dapp await openDapp(driver); // creates first transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.waitUntilXWindowHandles(3); await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); // creates second transaction - await driver.clickElement({ - text: 'Send EIP 1559 Transaction', - tag: 'button', - }); + await createDappTransaction(driver); await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); // rejects second transaction @@ -141,3 +129,10 @@ describe('Multiple transactions', function () { ); }); }); + +async function createDappTransaction(driver) { + await driver.clickElement({ + text: 'Send EIP 1559 Without Gas', + tag: 'button', + }); +} diff --git a/yarn.lock b/yarn.lock index 4352ef21767a..bb91a8129ac2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6458,9 +6458,9 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^38.1.0": - version: 38.1.0 - resolution: "@metamask/transaction-controller@npm:38.1.0" +"@metamask/transaction-controller@npm:^38.3.0": + version: 38.3.0 + resolution: "@metamask/transaction-controller@npm:38.3.0" dependencies: "@ethereumjs/common": "npm:^3.2.0" "@ethereumjs/tx": "npm:^4.2.0" @@ -6469,7 +6469,7 @@ __metadata: "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" "@metamask/base-controller": "npm:^7.0.2" - "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-query": "npm:^4.0.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" "@metamask/nonce-tracker": "npm:^6.0.0" @@ -6487,7 +6487,7 @@ __metadata: "@metamask/approval-controller": ^7.0.0 "@metamask/gas-fee-controller": ^22.0.0 "@metamask/network-controller": ^22.0.0 - checksum: 10/c1bdca52bbbce42a76ec9c640197534ec6c223b0f5d5815acfa53490dc1175850ea9aeeb6ae3c5ec34218f0bdbbbeb3e8731e2552aa9411e3ed7798a5dea8ab5 + checksum: 10/f4e8e3a1a31e3e62b0d1a59bbe15ebfa4dc3e4cf077fb95c1815c00661c60ef4676046c49f57eab9749cd31d3e55ac3fed7bc247e3f5a3d459f2dcb03998633d languageName: node linkType: hard @@ -26590,7 +26590,7 @@ __metadata: "@metamask/solana-wallet-snap": "npm:^0.1.9" "@metamask/test-bundler": "npm:^1.0.0" "@metamask/test-dapp": "npm:8.13.0" - "@metamask/transaction-controller": "npm:^38.1.0" + "@metamask/transaction-controller": "npm:^38.3.0" "@metamask/user-operation-controller": "npm:^13.0.0" "@metamask/utils": "npm:^10.0.1" "@ngraveio/bc-ur": "npm:^1.1.12" From c5643837734ce1cbdd0b2e55b5c35aa5accef816 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 8 Nov 2024 10:25:30 +0000 Subject: [PATCH 059/111] refactor: remove global network usage from transaction confirmations (#28236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Remove usages of global network selectors from transaction confirmation React components and hooks. Specifically: - Remove usages of the following selectors: - `getConversionRate` - `getCurrentChainId` - `getNativeCurrency` - `getNetworkIdentifier` - `getNftContracts` - `getNfts` - `getProviderConfig` - `getRpcPrefsForCurrentProvider` - Add new selectors: - `selectConversionRateByChainId` - `selectNftsByChainId` - `selectNftContractsByChainId` - `selectNetworkIdentifierByChainId` - Add ESLint rule to prevent further usage of global network selectors in confirmations directory. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28236?quickstart=1) ## **Related issues** Fixes: [#3469](https://github.com/MetaMask/MetaMask-planning/issues/3469) [#3373](https://github.com/MetaMask/MetaMask-planning/issues/3373) [#3486](https://github.com/MetaMask/MetaMask-planning/issues/3486) [#3487](https://github.com/MetaMask/MetaMask-planning/issues/3487) ## **Manual testing steps** Full regression of all transaction confirmations and related functionality. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --------- Co-authored-by: Jyoti Puri --- .eslintrc.js | 25 +++++++++++ .storybook/preview.js | 5 ++- .../confirmations/contract-interaction.ts | 3 +- .../advanced-gas-fee-defaults.js | 14 +++--- .../advanced-gas-fee-defaults.test.js | 4 +- .../confirm-hexdata/confirm-hexdata.test.js | 18 +++++++- .../confirm-page-container.component.js | 12 +++-- .../confirm-page-container.container.js | 3 -- .../confirm-subtitle/confirm-subtitle.test.js | 5 ++- .../approve-static-simulation.tsx | 1 + .../confirm/info/approve/approve.tsx | 1 + .../edit-spending-cap-modal.tsx | 1 + .../approve/spending-cap/spending-cap.tsx | 1 + .../confirm/info/hooks/use-token-values.ts | 1 + .../confirm/info/hooks/useFeeCalculations.ts | 12 +++-- .../advanced-details.stories.tsx | 2 +- .../native-send-heading.tsx | 13 ++++-- .../nft-send-heading/nft-send-heading.tsx | 2 + .../transaction-data.stories.tsx | 1 - .../transaction-details.stories.tsx | 3 -- .../title/hooks/useCurrentSpendingCap.ts | 8 +++- .../contract-details-modal.js | 2 +- .../contract-token-values.js | 8 ++-- .../useBalanceChanges.test.ts | 17 ++++--- .../simulation-details/useBalanceChanges.ts | 11 +++-- .../transaction-alerts/transaction-alerts.js | 11 ++++- .../transaction-alerts.stories.js | 42 ++++++----------- .../transaction-alerts.test.js | 40 ++++++++++++++++- .../confirm-approve-content.component.js | 12 ++--- .../confirm-approve/confirm-approve.js | 26 ++++++----- .../confirm-send-token/confirm-send-token.js | 20 ++++++--- .../confirm-token-transaction-base.js | 37 +++++++++------ .../confirm-transaction-base.component.js | 11 +---- .../confirm-transaction-base.container.js | 31 +++++++++---- .../confirm-transaction-base.test.js | 41 ----------------- .../confirm-token-transaction-switch.js | 4 +- .../contract-interaction.stories.tsx | 19 ++++---- .../confirmations/confirm/stories/utils.tsx | 21 +-------- ui/pages/confirmations/hooks/test-utils.js | 12 ++--- .../confirmations/hooks/useAssetDetails.js | 13 ++++-- .../hooks/useTransactionFunctionType.js | 16 +++++-- .../hooks/useTransactionFunctionType.test.js | 27 ++++++++--- .../confirmations/hooks/useTransactionInfo.js | 3 +- .../hooks/useTransactionInfo.test.js | 7 ++- .../send/gas-display/gas-display.js | 18 ++++---- .../token-allowance.test.js.snap | 14 +++--- .../token-allowance/token-allowance.js | 28 +++++++++--- .../token-allowance/token-allowance.test.js | 2 +- ui/selectors/selectors.js | 45 +++++++++++++++++++ 49 files changed, 416 insertions(+), 257 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 846158a741ef..4aa9ef688592 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -472,5 +472,30 @@ module.exports = { '@metamask/design-tokens/color-no-hex': 'off', }, }, + { + files: ['ui/pages/confirmations/**/*.{js,ts,tsx}'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: `ImportSpecifier[imported.name=/${[ + 'getConversionRate', + 'getCurrentChainId', + 'getNativeCurrency', + 'getNetworkIdentifier', + 'getNftContracts', + 'getNfts', + 'getProviderConfig', + 'getRpcPrefsForCurrentProvider', + 'getUSDConversionRate', + 'isCurrentProviderCustom', + ] + .map((method) => `(${method})`) + .join('|')}/]`, + message: 'Avoid using global network selectors in confirmations', + }, + ], + }, + }, ], }; diff --git a/.storybook/preview.js b/.storybook/preview.js index b46c91339273..525c364f2072 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -15,6 +15,7 @@ import MetaMetricsProviderStorybook from './metametrics'; import testData from './test-data.js'; import { Router } from 'react-router-dom'; import { createBrowserHistory } from 'history'; +import { MemoryRouter } from 'react-router-dom'; import { setBackgroundConnection } from '../ui/store/background-connection'; import { metamaskStorybookTheme } from './metamask-storybook-theme'; import { DocsContainer } from '@storybook/addon-docs'; @@ -147,7 +148,7 @@ const metamaskDecorator = (story, context) => { return ( - + { - + ); }; diff --git a/test/data/confirmations/contract-interaction.ts b/test/data/confirmations/contract-interaction.ts index 49a6e1aad1ab..cbe5dd2bd2ba 100644 --- a/test/data/confirmations/contract-interaction.ts +++ b/test/data/confirmations/contract-interaction.ts @@ -1,4 +1,5 @@ import { + CHAIN_IDS, SimulationData, TransactionMeta, TransactionStatus, @@ -18,7 +19,7 @@ export const CONTRACT_INTERACTION_SENDER_ADDRESS = export const DEPOSIT_METHOD_DATA = '0xd0e30db0'; -export const CHAIN_ID = '0xaa36a7'; +export const CHAIN_ID = CHAIN_IDS.GOERLI; export const genUnapprovedContractInteractionConfirmation = ({ address = CONTRACT_INTERACTION_SENDER_ADDRESS, diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js index a52b4c16f893..106984363699 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.js @@ -11,8 +11,7 @@ import { } from '../../../../../helpers/constants/design-system'; import { getAdvancedGasFeeValues, - getCurrentChainId, - getNetworkIdentifier, + selectNetworkIdentifierByChainId, } from '../../../../../selectors'; import { setAdvancedGasFee } from '../../../../../store/actions'; import { useGasFeeContext } from '../../../../../contexts/gasFee'; @@ -35,11 +34,14 @@ const AdvancedGasFeeDefaults = () => { 10, ).toString(); const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); - // This will need to use a different chainId in multinetwork - const chainId = useSelector(getCurrentChainId); - const networkIdentifier = useSelector(getNetworkIdentifier); const { updateTransactionEventFragment } = useTransactionEventFragment(); - const { editGasMode } = useGasFeeContext(); + const { editGasMode, transaction } = useGasFeeContext(); + const { chainId } = transaction; + + const networkIdentifier = useSelector((state) => + selectNetworkIdentifierByChainId(state, chainId), + ); + const [isDefaultSettingsSelected, setDefaultSettingsSelected] = useState( Boolean(advancedGasFeeValues) && advancedGasFeeValues.maxBaseFee === maxBaseFee && diff --git a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js index 5e330a8b90c7..07c7fc8faaab 100644 --- a/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js +++ b/ui/pages/confirmations/components/advanced-gas-fee-popover/advanced-gas-fee-defaults/advanced-gas-fee-defaults.test.js @@ -21,6 +21,7 @@ import { mockNetworkState } from '../../../../../../test/stub/networks'; import AdvancedGasFeeDefaults from './advanced-gas-fee-defaults'; const TEXT_SELECTOR = 'Save these values as my default for the Goerli network.'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; jest.mock('../../../../../store/actions', () => ({ gasFeeStartPollingByNetworkClientId: jest @@ -43,7 +44,7 @@ const render = async (defaultGasParams, contextParams) => { metamask: { ...mockState.metamask, ...defaultGasParams, - ...mockNetworkState({ chainId: CHAIN_IDS.GOERLI }), + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), accounts: { [mockSelectedInternalAccount.address]: { address: mockSelectedInternalAccount.address, @@ -71,6 +72,7 @@ const render = async (defaultGasParams, contextParams) => { (result = renderWithProvider( { - const store = configureStore(mockState); + const store = configureStore(STATE_MOCK); it('should render function type', async () => { const { findByText } = renderWithProvider( { const { container } = renderWithProvider( { const { container } = renderWithProvider( { const { getByText } = renderWithProvider( { useState(false); const isBuyableChain = useSelector(getIsNativeTokenBuyable); const contact = useSelector((state) => getAddressBookEntry(state, toAddress)); - const networkIdentifier = useSelector(getNetworkIdentifier); const defaultToken = useSelector(getSwapsDefaultToken); const accountBalance = defaultToken.string; const internalAccounts = useSelector(getInternalAccounts); @@ -139,8 +138,13 @@ const ConfirmPageContainer = (props) => { const shouldDisplayWarning = contentComponent && disabled && (errorKey || errorMessage); - const networkName = - NETWORK_TO_NAME_MAP[currentTransaction.chainId] || networkIdentifier; + const { chainId } = currentTransaction; + + const networkIdentifier = useSelector((state) => + selectNetworkIdentifierByChainId(state, chainId), + ); + + const networkName = NETWORK_TO_NAME_MAP[chainId] || networkIdentifier; const fetchCollectionBalance = useCallback(async () => { const tokenBalance = await fetchTokenBalance( diff --git a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js index db07ad4117e7..23eaa43d44ab 100644 --- a/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js +++ b/ui/pages/confirmations/components/confirm-page-container/confirm-page-container.container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; import { getAddressBookEntry, - getNetworkIdentifier, getSwapsDefaultToken, getMetadataContractName, getAccountName, @@ -12,7 +11,6 @@ import ConfirmPageContainer from './confirm-page-container.component'; function mapStateToProps(state, ownProps) { const to = ownProps.toAddress; const contact = getAddressBookEntry(state, to); - const networkIdentifier = getNetworkIdentifier(state); const defaultToken = getSwapsDefaultToken(state); const accountBalance = defaultToken.string; const internalAccounts = getInternalAccounts(state); @@ -26,7 +24,6 @@ function mapStateToProps(state, ownProps) { toMetadataName, recipientIsOwnedAccount: Boolean(ownedAccountName), to, - networkIdentifier, accountBalance, }; } diff --git a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js index baa2d112b671..fa00f0275350 100644 --- a/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js +++ b/ui/pages/confirmations/components/confirm-subtitle/confirm-subtitle.test.js @@ -1,11 +1,11 @@ import React from 'react'; import { ERC1155, ERC721 } from '@metamask/controller-utils'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import mockState from '../../../../../test/data/mock-state.json'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import configureStore from '../../../../store/store'; import { getSelectedInternalAccountFromMockState } from '../../../../../test/jest/mocks'; -import { getProviderConfig } from '../../../../ducks/metamask/metamask'; import ConfirmSubTitle from './confirm-subtitle'; const mockSelectedInternalAccount = @@ -51,7 +51,7 @@ describe('ConfirmSubTitle', () => { mockState.metamask.preferences.showFiatInTestnets = false; mockState.metamask.allNftContracts = { [mockSelectedInternalAccount.address]: { - [getProviderConfig(mockState).chainId]: [{ address: '0x9' }], + [CHAIN_IDS.GOERLI]: [{ address: '0x9' }], }, }; store = configureStore(mockState); @@ -59,6 +59,7 @@ describe('ConfirmSubTitle', () => { const { findByText } = renderWithProvider( { transactionMeta?.txParams?.to, transactionMeta?.txParams?.from, transactionMeta?.txParams?.data, + transactionMeta?.chainId, ); const decimals = initialDecimals || '0'; diff --git a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx index fa1766a8aa72..d1d9fbc08364 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/approve.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/approve.tsx @@ -30,6 +30,7 @@ const ApproveInfo = () => { transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const { spendingCap, pending } = useApproveTokenSimulation( diff --git a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx index 961e63ae8f92..1eb0ac2cde05 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/edit-spending-cap-modal/edit-spending-cap-modal.tsx @@ -54,6 +54,7 @@ export const EditSpendingCapModal = ({ transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const accountBalance = calcTokenAmount( diff --git a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx index 638ff638844c..8e7b522f050a 100644 --- a/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx +++ b/ui/pages/confirmations/components/confirm/info/approve/spending-cap/spending-cap.tsx @@ -87,6 +87,7 @@ export const SpendingCap = ({ transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const accountBalance = calcTokenAmount( diff --git a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts index f416282ee4ef..087a17c473c6 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/use-token-values.ts @@ -17,6 +17,7 @@ export const useTokenValues = (transactionMeta: TransactionMeta) => { transactionMeta.txParams.to, transactionMeta.txParams.from, transactionMeta.txParams.data, + transactionMeta.chainId, ); const decodedResponse = useDecodedTransactionData(); diff --git a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts index 70bd2c0e3af2..5b54dcc710b8 100644 --- a/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts +++ b/ui/pages/confirmations/components/confirm/info/hooks/useFeeCalculations.ts @@ -12,10 +12,12 @@ import { multiplyHexes, } from '../../../../../../../shared/modules/conversion.utils'; import { Numeric } from '../../../../../../../shared/modules/Numeric'; -import { getConversionRate } from '../../../../../../ducks/metamask/metamask'; import { useFiatFormatter } from '../../../../../../hooks/useFiatFormatter'; import { useGasFeeEstimates } from '../../../../../../hooks/useGasFeeEstimates'; -import { getCurrentCurrency } from '../../../../../../selectors'; +import { + getCurrentCurrency, + selectConversionRateByChainId, +} from '../../../../../../selectors'; import { getMultichainNetwork } from '../../../../../../selectors/multichain'; import { HEX_ZERO } from '../shared/constants'; import { useEIP1559TxFees } from './useEIP1559TxFees'; @@ -30,9 +32,13 @@ const EMPTY_FEES = { export function useFeeCalculations(transactionMeta: TransactionMeta) { const currentCurrency = useSelector(getCurrentCurrency); - const conversionRate = useSelector(getConversionRate); + const { chainId } = transactionMeta; const fiatFormatter = useFiatFormatter(); + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + const multichainNetwork = useSelector(getMultichainNetwork); const ticker = multichainNetwork?.network?.ticker; diff --git a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx index 8654400a0bfa..aa9ec6cdc87c 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/advanced-details/advanced-details.stories.tsx @@ -22,6 +22,6 @@ const Story = { export default Story; -export const DefaultStory = () => ; +export const DefaultStory = () => ; DefaultStory.storyName = 'Default'; diff --git a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx index bd5c7ba8b4f2..9eb70a856464 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/native-send-heading/native-send-heading.tsx @@ -14,7 +14,6 @@ import { } from '../../../../../../../components/component-library'; import Tooltip from '../../../../../../../components/ui/tooltip'; import { getIntlLocale } from '../../../../../../../ducks/locale/locale'; -import { getConversionRate } from '../../../../../../../ducks/metamask/metamask'; import { AlignItems, Display, @@ -25,7 +24,10 @@ import { } from '../../../../../../../helpers/constants/design-system'; import { MIN_AMOUNT } from '../../../../../../../hooks/useCurrencyDisplay'; import { useFiatFormatter } from '../../../../../../../hooks/useFiatFormatter'; -import { getPreferences } from '../../../../../../../selectors'; +import { + getPreferences, + selectConversionRateByChainId, +} from '../../../../../../../selectors'; import { getMultichainNetwork } from '../../../../../../../selectors/multichain'; import { useConfirmContext } from '../../../../../context/confirm'; import { @@ -38,11 +40,16 @@ const NativeSendHeading = () => { const { currentConfirmation: transactionMeta } = useConfirmContext(); + const { chainId } = transactionMeta; + const nativeAssetTransferValue = new BigNumber( transactionMeta.txParams.value as string, ).dividedBy(new BigNumber(10).pow(18)); - const conversionRate = useSelector(getConversionRate); + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + const fiatValue = conversionRate && nativeAssetTransferValue && diff --git a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx index 006613e206c9..2f36d10ce42c 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/nft-send-heading/nft-send-heading.tsx @@ -24,11 +24,13 @@ const NFTSendHeading = () => { const tokenAddress = transactionMeta.txParams.to; const userAddress = transactionMeta.txParams.from; const { data } = transactionMeta.txParams; + const { chainId } = transactionMeta; const { assetName, tokenImage, tokenId } = useAssetDetails( tokenAddress, userAddress, data, + chainId, ); const TokenImage = ; diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx index 091185ae23f3..105838af78c2 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/transaction-data.stories.tsx @@ -24,7 +24,6 @@ function getStore(transactionData?: string, to?: string) { const confirmation = { ...confirmationTemplate, - chainId: '0x1', txParams: { ...confirmationTemplate.txParams, to: to ?? confirmationTemplate.txParams.to, diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx index dfec9c8ef4d0..f37317c230c4 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.stories.tsx @@ -5,7 +5,6 @@ import { PAYMASTER_AND_DATA, genUnapprovedContractInteractionConfirmation, } from '../../../../../../../../test/data/confirmations/contract-interaction'; -import mockState from '../../../../../../../../test/data/mock-state.json'; import { getMockConfirmStateForTransaction } from '../../../../../../../../test/data/confirmations/helper'; import configureStore from '../../../../../../../store/store'; import { ConfirmContextProvider } from '../../../../../context/confirm'; @@ -20,9 +19,7 @@ function getStore() { return configureStore( getMockConfirmStateForTransaction(confirmation, { metamask: { - ...mockState.metamask, preferences: { - ...mockState.metamask.preferences, petnamesEnabled: true, }, userOperations: { diff --git a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts index 5f588a971561..b50948259734 100644 --- a/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts +++ b/ui/pages/confirmations/components/confirm/title/hooks/useCurrentSpendingCap.ts @@ -32,8 +32,14 @@ export function useCurrentSpendingCap(currentConfirmation: Confirmation) { const txParamsData = isTxWithSpendingCap ? currentConfirmation.txParams.data : null; + const chainId = isTxWithSpendingCap ? currentConfirmation.chainId : null; - const { decimals } = useAssetDetails(txParamsTo, txParamsFrom, txParamsData); + const { decimals } = useAssetDetails( + txParamsTo, + txParamsFrom, + txParamsData, + chainId, + ); const { spendingCap, pending } = useApproveTokenSimulation( currentConfirmation as TransactionMeta, diff --git a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js index 795401673691..f433bf4caee3 100644 --- a/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js +++ b/ui/pages/confirmations/components/contract-details-modal/contract-details-modal.js @@ -338,7 +338,7 @@ ContractDetailsModal.propTypes = { */ chainId: PropTypes.string, /** - * Block explorer URL of the current network + * Block explorer URL for the contract chain */ blockExplorerUrl: PropTypes.string, /** diff --git a/ui/pages/confirmations/components/contract-token-values/contract-token-values.js b/ui/pages/confirmations/components/contract-token-values/contract-token-values.js index 6858689f77d0..0e8890b607ec 100644 --- a/ui/pages/confirmations/components/contract-token-values/contract-token-values.js +++ b/ui/pages/confirmations/components/contract-token-values/contract-token-values.js @@ -25,7 +25,7 @@ export default function ContractTokenValues({ address, tokenName, chainId, - rpcPrefs, + blockExplorerUrl, }) { const t = useI18nContext(); const [copied, handleCopy] = useCopyToClipboard(); @@ -69,7 +69,7 @@ export default function ContractTokenValues({ address, chainId, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, null, ); @@ -98,7 +98,7 @@ ContractTokenValues.propTypes = { */ chainId: PropTypes.string, /** - * RPC prefs + * URL for the block explorer */ - rpcPrefs: PropTypes.object, + blockExplorerUrl: PropTypes.string, }; diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts index f77774c6ec4a..5f2e1dcbfd16 100644 --- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts +++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.test.ts @@ -6,10 +6,10 @@ import { } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; import { TokenStandard } from '../../../../../shared/constants/transaction'; -import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { getTokenStandardAndDetails } from '../../../../store/actions'; import { fetchTokenExchangeRates } from '../../../../helpers/utils/util'; import { memoizedGetTokenStandardAndDetails } from '../../utils/token'; +import { selectConversionRateByChainId } from '../../../../selectors'; import { useBalanceChanges } from './useBalanceChanges'; import { FIAT_UNAVAILABLE } from './types'; @@ -17,13 +17,10 @@ jest.mock('react-redux', () => ({ useSelector: jest.fn((selector) => selector()), })); -jest.mock('../../../../ducks/metamask/metamask', () => ({ - getConversionRate: jest.fn(), -})); - jest.mock('../../../../selectors', () => ({ getCurrentChainId: jest.fn(), getCurrentCurrency: jest.fn(), + selectConversionRateByChainId: jest.fn(), })); jest.mock('../../../../helpers/utils/util', () => ({ @@ -34,7 +31,9 @@ jest.mock('../../../../store/actions', () => ({ getTokenStandardAndDetails: jest.fn(), })); -const mockGetConversionRate = getConversionRate as jest.Mock; +const mockSelectConversionRateByChainId = jest.mocked( + selectConversionRateByChainId, +); const mockGetTokenStandardAndDetails = getTokenStandardAndDetails as jest.Mock; const mockFetchTokenExchangeRates = fetchTokenExchangeRates as jest.Mock; @@ -85,7 +84,7 @@ describe('useBalanceChanges', () => { } return Promise.reject(new Error('Unable to determine token standard')); }); - mockGetConversionRate.mockReturnValue(ETH_TO_FIAT_RATE); + mockSelectConversionRateByChainId.mockReturnValue(ETH_TO_FIAT_RATE); mockFetchTokenExchangeRates.mockResolvedValue({ [ERC20_TOKEN_ADDRESS_1_MOCK]: ERC20_TO_FIAT_RATE_1_MOCK, [ERC20_TOKEN_ADDRESS_2_MOCK]: ERC20_TO_FIAT_RATE_2_MOCK, @@ -344,7 +343,7 @@ describe('useBalanceChanges', () => { }); it('handles native fiat rate with more than 15 significant digits', async () => { - mockGetConversionRate.mockReturnValue(0.1234567890123456); + mockSelectConversionRateByChainId.mockReturnValue(0.1234567890123456); const { result, waitForNextUpdate } = setupHook({ ...dummyBalanceChange, difference: DIFFERENCE_ETH_MOCK, @@ -357,7 +356,7 @@ describe('useBalanceChanges', () => { }); it('handles unavailable native fiat rate', async () => { - mockGetConversionRate.mockReturnValue(null); + mockSelectConversionRateByChainId.mockReturnValue(null); const { result, waitForNextUpdate } = setupHook({ ...dummyBalanceChange, difference: DIFFERENCE_ETH_MOCK, diff --git a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts index 1bbc9cb5eec8..666682c95c76 100644 --- a/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts +++ b/ui/pages/confirmations/components/simulation-details/useBalanceChanges.ts @@ -10,8 +10,10 @@ import { BigNumber } from 'bignumber.js'; import { ContractExchangeRates } from '@metamask/assets-controllers'; import { useAsyncResultOrThrow } from '../../../../hooks/useAsyncResult'; import { TokenStandard } from '../../../../../shared/constants/transaction'; -import { getConversionRate } from '../../../../ducks/metamask/metamask'; -import { getCurrentCurrency } from '../../../../selectors'; +import { + getCurrentCurrency, + selectConversionRateByChainId, +} from '../../../../selectors'; import { fetchTokenExchangeRates } from '../../../../helpers/utils/util'; import { ERC20_DEFAULT_DECIMALS, fetchErc20Decimals } from '../../utils/token'; @@ -158,7 +160,10 @@ export const useBalanceChanges = ({ simulationData?: SimulationData; }): { pending: boolean; value: BalanceChange[] } => { const fiatCurrency = useSelector(getCurrentCurrency); - const nativeFiatRate = useSelector(getConversionRate); + + const nativeFiatRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); const { nativeBalanceChange, tokenBalanceChanges = [] } = simulationData ?? {}; diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js index a641ecbab93a..5127003a81e8 100644 --- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js +++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.js @@ -14,7 +14,10 @@ import { } from '../../../../components/component-library'; import SimulationErrorMessage from '../simulation-error-message'; import { SEVERITIES } from '../../../../helpers/constants/design-system'; +// eslint-disable-next-line import/no-duplicates +import { selectNetworkConfigurationByChainId } from '../../../../selectors'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) +// eslint-disable-next-line import/no-duplicates import { submittedPendingTransactionsSelector } from '../../../../selectors'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; ///: END:ONLY_INCLUDE_IF @@ -22,7 +25,6 @@ import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; import { isSuspiciousResponse } from '../../../../../shared/modules/security-provider.utils'; import BlockaidBannerAlert from '../security-provider-banner-alert/blockaid-banner-alert/blockaid-banner-alert'; import SecurityProviderBannerMessage from '../security-provider-banner-message/security-provider-banner-message'; -import { getNativeCurrency } from '../../../../ducks/metamask/metamask'; import { parseStandardTokenTransactionData } from '../../../../../shared/modules/transaction.utils'; import { getTokenValueParam } from '../../../../../shared/lib/metamask-controller-utils'; import { QueuedRequestsBannerAlert } from '../../confirmation/components/queued-requests-banner-alert'; @@ -47,7 +49,12 @@ const TransactionAlerts = ({ ///: END:ONLY_INCLUDE_IF const t = useI18nContext(); - const nativeCurrency = useSelector(getNativeCurrency); + const { chainId } = txData; + + const { nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const transactionData = txData.txParams.data; const currentTokenSymbol = tokenSymbol || nativeCurrency; let currentTokenAmount; diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js index ae46a4a83903..99a60a94e25d 100644 --- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js +++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.stories.js @@ -10,6 +10,8 @@ import { CHAIN_IDS } from '../../../../../shared/constants/network'; import { mockNetworkState } from '../../../../../test/stub/networks'; import TransactionAlerts from '.'; +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + const mockSelectedInternalAccount = getSelectedInternalAccountFromMockState(testData); @@ -24,7 +26,7 @@ const customTransaction = ({ userFeeLevel: estimateUsed ? 'low' : 'medium', blockNumber: `${10902987 + i}`, id: 4678200543090545 + i, - chainId: '0x1', + chainId: CHAIN_ID_MOCK, status: 'confirmed', time: 1600654021000, txParams: { @@ -48,23 +50,14 @@ const customTransaction = ({ }; // simulate gas fee state -const customStore = ({ - supportsEIP1559, - isNetworkBusy, - pendingCount = 0, -} = {}) => { +const customStore = ({ supportsEIP1559, pendingCount = 0 } = {}) => { const data = cloneDeep({ ...testData, metamask: { ...testData?.metamask, - // isNetworkBusy - gasFeeEstimates: { - ...testData?.metamask?.gasFeeEstimates, - networkCongestion: isNetworkBusy ? 1 : 0.1, - }, // supportsEIP1559 ...mockNetworkState({ - chainId: CHAIN_IDS.MAINNET, + chainId: CHAIN_ID_MOCK, metadata: { EIPS: { 1559: Boolean(supportsEIP1559), @@ -75,15 +68,12 @@ const customStore = ({ featureFlags: { ...testData?.metamask?.featureFlags, }, - incomingTransactions: { - ...testData?.metamask?.incomingTransactions, - ...Object.fromEntries( - Array.from({ length: pendingCount }).map((_, i) => { - const transaction = customTransaction({ i, status: 'submitted' }); - return [transaction?.hash, transaction]; - }), + transactions: [ + ...testData.metamask.transactions, + ...Array.from({ length: pendingCount }).map((_, i) => + customTransaction({ i, status: 'submitted' }), ), - }, + ], }, }); return configureStore(data); @@ -99,6 +89,7 @@ export default { args: { userAcknowledgedGasMissing: false, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -129,6 +120,7 @@ DefaultStory.storyName = 'Default'; DefaultStory.args = { ...DefaultStory.args, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -176,15 +168,6 @@ export const LowPriority = (args) => ( ); LowPriority.storyName = 'LowPriority'; -export const BusyNetwork = (args) => ( - - - - - -); -BusyNetwork.storyName = 'BusyNetwork'; - export const SendingZeroAmount = (args) => ( @@ -195,6 +178,7 @@ export const SendingZeroAmount = (args) => ( SendingZeroAmount.storyName = 'SendingZeroAmount'; SendingZeroAmount.args = { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, diff --git a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js index 6ae1a59591dd..4695120fbae7 100644 --- a/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js +++ b/ui/pages/confirmations/components/transaction-alerts/transaction-alerts.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import sinon from 'sinon'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { SECURITY_PROVIDER_MESSAGE_SEVERITY } from '../../../../../shared/constants/security-provider'; import { renderWithProvider } from '../../../../../test/jest'; import { submittedPendingTransactionsSelector } from '../../../../selectors/transactions'; @@ -9,6 +10,7 @@ import configureStore from '../../../../store/store'; import mockState from '../../../../../test/data/mock-state.json'; import * as txUtil from '../../../../../shared/modules/transaction.utils'; import * as metamaskControllerUtils from '../../../../../shared/lib/metamask-controller-utils'; +import { mockNetworkState } from '../../../../../test/stub/networks'; import TransactionAlerts from './transaction-alerts'; jest.mock('../../../../selectors/transactions', () => { @@ -22,17 +24,28 @@ jest.mock('../../../../contexts/gasFee'); jest.mock('../../../../selectors/account-abstraction'); +const CHAIN_ID_MOCK = CHAIN_IDS.MAINNET; + +const STATE_MOCK = { + ...mockState, + metamask: { + ...mockState.metamask, + ...mockNetworkState({ + chainId: CHAIN_ID_MOCK, + }), + }, +}; + function render({ componentProps = {}, useGasFeeContextValue = {}, submittedPendingTransactionsSelectorValue = null, - mockedStore = mockState, }) { useGasFeeContext.mockReturnValue(useGasFeeContextValue); submittedPendingTransactionsSelector.mockReturnValue( submittedPendingTransactionsSelectorValue, ); - const store = configureStore(mockedStore); + const store = configureStore(STATE_MOCK); return renderWithProvider(, store); } @@ -41,6 +54,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, securityAlertResponse: { resultType: 'Malicious', reason: 'blur_farming', @@ -64,6 +78,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, securityProviderResponse: { flagAsDangerous: '?', reason: 'Some reason...', @@ -89,6 +104,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, securityProviderResponse: { flagAsDangerous: SECURITY_PROVIDER_MESSAGE_SEVERITY.NOT_MALICIOUS, }, @@ -118,6 +134,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -141,6 +158,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -160,6 +178,7 @@ describe('TransactionAlerts', () => { componentProps: { setUserAcknowledgedGasMissing, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -181,6 +200,7 @@ describe('TransactionAlerts', () => { componentProps: { userAcknowledgedGasMissing: true, txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -199,6 +219,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -220,6 +241,7 @@ describe('TransactionAlerts', () => { submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -242,6 +264,7 @@ describe('TransactionAlerts', () => { ], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -261,6 +284,7 @@ describe('TransactionAlerts', () => { submittedPendingTransactionsSelectorValue: [], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -282,6 +306,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -301,6 +326,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -322,6 +348,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -345,6 +372,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -367,6 +395,7 @@ describe('TransactionAlerts', () => { submittedPendingTransactionsSelectorValue: [{ some: 'transaction' }], componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -388,6 +417,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -407,6 +437,7 @@ describe('TransactionAlerts', () => { }, componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, @@ -445,6 +476,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -461,6 +493,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -478,6 +511,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x5af3107a4000', }, @@ -492,6 +526,7 @@ describe('TransactionAlerts', () => { const { queryByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x0', }, @@ -508,6 +543,7 @@ describe('TransactionAlerts', () => { const { getByText } = render({ componentProps: { txData: { + chainId: CHAIN_ID_MOCK, txParams: { value: '0x1', }, diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js index ebd57c35a141..007aec372c62 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve-content/confirm-approve-content.component.js @@ -70,7 +70,7 @@ export default class ConfirmApproveContent extends Component { fromAddressIsLedger: PropTypes.bool, chainId: PropTypes.string, tokenAddress: PropTypes.string, - rpcPrefs: PropTypes.object, + blockExplorerUrl: PropTypes.string, isContract: PropTypes.bool, hexTransactionTotal: PropTypes.string, hexMinimumTransactionFee: PropTypes.string, @@ -375,10 +375,10 @@ export default class ConfirmApproveContent extends Component { } getTitleTokenDescription() { - const { tokenId, tokenAddress, rpcPrefs, chainId, userAddress } = + const { tokenId, tokenAddress, blockExplorerUrl, chainId, userAddress } = this.props; const useBlockExplorer = - rpcPrefs?.blockExplorerUrl || + blockExplorerUrl || [...TEST_CHAINS, CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET].includes( chainId, ); @@ -393,7 +393,7 @@ export default class ConfirmApproveContent extends Component { null, userAddress, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, ); const blockExplorerElement = ( @@ -529,7 +529,7 @@ export default class ConfirmApproveContent extends Component { fromAddressIsLedger, toAddress, chainId, - rpcPrefs, + blockExplorerUrl, assetStandard, tokenId, tokenAddress, @@ -612,7 +612,7 @@ export default class ConfirmApproveContent extends Component { tokenAddress={tokenAddress} toAddress={toAddress} chainId={chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} tokenId={tokenId} assetName={assetName} assetStandard={assetStandard} diff --git a/ui/pages/confirmations/confirm-approve/confirm-approve.js b/ui/pages/confirmations/confirm-approve/confirm-approve.js index 1b604ec43e6b..e167c15196ba 100644 --- a/ui/pages/confirmations/confirm-approve/confirm-approve.js +++ b/ui/pages/confirmations/confirm-approve/confirm-approve.js @@ -12,10 +12,7 @@ import { getTokenApprovedParam } from '../../../helpers/utils/token-util'; import { readAddressAsContract } from '../../../../shared/modules/contract-utils'; import { GasFeeContextProvider } from '../../../contexts/gasFee'; import { TransactionModalContextProvider } from '../../../contexts/transaction-modal'; -import { - getNativeCurrency, - isAddressLedger, -} from '../../../ducks/metamask/metamask'; +import { isAddressLedger } from '../../../ducks/metamask/metamask'; import ConfirmContractInteraction from '../confirm-contract-interaction'; import { getCurrentCurrency, @@ -23,10 +20,9 @@ import { getUseNonceField, getCustomNonceValue, getNextSuggestedNonce, - getCurrentChainId, - getRpcPrefsForCurrentProvider, checkNetworkAndAccountSupports1559, getUseCurrencyRateCheck, + selectNetworkConfigurationByChainId, } from '../../../selectors'; import { useApproveTransaction } from '../hooks/useApproveTransaction'; import { useSimulationFailureWarning } from '../hooks/useSimulationFailureWarning'; @@ -66,16 +62,23 @@ export default function ConfirmApprove({ isSetApproveForAll, }) { const dispatch = useDispatch(); - const { txParams: { data: transactionData, from } = {} } = transaction; + const { chainId, txParams: { data: transactionData, from } = {} } = + transaction; const currentCurrency = useSelector(getCurrentCurrency); - const nativeCurrency = useSelector(getNativeCurrency); + + const { nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const subjectMetadata = useSelector(getSubjectMetadata); const useNonceField = useSelector(getUseNonceField); const nextNonce = useSelector(getNextSuggestedNonce); const customNonceValue = useSelector(getCustomNonceValue); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const { blockExplorerUrls } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const blockExplorerUrl = blockExplorerUrls?.[0]; const networkAndAccountSupports1559 = useSelector( checkNetworkAndAccountSupports1559, ); @@ -291,7 +294,7 @@ export default function ConfirmApprove({ txData={transaction} fromAddressIsLedger={fromAddressIsLedger} chainId={chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} isContract={isContract} hasLayer1GasFee={layer1GasFee !== undefined} supportsEIP1559={supportsEIP1559} @@ -334,6 +337,7 @@ ConfirmApprove.propTypes = { userAddress: PropTypes.string, toAddress: PropTypes.string, transaction: PropTypes.shape({ + chainId: PropTypes.string, layer1GasFee: PropTypes.string, origin: PropTypes.string, txParams: PropTypes.shape({ diff --git a/ui/pages/confirmations/confirm-send-token/confirm-send-token.js b/ui/pages/confirmations/confirm-send-token/confirm-send-token.js index 3e24c72541d5..4e68783e9be7 100644 --- a/ui/pages/confirmations/confirm-send-token/confirm-send-token.js +++ b/ui/pages/confirmations/confirm-send-token/confirm-send-token.js @@ -8,11 +8,9 @@ import { editExistingTransaction } from '../../../ducks/send'; import { contractExchangeRateSelector, getCurrentCurrency, + selectConversionRateByChainId, + selectNetworkConfigurationByChainId, } from '../../../selectors'; -import { - getConversionRate, - getNativeCurrency, -} from '../../../ducks/metamask/metamask'; import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { showSendTokenPage } from '../../../store/actions'; import { @@ -49,8 +47,17 @@ export default function ConfirmSendToken({ history.push(SEND_ROUTE); }); }; - const conversionRate = useSelector(getConversionRate); - const nativeCurrency = useSelector(getNativeCurrency); + + const { chainId } = transaction; + + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + + const { nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const currentCurrency = useSelector(getCurrentCurrency); const contractExchangeRate = useSelector(contractExchangeRateSelector); @@ -98,6 +105,7 @@ ConfirmSendToken.propTypes = { toAddress: PropTypes.string, tokenAddress: PropTypes.string, transaction: PropTypes.shape({ + chainId: PropTypes.string, origin: PropTypes.string, txParams: PropTypes.shape({ data: PropTypes.string, diff --git a/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js b/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js index 7b60bf9ab4fe..077d5c757dcb 100644 --- a/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js +++ b/ui/pages/confirmations/confirm-token-transaction-base/confirm-token-transaction-base.js @@ -15,16 +15,12 @@ import { import { PRIMARY } from '../../../helpers/constants/common'; import { contractExchangeRateSelector, - getCurrentChainId, getCurrentCurrency, - getRpcPrefsForCurrentProvider, getSelectedInternalAccount, + selectConversionRateByChainId, + selectNetworkConfigurationByChainId, + selectNftContractsByChainId, } from '../../../selectors'; -import { - getConversionRate, - getNativeCurrency, - getNftContracts, -} from '../../../ducks/metamask/metamask'; import { TokenStandard } from '../../../../shared/constants/transaction'; import { getWeiHexFromDecimalValue, @@ -47,16 +43,28 @@ export default function ConfirmTokenTransactionBase({ ethTransactionTotal, fiatTransactionTotal, hexMaximumTransactionFee, + transaction, }) { const t = useContext(I18nContext); const contractExchangeRate = useSelector(contractExchangeRateSelector); - const nativeCurrency = useSelector(getNativeCurrency); + const { chainId } = transaction; + + const { blockExplorerUrls, nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + + const blockExplorerUrl = blockExplorerUrls?.[0]; const currentCurrency = useSelector(getCurrentCurrency); - const conversionRate = useSelector(getConversionRate); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const chainId = useSelector(getCurrentChainId); + + const conversionRate = useSelector((state) => + selectConversionRateByChainId(state, chainId), + ); + const { address: userAddress } = useSelector(getSelectedInternalAccount); - const nftCollections = useSelector(getNftContracts); + + const nftCollections = useSelector((state) => + selectNftContractsByChainId(state, chainId), + ); const ethTransactionTotalMaxAmount = Number( hexWEIToDecETH(hexMaximumTransactionFee), @@ -64,7 +72,7 @@ export default function ConfirmTokenTransactionBase({ const getTitleTokenDescription = (renderType) => { const useBlockExplorer = - rpcPrefs?.blockExplorerUrl || + blockExplorerUrl || [...TEST_CHAINS, CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET].includes( chainId, ); @@ -87,7 +95,7 @@ export default function ConfirmTokenTransactionBase({ null, userAddress, { - blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null, + blockExplorerUrl: blockExplorerUrl ?? null, }, ); const blockExplorerElement = ( @@ -219,4 +227,5 @@ ConfirmTokenTransactionBase.propTypes = { ethTransactionTotal: PropTypes.string, fiatTransactionTotal: PropTypes.string, hexMaximumTransactionFee: PropTypes.string, + transaction: PropTypes.string, }; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js index 25d99e8a9f16..bcc3279752e2 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.component.js @@ -1015,24 +1015,15 @@ export default class ConfirmTransactionBase extends Component { const { toAddress, fromAddress, - txData: { origin, chainId: txChainId } = {}, + txData: { origin } = {}, getNextNonce, tryReverseResolveAddress, smartTransactionsPreferenceEnabled, currentChainSupportsSmartTransactions, setSwapsFeatureFlags, fetchSmartTransactionsLiveness, - chainId, } = this.props; - // If the user somehow finds themselves seeing a confirmation - // on a network which is not presently selected, throw - if (txChainId === undefined || txChainId !== chainId) { - throw new Error( - `Currently selected chainId (${chainId}) does not match chainId (${txChainId}) on which the transaction was proposed.`, - ); - } - const { trackEvent } = this.context; trackEvent({ category: MetaMetricsEventCategory.Transactions, diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js index 795dfae0c63a..c34025e2f0c5 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.container.js @@ -69,11 +69,8 @@ import { isAddressLedger, updateGasFees, getIsGasEstimatesLoading, - getNativeCurrency, getSendToAccounts, - getProviderConfig, findKeyringForAddress, - getConversionRate, } from '../../../ducks/metamask/metamask'; import { addHexPrefix, @@ -97,10 +94,19 @@ import { CUSTOM_GAS_ESTIMATE } from '../../../../shared/constants/gas'; // eslint-disable-next-line import/no-duplicates import { getIsUsingPaymaster } from '../../../selectors/account-abstraction'; +import { + selectConversionRateByChainId, + selectNetworkConfigurationByChainId, + // eslint-disable-next-line import/no-duplicates +} from '../../../selectors/selectors'; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) +import { + getAccountType, + selectDefaultRpcEndpointByChainId, + // eslint-disable-next-line import/no-duplicates +} from '../../../selectors/selectors'; // eslint-disable-next-line import/no-duplicates -import { getAccountType } from '../../../selectors/selectors'; - import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../shared/constants/app'; import { getIsNoteToTraderSupported, @@ -168,15 +174,16 @@ const mapStateToProps = (state, ownProps) => { const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state); const isBuyableChain = getIsNativeTokenBuyable(state); const { confirmTransaction, metamask } = state; - const conversionRate = getConversionRate(state); const { addressBook, nextNonce } = metamask; const unapprovedTxs = getUnapprovedTransactions(state); - const { chainId } = getProviderConfig(state); const { tokenData, txData, tokenProps, nonce } = confirmTransaction; const { txParams = {}, id: transactionId, type } = txData; const txId = transactionId || paramsTransactionId; const transaction = getUnapprovedTransaction(state, txId) ?? {}; + const { chainId } = transaction; + const conversionRate = selectConversionRateByChainId(state, chainId); + const { from: fromAddress, to: txParamsToAddress, @@ -184,6 +191,7 @@ const mapStateToProps = (state, ownProps) => { gas: gasLimit, data, } = (transaction && transaction.txParams) || txParams; + const accounts = getMetaMaskAccounts(state); const smartTransactionsPreferenceEnabled = getSmartTransactionsPreferenceEnabled(state); @@ -270,7 +278,10 @@ const mapStateToProps = (state, ownProps) => { fullTxData.userFeeLevel === CUSTOM_GAS_ESTIMATE || txParamsAreDappSuggested(fullTxData); const fromAddressIsLedger = isAddressLedger(state, fromAddress); - const nativeCurrency = getNativeCurrency(state); + + const { nativeCurrency } = + selectNetworkConfigurationByChainId(state, chainId) ?? {}; + ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) const accountType = getAccountType(state, fromAddress); const fromChecksumHexAddress = toChecksumHexAddress(fromAddress); @@ -281,7 +292,9 @@ const mapStateToProps = (state, ownProps) => { const custodianPublishesTransaction = getIsCustodianPublishesTransactionSupported(state, fromChecksumHexAddress); const builtinRpcUrl = CHAIN_ID_TO_RPC_URL_MAP[chainId]; - const { rpcUrl: customRpcUrl } = getProviderConfig(state); + + const { url: customRpcUrl } = + selectDefaultRpcEndpointByChainId(state, chainId) ?? {}; const rpcUrl = customRpcUrl || builtinRpcUrl; diff --git a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js index bea6aef1d84d..5a846874fed3 100644 --- a/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js +++ b/ui/pages/confirmations/confirm-transaction-base/confirm-transaction-base.test.js @@ -969,45 +969,4 @@ describe('Confirm Transaction Base', () => { expect(confirmButton).toBeDisabled(); }); }); - - describe('Preventing transaction submission', () => { - it('should throw error when on wrong chain', async () => { - const txParams = { - ...mockTxParams, - to: undefined, - data: '0xa22cb46500000000000000', - chainId: '0x5', - }; - const state = { - ...baseStore, - metamask: { - ...baseStore.metamask, - transactions: [ - { - id: baseStore.confirmTransaction.txData.id, - chainId: '0x5', - status: 'unapproved', - txParams, - }, - ], - ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }), - }, - confirmTransaction: { - ...baseStore.confirmTransaction, - txData: { - ...baseStore.confirmTransaction.txData, - value: '0x0', - isUserOperation: true, - txParams, - chainId: '0x5', - }, - }, - }; - - // Error will be triggered by componentDidMount - await expect(render({ state })).rejects.toThrow( - 'Currently selected chainId (0xaa36a7) does not match chainId (0x5) on which the transaction was proposed.', - ); - }); - }); }); diff --git a/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js b/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js index 1bf5c8771b4f..72f570c5b98b 100644 --- a/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js +++ b/ui/pages/confirmations/confirm-transaction/confirm-token-transaction-switch.js @@ -27,6 +27,7 @@ import { useAssetDetails } from '../hooks/useAssetDetails'; export default function ConfirmTokenTransactionSwitch({ transaction }) { const { + chainId, txParams: { data, to: tokenAddress, from: userAddress } = {}, layer1GasFee, } = transaction; @@ -44,7 +45,7 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) { tokenAmount, tokenId, toAddress, - } = useAssetDetails(tokenAddress, userAddress, data); + } = useAssetDetails(tokenAddress, userAddress, data, chainId); const { ethTransactionTotal, @@ -221,6 +222,7 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) { ConfirmTokenTransactionSwitch.propTypes = { transaction: PropTypes.shape({ + chainId: PropTypes.string, origin: PropTypes.string, txParams: PropTypes.shape({ data: PropTypes.string, diff --git a/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx b/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx index 33a8c2e67f0b..5a531a97787d 100644 --- a/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx +++ b/ui/pages/confirmations/confirm/stories/transactions/contract-interaction.stories.tsx @@ -39,15 +39,16 @@ export const UserOperationStory = () => { }; const confirmState = getMockConfirmStateForTransaction(confirmation, { - metamask: {}, - preferences: { - ...mockState.metamask.preferences, - petnamesEnabled: true, - }, - userOperations: { - [confirmation.id]: { - userOperation: { - paymasterAndData: PAYMASTER_AND_DATA, + metamask: { + preferences: { + ...mockState.metamask.preferences, + petnamesEnabled: true, + }, + userOperations: { + [confirmation.id]: { + userOperation: { + paymasterAndData: PAYMASTER_AND_DATA, + }, }, }, }, diff --git a/ui/pages/confirmations/confirm/stories/utils.tsx b/ui/pages/confirmations/confirm/stories/utils.tsx index 9c68a392cbd7..18f9ae734e31 100644 --- a/ui/pages/confirmations/confirm/stories/utils.tsx +++ b/ui/pages/confirmations/confirm/stories/utils.tsx @@ -1,17 +1,11 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { MemoryRouter, Route } from 'react-router-dom'; import configureStore from '../../../../store/store'; -import { ConfirmContextProvider } from '../../context/confirm'; import ConfirmPage from '../confirm'; export const CONFIRM_PAGE_DECORATOR = [ (story: () => React.ReactFragment) => { - return ( - -
{story()}
-
- ); + return
{story()}
; }, ]; @@ -36,18 +30,7 @@ export function ConfirmStoryTemplate( return ( - {/* Adding the MemoryRouter and Route is a workaround to bypass a 404 error in storybook that - is caused when the 'ui/pages/confirmations/hooks/syncConfirmPath.ts' hook calls - history.replace. To avoid history.replace, we can provide a param id. */} - - } /> - + ); } diff --git a/ui/pages/confirmations/hooks/test-utils.js b/ui/pages/confirmations/hooks/test-utils.js index 908f600564f8..5e327b0467c5 100644 --- a/ui/pages/confirmations/hooks/test-utils.js +++ b/ui/pages/confirmations/hooks/test-utils.js @@ -3,10 +3,6 @@ import { useSelector } from 'react-redux'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import { GasEstimateTypes } from '../../../../shared/constants/gas'; -import { - getConversionRate, - getNativeCurrency, -} from '../../../ducks/metamask/metamask'; import { getCurrentCurrency, getShouldShowFiat, @@ -14,6 +10,7 @@ import { getCurrentKeyring, getTokenExchangeRates, getPreferences, + selectConversionRateByChainId, } from '../../../selectors'; import { @@ -107,13 +104,10 @@ export const generateUseSelectorRouter = if (selector === getMultichainIsEvm) { return true; } - if (selector === getConversionRate) { + if (selector === selectConversionRateByChainId) { return MOCK_ETH_USD_CONVERSION_RATE; } - if ( - selector === getMultichainNativeCurrency || - selector === getNativeCurrency - ) { + if (selector === getMultichainNativeCurrency) { return EtherDenomination.ETH; } if (selector === getPreferences) { diff --git a/ui/pages/confirmations/hooks/useAssetDetails.js b/ui/pages/confirmations/hooks/useAssetDetails.js index 4a9afaf05468..bea2f6da89a8 100644 --- a/ui/pages/confirmations/hooks/useAssetDetails.js +++ b/ui/pages/confirmations/hooks/useAssetDetails.js @@ -1,7 +1,7 @@ import { isEqual } from 'lodash'; import { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { getNfts, getTokens } from '../../../ducks/metamask/metamask'; +import { getTokens } from '../../../ducks/metamask/metamask'; import { getAssetDetails } from '../../../helpers/utils/token-util'; import { hideLoadingIndication, @@ -10,11 +10,18 @@ import { import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; import { usePrevious } from '../../../hooks/usePrevious'; import { useTokenTracker } from '../../../hooks/useTokenTracker'; +import { selectNftsByChainId } from '../../../selectors'; -export function useAssetDetails(tokenAddress, userAddress, transactionData) { +export function useAssetDetails( + tokenAddress, + userAddress, + transactionData, + chainId, +) { const dispatch = useDispatch(); + // state selectors - const nfts = useSelector(getNfts); + const nfts = useSelector((state) => selectNftsByChainId(state, chainId)); const tokens = useSelector(getTokens, isEqual); const currentToken = tokens.find((token) => isEqualCaseInsensitive(token.address, tokenAddress), diff --git a/ui/pages/confirmations/hooks/useTransactionFunctionType.js b/ui/pages/confirmations/hooks/useTransactionFunctionType.js index 559367fff066..10255149eda7 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunctionType.js +++ b/ui/pages/confirmations/hooks/useTransactionFunctionType.js @@ -2,8 +2,10 @@ import { useSelector } from 'react-redux'; import { TransactionType } from '@metamask/transaction-controller'; import { ORIGIN_METAMASK } from '../../../../shared/constants/app'; -import { getKnownMethodData } from '../../../selectors'; -import { getNativeCurrency } from '../../../ducks/metamask/metamask'; +import { + getKnownMethodData, + selectNetworkConfigurationByChainId, +} from '../../../selectors'; import { getTransactionTypeTitle } from '../../../helpers/utils/transactions.util'; import { getMethodName } from '../../../helpers/utils/metrics'; @@ -11,8 +13,12 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; export const useTransactionFunctionType = (txData = {}) => { const t = useI18nContext(); - const nativeCurrency = useSelector(getNativeCurrency); - const { txParams } = txData; + const { chainId, txParams } = txData; + + const networkConfiguration = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const methodData = useSelector( (state) => getKnownMethodData(state, txParams?.data) || {}, ); @@ -21,6 +27,8 @@ export const useTransactionFunctionType = (txData = {}) => { return {}; } + const { nativeCurrency } = networkConfiguration ?? {}; + const isTokenApproval = txData.type === TransactionType.tokenMethodSetApprovalForAll || txData.type === TransactionType.tokenMethodApprove || diff --git a/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js b/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js index 2bdc008e05dd..da625ad68267 100644 --- a/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js +++ b/ui/pages/confirmations/hooks/useTransactionFunctionType.test.js @@ -1,47 +1,64 @@ -import { TransactionType } from '@metamask/transaction-controller'; +import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller'; import { renderHookWithProvider } from '../../../../test/lib/render-helpers'; import mockState from '../../../../test/data/mock-state.json'; +import { mockNetworkState } from '../../../../test/stub/networks'; import { useTransactionFunctionType } from './useTransactionFunctionType'; +const CHAIN_ID_MOCK = CHAIN_IDS.GOERLI; + +const STATE_MOCK = { + ...mockState, + metamask: { + ...mockState.metamask, + ...mockNetworkState({ chainId: CHAIN_ID_MOCK }), + }, +}; + describe('useTransactionFunctionType', () => { it('should return functionType depending on transaction data if present', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType({ + chainId: CHAIN_ID_MOCK, txParams: { data: '0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c9700000000000000000000000000000000000000000000000000000000000011170', }, type: TransactionType.tokenMethodApprove, }), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toStrictEqual('Approve spend limit'); }); + it('should return functionType depending on transaction type if method not present in transaction data', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType({ + chainId: CHAIN_ID_MOCK, txParams: {}, type: TransactionType.tokenMethodTransfer, }), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toStrictEqual('Transfer'); }); + it('should return functionType Contract interaction by default', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType({ + chainId: CHAIN_ID_MOCK, txParams: {}, }), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toStrictEqual('Contract interaction'); }); + it('should return undefined is txData is not present', () => { const { result } = renderHookWithProvider( () => useTransactionFunctionType(), - mockState, + STATE_MOCK, ); expect(result.current.functionType).toBeUndefined(); }); diff --git a/ui/pages/confirmations/hooks/useTransactionInfo.js b/ui/pages/confirmations/hooks/useTransactionInfo.js index 452e44c83dfb..ef709de50486 100644 --- a/ui/pages/confirmations/hooks/useTransactionInfo.js +++ b/ui/pages/confirmations/hooks/useTransactionInfo.js @@ -1,5 +1,4 @@ import { useSelector } from 'react-redux'; -import { getProviderConfig } from '../../../ducks/metamask/metamask'; import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils'; import { getSelectedInternalAccount } from '../../../selectors'; @@ -7,7 +6,7 @@ import { getSelectedInternalAccount } from '../../../selectors'; export const useTransactionInfo = (txData = {}) => { const { allNftContracts } = useSelector((state) => state.metamask); const selectedInternalAccount = useSelector(getSelectedInternalAccount); - const { chainId } = useSelector(getProviderConfig); + const { chainId } = txData; const isNftTransfer = Boolean( allNftContracts?.[selectedInternalAccount.address]?.[chainId]?.find( diff --git a/ui/pages/confirmations/hooks/useTransactionInfo.test.js b/ui/pages/confirmations/hooks/useTransactionInfo.test.js index 61ae036c4000..272c27abbfa4 100644 --- a/ui/pages/confirmations/hooks/useTransactionInfo.test.js +++ b/ui/pages/confirmations/hooks/useTransactionInfo.test.js @@ -1,7 +1,7 @@ +import { CHAIN_IDS } from '@metamask/transaction-controller'; import { renderHookWithProvider } from '../../../../test/lib/render-helpers'; import mockState from '../../../../test/data/mock-state.json'; import { getSelectedInternalAccountFromMockState } from '../../../../test/jest/mocks'; -import { getCurrentChainId } from '../../../selectors'; import { useTransactionInfo } from './useTransactionInfo'; const mockSelectedInternalAccount = @@ -13,22 +13,25 @@ describe('useTransactionInfo', () => { const { result } = renderHookWithProvider( () => useTransactionInfo({ + chainId: CHAIN_IDS.GOERLI, txParams: {}, }), mockState, ); expect(result.current.isNftTransfer).toStrictEqual(false); }); + it('should return true if transaction is NFT transfer', () => { mockState.metamask.allNftContracts = { [mockSelectedInternalAccount.address]: { - [getCurrentChainId(mockState)]: [{ address: '0x9' }], + [CHAIN_IDS.GOERLI]: [{ address: '0x9' }], }, }; const { result } = renderHookWithProvider( () => useTransactionInfo({ + chainId: CHAIN_IDS.GOERLI, txParams: { to: '0x9', }, diff --git a/ui/pages/confirmations/send/gas-display/gas-display.js b/ui/pages/confirmations/send/gas-display/gas-display.js index 33a011c2966a..312854d168a7 100644 --- a/ui/pages/confirmations/send/gas-display/gas-display.js +++ b/ui/pages/confirmations/send/gas-display/gas-display.js @@ -27,14 +27,11 @@ import { getIsTestnet, getUseCurrencyRateCheck, getUnapprovedTransactions, + selectNetworkConfigurationByChainId, } from '../../../../selectors'; import { INSUFFICIENT_TOKENS_ERROR } from '../send.constants'; import { getCurrentDraftTransaction } from '../../../../ducks/send'; -import { - getNativeCurrency, - getProviderConfig, -} from '../../../../ducks/metamask/metamask'; import { showModal } from '../../../../store/actions'; import { addHexes, @@ -52,25 +49,26 @@ import { getIsNativeTokenBuyable } from '../../../../ducks/ramps'; export default function GasDisplay({ gasError }) { const t = useContext(I18nContext); const dispatch = useDispatch(); - const { estimateUsed } = useGasFeeContext(); + const { estimateUsed, transaction } = useGasFeeContext(); + const { chainId } = transaction; const trackEvent = useContext(MetaMetricsContext); - const { openBuyCryptoInPdapp } = useRamps(); - const providerConfig = useSelector(getProviderConfig); + const { name: networkNickname, nativeCurrency } = useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const isTestnet = useSelector(getIsTestnet); const isBuyableChain = useSelector(getIsNativeTokenBuyable); const draftTransaction = useSelector(getCurrentDraftTransaction); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); const { showFiatInTestnets } = useSelector(getPreferences); const unapprovedTxs = useSelector(getUnapprovedTransactions); - const nativeCurrency = useSelector(getNativeCurrency); - const { chainId } = providerConfig; const networkName = NETWORK_TO_NAME_MAP[chainId]; const isInsufficientTokenError = draftTransaction?.amount?.error === INSUFFICIENT_TOKENS_ERROR; const editingTransaction = unapprovedTxs[draftTransaction.id]; - const currentNetworkName = networkName || providerConfig.nickname; + const currentNetworkName = networkName || networkNickname; const transactionData = { txParams: { diff --git a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap index 91ac3c735f84..bc44e728df86 100644 --- a/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap +++ b/ui/pages/confirmations/token-allowance/__snapshots__/token-allowance.test.js.snap @@ -148,14 +148,14 @@ exports[`TokenAllowancePage when mounted should match snapshot 1`] = `
- mainnet + Ethereum Mainnet
getTargetAccountWithSendEtherInfo(state, userAddress), ); - const networkIdentifier = useSelector(getNetworkIdentifier); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + + const { chainId } = txData; + + const networkIdentifier = useSelector((state) => + selectNetworkIdentifierByChainId(state, chainId), + ); + + const { blockExplorerUrls } = + useSelector((state) => + selectNetworkConfigurationByChainId(state, chainId), + ) ?? {}; + + const blockExplorerUrl = blockExplorerUrls?.[0]; const unapprovedTxCount = useSelector(getUnapprovedTxCount); const unapprovedTxs = useSelector(getUnapprovedTransactions); const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck); @@ -387,7 +403,7 @@ export default function TokenAllowance({ tokenName={tokenSymbol} address={tokenAddress} chainId={fullTxData.chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} /> ); @@ -710,7 +726,7 @@ export default function TokenAllowance({ tokenAddress={tokenAddress} toAddress={toAddress} chainId={fullTxData.chainId} - rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} /> )} diff --git a/ui/pages/confirmations/token-allowance/token-allowance.test.js b/ui/pages/confirmations/token-allowance/token-allowance.test.js index ddfc48a2cfaa..68df89777c7e 100644 --- a/ui/pages/confirmations/token-allowance/token-allowance.test.js +++ b/ui/pages/confirmations/token-allowance/token-allowance.test.js @@ -203,7 +203,7 @@ describe('TokenAllowancePage', () => { status: 'unapproved', originalGasEstimate: '0xea60', userEditedGasLimit: false, - chainId: '0x3', + chainId: CHAIN_IDS.MAINNET, loadingDefaults: false, dappSuggestedGasFees: { gasPrice: '0x4a817c800', diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 94d8f39252d8..c4f2928d8ef2 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -734,6 +734,51 @@ export const selectDefaultRpcEndpointByChainId = createSelector( }, ); +/** + * @type (state: any, chainId: string) => number | undefined + */ +export const selectConversionRateByChainId = createSelector( + selectNetworkConfigurationByChainId, + (state) => state, + (networkConfiguration, state) => { + if (!networkConfiguration) { + return undefined; + } + + const { nativeCurrency } = networkConfiguration; + return state.metamask.currencyRates[nativeCurrency]?.conversionRate; + }, +); + +export const selectNftsByChainId = createSelector( + getSelectedInternalAccount, + (state) => state.metamask.allNfts, + (_state, chainId) => chainId, + (selectedAccount, nfts, chainId) => { + return nfts?.[selectedAccount.address]?.[chainId] ?? []; + }, +); + +export const selectNftContractsByChainId = createSelector( + getSelectedInternalAccount, + (state) => state.metamask.allNftContracts, + (_state, chainId) => chainId, + (selectedAccount, nftContracts, chainId) => { + return nftContracts?.[selectedAccount.address]?.[chainId] ?? []; + }, +); + +export const selectNetworkIdentifierByChainId = createSelector( + selectNetworkConfigurationByChainId, + selectDefaultRpcEndpointByChainId, + (networkConfiguration, defaultRpcEndpoint) => { + const { name: nickname } = networkConfiguration ?? {}; + const { url: rpcUrl, networkClientId } = defaultRpcEndpoint ?? {}; + + return nickname || rpcUrl || networkClientId; + }, +); + export function getRequestingNetworkInfo(state, chainIds) { // If chainIds is undefined, set it to an empty array let processedChainIds = chainIds === undefined ? [] : chainIds; From e882da087d6028a5cd884306a2d4b6c8a59db366 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Fri, 8 Nov 2024 11:19:07 +0000 Subject: [PATCH 060/111] fix: Address design review for ERC20 token send (#28212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR implements usage of ENS resolved names existing in state for the PetNames component. It also tweaks various UI elements as per design review. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28212?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3585 ## **Manual testing steps** 1. Initiate an erc20 token send on the wallet UI 2. Check the confirmation screen ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- .../info/row/__snapshots__/row.test.tsx.snap | 2 +- ui/components/app/confirm/info/row/row.tsx | 2 +- .../__snapshots__/name-details.test.tsx.snap | 50 +++- .../name/name-details/name-details.test.tsx | 283 +++++++++++++++++- ui/hooks/useDisplayName.test.ts | 70 ++++- ui/hooks/useDisplayName.ts | 39 ++- .../__snapshots__/approve.test.tsx.snap | 2 +- .../native-transfer.test.tsx.snap | 193 ++++++------ .../native-transfer/native-transfer.test.tsx | 8 +- .../nft-token-transfer.test.tsx.snap | 197 ++++++------ .../nft-token-transfer.test.tsx | 8 +- .../transaction-data.test.tsx.snap | 8 +- .../token-details-section.test.tsx.snap | 17 +- .../token-transfer.test.tsx.snap | 242 ++++++++------- .../token-transfer/token-details-section.tsx | 7 +- .../token-transfer/token-transfer.test.tsx | 8 +- 16 files changed, 770 insertions(+), 366 deletions(-) diff --git a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap index 545d548fa3b7..7dad4ee50357 100644 --- a/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap +++ b/ui/components/app/confirm/info/row/__snapshots__/row.test.tsx.snap @@ -37,7 +37,7 @@ exports[`ConfirmInfoRow should match snapshot when copy is enabled 1`] = `
-
-
-
- -

- 0x2e0D7...5d09B -

-
-
- -
+ + + + + + +
-
-
-
-
+ -

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- E -
-

- ETH -

-
-
-
-
-
-
+ + + + + +
@@ -195,7 +173,7 @@ exports[`NativeTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" >
G
@@ -221,6 +199,21 @@ exports[`NativeTransferInfo renders correctly 1`] = ` > Interacting with

+
+
+ +
+
({ })); describe('NativeTransferInfo', () => { - it('renders correctly', async () => { + it('renders correctly', () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( @@ -32,10 +30,6 @@ describe('NativeTransferInfo', () => { mockStore, ); - await waitFor(() => { - expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); - }); - expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap index 8bd4a73fe440..77794c9e70a4 100644 --- a/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/nft-token-transfer/__snapshots__/nft-token-transfer.test.tsx.snap @@ -15,39 +15,50 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = ` />

- 1 -

+ />
-
-
-
- -

- 0x2e0D7...5d09B -

-
-
- -
+ + + + + + +
-
-
-
-
+ -

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- E -
-

- ETH -

-
-
-
-
-
-
+ + + + + +
@@ -198,7 +174,7 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" >
G
@@ -224,6 +200,21 @@ exports[`NFTTokenTransferInfo renders correctly 1`] = ` > Interacting with

+
+
+ +
+
({ })); describe('NFTTokenTransferInfo', () => { - it('renders correctly', async () => { + it('renders correctly', () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( @@ -32,10 +30,6 @@ describe('NFTTokenTransferInfo', () => { mockStore, ); - await waitFor(() => { - expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); - }); - expect(container).toMatchSnapshot(); }); }); diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap index d03e04d2c673..1b7fd2aeb460 100644 --- a/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap +++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-data/__snapshots__/transaction-data.test.tsx.snap @@ -13,7 +13,7 @@ exports[`TransactionData renders decoded data with names and descriptions 1`] =
-
- ? -
-

- 0 Unknown -

+ + + + + + + +
-
-
-
- -

- 0x2e0D7...5d09B -

-
-
- -
+ + + + + + +
-
-
-
-
+ -

- You send -

-
-
-
-
-
-

- - 4 -

-
-
-
-
-
-
- E -
-

- ETH -

-
-
-
-
-
-
+ + + + + +
@@ -195,7 +202,7 @@ exports[`TokenTransferInfo renders correctly 1`] = ` class="mm-box mm-box--display-flex mm-box--gap-2 mm-box--flex-wrap-wrap mm-box--align-items-center mm-box--min-width-0" >
G
@@ -221,6 +228,21 @@ exports[`TokenTransferInfo renders correctly 1`] = ` > Interacting with

+
+
+ +
+
{ > { ); const tokenRow = transactionMeta.type !== TransactionType.simpleSend && ( - + ({ })); describe('TokenTransferInfo', () => { - it('renders correctly', async () => { + it('renders correctly', () => { const state = getMockTokenTransferConfirmState({}); const mockStore = configureMockStore([])(state); const { container } = renderWithConfirmContextProvider( @@ -32,10 +30,6 @@ describe('TokenTransferInfo', () => { mockStore, ); - await waitFor(() => { - expect(screen.getByText(tEn('networkFee') as string)).toBeInTheDocument(); - }); - expect(container).toMatchSnapshot(); }); }); From 1d4fcc0c31b497b9c4d3a867d31b37c6dd38e32d Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 8 Nov 2024 19:22:56 +0800 Subject: [PATCH 061/111] fix: disable buy for btc testnet accounts (#28341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR disables the Buy / sell button for btc testnet accounts ## **Related issues** Fixes: https://github.com/MetaMask/accounts-planning/issues/655 ## **Manual testing steps** 1. Create a testnet btc account 2. Go to the overview page 3. See that the buy/sell button is disabled. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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 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/develop/.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. --- .../app/wallet-overview/btc-overview.test.tsx | 30 +++++++++++++++++++ .../app/wallet-overview/btc-overview.tsx | 12 +++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/ui/components/app/wallet-overview/btc-overview.test.tsx b/ui/components/app/wallet-overview/btc-overview.test.tsx index abff2cb2b239..ffaed244e958 100644 --- a/ui/components/app/wallet-overview/btc-overview.test.tsx +++ b/ui/components/app/wallet-overview/btc-overview.test.tsx @@ -233,4 +233,34 @@ describe('BtcOverview', () => { const receiveButton = queryByTestId(BTC_OVERVIEW_RECEIVE); expect(receiveButton).toBeInTheDocument(); }); + + it('"Buy & Sell" button is disabled for testnet accounts', () => { + const storeWithBtcBuyable = getStore({ + metamask: { + ...mockMetamaskStore, + internalAccounts: { + ...mockMetamaskStore.internalAccounts, + accounts: { + [mockNonEvmAccount.id]: { + ...mockNonEvmAccount, + address: 'tb1q9lakrt5sw0w0twnc6ww4vxs7hm0q23e03286k8', + }, + }, + }, + }, + ramps: { + buyableChains: mockBuyableChainsWithBtc, + }, + }); + + const { queryByTestId } = renderWithProvider( + , + storeWithBtcBuyable, + ); + + const buyButton = queryByTestId(BTC_OVERVIEW_BUY); + + expect(buyButton).toBeInTheDocument(); + expect(buyButton).toBeDisabled(); + }); }); diff --git a/ui/components/app/wallet-overview/btc-overview.tsx b/ui/components/app/wallet-overview/btc-overview.tsx index dc47df7567b5..2ddaefd92f58 100644 --- a/ui/components/app/wallet-overview/btc-overview.tsx +++ b/ui/components/app/wallet-overview/btc-overview.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + getMultichainIsMainnet, + ///: END:ONLY_INCLUDE_IF getMultichainProviderConfig, getMultichainSelectedAccountCachedBalance, } from '../../../selectors/multichain'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getIsBitcoinBuyable } from '../../../ducks/ramps'; +import { getSelectedInternalAccount } from '../../../selectors'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; ///: END:ONLY_INCLUDE_IF import { CoinOverview } from './coin-overview'; @@ -17,6 +22,11 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { const { chainId } = useSelector(getMultichainProviderConfig); const balance = useSelector(getMultichainSelectedAccountCachedBalance); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + const selectedAccount = useSelector(getSelectedInternalAccount); + const isBtcMainnetAccount = useMultichainSelector( + getMultichainIsMainnet, + selectedAccount, + ); const isBtcBuyable = useSelector(getIsBitcoinBuyable); ///: END:ONLY_INCLUDE_IF @@ -31,7 +41,7 @@ const BtcOverview = ({ className }: BtcOverviewProps) => { isSwapsChain={false} ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) isBridgeChain={false} - isBuyableChain={isBtcBuyable} + isBuyableChain={isBtcBuyable && isBtcMainnetAccount} ///: END:ONLY_INCLUDE_IF /> ); From 152a50ef8fec9d8a038313c50df107f97ec80c20 Mon Sep 17 00:00:00 2001 From: Danica Shen Date: Fri, 8 Nov 2024 14:13:07 +0000 Subject: [PATCH 062/111] feat(1852): Implement sentry user report on error screen (#27857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Sentry provides a feature to collect user feedback to users when an issue occurs. This could be a great help to fix Sentry issues which sometimes come without a lot of context and are difficult to reproduce. Hence we use out-of-box solution from sentry to implement [User Feedback Widget](https://docs.sentry.io/platforms/javascript/user-feedback/configuration/#user-feedback-widget) via `Sentry.feedbackIntegration`. You can find more technical details in [this comment](https://github.com/MetaMask/MetaMask-planning/issues/1852#issuecomment-2392480544). Design Figma link [here](https://www.figma.com/design/DM5G1pyp74sMJwyKbw1KiR/Error-message-and-bug-report?node-id=1-4243&node-type=frame&t=iZI13qsxataukM4a-0). ### What's expected in this PR: - Refactor original `ui/pages/error/error.component.js` to typescript, clean up / update language content, and improve the layout based on new design (see above Figma link) that would be consistent as mobile implementation - Add a new option in `develop options` to cause a page crash by remove one language file (for me was easiest way to trigger), which will bring us to error page - In new error page, we have 3 options: 1. Describe what happened - open a form to sent a message to sentry 2. Contact support - existing link to redirect to `process.env.SUPPORT_REQUEST_LINK` 3. Try again - close the extension and allow user to open again - Convert `ui/ducks/locale/locale.js` to typescript and add related tests - Add e2e tests with POM pattern **This is the scenario for extension:** - GIVEN a user has MM installed - AND Sentry is enabled (user enabled MetaMetrics) - WHEN an unhandled issue occurs in MM - THEN MM crashes - AND an event is sent to Sentry - AND user is given the possibility to describe what happened to him by filling a form - AND his feedback gets paired to the Sentry event once user presses the "submit" button at the bottom of the form - AND user is given more comprehensive error screen when it crashes [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27857?quickstart=1) ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/1852 ## **Manual testing steps** 1. Set up sentry (https://github.com/MetaMask/metamask-extension/blob/develop/development/README.md) 2. Add DSN to `SENTRY_DSN_DEV` in local env set up, and mark `ENABLE_SETTINGS_PAGE_DEV_OPTIONS`=true 3. Run `yarn webpack --watch --sentry` 4. Ensure `Participate in MetaMetrics` is opt in 5. Click "develop options" in settings 6. Click "Generate A Page Crash" button 7. User is redirected to new error page 8. Click `Describe what happened` can open a sentry feedback form, then in your sentry project you can find the submitted form within `User Feedback` section 9. Click `Contact support` will redirect user to metamask support page 10. Click `Try again` will close the extension and ready for reload ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/56145a44-82d3-4d07-be03-87d81ee6a9d7 ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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** - [ ] 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. --- app/_locales/de/messages.json | 12 - app/_locales/el/messages.json | 12 - app/_locales/en/messages.json | 40 ++- app/_locales/en_GB/messages.json | 12 - app/_locales/es/messages.json | 12 - app/_locales/es_419/messages.json | 12 - app/_locales/fr/messages.json | 12 - app/_locales/hi/messages.json | 12 - app/_locales/id/messages.json | 12 - app/_locales/it/messages.json | 4 - app/_locales/ja/messages.json | 12 - app/_locales/ko/messages.json | 12 - app/_locales/ph/messages.json | 12 - app/_locales/pt/messages.json | 12 - app/_locales/pt_BR/messages.json | 12 - app/_locales/ru/messages.json | 12 - app/_locales/tl/messages.json | 12 - app/_locales/tr/messages.json | 12 - app/_locales/vi/messages.json | 12 - app/_locales/zh_CN/messages.json | 12 - app/_locales/zh_TW/messages.json | 12 - privacy-snapshot.json | 2 + shared/constants/metametrics.ts | 4 + shared/modules/i18n.test.ts | 16 +- shared/modules/i18n.ts | 3 +- test/data/mock-state.json | 8 +- test/e2e/helpers.js | 5 +- .../pages/developer-options-page.ts | 38 +++ test/e2e/page-objects/pages/error-page.ts | 95 ++++++ test/e2e/page-objects/pages/settings-page.ts | 10 + .../metrics/developer-options-sentry.spec.ts | 88 +++++ test/e2e/tests/metrics/errors.spec.js | 3 +- ui/ducks/locale/locale.js | 42 --- ui/ducks/locale/locale.test.ts | 80 ++++- ui/ducks/locale/locale.ts | 108 ++++++ ui/pages/error-page/error-component.test.tsx | 192 +++++++++++ ui/pages/error-page/error-page.component.tsx | 312 ++++++++++++++++++ ui/pages/error-page/index.scss | 41 +++ ui/pages/error-page/index.ts | 1 + ui/pages/error/error.component.js | 106 ------ ui/pages/error/index.js | 1 - ui/pages/error/index.scss | 47 --- ui/pages/index.js | 7 +- ui/pages/pages.scss | 2 +- .../developer-options-tab.test.tsx.snap | 46 ++- .../developer-options-tab.tsx | 2 +- .../developer-options-tab/sentry-test.tsx | 66 +++- ui/pages/settings/settings.component.js | 5 +- 48 files changed, 1124 insertions(+), 478 deletions(-) create mode 100644 test/e2e/page-objects/pages/developer-options-page.ts create mode 100644 test/e2e/page-objects/pages/error-page.ts create mode 100644 test/e2e/tests/metrics/developer-options-sentry.spec.ts delete mode 100644 ui/ducks/locale/locale.js create mode 100644 ui/ducks/locale/locale.ts create mode 100644 ui/pages/error-page/error-component.test.tsx create mode 100644 ui/pages/error-page/error-page.component.tsx create mode 100644 ui/pages/error-page/index.scss create mode 100644 ui/pages/error-page/index.ts delete mode 100644 ui/pages/error/error.component.js delete mode 100644 ui/pages/error/index.js delete mode 100644 ui/pages/error/index.scss diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json index 19c7731c778f..f62b15c08c93 100644 --- a/app/_locales/de/messages.json +++ b/app/_locales/de/messages.json @@ -1696,10 +1696,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Fehlerdetails", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Fehler beim Abrufen der Liste sicherer Ketten, bitte mit Vorsicht fortfahren." }, @@ -1711,14 +1707,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Versuchen Sie es erneut, indem Sie die Seite neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Versuchen Sie es erneut, indem das Popup schließen und neu laden oder kontaktieren Sie den Support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask hat einen Fehler festgestellt.", "description": "Title of generic error page" diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json index ac4542893959..3bede7c85edb 100644 --- a/app/_locales/el/messages.json +++ b/app/_locales/el/messages.json @@ -1696,10 +1696,6 @@ "message": "Κωδικός: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Λεπτομέρειες σφάλματος", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Σφάλμα κατά τη λήψη της λίστας ασφαλών αλυσίδων, συνεχίστε με προσοχή." }, @@ -1711,14 +1707,6 @@ "message": "Κωδικός: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Προσπαθήστε ξανά με επαναφόρτωση της σελίδας ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Προσπαθήστε ξανά κλείνοντας και ανοίγοντας ξανά το αναδυόμενο παράθυρο ή επικοινωνήστε με την υποστήριξη $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Το MetaMask αντιμετώπισε ένα σφάλμα", "description": "Title of generic error page" diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 202dc04c2fa8..56e3614e3f39 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1940,10 +1940,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1955,18 +1951,42 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageContactSupport": { + "message": "Contact support", + "description": "Button for contact MM support" + }, + "errorPageDescribeUsWhatHappened": { + "message": "Describe what happened", + "description": "Button for submitting report to sentry" + }, + "errorPageInfo": { + "message": "Your information can’t be shown. Don’t worry, your wallet and funds are safe.", + "description": "Information banner shown in the error page" + }, + "errorPageMessageTitle": { + "message": "Error message", + "description": "Title for description, which is displayed for debugging purposes" }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." + "errorPageSentryFormTitle": { + "message": "Describe what happened", + "description": "In sentry feedback form, The title at the top of the feedback form." + }, + "errorPageSentryMessagePlaceholder": { + "message": "Sharing details like how we can reproduce the bug will help us fix the problem.", + "description": "In sentry feedback form, The placeholder for the feedback description input field." + }, + "errorPageSentrySuccessMessageText": { + "message": "Thanks! We will take a look soon.", + "description": "In sentry feedback form, The message displayed after a successful feedback submission." }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" }, + "errorPageTryAgain": { + "message": "Try again", + "description": "Button for try again" + }, "errorStack": { "message": "Stack:", "description": "Title for error stack, which is displayed for debugging purposes" diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 8dd2e32dac39..9ee7771947ba 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1792,10 +1792,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Error details", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error while getting safe chain list, please continue with caution." }, @@ -1807,14 +1803,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Try again by reloading the page, or contact support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Try again by closing and reopening the popup, or contact support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encountered an error", "description": "Title of generic error page" diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json index 1d23b736fbfd..f13313976e74 100644 --- a/app/_locales/es/messages.json +++ b/app/_locales/es/messages.json @@ -1693,10 +1693,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Error al obtener la lista de cadenas seguras, por favor continúe con precaución." }, @@ -1708,14 +1704,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" diff --git a/app/_locales/es_419/messages.json b/app/_locales/es_419/messages.json index cebfc3cef106..511ee6cbef71 100644 --- a/app/_locales/es_419/messages.json +++ b/app/_locales/es_419/messages.json @@ -686,10 +686,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalles del error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensaje: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -698,14 +694,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Vuelva a cargar la página para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Cierre la ventana emergente y vuelva a abrirla para intentarlo de nuevo o comuníquese con soporte técnico $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask encontró un error", "description": "Title of generic error page" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index e9b8fd588b89..c785d2e0a00a 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -1696,10 +1696,6 @@ "message": "Code : $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Détails de l’erreur", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erreur lors de l’obtention de la liste des chaînes sécurisées, veuillez continuer avec précaution." }, @@ -1711,14 +1707,6 @@ "message": "Code : $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Essayez à nouveau en rechargeant la page, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Réessayez en fermant puis en rouvrant le pop-up, ou contactez le service d’assistance $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask a rencontré une erreur", "description": "Title of generic error page" diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json index 9b2c06f96c11..554bb2e91b84 100644 --- a/app/_locales/hi/messages.json +++ b/app/_locales/hi/messages.json @@ -1696,10 +1696,6 @@ "message": "कोड: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "गड़बड़ी की जानकारी", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "सेफ चेन लिस्ट पाते समय गड़बड़ी हुई, कृपया सावधानी के साथ जारी रखें।" }, @@ -1711,14 +1707,6 @@ "message": "कोड: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "पेज को दोबारा लोड करके फिर से कोशिश करें या सपोर्ट $1 से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "पॉपअप को बंद करके और फिर से खोलने की कोशिश करें या $1 पर सपोर्ट से कॉन्टेक्ट करें।", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask में कोई गड़बड़ी हुई", "description": "Title of generic error page" diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json index 1331d7b9c482..8e60e6fe5a50 100644 --- a/app/_locales/id/messages.json +++ b/app/_locales/id/messages.json @@ -1696,10 +1696,6 @@ "message": "Kode: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detail kesalahan", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Terjadi kesalahan saat mendapatkan daftar rantai aman, lanjutkan dengan hati-hati." }, @@ -1711,14 +1707,6 @@ "message": "Kode: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Coba lagi dengan memuat kembali halaman, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Coba lagi dengan menutup dan membuka kembali sembulan, atau hubungi dukungan $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask mengalami kesalahan", "description": "Title of generic error page" diff --git a/app/_locales/it/messages.json b/app/_locales/it/messages.json index 0c79dd8fc6fc..05cccdac0359 100644 --- a/app/_locales/it/messages.json +++ b/app/_locales/it/messages.json @@ -736,10 +736,6 @@ "message": "Codice: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Dettagli Errore", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Messaggio: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json index 3107eb3cb152..69824ae33b52 100644 --- a/app/_locales/ja/messages.json +++ b/app/_locales/ja/messages.json @@ -1696,10 +1696,6 @@ "message": "コード: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "エラーの詳細", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "安全なチェーンリストの取得中にエラーが発生しました。慎重に続けてください。" }, @@ -1711,14 +1707,6 @@ "message": "コード: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "ページを再ロードしてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "ポップアップを閉じてから再び開いてもう一度実行するか、$1からサポートまでお問い合わせください。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMaskにエラーが発生しました", "description": "Title of generic error page" diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json index 6dc468cdd5ef..f160b3dccbc2 100644 --- a/app/_locales/ko/messages.json +++ b/app/_locales/ko/messages.json @@ -1696,10 +1696,6 @@ "message": "코드: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "오류 세부 정보", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "안전 체인 목록을 가져오는 동안 오류가 발생했습니다. 주의하여 계속 진행하세요." }, @@ -1711,14 +1707,6 @@ "message": "코드: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "페이지를 새로고침하여 다시 시도하거나 $1로 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "팝업을 닫은 후 다시 열어 다시 시도하거나 $1에서 지원을 요청하여 도움을 받으세요.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 오류 발생", "description": "Title of generic error page" diff --git a/app/_locales/ph/messages.json b/app/_locales/ph/messages.json index e6b4bc3e7811..3c08d76cb186 100644 --- a/app/_locales/ph/messages.json +++ b/app/_locales/ph/messages.json @@ -423,10 +423,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensahe: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -435,14 +431,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukan ulit sa pamamagitan ng pag-reload ng page, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan ulit sa pamamagitan ng pagsara at pagbukas ulit ng popup, o makipag-ugnayan sa suporta sa $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json index 03f41c1c62b8..589a58e94907 100644 --- a/app/_locales/pt/messages.json +++ b/app/_locales/pt/messages.json @@ -1696,10 +1696,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Erro ao obter uma lista segura da cadeia. Por favor, prossiga com cautela." }, @@ -1711,14 +1707,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" diff --git a/app/_locales/pt_BR/messages.json b/app/_locales/pt_BR/messages.json index 58d7f6ea8718..ebf2af88c186 100644 --- a/app/_locales/pt_BR/messages.json +++ b/app/_locales/pt_BR/messages.json @@ -686,10 +686,6 @@ "message": "Código: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Detalhes do erro", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "Mensagem: $1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -698,14 +694,6 @@ "message": "Código: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Recarregue a página para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Feche e reabra o pop-up para tentar novamente ou entre em contato com o suporte $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "A MetaMask encontrou um erro", "description": "Title of generic error page" diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json index e56afc418785..9462d9c6eb4a 100644 --- a/app/_locales/ru/messages.json +++ b/app/_locales/ru/messages.json @@ -1696,10 +1696,6 @@ "message": "Код: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Сведения об ошибке", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Ошибка при получении списка безопасных блокчейнов. Продолжайте с осторожностью." }, @@ -1711,14 +1707,6 @@ "message": "Код: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Повторите попытку, перезагрузив страницу, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Повторите попытку, закрыв и вновь открыв всплывающее окно, или обратитесь в поддержку $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask обнаружил ошибку", "description": "Title of generic error page" diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json index af56d958c313..41612a6ce177 100644 --- a/app/_locales/tl/messages.json +++ b/app/_locales/tl/messages.json @@ -1696,10 +1696,6 @@ "message": "Code: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Mga Detalye ng Error", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "May error habang kinukuha ang ligtas na chain list, mangyaring magpatuloy nang may pag-iingat." }, @@ -1711,14 +1707,6 @@ "message": "Code: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Subukang muling i-reload ang page, o kontakin ang support $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Subukan muli sa pamamagitan ng pagsasara o muling pagbubukas ng pop-up, kontakin ang support $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "Nagkaroon ng error sa MetaMask", "description": "Title of generic error page" diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json index 6732513395d8..f11cc0d17523 100644 --- a/app/_locales/tr/messages.json +++ b/app/_locales/tr/messages.json @@ -1696,10 +1696,6 @@ "message": "Kod: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Hata ayrıntıları", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Güvenli zincir listesi alınırken hata oluştu, lütfen dikkatli bir şekilde devam edin." }, @@ -1711,14 +1707,6 @@ "message": "Kod: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Sayfayı yeniden yükleyerek tekrar deneyin veya $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Açılır pencereyi kapatarak ve yeniden açarak tekrar deneyin $1 destek bölümümüze ulaşın.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask bir hata ile karşılaştı", "description": "Title of generic error page" diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json index 9f6ff5119366..dd518df1bf22 100644 --- a/app/_locales/vi/messages.json +++ b/app/_locales/vi/messages.json @@ -1696,10 +1696,6 @@ "message": "Mã: $1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "Chi tiết về lỗi", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "Lỗi khi lấy danh sách chuỗi an toàn, vui lòng tiếp tục một cách thận trọng." }, @@ -1711,14 +1707,6 @@ "message": "Mã: $1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "Hãy thử lại bằng cách tải lại trang hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "Hãy thử lại bằng cách đóng và mở lại cửa sổ bật lên hoặc liên hệ với bộ phận hỗ trợ $1.", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask đã gặp lỗi", "description": "Title of generic error page" diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json index eef758dacdad..818f1ffdb82b 100644 --- a/app/_locales/zh_CN/messages.json +++ b/app/_locales/zh_CN/messages.json @@ -1696,10 +1696,6 @@ "message": "代码:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "错误详情", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorGettingSafeChainList": { "message": "获取安全链列表时出错,请谨慎继续。" }, @@ -1711,14 +1707,6 @@ "message": "代码:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "通过重新加载页面再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "通过关闭并重新打开弹出窗口再试一次,或联系支持团队 $1。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遇到了一个错误", "description": "Title of generic error page" diff --git a/app/_locales/zh_TW/messages.json b/app/_locales/zh_TW/messages.json index 7a7fdb68cf1f..29953f67c941 100644 --- a/app/_locales/zh_TW/messages.json +++ b/app/_locales/zh_TW/messages.json @@ -425,10 +425,6 @@ "message": "代碼:$1", "description": "Displayed error code for debugging purposes. $1 is the error code" }, - "errorDetails": { - "message": "錯誤詳細資訊", - "description": "Title for collapsible section that displays error details for debugging purposes" - }, "errorMessage": { "message": "訊息:$1", "description": "Displayed error message for debugging purposes. $1 is the error message" @@ -437,14 +433,6 @@ "message": "代碼:$1", "description": "Displayed error name for debugging purposes. $1 is the error name" }, - "errorPageMessage": { - "message": "重新整理頁面然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the fullscreen or notification UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, - "errorPagePopupMessage": { - "message": "重新開啟彈跳視窗然後再試一次,或從$1聯絡我們尋求支援。", - "description": "Message displayed on generic error page in the popup UI, $1 is a clickable link with text defined by the 'here' key. The link will open to a form where users can file support tickets." - }, "errorPageTitle": { "message": "MetaMask 遭遇錯誤", "description": "Title of generic error page" diff --git a/privacy-snapshot.json b/privacy-snapshot.json index 6e041ea3d71b..817d1e102bff 100644 --- a/privacy-snapshot.json +++ b/privacy-snapshot.json @@ -56,6 +56,8 @@ "sourcify.dev", "start.metamask.io", "static.cx.metamask.io", + "support.metamask.io", + "support.metamask-institutional.io", "swap.api.cx.metamask.io", "test.metamask-phishing.io", "token.api.cx.metamask.io", diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 4096d04b11d6..103a24c2463d 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -158,6 +158,10 @@ export type MetaMetricsEventOptions = { * as not conforming to our schema. */ matomoEvent?: boolean; + /** + * Values that can used in the "properties" tracking object as keys, + */ + contextPropsIntoEventProperties?: string | string[]; }; export type MetaMetricsEventFragment = { diff --git a/shared/modules/i18n.test.ts b/shared/modules/i18n.test.ts index 8452ef48238c..7a2f49bd9f68 100644 --- a/shared/modules/i18n.test.ts +++ b/shared/modules/i18n.test.ts @@ -109,7 +109,21 @@ describe('I18N Module', () => { ); }); - it('throws if test env set', () => { + it('throws if IN_TEST is set true', () => { + expect(() => + getMessage( + FALLBACK_LOCALE, + {} as unknown as I18NMessageDict, + keyMock, + ), + ).toThrow( + `Unable to find value of key "${keyMock}" for locale "${FALLBACK_LOCALE}"`, + ); + }); + + it('throws if ENABLE_SETTINGS_PAGE_DEV_OPTIONS is set true', () => { + process.env.IN_TEST = String(false); + process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS = String(true); expect(() => getMessage( FALLBACK_LOCALE, diff --git a/shared/modules/i18n.ts b/shared/modules/i18n.ts index d19cfa4b3b11..b5c22c869b54 100644 --- a/shared/modules/i18n.ts +++ b/shared/modules/i18n.ts @@ -177,7 +177,7 @@ function missingKeyError( onError?.(error); log.error(error); - if (process.env.IN_TEST) { + if (process.env.IN_TEST || process.env.ENABLE_SETTINGS_PAGE_DEV_OPTIONS) { throw error; } } @@ -188,7 +188,6 @@ function missingKeyError( warned[localeCode] = warned[localeCode] ?? {}; warned[localeCode][key] = true; - log.warn( `Translator - Unable to find value of key "${key}" for locale "${localeCode}"`, ); diff --git a/test/data/mock-state.json b/test/data/mock-state.json index 184787b07836..80e5499447d7 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -46,7 +46,13 @@ "mostRecentOverviewPage": "/mostRecentOverviewPage" }, "localeMessages": { - "currentLocale": "en" + "currentLocale": "en", + "current": { + "user": "user" + }, + "en": { + "user": "user" + } }, "metamask": { "use4ByteResolution": true, diff --git a/test/e2e/helpers.js b/test/e2e/helpers.js index c3705c1ebf6c..1c3f53b30224 100644 --- a/test/e2e/helpers.js +++ b/test/e2e/helpers.js @@ -55,7 +55,7 @@ const convertETHToHexGwei = (eth) => convertToHexValue(eth * 10 ** 18); /** * * @param {object} options - * @param {(fixtures: Fixtures) => Promise} testSuite + * @param {({driver: Driver, mockedEndpoint: MockedEndpoint}: TestSuiteArguments) => Promise} testSuite */ async function withFixtures(options, testSuite) { const { @@ -1308,6 +1308,8 @@ async function openMenuSafe(driver) { } } +const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu; + module.exports = { DAPP_HOST_ADDRESS, DAPP_URL, @@ -1379,4 +1381,5 @@ module.exports = { getSelectedAccountAddress, tempToggleSettingRedesignedConfirmations, openMenuSafe, + sentryRegEx, }; diff --git a/test/e2e/page-objects/pages/developer-options-page.ts b/test/e2e/page-objects/pages/developer-options-page.ts new file mode 100644 index 000000000000..c15f6c767a82 --- /dev/null +++ b/test/e2e/page-objects/pages/developer-options-page.ts @@ -0,0 +1,38 @@ +import { Driver } from '../../webdriver/driver'; + +class DevelopOptions { + private readonly driver: Driver; + + // Locators + private readonly generatePageCrashButton: string = + '[data-testid="developer-options-generate-page-crash-button"]'; + + private readonly developOptionsPageTitle: object = { + text: 'Developer Options', + css: 'h4', + }; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.developOptionsPageTitle); + } catch (e) { + console.log( + 'Timeout while waiting for Developer options page to be loaded', + e, + ); + throw e; + } + console.log('Developer option page is loaded'); + } + + async clickGenerateCrashButton(): Promise { + console.log('Generate a page crash in Developer option page'); + await this.driver.clickElement(this.generatePageCrashButton); + } +} + +export default DevelopOptions; diff --git a/test/e2e/page-objects/pages/error-page.ts b/test/e2e/page-objects/pages/error-page.ts new file mode 100644 index 000000000000..76acacdcf9dd --- /dev/null +++ b/test/e2e/page-objects/pages/error-page.ts @@ -0,0 +1,95 @@ +import { Driver } from '../../webdriver/driver'; +import HeaderNavbar from './header-navbar'; +import SettingsPage from './settings-page'; +import DevelopOptionsPage from './developer-options-page'; + +const FEEDBACK_MESSAGE = + 'Message: Unable to find value of key "developerOptions" for locale "en"'; + +class ErrorPage { + private readonly driver: Driver; + + // Locators + private readonly errorPageTitle: object = { + text: 'MetaMask encountered an error', + css: 'h3', + }; + + private readonly errorMessage = '[data-testid="error-page-error-message"]'; + + private readonly sendReportToSentryButton = + '[data-testid="error-page-describe-what-happened-button"]'; + + private readonly sentryReportForm = + '[data-testid="error-page-sentry-feedback-modal"]'; + + private readonly contactSupportButton = + '[data-testid="error-page-contact-support-button"]'; + + private readonly sentryFeedbackTextarea = + '[data-testid="error-page-sentry-feedback-textarea"]'; + + private readonly sentryFeedbackSubmitButton = + '[data-testid="error-page-sentry-feedback-submit-button"]'; + + private readonly sentryFeedbackSuccessModal = + '[data-testid="error-page-sentry-feedback-success-modal"]'; + + constructor(driver: Driver) { + this.driver = driver; + } + + async check_pageIsLoaded(): Promise { + try { + await this.driver.waitForSelector(this.errorPageTitle); + } catch (e) { + console.log('Timeout while waiting for Error page to be loaded', e); + throw e; + } + console.log('Error page is loaded'); + } + + async triggerPageCrash(): Promise { + const headerNavbar = new HeaderNavbar(this.driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(this.driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptionsPage(this.driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); + } + + async validate_errorMessage(): Promise { + await this.driver.waitForSelector({ + text: `Message: Unable to find value of key "developerOptions" for locale "en"`, + css: this.errorMessage, + }); + } + + async submitToSentryUserFeedbackForm(): Promise { + console.log(`Open sentry user feedback form in error page`); + await this.driver.clickElement(this.sendReportToSentryButton); + await this.driver.waitForSelector(this.sentryReportForm); + await this.driver.fill(this.sentryFeedbackTextarea, FEEDBACK_MESSAGE); + await this.driver.clickElementAndWaitToDisappear( + this.sentryFeedbackSubmitButton, + ); + } + + async contactAndValidateMetaMaskSupport(): Promise { + console.log(`Contact metamask support form in a separate page`); + await this.driver.waitUntilXWindowHandles(1); + await this.driver.clickElement(this.contactSupportButton); + // metamask, help page + await this.driver.waitUntilXWindowHandles(2); + } + + async waitForSentrySuccessModal(): Promise { + await this.driver.waitForSelector(this.sentryFeedbackSuccessModal); + await this.driver.assertElementNotPresent(this.sentryFeedbackSuccessModal); + } +} + +export default ErrorPage; diff --git a/test/e2e/page-objects/pages/settings-page.ts b/test/e2e/page-objects/pages/settings-page.ts index 547f9e43a34e..c029e34efc7e 100644 --- a/test/e2e/page-objects/pages/settings-page.ts +++ b/test/e2e/page-objects/pages/settings-page.ts @@ -9,6 +9,11 @@ class SettingsPage { css: '.tab-bar__tab__content__title', }; + private readonly developerOptionsButton: object = { + text: 'Developer Options', + css: '.tab-bar__tab__content__title', + }; + private readonly settingsPageTitle: object = { text: 'Settings', css: 'h3', @@ -32,6 +37,11 @@ class SettingsPage { console.log('Navigating to Experimental Settings page'); await this.driver.clickElement(this.experimentalSettingsButton); } + + async goToDevelopOptionSettings(): Promise { + console.log('Navigating to Develop options page'); + await this.driver.clickElement(this.developerOptionsButton); + } } export default SettingsPage; diff --git a/test/e2e/tests/metrics/developer-options-sentry.spec.ts b/test/e2e/tests/metrics/developer-options-sentry.spec.ts new file mode 100644 index 000000000000..3c20f931f302 --- /dev/null +++ b/test/e2e/tests/metrics/developer-options-sentry.spec.ts @@ -0,0 +1,88 @@ +import { Suite } from 'mocha'; +import { MockttpServer } from 'mockttp'; +import { withFixtures, sentryRegEx } from '../../helpers'; +import FixtureBuilder from '../../fixture-builder'; +import { Driver } from '../../webdriver/driver'; +import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; +import SettingsPage from '../../page-objects/pages/settings-page'; +import HeaderNavbar from '../../page-objects/pages/header-navbar'; +import DevelopOptions from '../../page-objects/pages/developer-options-page'; +import ErrorPage from '../../page-objects/pages/error-page'; + +const triggerCrash = async (driver: Driver): Promise => { + const headerNavbar = new HeaderNavbar(driver); + await headerNavbar.openSettingsPage(); + const settingsPage = new SettingsPage(driver); + await settingsPage.check_pageIsLoaded(); + await settingsPage.goToDevelopOptionSettings(); + + const developOptionsPage = new DevelopOptions(driver); + await developOptionsPage.check_pageIsLoaded(); + await developOptionsPage.clickGenerateCrashButton(); +}; + +async function mockSentryError(mockServer: MockttpServer) { + return [ + await mockServer + .forPost(sentryRegEx) + .withBodyIncluding('feedback') + .thenCallback(() => { + return { + statusCode: 200, + json: {}, + }; + }), + ]; +} + +describe('Developer Options - Sentry', function (this: Suite) { + it('gives option to cause a page crash and provides sentry form to report', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder() + .withMetaMetricsController({ + metaMetricsId: 'fake-metrics-id', + participateInMetaMetrics: true, + }) + .build(), + title: this.test?.fullTitle(), + testSpecificMock: mockSentryError, + ignoredConsoleErrors: [ + 'Error#1: Unable to find value of key "developerOptions" for locale "en"', + 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.', + ], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + await errorPage.validate_errorMessage(); + await errorPage.submitToSentryUserFeedbackForm(); + await errorPage.waitForSentrySuccessModal(); + }, + ); + }); + + it('gives option to cause a page crash and offer contact support option', async function () { + await withFixtures( + { + fixtures: new FixtureBuilder().build(), + title: this.test?.fullTitle(), + ignoredConsoleErrors: [ + 'Error#1: Unable to find value of key "developerOptions" for locale "en"', + 'React will try to recreate this component tree from scratch using the error boundary you provided, Index.', + ], + }, + async ({ driver }: { driver: Driver }) => { + await loginWithBalanceValidation(driver); + await triggerCrash(driver); + + const errorPage = new ErrorPage(driver); + await errorPage.check_pageIsLoaded(); + + await errorPage.contactAndValidateMetaMaskSupport(); + }, + ); + }); +}); diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index 3b003b044b5a..d4c27a87aba6 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -13,6 +13,7 @@ const { convertToHexValue, logInWithBalanceValidation, withFixtures, + sentryRegEx, } = require('../../helpers'); const { PAGES } = require('../../webdriver/driver'); @@ -181,8 +182,6 @@ function getMissingProperties(complete, object) { } describe('Sentry errors', function () { - const sentryRegEx = /^https:\/\/sentry\.io\/api\/\d+\/envelope/gu; - const migrationError = process.env.SELENIUM_BROWSER === Browser.CHROME ? `"type":"TypeError","value":"Cannot read properties of undefined (reading 'version')` diff --git a/ui/ducks/locale/locale.js b/ui/ducks/locale/locale.js deleted file mode 100644 index 5118a749ab58..000000000000 --- a/ui/ducks/locale/locale.js +++ /dev/null @@ -1,42 +0,0 @@ -import { createSelector } from 'reselect'; -import * as actionConstants from '../../store/actionConstants'; - -export default function reduceLocaleMessages(state = {}, { type, payload }) { - switch (type) { - case actionConstants.SET_CURRENT_LOCALE: - return { - ...state, - current: payload.messages, - currentLocale: payload.locale, - }; - default: - return state; - } -} - -/** - * This selector returns a code from file://./../../../app/_locales/index.json. - * - * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. - * - * @param state - * @returns {string} the user's selected locale. - * These codes are not safe to use with the Intl API. - */ -export const getCurrentLocale = (state) => state.localeMessages.currentLocale; - -/** - * This selector returns a - * [BCP 47 Language Tag](https://en.wikipedia.org/wiki/IETF_language_tag) - * for use with the Intl API. - * - * @returns {Intl.UnicodeBCP47LocaleIdentifier} the user's selected locale. - */ -export const getIntlLocale = createSelector( - getCurrentLocale, - (locale) => Intl.getCanonicalLocales(locale?.replace(/_/gu, '-'))[0], -); - -export const getCurrentLocaleMessages = (state) => state.localeMessages.current; - -export const getEnLocaleMessages = (state) => state.localeMessages.en; diff --git a/ui/ducks/locale/locale.test.ts b/ui/ducks/locale/locale.test.ts index 37fc1f99e29a..67627bb73423 100644 --- a/ui/ducks/locale/locale.test.ts +++ b/ui/ducks/locale/locale.test.ts @@ -1,32 +1,80 @@ // TODO: Remove restricted import // eslint-disable-next-line import/no-restricted-paths import locales from '../../../app/_locales/index.json'; -import { getIntlLocale } from './locale'; +import testData from '../../../test/data/mock-state.json'; +import { + getCurrentLocale, + getIntlLocale, + getCurrentLocaleMessages, + getEnLocaleMessages, +} from './locale'; + +// Mock state creation functions const createMockStateWithLocale = (locale: string) => ({ localeMessages: { currentLocale: locale }, }); -describe('getIntlLocale', () => { - it('returns the canonical BCP 47 language tag for the currently selected locale', () => { - const mockState = createMockStateWithLocale('ab-cd'); +describe('Locale Selectors', () => { + describe('getCurrentLocale', () => { + it('returns the current locale from the state', () => { + expect(getCurrentLocale(testData)).toBe('en'); + }); - expect(getIntlLocale(mockState)).toBe('ab-CD'); + it('returns undefined if no current locale is set', () => { + const newAppState = { + ...testData, + localeMessages: { + currentLocale: undefined, + }, + }; + expect(getCurrentLocale(newAppState)).toBeUndefined(); + }); }); - it('throws an error if locale cannot be made into BCP 47 language tag', () => { - const mockState = createMockStateWithLocale('xxxinvalid-locale'); + describe('getIntlLocale', () => { + it('returns the canonical BCP 47 language tag for the currently selected locale', () => { + const mockState = createMockStateWithLocale('en_US'); + expect(getIntlLocale(mockState)).toBe('en-US'); + }); - expect(() => getIntlLocale(mockState)).toThrow(); + locales.forEach((locale: { code: string; name: string }) => { + it(`handles supported locale - "${locale.code}"`, () => { + const mockState = createMockStateWithLocale(locale.code); + expect(() => getIntlLocale(mockState)).not.toThrow(); + }); + }); }); - // @ts-expect-error This is missing from the Mocha type definitions - it.each(locales)( - 'handles all supported locales – "%s"', - (locale: { code: string; name: string }) => { - const mockState = createMockStateWithLocale(locale.code); + describe('getCurrentLocaleMessages', () => { + it('returns the current locale messages from the state', () => { + expect(getCurrentLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no current locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + current: undefined, + }, + }; + expect(getCurrentLocaleMessages(newAppState)).toEqual(undefined); + }); + }); - expect(() => getIntlLocale(mockState)).not.toThrow(); - }, - ); + describe('getEnLocaleMessages', () => { + it('returns the English locale messages from the state', () => { + expect(getEnLocaleMessages(testData)).toEqual({ user: 'user' }); + }); + + it('returns undefined if there are no English locale messages', () => { + const newAppState = { + ...testData, + localeMessages: { + en: undefined, + }, + }; + expect(getEnLocaleMessages(newAppState)).toBeUndefined(); + }); + }); }); diff --git a/ui/ducks/locale/locale.ts b/ui/ducks/locale/locale.ts new file mode 100644 index 000000000000..2ca0cbaac4dc --- /dev/null +++ b/ui/ducks/locale/locale.ts @@ -0,0 +1,108 @@ +import { createSelector } from 'reselect'; +import { Action } from 'redux'; // Import types for actions +import * as actionConstants from '../../store/actionConstants'; +import { FALLBACK_LOCALE } from '../../../shared/modules/i18n'; + +/** + * Type for the locale messages part of the state + */ +type LocaleMessagesState = { + current?: { [key: string]: string }; // Messages for the current locale + currentLocale?: string; // User's selected locale (unsafe for Intl API) + en?: { [key: string]: string }; // English locale messages +}; + +/** + * Payload for the SET_CURRENT_LOCALE action + */ +type SetCurrentLocaleAction = Action & { + type: typeof actionConstants.SET_CURRENT_LOCALE; + payload: { + messages: { [key: string]: string }; + locale: string; + }; +}; + +/** + * Type for actions that can be handled by reduceLocaleMessages + */ +type LocaleMessagesActions = SetCurrentLocaleAction; + +/** + * Initial state for localeMessages reducer + */ +const initialState: LocaleMessagesState = {}; + +/** + * Reducer for localeMessages + * + * @param state - The current state + * @param action - The action being dispatched + * @returns The updated locale messages state + */ +export default function reduceLocaleMessages( + // eslint-disable-next-line @typescript-eslint/default-param-last + state: LocaleMessagesState = initialState, + action: LocaleMessagesActions, +): LocaleMessagesState { + switch (action.type) { + case actionConstants.SET_CURRENT_LOCALE: + return { + ...state, + current: action.payload.messages, + currentLocale: action.payload.locale, + }; + default: + return state; + } +} + +/** + * Type for the overall Redux state + */ +type AppState = { + localeMessages: LocaleMessagesState; +}; + +/** + * This selector returns a code from file://./../../../app/_locales/index.json. + * NOT SAFE FOR INTL API USE. Use getIntlLocale instead for that. + * + * @param state - The overall state + * @returns The user's selected locale + */ +export const getCurrentLocale = (state: AppState): string | undefined => + state.localeMessages.currentLocale; + +/** + * This selector returns a BCP 47 Language Tag for use with the Intl API. + * + * @returns The user's selected locale in BCP 47 format + */ +export const getIntlLocale = createSelector( + getCurrentLocale, + (locale): string => + Intl.getCanonicalLocales( + locale ? locale.replace(/_/gu, '-') : FALLBACK_LOCALE, + )[0], +); + +/** + * This selector returns the current locale messages. + * + * @param state - The overall state + * @returns The current locale's messages + */ +export const getCurrentLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.current; + +/** + * This selector returns the English locale messages. + * + * @param state - The overall state + * @returns The English locale's messages + */ +export const getEnLocaleMessages = ( + state: AppState, +): Record | undefined => state.localeMessages.en; diff --git a/ui/pages/error-page/error-component.test.tsx b/ui/pages/error-page/error-component.test.tsx new file mode 100644 index 000000000000..16188c30881a --- /dev/null +++ b/ui/pages/error-page/error-component.test.tsx @@ -0,0 +1,192 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import '@testing-library/jest-dom/extend-expect'; +import browser from 'webextension-polyfill'; +import { fireEvent } from '@testing-library/react'; +import { renderWithProvider } from '../../../test/lib/render-helpers'; +import { useI18nContext } from '../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../contexts/metametrics'; +import { getParticipateInMetaMetrics } from '../../selectors'; +import { getMessage } from '../../helpers/utils/i18n-helper'; +// eslint-disable-next-line import/no-restricted-paths +import messages from '../../../app/_locales/en/messages.json'; +import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; +import { + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; +import ErrorPage from './error-page.component'; + +jest.mock('../../hooks/useI18nContext', () => ({ + useI18nContext: jest.fn(), +})); + +jest.mock('webextension-polyfill', () => ({ + runtime: { + reload: jest.fn(), + }, +})); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +describe('ErrorPage', () => { + const useSelectorMock = useSelector as jest.Mock; + const mockTrackEvent = jest.fn(); + const MockError = new Error( + "Cannot read properties of undefined (reading 'message')", + ) as Error & { code?: string }; + MockError.code = '500'; + + const mockI18nContext = jest + .fn() + .mockReturnValue((key: string, variables: string[]) => + getMessage('en', messages, key, variables), + ); + + beforeEach(() => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getParticipateInMetaMetrics) { + return true; + } + return undefined; + }); + (useI18nContext as jest.Mock).mockImplementation(mockI18nContext); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should render the error message, code, and name if provided', () => { + const { getByTestId } = renderWithProvider( + + + , + ); + + expect( + getByTestId('error-page-error-message').textContent, + ).toMatchInlineSnapshot( + `"Message: Cannot read properties of undefined (reading 'message')"`, + ); + expect( + getByTestId('error-page-error-code').textContent, + ).toMatchInlineSnapshot(`"Code: 500"`); + expect( + getByTestId('error-page-error-name').textContent, + ).toMatchInlineSnapshot(`"Code: Error"`); + }); + + it('should not render error details if no error information is provided', () => { + const error = {}; + + const { queryByTestId } = renderWithProvider( + + + , + ); + + expect(queryByTestId('error-page-error-message')).toBeNull(); + expect(queryByTestId('error-page-error-code')).toBeNull(); + expect(queryByTestId('error-page-error-name')).toBeNull(); + expect(queryByTestId('error-page-error-stack')).toBeNull(); + }); + + it('should render sentry user feedback form and submit sentry report successfully when metrics is opted in', () => { + const { getByTestId, queryByTestId } = renderWithProvider( + + + , + ); + const describeButton = getByTestId( + 'error-page-describe-what-happened-button', + ); + fireEvent.click(describeButton); + expect( + queryByTestId('error-page-sentry-feedback-modal'), + ).toBeInTheDocument(); + const textarea = getByTestId('error-page-sentry-feedback-textarea'); + fireEvent.change(textarea, { + target: { value: 'Something went wrong on develop option page' }, + }); + const submitButton = getByTestId( + 'error-page-sentry-feedback-submit-button', + ); + fireEvent.click(submitButton); + expect( + queryByTestId('error-page-sentry-feedback-modal'), + ).not.toBeInTheDocument(); + expect( + queryByTestId('error-page-sentry-feedback-success-modal'), + ).toBeInTheDocument(); + jest.advanceTimersByTime(5000); + expect( + queryByTestId('error-page-sentry-feedback-modal'), + ).not.toBeInTheDocument(); + }); + + it('should render not sentry user feedback option when metrics is not opted in', () => { + useSelectorMock.mockImplementation((selector) => { + if (selector === getParticipateInMetaMetrics) { + return false; + } + return undefined; + }); + const { queryByTestId } = renderWithProvider( + + + , + ); + const describeButton = queryByTestId( + 'error-page-describe-what-happened-button', + ); + + expect(describeButton).toBeNull(); + }); + + it('should reload the extension when the "Try Again" button is clicked', () => { + const { getByTestId } = renderWithProvider( + + + , + ); + const tryAgainButton = getByTestId('error-page-try-again-button'); + fireEvent.click(tryAgainButton); + expect(browser.runtime.reload).toHaveBeenCalled(); + }); + + it('should open the support link and track the MetaMetrics event when the "Contact Support" button is clicked', () => { + window.open = jest.fn(); + + const { getByTestId } = renderWithProvider( + + + , + ); + + const contactSupportButton = getByTestId( + 'error-page-contact-support-button', + ); + fireEvent.click(contactSupportButton); + + expect(window.open).toHaveBeenCalledWith(SUPPORT_REQUEST_LINK, '_blank'); + + expect(mockTrackEvent).toHaveBeenCalledWith( + { + category: MetaMetricsEventCategory.Error, + event: MetaMetricsEventName.SupportLinkClicked, + properties: { + url: SUPPORT_REQUEST_LINK, + }, + }, + { + contextPropsIntoEventProperties: [MetaMetricsContextProp.PageTitle], + }, + ); + }); +}); diff --git a/ui/pages/error-page/error-page.component.tsx b/ui/pages/error-page/error-page.component.tsx new file mode 100644 index 000000000000..9a094057dcc0 --- /dev/null +++ b/ui/pages/error-page/error-page.component.tsx @@ -0,0 +1,312 @@ +import React, { useEffect, useContext, useState } from 'react'; +import { useSelector } from 'react-redux'; +import * as Sentry from '@sentry/browser'; +import browser from 'webextension-polyfill'; +import { + MetaMetricsContextProp, + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../shared/constants/metametrics'; + +import { getParticipateInMetaMetrics } from '../../selectors'; +import { MetaMetricsContext } from '../../contexts/metametrics'; +import { useI18nContext } from '../../hooks/useI18nContext'; +import { + BannerAlert, + Box, + Icon, + IconName, + IconSize, + Text, + Button, + ButtonVariant, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, +} from '../../components/component-library'; +import { + AlignItems, + BackgroundColor, + BlockSize, + BorderRadius, + Display, + FlexDirection, + IconColor, + JustifyContent, + TextColor, + TextVariant, +} from '../../helpers/constants/design-system'; + +import { SUPPORT_REQUEST_LINK } from '../../helpers/constants/common'; + +import { Textarea } from '../../components/component-library/textarea/textarea'; +import { TextareaResize } from '../../components/component-library/textarea/textarea.types'; +import { ButtonSize } from '../../components/component-library/button/button.types'; + +type ErrorPageProps = { + error: { + message?: string; + code?: string; + name?: string; + stack?: string; + }; +}; + +const ErrorPage: React.FC = ({ error }) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); + + const [feedbackMessage, setFeedbackMessage] = useState(''); + const [isFeedbackModalOpen, setIsFeedbackModalOpen] = useState(false); + const [isSuccessModalShown, setIsSuccessModalShown] = useState(false); + + const handleClickDescribeButton = (): void => { + setIsFeedbackModalOpen(true); + }; + + const handleCloseDescribeModal = (): void => { + setIsFeedbackModalOpen(false); + }; + + const handleSubmitFeedback = (e: React.MouseEvent) => { + e.preventDefault(); + const eventId = Sentry.lastEventId(); + + Sentry.captureFeedback({ + message: feedbackMessage, + associatedEventId: eventId, + }); + handleCloseDescribeModal(); + setIsSuccessModalShown(true); + }; + + useEffect(() => { + if (isSuccessModalShown) { + const timeoutId = setTimeout(() => { + setIsSuccessModalShown(false); // Close the modal after 5 seconds + }, 5000); + + // Cleanup function to clear timeout if the component unmounts or state changes + return () => clearTimeout(timeoutId); + } + return undefined; + }, [isSuccessModalShown]); + + return ( +
+
+ + + + {t('errorPageTitle')} + + + +
+ {t('errorPageInfo')} +
+ + {t('errorPageMessageTitle')} + + + {error.message ? ( + + {t('errorMessage', [error.message])} + + ) : null} + {error.code ? ( + + {t('errorCode', [error.code])} + + ) : null} + {error.name ? ( + + {t('errorName', [error.name])} + + ) : null} + {error.stack ? ( + <> + + {t('errorStack')} + +
+                {error.stack}
+              
+ + ) : null} +
+ + {isFeedbackModalOpen && ( + + + + + {t('errorPageSentryFormTitle')} + + +