From 82fb18c3c3de0ae2c7c2b3d7a113f4ee5e3ca262 Mon Sep 17 00:00:00 2001 From: Vince Howard Date: Wed, 18 Dec 2024 12:23:55 -0700 Subject: [PATCH 01/19] perf: no multichain list calculations are made when feature flag is off (#12766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** ### Problem The `selectAccountTokensAcrossChains` selector is being executed even when the `PORTFOLIO_VIEW` feature flag is disabled. This causes unnecessary state computations and potential performance overhead since the selector's results aren't used when the feature is off. ### Solution Modified the selector usage to conditionally return an empty object when the feature flag is disabled: ### Testing Added test cases within the Portfolio View test suite to verify: - Selector is called when the feature flag is enabled - Selector is not called when the feature flag is disabled This change optimizes performance by preventing unnecessary selector computations ## **Related issues** Fixes: ## **Manual testing steps** 1. Run `yarn watch` with no feature flag. The app should be the exactly the same 2. Run `PORTFOLIO_VIEW=true yarn watch`. The app should be have the multi chain list 3. You can log the `selectedAccountTokensChains` selector with the feature flag on and off to confirm it works ## **Screenshots/Recordings** NA ### **Before** NA ### **After** NA ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.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. --- .../Tokens/__snapshots__/index.test.tsx.snap | 2 +- app/components/UI/Tokens/index.test.tsx | 25 ++++++++++++++++++- app/components/UI/Tokens/index.tsx | 6 ++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap b/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap index 3d53ad137db..4b1a0b3e3d0 100644 --- a/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/Tokens/__snapshots__/index.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Tokens Portfolio View should match the snapshot when portfolio view is enabled 1`] = ` +exports[`Tokens Portfolio View should match the snapshot when portfolio view is enabled 1`] = ` ({ showSimpleNotification: jest.fn(() => Promise.resolve()), @@ -597,15 +599,36 @@ describe('Tokens', () => { }); describe('Portfolio View', () => { + let selectAccountTokensAcrossChainsSpy: jest.SpyInstance; + beforeEach(() => { + selectAccountTokensAcrossChainsSpy = jest.spyOn( + multichain, + 'selectAccountTokensAcrossChains', + ); jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true); }); - it('should match the snapshot when portfolio view is enabled ', () => { + afterEach(() => { + selectAccountTokensAcrossChainsSpy.mockRestore(); + }); + + it('should match the snapshot when portfolio view is enabled', () => { const { toJSON } = renderComponent(initialState); expect(toJSON()).toMatchSnapshot(); }); + it('should call selectAccountTokensAcrossChains when enabled', () => { + renderComponent(initialState); + expect(selectAccountTokensAcrossChainsSpy).toHaveBeenCalled(); + }); + + it('should not call selectAccountTokensAcrossChains when disabled', () => { + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(false); + renderComponent(initialState); + expect(selectAccountTokensAcrossChainsSpy).not.toHaveBeenCalled(); + }); + it('should handle network filtering correctly', () => { const multiNetworkState = { ...initialState, diff --git a/app/components/UI/Tokens/index.tsx b/app/components/UI/Tokens/index.tsx index 794a6ea0585..3f7bf864395 100644 --- a/app/components/UI/Tokens/index.tsx +++ b/app/components/UI/Tokens/index.tsx @@ -122,8 +122,9 @@ const Tokens: React.FC = ({ tokens }) => { ), ), ]; - const selectedAccountTokensChains = useSelector( - selectAccountTokensAcrossChains, + + const selectedAccountTokensChains = useSelector((state: RootState) => + isPortfolioViewEnabled() ? selectAccountTokensAcrossChains(state) : {}, ); const actionSheet = useRef(); @@ -153,7 +154,6 @@ const Tokens: React.FC = ({ tokens }) => { const allTokens = Object.values( selectedAccountTokensChains, ).flat() as TokenI[]; - /* If hideZeroBalanceTokens is ON and user is on "all Networks" we respect the setting and filter native and ERC20 tokens when zero If user is on "current Network" we want to show native tokens, even with zero balance From 3188e229012157dd372b806cce59ac4c27c8b6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n?= Date: Thu, 19 Dec 2024 12:23:03 +0100 Subject: [PATCH 02/19] test: approve erc721 token e2e (#12767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR is within the scope of the Quality Quest. We're including an e2e test case to approve an ERC721 token. **Test Steps:** Given I am on the test dApp When I tap on the Approve From button under the ERC 721 section Then the transaction bottom sheet should appear When I submit the transaction Then the transaction should appear in the transaction history ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- e2e/pages/Browser/TestDApp.js | 28 +++++++-- e2e/selectors/Browser/TestDapp.selectors.js | 3 +- .../Transactions/ActivitiesView.selectors.js | 1 + .../approve-custom-erc20.spec.js | 2 +- .../approve-default-erc20.spec.js | 2 +- .../confirmations/approve-erc721.spec.js | 61 +++++++++++++++++++ 6 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 e2e/specs/confirmations/approve-erc721.spec.js diff --git a/e2e/pages/Browser/TestDApp.js b/e2e/pages/Browser/TestDApp.js index cdf89c571f8..a09c4b8d5fd 100644 --- a/e2e/pages/Browser/TestDApp.js +++ b/e2e/pages/Browser/TestDApp.js @@ -12,6 +12,7 @@ import Matchers from '../../utils/Matchers'; export const TEST_DAPP_LOCAL_URL = `http://localhost:${getLocalTestDappPort()}`; const CONFIRM_BUTTON_TEXT = enContent.confirmation_modal.confirm_cta; +const APPROVE_BUTTON_TEXT = enContent.transactions.tx_review_approve; class TestDApp { get androidContainer() { @@ -22,6 +23,10 @@ class TestDApp { return Matchers.getElementByText(CONFIRM_BUTTON_TEXT); } + get approveButtonText() { + return Matchers.getElementByText(APPROVE_BUTTON_TEXT); + } + get DappConnectButton() { return Matchers.getElementByWebID( BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, @@ -29,10 +34,17 @@ class TestDApp { ); } - get ApproveButton() { + get ApproveERC20TokensButton() { + return Matchers.getElementByWebID( + BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, + TestDappSelectorsWebIDs.APPROVE_ERC_20_TOKENS_BUTTON_ID, + ); + } + + get ApproveERC721TokenButton() { return Matchers.getElementByWebID( BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, - TestDappSelectorsWebIDs.APPROVE_TOKENS_BUTTON_ID, + TestDappSelectorsWebIDs.APPROVE_ERC_721_TOKEN_BUTTON_ID, ); } // This taps on the transfer tokens button under the "SEND TOKENS section" @@ -104,8 +116,12 @@ class TestDApp { await this.tapButton(this.DappConnectButton); } - async tapApproveButton() { - await this.tapButton(this.ApproveButton); + async tapApproveERC20TokensButton() { + await this.tapButton(this.ApproveERC20TokensButton); + } + + async tapApproveERC721TokenButton() { + await this.tapButton(this.ApproveERC721TokenButton); } async tapIncreaseAllowanceButton() { @@ -150,6 +166,10 @@ class TestDApp { await Gestures.tap(this.confirmButtonText, 0); } + async tapApproveButton() { + await Gestures.tap(this.approveButtonText, 0); + } + async tapButton(elementId) { await Gestures.scrollToWebViewPort(elementId); await Gestures.tapWebElement(elementId); diff --git a/e2e/selectors/Browser/TestDapp.selectors.js b/e2e/selectors/Browser/TestDapp.selectors.js index 0f2b7cad254..4407c27a950 100644 --- a/e2e/selectors/Browser/TestDapp.selectors.js +++ b/e2e/selectors/Browser/TestDapp.selectors.js @@ -1,5 +1,6 @@ export const TestDappSelectorsWebIDs = { - APPROVE_TOKENS_BUTTON_ID: 'approveTokens', + APPROVE_ERC_20_TOKENS_BUTTON_ID: 'approveTokens', + APPROVE_ERC_721_TOKEN_BUTTON_ID: 'approveButton', CONNECT_BUTTON: 'connectButton', ERC_20_SEND_TOKENS_TRANSFER_TOKENS_BUTTON_ID: 'transferTokens', INCREASE_ALLOWANCE_BUTTON_ID: 'increaseTokenAllowance', diff --git a/e2e/selectors/Transactions/ActivitiesView.selectors.js b/e2e/selectors/Transactions/ActivitiesView.selectors.js index 50a3cb95384..4327c397700 100644 --- a/e2e/selectors/Transactions/ActivitiesView.selectors.js +++ b/e2e/selectors/Transactions/ActivitiesView.selectors.js @@ -16,4 +16,5 @@ export const ActivitiesViewSelectorsText = { SET_APPROVAL_FOR_ALL_METHOD: enContent.transactions.set_approval_for_all, SWAP: enContent.swaps.transaction_label.swap, TITLE: enContent.transactions_view.title, + }; diff --git a/e2e/specs/confirmations/approve-custom-erc20.spec.js b/e2e/specs/confirmations/approve-custom-erc20.spec.js index 1c8d9e96383..ed24c9c3c6a 100644 --- a/e2e/specs/confirmations/approve-custom-erc20.spec.js +++ b/e2e/specs/confirmations/approve-custom-erc20.spec.js @@ -48,7 +48,7 @@ describe(SmokeConfirmations('ERC20 tokens'), () => { await TestDApp.navigateToTestDappWithContract({ contractAddress: hstAddress, }); - await TestDApp.tapApproveButton(); + await TestDApp.tapApproveERC20TokensButton(); //Input custom token amount await Assertions.checkIfVisible( diff --git a/e2e/specs/confirmations/approve-default-erc20.spec.js b/e2e/specs/confirmations/approve-default-erc20.spec.js index ed9c02188c5..ca7a2889a0f 100644 --- a/e2e/specs/confirmations/approve-default-erc20.spec.js +++ b/e2e/specs/confirmations/approve-default-erc20.spec.js @@ -50,7 +50,7 @@ describe(SmokeConfirmations('ERC20 tokens'), () => { await TestDApp.navigateToTestDappWithContract({ contractAddress: hstAddress, }); - await TestDApp.tapApproveButton(); + await TestDApp.tapApproveERC20TokensButton(); await Assertions.checkIfVisible( ContractApprovalBottomSheet.approveTokenAmount, diff --git a/e2e/specs/confirmations/approve-erc721.spec.js b/e2e/specs/confirmations/approve-erc721.spec.js new file mode 100644 index 00000000000..d06a16d4909 --- /dev/null +++ b/e2e/specs/confirmations/approve-erc721.spec.js @@ -0,0 +1,61 @@ +'use strict'; + +import { SmokeConfirmations } from '../../tags'; +import TestHelpers from '../../helpers'; +import { loginToApp } from '../../viewHelper'; + +import TabBarComponent from '../../pages/wallet/TabBarComponent'; +import TestDApp from '../../pages/Browser/TestDApp'; +import FixtureBuilder from '../../fixtures/fixture-builder'; +import { + withFixtures, + defaultGanacheOptions, +} from '../../fixtures/fixture-helper'; +import { SMART_CONTRACTS } from '../../../app/util/test/smart-contracts'; +import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors'; +import Assertions from '../../utils/Assertions'; + +describe(SmokeConfirmations('ERC721 tokens'), () => { + const NFT_CONTRACT = SMART_CONTRACTS.NFTS; + + beforeAll(async () => { + jest.setTimeout(150000); + await TestHelpers.reverseServerPort(); + }); + + it('approve an ERC721 token from a dapp', async () => { + await withFixtures( + { + dapp: true, + fixture: new FixtureBuilder() + .withGanacheNetwork() + .withPermissionControllerConnectedToTestDapp() + .build(), + restartDevice: true, + ganacheOptions: defaultGanacheOptions, + smartContract: NFT_CONTRACT, + }, + async ({ contractRegistry }) => { + const nftsAddress = await contractRegistry.getContractAddress( + NFT_CONTRACT, + ); + await loginToApp(); + // Navigate to the browser screen + await TabBarComponent.tapBrowser(); + await TestDApp.navigateToTestDappWithContract({ + contractAddress: nftsAddress, + }); + // Approve NFT + await TestDApp.tapApproveERC721TokenButton(); + await TestHelpers.delay(3000); + await TestDApp.tapApproveButton(); + // Navigate to the activity screen + await TabBarComponent.tapActivity(); + // Assert NFT is approved + await Assertions.checkIfTextIsDisplayed( + ActivitiesViewSelectorsText.CONFIRM_TEXT, + ); + }, + ); + }); +}); From d2b2b1a14821e54e8d599b10e86b204da3b7706f Mon Sep 17 00:00:00 2001 From: EtherWizard33 <165834542+EtherWizard33@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:09:46 +0300 Subject: [PATCH 03/19] chore: update js.env.example to include examples of chain permissions to true (#12791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Inluding an example of multichain permissions feature to true in js.env.example ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .js.env.example | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.js.env.example b/.js.env.example index 56c7c1bb865..325ee52559d 100644 --- a/.js.env.example +++ b/.js.env.example @@ -97,7 +97,10 @@ export MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS="true" # Per dapp selected network (Amon Hen) feature flag export MM_PER_DAPP_SELECTED_NETWORK="" -export MM_CHAIN_PERMISSIONS="" +# Multichain permissions now set to true in production via the CI +# MM_MULTICHAIN_V1_ENABLED is the UI, and MM_CHAIN_PERMISSIONS is the engine +export MM_MULTICHAIN_V1_ENABLED="true" +export MM_CHAIN_PERMISSIONS="true" # Multichain feature flag specific to UI changes export MM_MULTICHAIN_V1_ENABLED="" From dd81987146e2ca66df0f3237eb1a1242ee2c0669 Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Thu, 19 Dec 2024 14:10:25 +0100 Subject: [PATCH 04/19] fix: set default selectedNetworkClientId to 'mainnet' if no matching with entry on network list (#12227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR implements a fix for Migration 60 to handle cases where selectedNetworkClientId does not match any entry within networkConfigurationsByChainId. If no corresponding networkClientId is found, the migration will now set selectedNetworkClientId to 'mainnet' by default. ## **Related issues** Fixes: [#11657](https://github.com/MetaMask/metamask-mobile/issues/11657) ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/store/migrations/064.test.ts | 173 +++++++++++++++++++++++++++++++ app/store/migrations/064.ts | 167 +++++++++++++++++++++++++++++ app/store/migrations/index.ts | 2 + 3 files changed, 342 insertions(+) create mode 100644 app/store/migrations/064.test.ts create mode 100644 app/store/migrations/064.ts diff --git a/app/store/migrations/064.test.ts b/app/store/migrations/064.test.ts new file mode 100644 index 00000000000..680fe4fc378 --- /dev/null +++ b/app/store/migrations/064.test.ts @@ -0,0 +1,173 @@ +import migration from './064'; +import { merge } from 'lodash'; +import initialRootState from '../../util/test/initial-root-state'; +import { captureException } from '@sentry/react-native'; +import { RootState } from '../../reducers'; + +const oldState = { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'unknown-client-id', + networkConfigurationsByChainId: { + '0x1': { + rpcEndpoints: [{ networkClientId: 'mainnet' }], + }, + '0x5': { + rpcEndpoints: [{ networkClientId: 'goerli' }], + }, + }, + }, + }, + }, +}; + +const expectedNewState = { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { + rpcEndpoints: [{ networkClientId: 'mainnet' }], + }, + '0x5': { + rpcEndpoints: [{ networkClientId: 'goerli' }], + }, + }, + }, + }, + }, +}; + +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); +const mockedCaptureException = jest.mocked(captureException); + +describe('Migration #64', () => { + beforeEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + + const invalidStates = [ + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: null, + }, + }, + }), + errorMessage: + "Migration 64: Invalid or missing 'NetworkController' in backgroundState: 'object'", + scenario: 'NetworkController state is invalid', + }, + { + state: merge({}, initialRootState, { + engine: { + backgroundState: { + NetworkController: { networkConfigurationsByChainId: null }, + }, + }, + }), + errorMessage: + "Migration 64: Missing or invalid 'networkConfigurationsByChainId' in NetworkController", + scenario: 'networkConfigurationsByChainId is invalid', + }, + ]; + + for (const { errorMessage, scenario, state } of invalidStates) { + it(`should capture exception if ${scenario}`, async () => { + const newState = await migration(state); + + expect(newState).toStrictEqual(state); + expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error)); + expect(mockedCaptureException.mock.calls[0][0].message).toBe( + errorMessage, + ); + }); + } + + it('should set selectedNetworkClientId to "mainnet" if it does not exist in networkConfigurationsByChainId', async () => { + const newState = await migration(oldState); + expect(newState).toStrictEqual(expectedNewState); + }); + + it('should keep selectedNetworkClientId unchanged if it exists in networkConfigurationsByChainId', async () => { + const validState = merge({}, oldState, { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'mainnet', + }, + }, + }, + }); + const newState = await migration(validState); + + expect(newState).toStrictEqual(validState); + }); + + it('should set selectedNetworkClientId to the default mainnet client ID if mainnet configuration exists but selectedNetworkClientId is invalid', async () => { + const invalidClientState = merge({}, oldState, { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'invalid-client-id', + }, + }, + }, + }); + + const newState = await migration(invalidClientState); + expect( + (newState as RootState).engine.backgroundState.NetworkController + .selectedNetworkClientId, + ).toBe('mainnet'); + }); + + it('should handle the absence of mainnet configuration gracefully', async () => { + const noMainnetState = merge({}, oldState, { + engine: { + backgroundState: { + NetworkController: { + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + defaultRpcEndpointIndex: 0, + rpcEndpoints: [{ networkClientId: 'another-mainnet' }], + }, + '0x5': { + rpcEndpoints: [{ networkClientId: 'goerli' }], + }, + }, + selectedNetworkClientId: 'unknown-client-id', + }, + }, + }, + }); + + const newState = await migration(noMainnetState); + expect( + (newState as RootState).engine.backgroundState.NetworkController + .selectedNetworkClientId, + ).toBe('another-mainnet'); + }); + + it('should not modify the state if it is already valid', async () => { + const validState = merge({}, oldState, { + engine: { + backgroundState: { + NetworkController: { + selectedNetworkClientId: 'mainnet', + }, + }, + }, + }); + + const newState = await migration(validState); + expect(newState).toStrictEqual(validState); + }); +}); diff --git a/app/store/migrations/064.ts b/app/store/migrations/064.ts new file mode 100644 index 00000000000..048fcbb341c --- /dev/null +++ b/app/store/migrations/064.ts @@ -0,0 +1,167 @@ +import { captureException } from '@sentry/react-native'; +import { isObject, hasProperty, Hex } from '@metamask/utils'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { + NetworkClientId, + NetworkConfiguration, + NetworkState, +} from '@metamask/network-controller'; +import { ensureValidState } from './util'; +import { RootState } from '../../reducers'; + +/** + * This migration checks if `selectedNetworkClientId` exists in any entry within `networkConfigurationsByChainId`. + * If it does not, or if `selectedNetworkClientId` is undefined or invalid, it sets `selectedNetworkClientId` to `'mainnet'`. + * @param {unknown} stateAsync - Redux state. + * @returns Migrated Redux state. + */ +export default async function migrate(stateAsync: unknown) { + const migrationVersion = 64; + const mainnetChainId = CHAIN_IDS.MAINNET; + + const state = await stateAsync; + + if (!ensureValidState(state, migrationVersion)) { + return state; + } + + const networkControllerState = state.engine.backgroundState + .NetworkController as NetworkState; + + if ( + !isValidNetworkControllerState( + networkControllerState, + state as RootState, + migrationVersion, + ) + ) { + return state; + } + + const { networkConfigurationsByChainId, selectedNetworkClientId } = + networkControllerState; + + const networkClientIdExists = doesNetworkClientIdExist( + selectedNetworkClientId, + networkConfigurationsByChainId, + migrationVersion, + ); + + const isMainnetRpcExists = isMainnetRpcConfigured( + networkConfigurationsByChainId, + ); + + ensureSelectedNetworkClientId( + networkControllerState, + networkClientIdExists, + isMainnetRpcExists, + networkConfigurationsByChainId, + mainnetChainId, + ); + + return state; +} + +function isValidNetworkControllerState( + networkControllerState: NetworkState, + state: RootState, + migrationVersion: number, +) { + if ( + !isObject(networkControllerState) || + !hasProperty(state.engine.backgroundState, 'NetworkController') + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid or missing 'NetworkController' in backgroundState: '${typeof networkControllerState}'`, + ), + ); + return false; + } + + if ( + !hasProperty(networkControllerState, 'networkConfigurationsByChainId') || + !isObject(networkControllerState.networkConfigurationsByChainId) + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Missing or invalid 'networkConfigurationsByChainId' in NetworkController`, + ), + ); + return false; + } + + return true; +} + +function doesNetworkClientIdExist( + selectedNetworkClientId: NetworkClientId, + networkConfigurationsByChainId: Record, + migrationVersion: number, +) { + for (const chainId in networkConfigurationsByChainId) { + const networkConfig = networkConfigurationsByChainId[chainId as Hex]; + + if ( + isObject(networkConfig) && + hasProperty(networkConfig, 'rpcEndpoints') && + Array.isArray(networkConfig.rpcEndpoints) + ) { + if ( + networkConfig.rpcEndpoints.some( + (endpoint) => + isObject(endpoint) && + hasProperty(endpoint, 'networkClientId') && + endpoint.networkClientId === selectedNetworkClientId, + ) + ) { + return true; + } + } else { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid network configuration or missing 'rpcEndpoints' for chainId: '${chainId}'`, + ), + ); + } + } + + return false; +} + +function isMainnetRpcConfigured( + networkConfigurationsByChainId: Record, +) { + return Object.values(networkConfigurationsByChainId).some((networkConfig) => + networkConfig.rpcEndpoints.some( + (endpoint) => endpoint.networkClientId === 'mainnet', + ), + ); +} + +function ensureSelectedNetworkClientId( + networkControllerState: NetworkState, + networkClientIdExists: boolean, + isMainnetRpcExists: boolean, + networkConfigurationsByChainId: Record, + mainnetChainId: Hex, +) { + const setDefaultMainnetClientId = () => { + networkControllerState.selectedNetworkClientId = isMainnetRpcExists + ? 'mainnet' + : networkConfigurationsByChainId[mainnetChainId].rpcEndpoints[ + networkConfigurationsByChainId[mainnetChainId].defaultRpcEndpointIndex + ].networkClientId; + }; + + if ( + !hasProperty(networkControllerState, 'selectedNetworkClientId') || + typeof networkControllerState.selectedNetworkClientId !== 'string' + ) { + setDefaultMainnetClientId(); + } + + if (!networkClientIdExists) { + setDefaultMainnetClientId(); + } +} diff --git a/app/store/migrations/index.ts b/app/store/migrations/index.ts index 06f50319276..44ef0d4f7d2 100644 --- a/app/store/migrations/index.ts +++ b/app/store/migrations/index.ts @@ -64,6 +64,7 @@ import migration60 from './060'; import migration61 from './061'; import migration62 from './062'; import migration63 from './063'; +import migration64 from './064'; type MigrationFunction = (state: unknown) => unknown; type AsyncMigrationFunction = (state: unknown) => Promise; @@ -140,6 +141,7 @@ export const migrationList: MigrationsList = { 61: migration61, 62: migration62, 63: migration63, + 64: migration64, }; // Enable both synchronous and asynchronous migrations From 425669e99d92bc0a5a9a8471ade3f3ebe35595ae Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 19 Dec 2024 15:01:50 +0100 Subject: [PATCH 05/19] fix: set token network filter when adding network from dapp (#12661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** PR to setTokenNetworkFilter value when user is adding a new network from dapp ## **Related issues** Fixes: ## **Manual testing steps** 1. Click on current network 2. Add new network from chainlist 3. Confirm switching to this network 4. Go back to wallet and you should see your new network with the correct token list ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/03b43fd3-239e-47e5-a419-2c87a76e84d5 ### **After** https://github.com/user-attachments/assets/c11c4fb4-396f-4db9-b359-2c91108a0b42 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Daniel Cross --- .../SwitchChainApproval.test.tsx | 39 +++++++++++++++++++ .../SwitchChainApproval.tsx | 25 +++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx index 4630f1984cc..1b1483b2dcc 100644 --- a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx +++ b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.test.tsx @@ -5,13 +5,26 @@ import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware'; import { ApprovalRequest } from '@metamask/approval-controller'; import SwitchChainApproval from './SwitchChainApproval'; import { networkSwitched } from '../../../actions/onboardNetwork'; +// eslint-disable-next-line import/no-namespace +import * as networks from '../../../util/networks'; +import Engine from '../../../core/Engine'; +const { PreferencesController } = Engine.context; jest.mock('../../Views/confirmations/hooks/useApprovalRequest'); jest.mock('../../../actions/onboardNetwork'); +jest.mock('../../../core/Engine', () => ({ + context: { + PreferencesController: { + setTokenNetworkFilter: jest.fn(), + }, + }, +})); + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useDispatch: () => jest.fn(), + useSelector: jest.fn(), })); const URL_MOCK = 'test.com'; @@ -32,6 +45,7 @@ const mockApprovalRequest = (approvalRequest?: ApprovalRequest) => { describe('SwitchChainApproval', () => { beforeEach(() => { jest.resetAllMocks(); + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(false); }); it('renders', () => { @@ -81,4 +95,29 @@ describe('SwitchChainApproval', () => { networkStatus: true, }); }); + + it('invokes network switched on confirm when portfolio view is enabled', () => { + jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true); + const tokenNetworkFilterSpy = jest.spyOn( + PreferencesController, + 'setTokenNetworkFilter', + ); + mockApprovalRequest({ + type: ApprovalTypes.SWITCH_ETHEREUM_CHAIN, + requestData: { + rpcUrl: URL_MOCK, + }, + } as ApprovalRequest<{ + rpcUrl: string; + }>); + + const wrapper = shallow(); + wrapper.find('SwitchCustomNetwork').simulate('confirm'); + expect(tokenNetworkFilterSpy).toHaveBeenCalledTimes(1); + expect(networkSwitched).toHaveBeenCalledTimes(1); + expect(networkSwitched).toHaveBeenCalledWith({ + networkUrl: URL_MOCK, + networkStatus: true, + }); + }); }); diff --git a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx index 9a3310addf9..ff4a9814ce1 100644 --- a/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx +++ b/app/components/Approvals/SwitchChainApproval/SwitchChainApproval.tsx @@ -4,7 +4,11 @@ import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware'; import ApprovalModal from '../ApprovalModal'; import SwitchCustomNetwork from '../../UI/SwitchCustomNetwork'; import { networkSwitched } from '../../../actions/onboardNetwork'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import Engine from '../../../core/Engine'; +import { selectIsAllNetworks } from '../../../selectors/networkController'; +import { selectTokenNetworkFilter } from '../../../selectors/preferencesController'; +import { isPortfolioViewEnabled } from '../../../util/networks'; const SwitchChainApproval = () => { const { @@ -15,17 +19,34 @@ const SwitchChainApproval = () => { } = useApprovalRequest(); const dispatch = useDispatch(); + const isAllNetworks = useSelector(selectIsAllNetworks); + const tokenNetworkFilter = useSelector(selectTokenNetworkFilter); const onConfirm = useCallback(() => { defaultOnConfirm(); + // If portfolio view is enabled should set network filter + if (isPortfolioViewEnabled()) { + const { PreferencesController } = Engine.context; + PreferencesController.setTokenNetworkFilter({ + ...(isAllNetworks ? tokenNetworkFilter : {}), + [approvalRequest?.requestData?.chainId]: true, + }); + } + dispatch( networkSwitched({ networkUrl: approvalRequest?.requestData?.rpcUrl, networkStatus: true, }), ); - }, [approvalRequest, defaultOnConfirm, dispatch]); + }, [ + approvalRequest, + defaultOnConfirm, + dispatch, + isAllNetworks, + tokenNetworkFilter, + ]); if (approvalRequest?.type !== ApprovalTypes.SWITCH_ETHEREUM_CHAIN) return null; From 48a86c7c535111a1c7f33cecaa179d6eb354a90d Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Thu, 19 Dec 2024 15:12:02 +0100 Subject: [PATCH 06/19] fix: filter token activity when clicking on native token (#12732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR filters out token related activity from activity list when clicking on a native token. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/6164 ## **Manual testing steps** 1. Create an ERC20 token 2. Add the new token to your wallet 3. Send some tokens to another wallet 4. Send ETH to another account 5. Click on the native token and scroll down to see activity, you should not see ERC2O related activity 6. Click on the token; you should see the activity related to token ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/ceb7f311-6163-4bb1-ba4e-1a29255b83a6 ### **After** https://github.com/user-attachments/assets/66827a01-6ce5-4c7b-9d93-8a9c551ad830 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: salimtb --- app/components/UI/TransactionElement/utils.js | 13 ++- app/components/Views/Asset/index.js | 11 ++- app/components/Views/Asset/index.test.js | 82 ++++++++++++++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/app/components/UI/TransactionElement/utils.js b/app/components/UI/TransactionElement/utils.js index 22b0e562005..4f9c30e6bbc 100644 --- a/app/components/UI/TransactionElement/utils.js +++ b/app/components/UI/TransactionElement/utils.js @@ -32,7 +32,10 @@ import { swapsUtils } from '@metamask/swaps-controller'; import { isSwapsNativeAsset } from '../Swaps/utils'; import { toLowerCaseEquals } from '../../../util/general'; import Engine from '../../../core/Engine'; -import { isEIP1559Transaction } from '@metamask/transaction-controller'; +import { + isEIP1559Transaction, + TransactionType, +} from '@metamask/transaction-controller'; const { getSwapsContractAddress } = swapsUtils; @@ -920,3 +923,11 @@ export default async function decodeTransaction(args) { } return [transactionElement, transactionDetails]; } + +export const TOKEN_CATEGORY_HASH = { + [TransactionType.tokenMethodApprove]: true, + [TransactionType.tokenMethodSetApprovalForAll]: true, + [TransactionType.tokenMethodTransfer]: true, + [TransactionType.tokenMethodTransferFrom]: true, + [TransactionType.tokenMethodIncreaseAllowance]: true, +}; diff --git a/app/components/Views/Asset/index.js b/app/components/Views/Asset/index.js index 4610949677e..65e1fa1341c 100644 --- a/app/components/Views/Asset/index.js +++ b/app/components/Views/Asset/index.js @@ -61,6 +61,7 @@ import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import { store } from '../../../store'; import { toChecksumHexAddress } from '@metamask/controller-utils'; import { selectSwapsTransactions } from '../../../selectors/transactionController'; +import { TOKEN_CATEGORY_HASH } from '../../UI/TransactionElement/utils'; const createStyles = (colors) => StyleSheet.create({ @@ -248,6 +249,7 @@ class Asset extends PureComponent { }); this.navSymbol = (this.props.route.params?.symbol ?? '').toLowerCase(); this.navAddress = (this.props.route.params?.address ?? '').toLowerCase(); + if (this.navSymbol.toUpperCase() !== 'ETH' && this.navAddress !== '') { this.filter = this.noEthFilter; } else { @@ -287,6 +289,7 @@ class Asset extends PureComponent { txParams: { from, to }, isTransfer, transferInformation, + type, } = tx; if ( @@ -295,10 +298,15 @@ class Asset extends PureComponent { (chainId === tx.chainId || (!tx.chainId && networkId === tx.networkID)) && tx.status !== 'unapproved' ) { - if (isTransfer) + if (TOKEN_CATEGORY_HASH[type]) { + return false; + } + if (isTransfer) { return this.props.tokens.find(({ address }) => toLowerCaseEquals(address, transferInformation.contractAddress), ); + } + return true; } return false; @@ -493,7 +501,6 @@ class Asset extends PureComponent { const displayBuyButton = asset.isETH ? this.props.isNetworkBuyNativeTokenSupported : this.props.isNetworkRampSupported; - return ( {loading ? ( diff --git a/app/components/Views/Asset/index.test.js b/app/components/Views/Asset/index.test.js index a7461510901..c67129f7409 100644 --- a/app/components/Views/Asset/index.test.js +++ b/app/components/Views/Asset/index.test.js @@ -1,10 +1,25 @@ import React from 'react'; +import { TransactionType } from '@metamask/transaction-controller'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { backgroundState } from '../../../util/test/initial-root-state'; import Asset from './'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; const mockInitialState = { + swaps: { '0x1': { isLive: true }, hasOnboarded: false, isLive: true }, + fiatOrders: { + networks: [ + { + active: true, + chainId: '1', + chainName: 'Ethereum Mainnet', + nativeTokenSupported: true, + }, + ], + }, + inpageProvider: { + networkId: '0x1', + }, engine: { backgroundState: { ...backgroundState, @@ -16,10 +31,58 @@ const mockInitialState = { }, }, }, + NetworkController: { + selectedNetworkClientId: 'selectedNetworkClientId', + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'selectedNetworkClientId', + }, + ], + defaultRpcEndpointIndex: 0, + defaultBlockExplorerUrl: 0, + blockExplorerUrls: ['https://block.com'], + }, + '0x89': { + chainId: '0x89', + rpcEndpoints: [ + { + networkClientId: 'otherNetworkClientId', + }, + ], + defaultRpcEndpointIndex: 0, + }, + }, + }, + TransactionController: { + transactions: [ + { + txParams: { + from: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', + to: '0x0000000000000000000000000000000000000000', + }, + hash: '0x3148', + status: 'confirmed', + chainId: '0x1', + networkID: '0x1', + type: TransactionType.simpleSend, + }, + ], + }, }, }, }; +jest.mock('../../../store', () => ({ + store: { + getState: () => mockInitialState, + }, +})); + +jest.unmock('react-native/Libraries/Interaction/InteractionManager'); + jest.mock('../../../core/Engine', () => { const { MOCK_ADDRESS_1, @@ -48,9 +111,8 @@ describe('Asset', () => { it('should render correctly', () => { const { toJSON } = renderWithProvider( null }} + navigation={{ setOptions: jest.fn() }} route={{ params: { symbol: 'ETH', address: 'something', isETH: true } }} - transactions={[]} />, { state: mockInitialState, @@ -58,4 +120,20 @@ describe('Asset', () => { ); expect(toJSON()).toMatchSnapshot(); }); + + it('should call navigation.setOptions on mount', () => { + const mockSetOptions = jest.fn(); + renderWithProvider( + , + { + state: mockInitialState, + }, + ); + + expect(mockSetOptions).toHaveBeenCalled(); + }); }); From f96fb4ea2eeaaf14226d7479c0e9257151047eb2 Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:10:14 +0000 Subject: [PATCH 07/19] chore: remove duplicated dependencies (#12722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** remove eth-json-rpc-filters and eth-json-rpc-middleware duplicated deps ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 - yarn.lock | 114 +++++---------------------------------------------- 2 files changed, 10 insertions(+), 106 deletions(-) diff --git a/package.json b/package.json index db621739117..be591d65c21 100644 --- a/package.json +++ b/package.json @@ -162,8 +162,6 @@ "@metamask/eth-json-rpc-filters": "^9.0.0", "@metamask/eth-json-rpc-middleware": "^15.0.0", "@metamask/eth-hd-keyring": "^9.0.0", - "@metamask/eth-json-rpc-filters": "^8.0.0", - "@metamask/eth-json-rpc-middleware": "^11.0.2", "@metamask/eth-ledger-bridge-keyring": "^8.0.0", "@metamask/eth-query": "^4.0.0", "@metamask/eth-sig-util": "^8.0.0", diff --git a/yarn.lock b/yarn.lock index 1b9e5218c5a..d3ab8f8bdbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4360,14 +4360,6 @@ resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-7.8.0.tgz#fc32e07746689459c4b049dc581d1dbda5545686" integrity sha512-+70fkgjhVJeJ+nJqnburIM3UAsfvxat1Low9HMPobLbv64FIdB4Nzu5ct3qojNQ58r5sK01tg5UoFIJYslaVrg== -"@metamask/abi-utils@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@metamask/abi-utils/-/abi-utils-1.2.0.tgz#068e1b0f5e423dfae96961e0e5276a7c1babc03a" - integrity sha512-Hf7fnBDM9ptCPDtq/wQffWbw859CdVGMwlpWUEsTH6gLXhXONGrRXHA2piyYPRuia8YYTdJvRC/zSK1/nyLvYg== - dependencies: - "@metamask/utils" "^3.4.1" - superstruct "^1.0.3" - "@metamask/abi-utils@^2.0.3", "@metamask/abi-utils@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@metamask/abi-utils/-/abi-utils-2.0.4.tgz#20908c1d910f7a17a89fdf5778a5c59d5cb8b8be" @@ -4555,13 +4547,13 @@ "@metamask/utils" "^9.2.1" ethereum-cryptography "^2.1.2" -"@metamask/eth-json-rpc-filters@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-filters/-/eth-json-rpc-filters-8.0.0.tgz#fd0ca224dc198e270e142c1f2007e05cacb5f16a" - integrity sha512-kDwSoas8gYWtN79AO4vvyKvaL8bIMstpuwZdsWTSc1goBFTOJEscCD6zUX+MOQFnQohFoC512mNeA5tPLRV46A== +"@metamask/eth-json-rpc-filters@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-filters/-/eth-json-rpc-filters-9.0.0.tgz#9efe770d12f7d1d8289d9f2ed601911c642c31b9" + integrity sha512-mn3clrrNF1zl3E729IgNHV9ia6wvRl+eRwc98e38GM+Se2EcDqBvx1oa9e3oo6BTlqmzzIwdeTvF4/jHU1CDWQ== dependencies: "@metamask/eth-query" "^4.0.0" - "@metamask/json-rpc-engine" "^9.0.0" + "@metamask/json-rpc-engine" "^10.0.0" "@metamask/safe-event-emitter" "^3.0.0" async-mutex "^0.5.0" pify "^5.0.0" @@ -4576,21 +4568,6 @@ "@metamask/rpc-errors" "^7.0.0" "@metamask/utils" "^9.1.0" -"@metamask/eth-json-rpc-middleware@^11.0.2": - version "11.0.2" - resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-middleware/-/eth-json-rpc-middleware-11.0.2.tgz#85e6639f5d159a3277d13609dea9f12ebfb5b4e8" - integrity sha512-/HqtuK/6E8sIJmzg0O3Ey5JsgK6O/VbDqg5R9thHFQMi9EtKXnnZFc8Blir7IOQraGVJFiZQIKZMHRTNQRyreg== - dependencies: - "@metamask/eth-json-rpc-provider" "^1.0.0" - "@metamask/eth-sig-util" "^6.0.0" - "@metamask/utils" "^5.0.1" - clone "^2.1.1" - eth-block-tracker "^7.0.1" - eth-rpc-errors "^4.0.3" - json-rpc-engine "^6.1.0" - pify "^3.0.0" - safe-stable-stringify "^2.3.2" - "@metamask/eth-json-rpc-middleware@^15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-middleware/-/eth-json-rpc-middleware-15.0.0.tgz#167288ad4618438af9d1bda75d238cb0facfde3f" @@ -4608,15 +4585,6 @@ pify "^5.0.0" safe-stable-stringify "^2.4.3" -"@metamask/eth-json-rpc-provider@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-1.0.1.tgz#3fd5316c767847f4ca107518b611b15396a5a32c" - integrity sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA== - dependencies: - "@metamask/json-rpc-engine" "^7.0.0" - "@metamask/safe-event-emitter" "^3.0.0" - "@metamask/utils" "^5.0.1" - "@metamask/eth-json-rpc-provider@^4.1.5", "@metamask/eth-json-rpc-provider@^4.1.6": version "4.1.6" resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-4.1.6.tgz#5d86ee7db6ff94b0abe1f00ef02aeffa60536497" @@ -4648,19 +4616,6 @@ json-rpc-random-id "^1.0.0" xtend "^4.0.1" -"@metamask/eth-sig-util@^6.0.0": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-6.0.2.tgz#d81dc87e0cd5a6580010911501976b48821746ad" - integrity sha512-D6IIefM2vS+4GUGGtezdBbkwUYQC4bCosYx/JteUuF0zfe6lyxR4cruA8+2QHoUg7F7edNH1xymYpqYq1BeOkw== - dependencies: - "@ethereumjs/util" "^8.1.0" - "@metamask/abi-utils" "^1.2.0" - "@metamask/utils" "^5.0.2" - ethereum-cryptography "^2.1.2" - ethjs-util "^0.1.6" - tweetnacl "^1.0.3" - tweetnacl-util "^0.15.1" - "@metamask/eth-sig-util@^7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-7.0.3.tgz#be9e444fe0b8474c04e2ff42fd983173767f6ac0" @@ -4801,15 +4756,6 @@ "@metamask/safe-event-emitter" "^3.0.0" "@metamask/utils" "^10.0.0" -"@metamask/json-rpc-engine@^7.0.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz#f2b30a2164558014bfcca45db10f5af291d989af" - integrity sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg== - dependencies: - "@metamask/rpc-errors" "^6.2.1" - "@metamask/safe-event-emitter" "^3.0.0" - "@metamask/utils" "^8.3.0" - "@metamask/json-rpc-engine@^8.0.1": version "8.0.2" resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz#29510a871a8edef892f838ee854db18de0bf0d14" @@ -4819,15 +4765,6 @@ "@metamask/safe-event-emitter" "^3.0.0" "@metamask/utils" "^8.3.0" -"@metamask/json-rpc-engine@^9.0.0": - version "9.0.3" - resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-9.0.3.tgz#491eb6085b63b040979d4c65f2a01107d22a162a" - integrity sha512-efeRXW7KaL0BJcAeudSGhzu6sD3hMpxx9nl3V+Yemm1bsyc66yVUhYPR+XH+Y6ZvB2p05ywgvd1Ev5PBwFzr/g== - dependencies: - "@metamask/rpc-errors" "^6.3.1" - "@metamask/safe-event-emitter" "^3.0.0" - "@metamask/utils" "^9.1.0" - "@metamask/json-rpc-middleware-stream@^7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-7.0.1.tgz#3e10c93c88507b1a55eea5d125ebf87db0f8fead" @@ -5200,7 +5137,7 @@ "@metamask/utils" "^10.0.0" cockatiel "^3.1.2" -"@metamask/rpc-errors@7.0.1", "@metamask/rpc-errors@^6.2.1", "@metamask/rpc-errors@^6.3.1", "@metamask/rpc-errors@^7.0.0", "@metamask/rpc-errors@^7.0.1": +"@metamask/rpc-errors@7.0.1", "@metamask/rpc-errors@^6.2.1", "@metamask/rpc-errors@^7.0.0", "@metamask/rpc-errors@^7.0.1": version "7.0.1" resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-7.0.1.tgz#0eb2231a1d5e6bb102df5ac07f365c695bf70055" integrity sha512-EeQGYioq845w2iBmiR9LHYqHhYIaeDTmxprHpPE3BTlkLB74P0xLv/TivOn4snNLowiC5ekOXfcUzCQszTDmSg== @@ -5470,7 +5407,7 @@ lodash "^4.17.21" uuid "^8.3.2" -"@metamask/utils@^10.0.0", "@metamask/utils@^10.0.1", "@metamask/utils@^3.4.1", "@metamask/utils@^5.0.1", "@metamask/utils@^5.0.2", "@metamask/utils@^8.2.0", "@metamask/utils@^8.3.0", "@metamask/utils@^9.0.0", "@metamask/utils@^9.1.0", "@metamask/utils@^9.2.1": +"@metamask/utils@^10.0.0", "@metamask/utils@^10.0.1", "@metamask/utils@^8.2.0", "@metamask/utils@^8.3.0", "@metamask/utils@^9.0.0", "@metamask/utils@^9.1.0", "@metamask/utils@^9.2.1": version "10.0.1" resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-10.0.1.tgz#a765f96c20e35fc51c068fb9f88a3332b40b215e" integrity sha512-zHgAitJtRwviVVFnRUA2PLRMaAwatr3jiHgiH7mPicJaeSK4ma01aGR4fHy0iy5tlVo1ZiioTmJ1Hbp8FZ6pSg== @@ -13683,7 +13620,7 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= -clone@^2.1.1, clone@^2.1.2: +clone@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== @@ -16346,17 +16283,6 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= -eth-block-tracker@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/eth-block-tracker/-/eth-block-tracker-7.1.0.tgz#dfc16085c6817cc30caabba381deb8d204c1c766" - integrity sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg== - dependencies: - "@metamask/eth-json-rpc-provider" "^1.0.0" - "@metamask/safe-event-emitter" "^3.0.0" - "@metamask/utils" "^5.0.1" - json-rpc-random-id "^1.0.1" - pify "^3.0.0" - eth-ens-namehash@2.0.8, eth-ens-namehash@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf" @@ -16388,13 +16314,6 @@ eth-phishing-detect@^1.2.0: dependencies: fast-levenshtein "^2.0.6" -eth-rpc-errors@^4.0.2, eth-rpc-errors@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz#6ddb6190a4bf360afda82790bb7d9d5e724f423a" - integrity sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg== - dependencies: - fast-safe-stringify "^2.0.6" - eth-url-parser@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/eth-url-parser/-/eth-url-parser-1.0.4.tgz#310a99f331abdb8d603c74131568fb773e609cd8" @@ -16620,7 +16539,7 @@ ethjs-util@0.1.3: is-hex-prefixed "1.0.0" strip-hex-prefix "1.0.0" -ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: +ethjs-util@0.1.6, ethjs-util@^0.1.3: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -20428,14 +20347,6 @@ json-pointer@^0.6.2: dependencies: foreach "^2.0.4" -json-rpc-engine@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz#bf5ff7d029e1c1bf20cb6c0e9f348dcd8be5a393" - integrity sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ== - dependencies: - "@metamask/safe-event-emitter" "^2.0.0" - eth-rpc-errors "^4.0.2" - json-rpc-random-id@^1.0.0, json-rpc-random-id@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz#ba49d96aded1444dbb8da3d203748acbbcdec8c8" @@ -26124,7 +26035,7 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.3.2, safe-stable-stringify@^2.4.3: +safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.3: version "2.5.0" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== @@ -27949,11 +27860,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tweetnacl-util@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" - integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== - tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" From 9708fe6ec853fa7b13fb546af8d2806b941e889b Mon Sep 17 00:00:00 2001 From: Salim TOUBAL Date: Thu, 19 Dec 2024 16:20:37 +0100 Subject: [PATCH 08/19] fix: fix swap flow (#12788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** ## **Related issues** Fixes: #12757 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** https://github.com/user-attachments/assets/480510e9-b2fe-4e92-9bd0-3a30d0d2ae07 ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../Asset/__snapshots__/index.test.js.snap | 39 ++++++++++++++++++ app/components/Views/Asset/index.js | 40 +++++++++++++++++- app/components/Views/Asset/index.test.js | 41 ++++++++++++++++++- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/app/components/Views/Asset/__snapshots__/index.test.js.snap b/app/components/Views/Asset/__snapshots__/index.test.js.snap index 627aedf5a32..333565b0718 100644 --- a/app/components/Views/Asset/__snapshots__/index.test.js.snap +++ b/app/components/Views/Asset/__snapshots__/index.test.js.snap @@ -1,5 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Asset should not display swaps button if the asset is not allowed 1`] = ` + + + + + + + +`; + exports[`Asset should render correctly 1`] = ` @@ -169,6 +172,10 @@ class Asset extends PureComponent { * Boolean that indicates if native token is supported to buy */ isNetworkBuyNativeTokenSupported: PropTypes.bool, + /** + * Function to set the swaps liveness + */ + setLiveness: PropTypes.func, }; state = { @@ -241,8 +248,27 @@ class Asset extends PureComponent { this.updateNavBar(contentOffset); }; + checkLiveness = async (chainId) => { + try { + const featureFlags = await swapsUtils.fetchSwapsFeatureFlags( + getFeatureFlagChainId(chainId), + AppConstants.SWAPS.CLIENT_ID, + ); + this.props.setLiveness(chainId, featureFlags); + } catch (error) { + Logger.error(error, 'Swaps: error while fetching swaps liveness'); + this.props.setLiveness(chainId, null); + } + }; + componentDidMount() { this.updateNavBar(); + + const tokenChainId = this.props.route?.params?.chainId; + if (tokenChainId) { + this.checkLiveness(tokenChainId); + } + InteractionManager.runAfterInteractions(() => { this.normalizeTransactions(); this.mounted = true; @@ -490,7 +516,9 @@ class Asset extends PureComponent { : isSwapsAllowed(chainId); const isAssetAllowed = - asset.isETH || asset.address?.toLowerCase() in this.props.swapsTokens; + asset.isETH || + asset.isNative || + asset.address?.toLowerCase() in this.props.swapsTokens; const displaySwapsButton = isSwapsFeatureLive && @@ -566,4 +594,12 @@ const mapStateToProps = (state, { route }) => ({ networkClientId: selectNetworkClientId(state), }); -export default connect(mapStateToProps)(withMetricsAwareness(Asset)); +const mapDispatchToProps = (dispatch) => ({ + setLiveness: (chainId, featureFlags) => + dispatch(setSwapsLiveness(chainId, featureFlags)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withMetricsAwareness(Asset)); diff --git a/app/components/Views/Asset/index.test.js b/app/components/Views/Asset/index.test.js index c67129f7409..23ab4e6da7b 100644 --- a/app/components/Views/Asset/index.test.js +++ b/app/components/Views/Asset/index.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { TransactionType } from '@metamask/transaction-controller'; +import { swapsUtils } from '@metamask/swaps-controller/'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { backgroundState } from '../../../util/test/initial-root-state'; import Asset from './'; @@ -112,7 +113,14 @@ describe('Asset', () => { const { toJSON } = renderWithProvider( , { state: mockInitialState, @@ -126,7 +134,14 @@ describe('Asset', () => { renderWithProvider( , { @@ -136,4 +151,26 @@ describe('Asset', () => { expect(mockSetOptions).toHaveBeenCalled(); }); + + it('should not display swaps button if the asset is not allowed', () => { + jest.spyOn(swapsUtils, 'fetchSwapsFeatureFlags').mockRejectedValue('error'); + const { toJSON } = renderWithProvider( + , + { + state: mockInitialState, + }, + ); + + expect(toJSON()).toMatchSnapshot(); + }); }); From d3e0771e37aefb4ed5a23c25e47a801bc3bbb9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n?= Date: Thu, 19 Dec 2024 17:00:31 +0100 Subject: [PATCH 09/19] test: ERC1155 set approval for all e2e (#12774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR is within the scope of the Quality Quest. We're including an e2e test case to check the set approval for all for ERC1155 in test dApp. **Test steps:** Given I am on the test dapp When I tap on the Set Approval for All button under the ERC 1155 section Then the transaction bottom sheet should appear When I submit the transaction Then the transaction should appear in the transaction history ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- e2e/pages/Browser/TestDApp.js | 13 +++- e2e/selectors/Browser/TestDapp.selectors.js | 3 +- .../set-approval-for-all-erc1155.spec.js | 73 +++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 e2e/specs/confirmations/set-approval-for-all-erc1155.spec.js diff --git a/e2e/pages/Browser/TestDApp.js b/e2e/pages/Browser/TestDApp.js index a09c4b8d5fd..ab411415955 100644 --- a/e2e/pages/Browser/TestDApp.js +++ b/e2e/pages/Browser/TestDApp.js @@ -101,7 +101,7 @@ class TestDApp { get nftSetApprovalForAllButton() { return Matchers.getElementByWebID( BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, - TestDappSelectorsWebIDs.SET_APPROVAL_FOR_ALL_BUTTON_ID, + TestDappSelectorsWebIDs.SET_APPROVAL_FOR_ALL_NFT_BUTTON_ID, ); } @@ -112,6 +112,13 @@ class TestDApp { ); } + get erc1155SetApprovalForAllButton() { + return Matchers.getElementByWebID( + BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, + TestDappSelectorsWebIDs.SET_APPROVAL_FOR_ALL_ERC1155_BUTTON_ID, + ); + } + async connect() { await this.tapButton(this.DappConnectButton); } @@ -162,6 +169,10 @@ class TestDApp { await this.tapButton(this.nftSetApprovalForAllButton); } + async tapERC1155SetApprovalForAllButton() { + await this.tapButton(this.erc1155SetApprovalForAllButton); + } + async tapConfirmButton() { await Gestures.tap(this.confirmButtonText, 0); } diff --git a/e2e/selectors/Browser/TestDapp.selectors.js b/e2e/selectors/Browser/TestDapp.selectors.js index 4407c27a950..4725a436c92 100644 --- a/e2e/selectors/Browser/TestDapp.selectors.js +++ b/e2e/selectors/Browser/TestDapp.selectors.js @@ -6,7 +6,8 @@ export const TestDappSelectorsWebIDs = { INCREASE_ALLOWANCE_BUTTON_ID: 'increaseTokenAllowance', NFT_TRANSFER_FROM_BUTTON_ID: 'transferFromButton', PERSONAL_SIGN: 'personalSign', - SET_APPROVAL_FOR_ALL_BUTTON_ID: 'setApprovalForAllButton', + SET_APPROVAL_FOR_ALL_NFT_BUTTON_ID: 'setApprovalForAllButton', + SET_APPROVAL_FOR_ALL_ERC1155_BUTTON_ID: 'setApprovalForAllERC1155Button', SIGN_TYPE_DATA: 'signTypedData', SIGN_TYPE_DATA_V3: 'signTypedDataV3', SIGN_TYPE_DATA_V4: 'signTypedDataV4', diff --git a/e2e/specs/confirmations/set-approval-for-all-erc1155.spec.js b/e2e/specs/confirmations/set-approval-for-all-erc1155.spec.js new file mode 100644 index 00000000000..8e646db9efd --- /dev/null +++ b/e2e/specs/confirmations/set-approval-for-all-erc1155.spec.js @@ -0,0 +1,73 @@ +'use strict'; + +import { SmokeConfirmations } from '../../tags'; +import TestHelpers from '../../helpers'; +import { loginToApp } from '../../viewHelper'; + +import TabBarComponent from '../../pages/wallet/TabBarComponent'; +import TestDApp from '../../pages/Browser/TestDApp'; +import FixtureBuilder from '../../fixtures/fixture-builder'; +import { + withFixtures, + defaultGanacheOptions, +} from '../../fixtures/fixture-helper'; +import { SMART_CONTRACTS } from '../../../app/util/test/smart-contracts'; +import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors'; +import Assertions from '../../utils/Assertions'; +import { ContractApprovalBottomSheetSelectorsText } from '../../selectors/Browser/ContractApprovalBottomSheet.selectors'; +import ContractApprovalBottomSheet from '../../pages/Browser/ContractApprovalBottomSheet'; + +describe(SmokeConfirmations('ERC1155 token'), () => { + const ERC1155_CONTRACT = SMART_CONTRACTS.ERC1155; + + beforeAll(async () => { + await TestHelpers.reverseServerPort(); + }); + + it('approve all ERC1155 tokens', async () => { + await withFixtures( + { + dapp: true, + fixture: new FixtureBuilder() + .withGanacheNetwork() + .withPermissionControllerConnectedToTestDapp() + .build(), + restartDevice: true, + ganacheOptions: defaultGanacheOptions, + smartContract: ERC1155_CONTRACT, + }, + async ({ contractRegistry }) => { + const erc1155Address = await contractRegistry.getContractAddress( + ERC1155_CONTRACT, + ); + await loginToApp(); + + // Navigate to the browser screen + await TabBarComponent.tapBrowser(); + await TestDApp.navigateToTestDappWithContract({ + contractAddress: erc1155Address, + }); + + // Set approval for all ERC1155 tokens + await TestDApp.tapERC1155SetApprovalForAllButton(); + await Assertions.checkIfTextIsDisplayed( + ContractApprovalBottomSheetSelectorsText.APPROVE, + ); + + // Tap approve button + await ContractApprovalBottomSheet.tapApproveButton(); + + // Navigate to the activity screen + await TabBarComponent.tapActivity(); + + // Assert that the ERC1155 activity is an set approve for all and it is confirmed + await Assertions.checkIfTextIsDisplayed( + ActivitiesViewSelectorsText.SET_APPROVAL_FOR_ALL_METHOD, + ); + await Assertions.checkIfTextIsDisplayed( + ActivitiesViewSelectorsText.CONFIRM_TEXT, + ); + }, + ); + }); +}); From 77931ab29e4c903b44dc4bef5c0872241ca94857 Mon Sep 17 00:00:00 2001 From: Curtis David Date: Thu, 19 Dec 2024 11:01:17 -0500 Subject: [PATCH 10/19] test: E2E Send to Contract Address (#12777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The purpose of this PR is to add e2e coverage for sending to a contract address. The scenario is as follows: ``` Given I am on the Send View And I input a contract address When I proceed to send the transaction Then the transaction is submitted ``` ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../Transactions/ActivitiesView.selectors.js | 1 + .../send-to-contract-address.spec.js | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 e2e/specs/confirmations/send-to-contract-address.spec.js diff --git a/e2e/selectors/Transactions/ActivitiesView.selectors.js b/e2e/selectors/Transactions/ActivitiesView.selectors.js index 4327c397700..1aba946d5ec 100644 --- a/e2e/selectors/Transactions/ActivitiesView.selectors.js +++ b/e2e/selectors/Transactions/ActivitiesView.selectors.js @@ -10,6 +10,7 @@ export const ActivitiesViewSelectorsIDs = { export const ActivitiesViewSelectorsText = { CONFIRM_TEXT: enContent.transaction.confirmed, + SMART_CONTRACT_INTERACTION: enContent.transactions.smart_contract_interaction, INCREASE_ALLOWANCE_METHOD: enContent.transactions.increase_allowance, SENT_COLLECTIBLE_MESSAGE_TEXT: enContent.transactions.sent_collectible, SENT_TOKENS_MESSAGE_TEXT: (unit) => getSentUnitMessage(unit), diff --git a/e2e/specs/confirmations/send-to-contract-address.spec.js b/e2e/specs/confirmations/send-to-contract-address.spec.js new file mode 100644 index 00000000000..ddcbad62bc9 --- /dev/null +++ b/e2e/specs/confirmations/send-to-contract-address.spec.js @@ -0,0 +1,70 @@ +'use strict'; + +import { SmokeConfirmations } from '../../tags'; +import AmountView from '../../pages/Send/AmountView'; +import SendView from '../../pages/Send/SendView'; +import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView'; +import { loginToApp } from '../../viewHelper'; +import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet'; + +import TestHelpers from '../../helpers'; +import FixtureBuilder from '../../fixtures/fixture-builder'; +import { + withFixtures, + defaultGanacheOptions, +} from '../../fixtures/fixture-helper'; +import { + SMART_CONTRACTS, + contractConfiguration, +} from '../../../app/util/test/smart-contracts'; +import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors'; + +import TabBarComponent from '../../pages/wallet/TabBarComponent'; +import Assertions from '../../utils/Assertions'; + +const HST_CONTRACT = SMART_CONTRACTS.HST; + +describe(SmokeConfirmations('Send to contract address'), () => { + beforeAll(async () => { + jest.setTimeout(170000); + await TestHelpers.reverseServerPort(); + }); + + it('should send ETH to a contract from inside the wallet', async () => { + const AMOUNT = '12'; + + await withFixtures( + { + dapp: true, + fixture: new FixtureBuilder().withGanacheNetwork().build(), + restartDevice: true, + ganacheOptions: defaultGanacheOptions, + smartContract: HST_CONTRACT, + }, + async ({ contractRegistry }) => { + const hstAddress = await contractRegistry.getContractAddress( + HST_CONTRACT, + ); + await loginToApp(); + + await TabBarComponent.tapActions(); + await WalletActionsBottomSheet.tapSendButton(); + + await SendView.inputAddress(hstAddress); + await SendView.tapNextButton(); + + await Assertions.checkIfVisible(AmountView.title); + + await AmountView.typeInTransactionAmount(AMOUNT); + await AmountView.tapNextButton(); + + await TransactionConfirmationView.tapConfirmButton(); + await TabBarComponent.tapActivity(); + + await Assertions.checkIfTextIsDisplayed( + ActivitiesViewSelectorsText.SMART_CONTRACT_INTERACTION, + ); + }, + ); + }); +}); From 1670ada53ec51ea2702ea584162bc8416f2fdf18 Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:03:11 +0100 Subject: [PATCH 11/19] chore: remove duplicate `@metamask/swaps-controller` in `package.json` (#12795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This pr remove duplicate `@metamask/swaps-controller` in `package.json` ## **Related issues** Fixes: ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index be591d65c21..e27ea614e5e 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,6 @@ "@metamask/swaps-controller": "^11.0.0", "@metamask/transaction-controller": "^42.0.0", "@metamask/utils": "^10.0.1", - "@metamask/swaps-controller": "^11.0.0", "@ngraveio/bc-ur": "^1.1.6", "@notifee/react-native": "^9.0.0", "@react-native-async-storage/async-storage": "^1.23.1", From 7a75c4a12ae4ad1c23fc4e7c6bb6c7feb0b3cd79 Mon Sep 17 00:00:00 2001 From: Amitabh Aggarwal Date: Thu, 19 Dec 2024 12:07:37 -0600 Subject: [PATCH 12/19] fix: improve error handling in staking eligibility hook (#12799) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR improves loose error handling in the eligibility hook when Staking API service is not available by moving the check to the existing try catch block. ## **Related issues** Fixes: https://github.com/MetaMask/metamask-mobile/issues/12759 [STAKE-910](https://consensyssoftware.atlassian.net/browse/STAKE-910) ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [STAKE-910]: https://consensyssoftware.atlassian.net/browse/STAKE-910?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../hooks/useStakingEligibility.test.tsx | 29 +++++++++++++++++-- .../UI/Stake/hooks/useStakingEligibility.ts | 12 ++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/components/UI/Stake/hooks/useStakingEligibility.test.tsx b/app/components/UI/Stake/hooks/useStakingEligibility.test.tsx index ac3b6739b78..ae15901fd82 100644 --- a/app/components/UI/Stake/hooks/useStakingEligibility.test.tsx +++ b/app/components/UI/Stake/hooks/useStakingEligibility.test.tsx @@ -27,10 +27,13 @@ const mockStakingApiService: Partial = { getPooledStakingEligibility: jest.fn(), }; -const mockSdkContext: Stake = { +const createMockStakeContext = (overrides?: Partial) => ({ setSdkType: jest.fn(), stakingApiService: mockStakingApiService as StakingApiService, -}; + ...overrides, +}); + +let mockSdkContext = createMockStakeContext(); // Mock the context jest.mock('./useStakeContext', () => ({ @@ -38,6 +41,10 @@ jest.mock('./useStakeContext', () => ({ })); describe('useStakingEligibility', () => { + beforeEach(() => { + mockSdkContext = createMockStakeContext(); + }); + afterEach(() => { jest.clearAllMocks(); }); @@ -125,4 +132,22 @@ describe('useStakingEligibility', () => { }); }); }); + + describe('when stakingApiService is undefined', () => { + it('handles undefined stakingApiService gracefully', async () => { + // Override the mock context with undefined stakingApiService + mockSdkContext = createMockStakeContext({ + stakingApiService: undefined, + }); + + const { result } = renderHookWithProvider(() => useStakingEligibility(), { + state: mockInitialState, + }); + + await waitFor(() => { + expect(result.current.isLoadingEligibility).toBe(false); + expect(result.current.isEligible).toBe(false); + }); + }); + }); }); diff --git a/app/components/UI/Stake/hooks/useStakingEligibility.ts b/app/components/UI/Stake/hooks/useStakingEligibility.ts index 6ba2c0add8a..a04f0a4f490 100644 --- a/app/components/UI/Stake/hooks/useStakingEligibility.ts +++ b/app/components/UI/Stake/hooks/useStakingEligibility.ts @@ -19,14 +19,12 @@ const useStakingEligibility = () => { const [error, setError] = useState(null); const fetchStakingEligibility = useCallback(async () => { - if (!stakingApiService) { - throw new Error('Staking API service is unavailable'); - } - - setIsLoading(true); - setError(null); - try { + if (!stakingApiService) { + return { isEligible: false }; + } + setIsLoading(true); + setError(null); const { eligible } = await stakingApiService.getPooledStakingEligibility([ selectedAddress, ]); From 4b9ea55044e8f8744c8185635a96253cf76550f1 Mon Sep 17 00:00:00 2001 From: cryptodev-2s <109512101+cryptodev-2s@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:13:53 +0100 Subject: [PATCH 13/19] chore: bump `@metamask/smart-transactions-controller` to `16.0.0` (#12790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR bumps `@metamask/smart-transactions-controller` to `16.0.0` [CHANGELOG](https://github.com/MetaMask/smart-transactions-controller/blob/main/CHANGELOG.md#1600) - `@metamask/transaction-controller` has been bumped to `42.0.0` which match the current version used in the client. ## **Related issues** Fixes: ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- package.json | 2 +- yarn.lock | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e27ea614e5e..b6b74ef38a3 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "@metamask/selected-network-controller": "^19.0.0", "@metamask/signature-controller": "^23.1.0", "@metamask/slip44": "^4.1.0", - "@metamask/smart-transactions-controller": "^15.0.0", + "@metamask/smart-transactions-controller": "^16.0.0", "@metamask/snaps-controllers": "^9.15.0", "@metamask/snaps-execution-environments": "^6.10.0", "@metamask/snaps-rpc-methods": "^11.7.0", diff --git a/yarn.lock b/yarn.lock index d3ab8f8bdbc..1c291169aa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5202,10 +5202,10 @@ resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-4.1.0.tgz#6f2702de7ba64dad3ab6586ea3ac4e5647804b0a" integrity sha512-RQ2MJO0X3QLnJo0rFlb83h2tNAkqqx/VNOPLc3/S2CvY3/cXy3UAEw/xRM/475BeAAkWI93yiIn/FoGUy3E0Ig== -"@metamask/smart-transactions-controller@^15.0.0": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-15.0.0.tgz#d9a3c2b3e3b1c5d9ddf68c03c0a537d348119fab" - integrity sha512-IN3mtNDt6YZZBlBn0hk5M+9ShUVD+I4IhAkwbKGp5aom1NdGqVqvl/N0axuhFCqgjBG9JM4zt+orvXIDIhDLXw== +"@metamask/smart-transactions-controller@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-16.0.0.tgz#d5f26e3f25945dc695c7e7152f8ab4c9ffa85ac9" + integrity sha512-NfX4yvWlB5MQvkpp+1hsInom1+f0D+xK6b3n/csGJgsDuTWXIS+C3hdYBMS5bpZIrjobFRBG1LH+YQBBsndPHg== dependencies: "@babel/runtime" "^7.24.1" "@ethereumjs/tx" "^5.2.1" @@ -5217,7 +5217,6 @@ "@metamask/eth-query" "^4.0.0" "@metamask/polling-controller" "^12.0.0" bignumber.js "^9.0.1" - events "^3.3.0" fast-json-patch "^3.1.0" lodash "^4.17.21" From 3b17cd311c16742cc3866ac1a23cc35a07465ad0 Mon Sep 17 00:00:00 2001 From: Frank von Hoven <141057783+frankvonhoven@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:29:12 -0600 Subject: [PATCH 14/19] fix: use correct import for MetricsEventBuilder (#12798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Fixes correct import for MetricsEventBuilder ## **Related issues** Fixes: [12530](https://github.com/MetaMask/metamask-mobile/issues/12530) [Related Slack message](https://consensys.slack.com/archives/C8RSKCNCD/p1734622035414719?thread_ts=1734535680.397999&cid=C8RSKCNCD) ## **Manual testing steps** 1. Go to this [dapp ](https://metamask.github.io/metamask-sdk/chore-update-vuejs-build/packages/examples/react-demo/build/index.html)in the inapp browser with a debug build 2. Check the console logs for the dapp 3. Switch chains -> See console ## **Screenshots/Recordings** [Link to zoom recording](https://consensys.zoom.us/rec/share/AJr-gv8tqRuEZx0PorROKgux41aYESIZ3k_PHRMoYTwh6EHIcZU40oliXHa28bYU.wYfZYWsUupmI5hQo) ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/core/RPCMethods/wallet_switchEthereumChain.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/RPCMethods/wallet_switchEthereumChain.js b/app/core/RPCMethods/wallet_switchEthereumChain.js index 69a7d85cee4..7d9d3163545 100644 --- a/app/core/RPCMethods/wallet_switchEthereumChain.js +++ b/app/core/RPCMethods/wallet_switchEthereumChain.js @@ -1,6 +1,7 @@ import Engine from '../Engine'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { MetaMetricsEvents, MetaMetrics } from '../../core/Analytics'; +import { MetricsEventBuilder } from '../../core/Analytics/MetricsEventBuilder'; import { selectNetworkConfigurations } from '../../selectors/networkController'; import { store } from '../../store'; import { @@ -74,8 +75,7 @@ const wallet_switchEthereumChain = async ({ }); MetaMetrics.getInstance().trackEvent( - MetaMetrics.getInstance() - .createEventBuilder(MetaMetricsEvents.NETWORK_SWITCHED) + MetricsEventBuilder.createEventBuilder(MetaMetricsEvents.NETWORK_SWITCHED) .addProperties(analyticsParams) .build(), ); From e9c1617fc00dbfc7c80c1c047fa4998654a8f089 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 20 Dec 2024 08:29:16 +0100 Subject: [PATCH 15/19] fix: Sanitize `signTypedDatav3v4` params before calling security API (#12789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR aims to filter request params before calling security API call if method is `signTypedDatav3v4` ## **Related issues** Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3830 ## **Manual testing steps** 1. Use following payload in the local test-dapp sign typed data v3/v4 ``` // Request the current account addresses from the Ethereum provider const addresses = await window.ethereum.request({ "method": "eth_accounts" }); // Construct the JSON string for eth_signTypedData_v4, including the dynamic owner address const jsonData = { domain: { name: "USD Coin", version: "2", chainId: "1", verifyingContract: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" }, types: { EIP712Domain: [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" } ], Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" } ] }, primaryType: "Permit", message: { owner: addresses[0], spender: "0xa2d86c5ff6fbf5f455b1ba2737938776c24d7a58", value: "115792089237316195423570985008687907853269984665640564039457584007913129639935", nonce: "0", deadline: "115792089237316195423570985008687907853269984665640564039457584007913129639935" } }; ``` 2. Notice that the transaction is considered as malicious (which was not flagged before) ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [X] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/lib/ppom/ppom-util.test.ts | 42 +++++++++++++++++++++++++++++++++- app/lib/ppom/ppom-util.ts | 27 +++++++++++++++++++--- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/app/lib/ppom/ppom-util.test.ts b/app/lib/ppom/ppom-util.test.ts index 89faa23a585..6d0ab6bc03d 100644 --- a/app/lib/ppom/ppom-util.test.ts +++ b/app/lib/ppom/ppom-util.test.ts @@ -3,7 +3,10 @@ import * as SignatureRequestActions from '../../actions/signatureRequest'; // es import * as TransactionActions from '../../actions/transaction'; // eslint-disable-line import/no-namespace import * as NetworkControllerSelectors from '../../selectors/networkController'; // eslint-disable-line import/no-namespace import Engine from '../../core/Engine'; -import PPOMUtil from './ppom-util'; +import PPOMUtil, { + METHOD_SIGN_TYPED_DATA_V3, + METHOD_SIGN_TYPED_DATA_V4, +} from './ppom-util'; // eslint-disable-next-line import/no-namespace import * as securityAlertAPI from './security-alerts-api'; import { isBlockaidFeatureEnabled } from '../../util/blockaid'; @@ -22,6 +25,10 @@ import Logger from '../../util/Logger'; const CHAIN_ID_MOCK = '0x1'; +const SIGN_TYPED_DATA_PARAMS_MOCK_1 = '0x123'; +const SIGN_TYPED_DATA_PARAMS_MOCK_2 = + '{"primaryType":"Permit","domain":{},"types":{}}'; + jest.mock('./security-alerts-api'); jest.mock('../../util/blockaid'); @@ -439,5 +446,38 @@ describe('PPOM Utils', () => { source: SecurityAlertSource.Local, }); }); + + it.each([METHOD_SIGN_TYPED_DATA_V3, METHOD_SIGN_TYPED_DATA_V4])( + 'sanitizes request params if method is %s', + async (method: string) => { + isSecurityAlertsEnabledMock.mockReturnValue(true); + getSupportedChainIdsMock.mockResolvedValue([CHAIN_ID_MOCK]); + + const firstTwoParams = [ + SIGN_TYPED_DATA_PARAMS_MOCK_1, + SIGN_TYPED_DATA_PARAMS_MOCK_2, + ]; + + const unwantedParams = [{}, undefined, 1, null]; + + const params = [...firstTwoParams, ...unwantedParams]; + + const request = { + ...mockRequest, + method, + params, + }; + await PPOMUtil.validateRequest(request, CHAIN_ID_MOCK); + + expect(validateWithSecurityAlertsAPIMock).toHaveBeenCalledTimes(1); + expect(validateWithSecurityAlertsAPIMock).toHaveBeenCalledWith( + CHAIN_ID_MOCK, + { + ...request, + params: firstTwoParams, + }, + ); + }, + ); }); }); diff --git a/app/lib/ppom/ppom-util.ts b/app/lib/ppom/ppom-util.ts index 3a7716eb87f..b76d4bf8f4b 100644 --- a/app/lib/ppom/ppom-util.ts +++ b/app/lib/ppom/ppom-util.ts @@ -34,6 +34,8 @@ export interface PPOMRequest { const TRANSACTION_METHOD = 'eth_sendTransaction'; const TRANSACTION_METHODS = [TRANSACTION_METHOD, 'eth_sendRawTransaction']; +export const METHOD_SIGN_TYPED_DATA_V3 = 'eth_signTypedData_v3'; +export const METHOD_SIGN_TYPED_DATA_V4 = 'eth_signTypedData_v4'; const CONFIRMATION_METHODS = Object.freeze([ 'eth_sendRawTransaction', @@ -155,7 +157,7 @@ async function validateWithController( ppomController: PPOMController, request: PPOMRequest, ): Promise { - try{ + try { const response = (await ppomController.usePPOM((ppom) => ppom.validateJsonRpc(request as unknown as Record), )) as SecurityAlertResponse; @@ -166,7 +168,10 @@ async function validateWithController( }; } catch (e) { Logger.log(`Error validating request with PPOM: ${e}`); - return {...SECURITY_ALERT_RESPONSE_FAILED, source: SecurityAlertSource.Local,}; + return { + ...SECURITY_ALERT_RESPONSE_FAILED, + source: SecurityAlertSource.Local, + }; } } @@ -212,9 +217,25 @@ function isTransactionRequest(request: PPOMRequest) { return TRANSACTION_METHODS.includes(request.method); } +function sanitizeRequest(request: PPOMRequest): PPOMRequest { + // This is a temporary fix to prevent a PPOM bypass + if ( + request.method === METHOD_SIGN_TYPED_DATA_V4 || + request.method === METHOD_SIGN_TYPED_DATA_V3 + ) { + if (Array.isArray(request.params)) { + return { + ...request, + params: request.params.slice(0, 2), + }; + } + } + return request; +} + function normalizeRequest(request: PPOMRequest): PPOMRequest { if (request.method !== TRANSACTION_METHOD) { - return request; + return sanitizeRequest(request); } request.origin = request.origin From c87ac200f6663d891ccb2fd918e7b45e4b0d3eaa Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 20 Dec 2024 12:10:32 +0000 Subject: [PATCH 16/19] refactor: remove global network usage from transaction simulation (#12743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Remove any usage of the global network from transaction simulation. This requires using the `chainId` from the `TransactionMeta`, and so the `SimulationDetails` properties have been simplified to accept `TransactionMeta` directly to minimise the number of tightly coupled properties. ## **Related issues** Fixes: [#2011](https://github.com/MetaMask/mobile-planning/issues/2011) ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .eslintrc.js | 1 + .../AmountPill/AmountPill.test.tsx | 12 ++- .../AssetPill/AssetPill.test.tsx | 51 ++++++----- .../SimulationDetails/AssetPill/AssetPill.tsx | 29 +++---- .../BalanceChangeList.test.tsx | 4 + .../BalanceChangeRow.test.tsx | 8 +- .../SimulationDetails.stories.tsx | 58 ++++++++----- .../SimulationDetails.test.tsx | 86 +++++++++++++------ .../SimulationDetails/SimulationDetails.tsx | 11 ++- app/components/UI/SimulationDetails/types.ts | 10 +-- .../useBalanceChanges.test.ts | 41 +++++++-- .../UI/SimulationDetails/useBalanceChanges.ts | 31 +++++-- .../useSimulationMetrics.test.ts | 4 - .../SimulationDetails/useSimulationMetrics.ts | 8 +- .../confirmations/SendFlow/Confirm/index.js | 21 ++--- .../components/TransactionReview/index.js | 13 +-- 16 files changed, 243 insertions(+), 145 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 21a0949d4ea..70565eb4945 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -76,6 +76,7 @@ module.exports = { { files: [ 'app/components/UI/Name/**/*.{js,ts,tsx}', + 'app/components/UI/SimulationDetails/**/*.{js,ts,tsx}', 'app/components/hooks/DisplayName/**/*.{js,ts,tsx}' ], rules: { diff --git a/app/components/UI/SimulationDetails/AmountPill/AmountPill.test.tsx b/app/components/UI/SimulationDetails/AmountPill/AmountPill.test.tsx index 012f050320d..bf87cb1a899 100644 --- a/app/components/UI/SimulationDetails/AmountPill/AmountPill.test.tsx +++ b/app/components/UI/SimulationDetails/AmountPill/AmountPill.test.tsx @@ -5,25 +5,33 @@ import AmountPill from './AmountPill'; import { AssetIdentifier, AssetType, - NATIVE_ASSET_IDENTIFIER, + NativeAssetIdentifier, TokenAssetIdentifier, } from '../types'; const TOKEN_ID_MOCK = '0xabc'; +const CHAIN_ID_MOCK = '0x123'; const ERC20_ASSET_MOCK: TokenAssetIdentifier = { type: AssetType.ERC20, address: '0x456', + chainId: CHAIN_ID_MOCK, }; const ERC721_ASSET_MOCK: TokenAssetIdentifier = { type: AssetType.ERC721, address: '0x123', tokenId: TOKEN_ID_MOCK, + chainId: CHAIN_ID_MOCK, }; const ERC1155_ASSET_MOCK: TokenAssetIdentifier = { type: AssetType.ERC1155, address: '0x789', tokenId: TOKEN_ID_MOCK, + chainId: CHAIN_ID_MOCK, +}; +const NATIVE_ASSET_MOCK: NativeAssetIdentifier = { + type: AssetType.Native, + chainId: CHAIN_ID_MOCK, }; const renderAndExpect = ( @@ -83,7 +91,7 @@ describe('AmountPill', () => { it.each(nativeAndErc20Cases)( 'renders the correct sign and amount for $amount', ({ amount, expected }) => { - renderAndExpect(NATIVE_ASSET_IDENTIFIER, amount, expected); + renderAndExpect(NATIVE_ASSET_MOCK, amount, expected); }, ); }); diff --git a/app/components/UI/SimulationDetails/AssetPill/AssetPill.test.tsx b/app/components/UI/SimulationDetails/AssetPill/AssetPill.test.tsx index 220dad70717..291d2087c78 100644 --- a/app/components/UI/SimulationDetails/AssetPill/AssetPill.test.tsx +++ b/app/components/UI/SimulationDetails/AssetPill/AssetPill.test.tsx @@ -1,18 +1,10 @@ import React from 'react'; -import { render } from '@testing-library/react-native'; import AssetPill from './AssetPill'; -import { - selectChainId, - selectTicker, -} from '../../../../selectors/networkController'; import { AssetType, AssetIdentifier } from '../types'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { mockNetworkState } from '../../../../util/test/network'; -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn().mockImplementation((selector) => selector()), -})); -jest.mock('../../../../selectors/networkController'); jest.mock( '../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork', () => 'AvatarNetwork', @@ -22,18 +14,33 @@ jest.mock('../../../hooks/useStyles', () => ({ useStyles: () => ({ styles: {} }), })); -describe('AssetPill', () => { - const selectChainIdMock = jest.mocked(selectChainId); - const selectTickerMock = jest.mocked(selectTicker); +const CHAIN_ID_MOCK = '0x123'; - beforeAll(() => { - selectChainIdMock.mockReturnValue('0x1'); - selectTickerMock.mockReturnValue('ETH'); - }); +const STATE_MOCK = { + engine: { + backgroundState: { + NetworkController: { + ...mockNetworkState({ + chainId: CHAIN_ID_MOCK, + }), + }, + }, + }, +}; +describe('AssetPill', () => { it('renders correctly for native assets', () => { - const asset = { type: AssetType.Native } as AssetIdentifier; - const { getByText, getByTestId } = render(); + const asset = { + type: AssetType.Native, + chainId: CHAIN_ID_MOCK, + } as AssetIdentifier; + + const { getByText, getByTestId } = renderWithProvider( + , + { + state: STATE_MOCK, + }, + ); expect( getByTestId('simulation-details-asset-pill-avatar-network'), @@ -45,8 +52,12 @@ describe('AssetPill', () => { const asset = { type: AssetType.ERC20, address: '0xabc123', + chainId: CHAIN_ID_MOCK, } as AssetIdentifier; - const { getByTestId } = render(); + + const { getByTestId } = renderWithProvider(, { + state: STATE_MOCK, + }); expect(getByTestId('simulation-details-asset-pill-name')).toBeTruthy(); }); diff --git a/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx b/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx index 3399ed8b0d6..13e829b3795 100644 --- a/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx +++ b/app/components/UI/SimulationDetails/AssetPill/AssetPill.tsx @@ -9,16 +9,13 @@ import Text, { } from '../../../../component-library/components/Texts/Text'; import AvatarNetwork from '../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork'; import { AvatarSize } from '../../../../component-library/components/Avatars/Avatar/Avatar.types'; -import { - selectChainId, - selectTicker, -} from '../../../../selectors/networkController'; import { NetworkList } from '../../../../util/networks'; import { useStyles } from '../../../hooks/useStyles'; import Name from '../../Name/Name'; import { NameType } from '../../Name/Name.types'; import { AssetIdentifier, AssetType } from '../types'; import styleSheet from './AssetPill.styles'; +import { selectNetworkConfigurations } from '../../../../selectors/networkController'; interface AssetPillProperties extends ViewProps { asset: AssetIdentifier; @@ -35,21 +32,26 @@ const getNetworkImage = (chainId: Hex) => { return network?.imageSource || null; }; -const NativeAssetPill: React.FC = () => { +const NativeAssetPill: React.FC = ({ asset }) => { const { styles } = useStyles(styleSheet, {}); - const ticker = useSelector(selectTicker); - const chainId = useSelector(selectChainId); - const imageSource = getNetworkImage(chainId); + const imageSource = getNetworkImage(asset.chainId); + + const networkConfigurationsByChainId = useSelector( + selectNetworkConfigurations, + ); + + const { nativeCurrency } = + networkConfigurationsByChainId[asset.chainId] || {}; return ( - {ticker} + {nativeCurrency} ); }; @@ -57,20 +59,17 @@ const NativeAssetPill: React.FC = () => { const AssetPill: React.FC = ({ asset }) => { const { styles } = useStyles(styleSheet, {}); - // TODO: Remove global network selector usage once simulations refactored. - const chainId = useSelector(selectChainId); - return ( {asset.type === AssetType.Native ? ( - + ) : ( )} diff --git a/app/components/UI/SimulationDetails/BalanceChangeList/BalanceChangeList.test.tsx b/app/components/UI/SimulationDetails/BalanceChangeList/BalanceChangeList.test.tsx index dacf4df7de4..70ef9cb1116 100644 --- a/app/components/UI/SimulationDetails/BalanceChangeList/BalanceChangeList.test.tsx +++ b/app/components/UI/SimulationDetails/BalanceChangeList/BalanceChangeList.test.tsx @@ -14,11 +14,14 @@ jest.mock('../FiatDisplay/FiatDisplay', () => ({ TotalFiatDisplay: 'TotalFiatDisplay', })); +const CHAIN_ID_MOCK = '0x123'; + const balanceChangesMock = [ { asset: { type: 'ERC20', address: '0xabc123', + chainId: CHAIN_ID_MOCK, }, amount: new BigNumber(100), fiatAmount: 100, @@ -68,6 +71,7 @@ describe('BalanceChangeList', () => { asset: { type: 'ERC20', address: '0xabc123', + chainId: CHAIN_ID_MOCK, }, amount: new BigNumber(100), fiatAmount: 100, diff --git a/app/components/UI/SimulationDetails/BalanceChangeRow/BalanceChangeRow.test.tsx b/app/components/UI/SimulationDetails/BalanceChangeRow/BalanceChangeRow.test.tsx index ede8373ed23..3d95d368876 100644 --- a/app/components/UI/SimulationDetails/BalanceChangeRow/BalanceChangeRow.test.tsx +++ b/app/components/UI/SimulationDetails/BalanceChangeRow/BalanceChangeRow.test.tsx @@ -5,22 +5,24 @@ import { BigNumber } from 'bignumber.js'; import BalanceChangeRow from './BalanceChangeRow'; import { AssetType, BalanceChange } from '../types'; +jest.mock('../AmountPill/AmountPill', () => 'AmountPill'); +jest.mock('../AssetPill/AssetPill', () => 'AssetPill'); jest.mock('../FiatDisplay/FiatDisplay', () => ({ IndividualFiatDisplay: 'IndividualFiatDisplay', })); +const CHAIN_ID_MOCK = '0x123'; + const balanceChangeMock: BalanceChange = { asset: { type: AssetType.ERC20, address: '0xabc123', + chainId: CHAIN_ID_MOCK, }, amount: new BigNumber(100), fiatAmount: 0, } as BalanceChange; -jest.mock('../AmountPill/AmountPill', () => 'AmountPill'); -jest.mock('../AssetPill/AssetPill', () => 'AssetPill'); - describe('BalanceChangeList', () => { it('renders a balance change row', () => { const { getByText, getByTestId } = render( diff --git a/app/components/UI/SimulationDetails/SimulationDetails.stories.tsx b/app/components/UI/SimulationDetails/SimulationDetails.stories.tsx index b5d8994e194..97a826f55f6 100644 --- a/app/components/UI/SimulationDetails/SimulationDetails.stories.tsx +++ b/app/components/UI/SimulationDetails/SimulationDetails.stories.tsx @@ -9,6 +9,8 @@ import { SimulationErrorCode, SimulationTokenStandard, CHAIN_IDS, + TransactionMeta, + SimulationData, } from '@metamask/transaction-controller'; import { @@ -145,8 +147,20 @@ const meta: Meta = { }; export default meta; +function buildArgs({ + simulationData, +}: { + simulationData?: SimulationData; +}): Partial { + return { + transaction: { + simulationData, + } as TransactionMeta, + }; +} + export const MultipleTokens: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: { ...DUMMY_BALANCE_CHANGE, @@ -193,11 +207,11 @@ export const MultipleTokens: Story = { }, ], }, - }, + }), }; export const SendSmallAmount: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: { ...DUMMY_BALANCE_CHANGE, @@ -206,11 +220,11 @@ export const SendSmallAmount: Story = { }, tokenBalanceChanges: [], }, - }, + }), }; export const LongValuesAndNames: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: { ...DUMMY_BALANCE_CHANGE, @@ -234,11 +248,11 @@ export const LongValuesAndNames: Story = { }, ], }, - }, + }), }; export const PolygonNativeAsset: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: { ...DUMMY_BALANCE_CHANGE, @@ -247,14 +261,14 @@ export const PolygonNativeAsset: Story = { }, tokenBalanceChanges: [], }, - }, + }), decorators: [ (story) => {story()}, ], }; export const ArbitrumNativeAsset: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: { ...DUMMY_BALANCE_CHANGE, @@ -263,14 +277,14 @@ export const ArbitrumNativeAsset: Story = { }, tokenBalanceChanges: [], }, - }, + }), decorators: [ (story) => {story()}, ], }; export const ReceiveOnly: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: { previousBalance: '0x2', @@ -280,11 +294,11 @@ export const ReceiveOnly: Story = { }, tokenBalanceChanges: [], }, - }, + }), }; export const SendOnly: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: { previousBalance: '0x1', @@ -294,40 +308,40 @@ export const SendOnly: Story = { }, tokenBalanceChanges: [], }, - }, + }), }; export const NoBalanceChanges: Story = { - args: { + args: buildArgs({ simulationData: { nativeBalanceChange: undefined, tokenBalanceChanges: [], }, - }, + }), }; export const Loading: Story = { - args: { + args: buildArgs({ simulationData: undefined, - }, + }), }; export const TransactionReverted: Story = { - args: { + args: buildArgs({ simulationData: { error: { code: SimulationErrorCode.Reverted }, nativeBalanceChange: undefined, tokenBalanceChanges: [], }, - }, + }), }; export const GenericError: Story = { - args: { + args: buildArgs({ simulationData: { error: {}, nativeBalanceChange: undefined, tokenBalanceChanges: [], }, - }, + }), }; diff --git a/app/components/UI/SimulationDetails/SimulationDetails.test.tsx b/app/components/UI/SimulationDetails/SimulationDetails.test.tsx index e4054925201..3b64269f9f3 100644 --- a/app/components/UI/SimulationDetails/SimulationDetails.test.tsx +++ b/app/components/UI/SimulationDetails/SimulationDetails.test.tsx @@ -6,6 +6,7 @@ import { SimulationData, SimulationErrorCode, SimulationTokenStandard, + TransactionMeta, } from '@metamask/transaction-controller'; import AnimatedSpinner from '../AnimatedSpinner'; @@ -21,6 +22,7 @@ const DUMMY_BALANCE_CHANGE = { previousBalance: '0xIGNORED' as Hex, newBalance: '0xIGNORED' as Hex, }; +const CHAIN_ID_MOCK = '0x123'; const mockTransactionId = '0x1234567890'; const simulationDataMock = { nativeBalanceChange: { @@ -81,8 +83,12 @@ describe('SimulationDetails', () => { render( , ); @@ -95,11 +101,15 @@ describe('SimulationDetails', () => { expect( render( , ).toJSON(), @@ -110,11 +120,15 @@ describe('SimulationDetails', () => { expect( render( , ).toJSON(), @@ -126,11 +140,15 @@ describe('SimulationDetails', () => { it('if transaction will be reverted', () => { const { getByText } = render( , ); @@ -141,11 +159,15 @@ describe('SimulationDetails', () => { it('if simulation is failed', () => { const { getByText } = render( , ); @@ -159,8 +181,12 @@ describe('SimulationDetails', () => { it('renders if no balance change', () => { const { getByText } = render( , ); @@ -175,7 +201,7 @@ describe('SimulationDetails', () => { { amount: new BigNumber('0x1', 16).times(-1), fiatAmount: 10, - asset: { type: AssetType.Native }, + asset: { type: AssetType.Native, chainId: CHAIN_ID_MOCK }, }, { amount: new BigNumber('0x123456', 16).times(1), @@ -184,6 +210,7 @@ describe('SimulationDetails', () => { address: FIRST_PARTY_CONTRACT_ADDRESS_1_MOCK, tokenId: undefined, type: AssetType.ERC20, + chainId: CHAIN_ID_MOCK, }, }, { @@ -193,6 +220,7 @@ describe('SimulationDetails', () => { address: FIRST_PARTY_CONTRACT_ADDRESS_2_MOCK, tokenId: undefined, type: AssetType.ERC20, + chainId: CHAIN_ID_MOCK, }, }, ], @@ -200,8 +228,12 @@ describe('SimulationDetails', () => { const { getByTestId } = render( , ); diff --git a/app/components/UI/SimulationDetails/SimulationDetails.tsx b/app/components/UI/SimulationDetails/SimulationDetails.tsx index 474bafc3ebe..04c8406b5e3 100644 --- a/app/components/UI/SimulationDetails/SimulationDetails.tsx +++ b/app/components/UI/SimulationDetails/SimulationDetails.tsx @@ -2,9 +2,9 @@ import React, { useState } from 'react'; import { View, Pressable } from 'react-native'; import { - SimulationData, SimulationErrorCode, SimulationError, + TransactionMeta, } from '@metamask/transaction-controller'; import { strings } from '../../../../locales/i18n'; @@ -25,8 +25,7 @@ import styleSheet from './SimulationDetails.styles'; import { useSimulationMetrics } from './useSimulationMetrics'; export interface SimulationDetailsProps { - simulationData?: SimulationData; - transactionId: string; + transaction: TransactionMeta; enableMetrics: boolean; } @@ -140,12 +139,12 @@ const SimulationDetailsLayout: React.FC<{ * @returns The simulation details. */ export const SimulationDetails: React.FC = ({ - simulationData, + transaction, enableMetrics = false, - transactionId, }: SimulationDetailsProps) => { const { styles } = useStyles(styleSheet, {}); - const balanceChangesResult = useBalanceChanges(simulationData); + const { chainId, id: transactionId, simulationData } = transaction; + const balanceChangesResult = useBalanceChanges({ chainId, simulationData }); const loading = !simulationData || balanceChangesResult.pending; useSimulationMetrics({ diff --git a/app/components/UI/SimulationDetails/types.ts b/app/components/UI/SimulationDetails/types.ts index 9b87ad84af3..93468745472 100644 --- a/app/components/UI/SimulationDetails/types.ts +++ b/app/components/UI/SimulationDetails/types.ts @@ -8,10 +8,6 @@ export enum AssetType { ERC1155 = 'ERC1155', } -export const NATIVE_ASSET_IDENTIFIER: NativeAssetIdentifier = { - type: AssetType.Native, -}; - /** * Describes an amount of fiat. */ @@ -23,18 +19,20 @@ export type FiatAmount = FiatAmountAvailable | typeof FIAT_UNAVAILABLE; * Identifies the native asset of a chain. */ export interface NativeAssetIdentifier { - type: AssetType.Native; address?: undefined; + chainId: Hex; tokenId?: undefined; + type: AssetType.Native; } /** * Uniquely identifies a token asset on a chain. */ export interface TokenAssetIdentifier { - type: Exclude; address: Hex; + chainId: Hex; tokenId?: Hex; + type: Exclude; } export type AssetIdentifier = Readonly< diff --git a/app/components/UI/SimulationDetails/useBalanceChanges.test.ts b/app/components/UI/SimulationDetails/useBalanceChanges.test.ts index 720c09c5397..16fe3af39e9 100644 --- a/app/components/UI/SimulationDetails/useBalanceChanges.test.ts +++ b/app/components/UI/SimulationDetails/useBalanceChanges.test.ts @@ -61,6 +61,8 @@ const DIFFERENCE_1_MOCK: Hex = '0x11'; const DIFFERENCE_2_MOCK: Hex = '0x2'; const DIFFERENCE_ETH_MOCK: Hex = '0x1234567890123456789'; +const CHAIN_ID_MOCK = '0x123'; + const dummyBalanceChange = { previousBalance: '0xIGNORE' as Hex, newBalance: '0xIGNORE' as Hex, @@ -98,7 +100,10 @@ describe('useBalanceChanges', () => { describe('pending states', () => { it('returns pending=true if no simulation data', async () => { const { result, waitForNextUpdate } = renderHook(() => - useBalanceChanges(undefined), + useBalanceChanges({ + chainId: CHAIN_ID_MOCK, + simulationData: undefined, + }), ); expect(result.current).toEqual({ pending: true, value: [] }); await waitForNextUpdate(); @@ -119,7 +124,10 @@ describe('useBalanceChanges', () => { ], }; const { result, unmount, waitForNextUpdate } = renderHook(() => - useBalanceChanges(simulationData), + useBalanceChanges({ + chainId: CHAIN_ID_MOCK, + simulationData, + }), ); await waitForNextUpdate(); @@ -143,7 +151,10 @@ describe('useBalanceChanges', () => { ], }; const { result, unmount } = renderHook(() => - useBalanceChanges(simulationData), + useBalanceChanges({ + chainId: CHAIN_ID_MOCK, + simulationData, + }), ); expect(result.current).toEqual({ pending: true, value: [] }); @@ -159,7 +170,12 @@ describe('useBalanceChanges', () => { nativeBalanceChange: undefined, tokenBalanceChanges, }; - return renderHook(() => useBalanceChanges(simulationData)); + return renderHook(() => + useBalanceChanges({ + chainId: CHAIN_ID_MOCK, + simulationData, + }), + ); }; it('maps token balance changes correctly', async () => { @@ -182,6 +198,7 @@ describe('useBalanceChanges', () => { address: ERC20_TOKEN_ADDRESS_1_MOCK, type: AssetType.ERC20, tokenId: undefined, + chainId: CHAIN_ID_MOCK, }, amount: new BigNumber('-0.017'), fiatAmount: -0.0255, @@ -238,6 +255,7 @@ describe('useBalanceChanges', () => { address: NFT_TOKEN_ADDRESS_MOCK, type: AssetType.ERC721, tokenId: TOKEN_ID_1_MOCK, + chainId: CHAIN_ID_MOCK, }, amount: new BigNumber('-1'), fiatAmount: FIAT_UNAVAILABLE, @@ -305,7 +323,12 @@ describe('useBalanceChanges', () => { nativeBalanceChange, tokenBalanceChanges: [], }; - return renderHook(() => useBalanceChanges(simulationData)); + return renderHook(() => + useBalanceChanges({ + chainId: CHAIN_ID_MOCK, + simulationData, + }), + ); }; it('maps native balance change correctly', async () => { @@ -322,6 +345,7 @@ describe('useBalanceChanges', () => { { asset: { type: AssetType.Native, + chainId: CHAIN_ID_MOCK, }, amount: new BigNumber('-5373.003641998677469065'), fiatAmount: Number('-16119.010925996032'), @@ -367,7 +391,10 @@ describe('useBalanceChanges', () => { ], }; const { result, waitForNextUpdate } = renderHook(() => - useBalanceChanges(simulationData), + useBalanceChanges({ + chainId: CHAIN_ID_MOCK, + simulationData, + }), ); await waitForNextUpdate(); @@ -376,6 +403,7 @@ describe('useBalanceChanges', () => { expect(changes).toHaveLength(2); expect(changes[0].asset).toEqual({ type: AssetType.Native, + chainId: CHAIN_ID_MOCK, }); expect(changes[0].amount).toEqual( new BigNumber('-5373.003641998677469065'), @@ -384,6 +412,7 @@ describe('useBalanceChanges', () => { expect(changes[1].asset).toEqual({ address: ERC20_TOKEN_ADDRESS_1_MOCK, type: AssetType.ERC20, + chainId: CHAIN_ID_MOCK, }); expect(changes[1].amount).toEqual(new BigNumber('0.002')); }); diff --git a/app/components/UI/SimulationDetails/useBalanceChanges.ts b/app/components/UI/SimulationDetails/useBalanceChanges.ts index faa7aeb14bf..e383058c0c4 100644 --- a/app/components/UI/SimulationDetails/useBalanceChanges.ts +++ b/app/components/UI/SimulationDetails/useBalanceChanges.ts @@ -15,17 +15,16 @@ import { import { BalanceChange, - NATIVE_ASSET_IDENTIFIER, TokenAssetIdentifier, AssetType, FIAT_UNAVAILABLE, + NativeAssetIdentifier, } from './types'; import { getTokenDetails } from '../../../util/address'; import { selectConversionRate, selectCurrentCurrency, } from '../../../selectors/currencyRateController'; -import { selectChainId } from '../../../selectors/networkController'; import { useAsyncResultOrThrow } from '../../hooks/useAsyncResult'; const NATIVE_DECIMALS = 18; @@ -132,14 +131,20 @@ async function fetchTokenFiatRates( function getNativeBalanceChange( nativeBalanceChange: SimulationBalanceChange | undefined, nativeFiatRate: number, + chainId: Hex, ): BalanceChange | undefined { if (!nativeBalanceChange) { return undefined; } - const asset = NATIVE_ASSET_IDENTIFIER; - const amount = getAssetAmount(nativeBalanceChange, NATIVE_DECIMALS); + const asset: NativeAssetIdentifier = { + type: AssetType.Native, + chainId, + }; + + const amount = getAssetAmount(nativeBalanceChange, NATIVE_DECIMALS); const fiatAmount = amount.times(nativeFiatRate).toNumber(); + return { asset, amount, fiatAmount }; } @@ -148,12 +153,14 @@ function getTokenBalanceChanges( tokenBalanceChanges: SimulationTokenBalanceChange[], erc20Decimals: Record, erc20FiatRates: Partial>, + chainId: Hex, ): BalanceChange[] { return tokenBalanceChanges.map((tokenBc) => { const asset: TokenAssetIdentifier = { type: convertStandard(tokenBc.standard), address: tokenBc.address.toLowerCase() as Hex, tokenId: tokenBc.id, + chainId, }; const decimals = @@ -172,12 +179,15 @@ function getTokenBalanceChanges( } // Compiles a list of balance changes from simulation data -export default function useBalanceChanges( - simulationData: SimulationData | undefined, -): { pending: boolean; value: BalanceChange[] } { +export default function useBalanceChanges({ + chainId, + simulationData, +}: { + chainId: Hex; + simulationData?: SimulationData; +}): { pending: boolean; value: BalanceChange[] } { const nativeFiatRate = useSelector(selectConversionRate) as number; const fiatCurrency = useSelector(selectCurrentCurrency); - const chainId = useSelector(selectChainId); const { nativeBalanceChange, tokenBalanceChanges = [] } = simulationData ?? {}; @@ -200,18 +210,21 @@ export default function useBalanceChanges( [JSON.stringify(erc20TokenAddresses), chainId, fiatCurrency], ); - if (erc20Decimals.pending || erc20FiatRates.pending || !simulationData ) { + if (erc20Decimals.pending || erc20FiatRates.pending || !simulationData) { return { pending: true, value: [] }; } const nativeChange = getNativeBalanceChange( nativeBalanceChange, nativeFiatRate, + chainId, ); + const tokenChanges = getTokenBalanceChanges( tokenBalanceChanges, erc20Decimals.value, erc20FiatRates.value, + chainId, ); const balanceChanges: BalanceChange[] = [ diff --git a/app/components/UI/SimulationDetails/useSimulationMetrics.test.ts b/app/components/UI/SimulationDetails/useSimulationMetrics.test.ts index cf9eacf54a6..920aded4148 100644 --- a/app/components/UI/SimulationDetails/useSimulationMetrics.test.ts +++ b/app/components/UI/SimulationDetails/useSimulationMetrics.test.ts @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { - CHAIN_IDS, SimulationData, SimulationErrorCode, } from '@metamask/transaction-controller'; @@ -23,7 +22,6 @@ import { useSimulationMetrics, } from './useSimulationMetrics'; import useLoadingTime from './useLoadingTime'; -import { selectChainId } from '../../../selectors/networkController'; import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder'; jest.mock('react-redux', () => ({ @@ -89,7 +87,6 @@ describe('useSimulationMetrics', () => { const useDisplayNamesMock = jest.mocked(useDisplayNames); const useLoadingTimeMock = jest.mocked(useLoadingTime); const setLoadingCompleteMock = jest.fn(); - const selectChainIdMock = jest.mocked(selectChainId); function expectUpdateTransactionMetricsCalled( { @@ -141,7 +138,6 @@ describe('useSimulationMetrics', () => { loadingTime: LOADING_TIME_MOCK, setLoadingComplete: setLoadingCompleteMock, }); - selectChainIdMock.mockReturnValue(CHAIN_IDS.MAINNET); }); describe('updates transaction simulation metrics', () => { diff --git a/app/components/UI/SimulationDetails/useSimulationMetrics.ts b/app/components/UI/SimulationDetails/useSimulationMetrics.ts index ed7e7f2f4c9..0d16524d9ff 100644 --- a/app/components/UI/SimulationDetails/useSimulationMetrics.ts +++ b/app/components/UI/SimulationDetails/useSimulationMetrics.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { SimulationData, SimulationErrorCode, @@ -17,7 +17,6 @@ import { NameType } from '../../UI/Name/Name.types'; import useLoadingTime from './useLoadingTime'; import { calculateTotalFiat } from './FiatDisplay/FiatDisplay'; import { BalanceChange } from './types'; -import { selectChainId } from '../../../selectors/networkController'; export interface UseSimulationMetricsProps { balanceChanges: BalanceChange[]; @@ -50,9 +49,6 @@ export function useSimulationMetrics({ const { loadingTime, setLoadingComplete } = useLoadingTime(); const dispatch = useDispatch(); - // TODO: Remove global network selector usage once simulations refactored. - const chainId = useSelector(selectChainId); - if (!loading) { setLoadingComplete(); } @@ -62,7 +58,7 @@ export function useSimulationMetrics({ value: asset.address ?? '', type: NameType.EthereumAddress, preferContractSymbol: true, - variation: chainId, + variation: asset.chainId, }), ); diff --git a/app/components/Views/confirmations/SendFlow/Confirm/index.js b/app/components/Views/confirmations/SendFlow/Confirm/index.js index 8c06868102f..7aa29a398e1 100644 --- a/app/components/Views/confirmations/SendFlow/Confirm/index.js +++ b/app/components/Views/confirmations/SendFlow/Confirm/index.js @@ -264,16 +264,14 @@ class Confirm extends PureComponent { * Boolean that indicates if smart transaction should be used */ shouldUseSmartTransaction: PropTypes.bool, - /** * Object containing transaction metrics by id */ transactionMetricsById: PropTypes.object, - /** - * Object containing the transaction simulation data + * Transaction metadata from the transaction controller */ - transactionSimulationData: PropTypes.object, + transactionMetadata: PropTypes.object, /** * Update transaction metrics */ @@ -915,9 +913,12 @@ class Confirm extends PureComponent { resetTransaction, gasEstimateType, shouldUseSmartTransaction, - transactionSimulationData: { isUpdatedAfterSecurityCheck } = {}, + transactionMetadata, } = this.props; + const transactionSimulationData = transactionMetadata?.simulationData; + const { isUpdatedAfterSecurityCheck } = transactionSimulationData ?? {}; + const { legacyGasTransaction, transactionConfirmed, @@ -1326,7 +1327,7 @@ class Confirm extends PureComponent { gasEstimateType, isNativeTokenBuySupported, shouldUseSmartTransaction, - transactionSimulationData, + transactionMetadata, transactionState, useTransactionSimulations, } = this.props; @@ -1359,6 +1360,7 @@ class Confirm extends PureComponent { const isLedgerAccount = isHardwareAccount(fromSelectedAddress, [ ExtendedKeyringTypes.ledger, ]); + const transactionSimulationData = transactionMetadata?.simulationData; const isTestNetwork = isTestNet(chainId); @@ -1430,9 +1432,8 @@ class Confirm extends PureComponent { {useTransactionSimulations && transactionState?.id && ( )} @@ -1575,8 +1576,8 @@ const mapStateToProps = (state) => ({ ), shouldUseSmartTransaction: selectShouldUseSmartTransaction(state), transactionMetricsById: selectTransactionMetrics(state), - transactionSimulationData: - selectCurrentTransactionMetadata(state)?.simulationData, + transactionMetadata: + selectCurrentTransactionMetadata(state), useTransactionSimulations: selectUseTransactionSimulations(state), securityAlertResponse: selectCurrentTransactionSecurityAlertResponse(state), }); diff --git a/app/components/Views/confirmations/components/TransactionReview/index.js b/app/components/Views/confirmations/components/TransactionReview/index.js index c548b692d6c..6c43963b376 100644 --- a/app/components/Views/confirmations/components/TransactionReview/index.js +++ b/app/components/Views/confirmations/components/TransactionReview/index.js @@ -261,10 +261,6 @@ class TransactionReview extends PureComponent { * Boolean that indicates if smart transaction should be used */ shouldUseSmartTransaction: PropTypes.bool, - /** - * Transaction simulation data - */ - transactionSimulationData: PropTypes.object, /** * Boolean that indicates if transaction simulations should be enabled */ @@ -523,10 +519,12 @@ class TransactionReview extends PureComponent { transaction, transaction: { to, origin, from, ensRecipient, id: transactionId }, error, - transactionSimulationData, + transactionMetadata, useTransactionSimulations, } = this.props; + const transactionSimulationData = transactionMetadata?.simulationData; + const { actionKey, assetAmount, @@ -619,9 +617,8 @@ class TransactionReview extends PureComponent { {useTransactionSimulations && transactionSimulationData && ( )} @@ -720,8 +717,6 @@ const mapStateToProps = (state) => ({ primaryCurrency: state.settings.primaryCurrency, tokenList: selectTokenList(state), shouldUseSmartTransaction: selectShouldUseSmartTransaction(state), - transactionSimulationData: - selectCurrentTransactionMetadata(state)?.simulationData, useTransactionSimulations: selectUseTransactionSimulations(state), securityAlertResponse: selectCurrentTransactionSecurityAlertResponse(state), transactionMetadata: selectCurrentTransactionMetadata(state), From c7eee9c2f8d88da24914e50c8d1e6cbf1575e580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n?= Date: Fri, 20 Dec 2024 13:55:39 +0100 Subject: [PATCH 17/19] test: erc 1155 batch transfer (#12800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR is within the scope of the Quality Quest. We're including an e2e test case to check the batch transfer for ERC1155 in test dApp. **Steps:** Given I am on the test dapp When I tap on the batch transfer button in the ERC-1155 section Then the contract bottom sheet should appear When I submit the transaction Then the transaction should appear in the transaction history ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../Browser/ContractApprovalBottomSheet.js | 8 ++ e2e/pages/Browser/TestDApp.js | 11 +++ .../ContractApprovalBottomSheet.selectors.js | 1 + e2e/selectors/Browser/TestDapp.selectors.js | 1 + .../batch-transfer-erc1155.spec.js | 73 +++++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 e2e/specs/confirmations/batch-transfer-erc1155.spec.js diff --git a/e2e/pages/Browser/ContractApprovalBottomSheet.js b/e2e/pages/Browser/ContractApprovalBottomSheet.js index e99985f84c4..489fe6a83c0 100644 --- a/e2e/pages/Browser/ContractApprovalBottomSheet.js +++ b/e2e/pages/Browser/ContractApprovalBottomSheet.js @@ -48,6 +48,10 @@ class ContractApprovalBottomSheet { ); } + get confirmButton() { + return Matchers.getElementByText(ContractApprovalBottomSheetSelectorsText.CONFIRM); + } + async tapAddNickName() { await Gestures.waitAndTap(this.addNickName); } @@ -63,6 +67,10 @@ class ContractApprovalBottomSheet { await Gestures.waitAndTap(this.approveButton); } + async tapConfirmButton() { + await Gestures.waitAndTap(this.confirmButton); + } + async tapToCopyContractAddress() { await Gestures.waitAndTap(this.contractAddress); } diff --git a/e2e/pages/Browser/TestDApp.js b/e2e/pages/Browser/TestDApp.js index ab411415955..a9bc65d01d6 100644 --- a/e2e/pages/Browser/TestDApp.js +++ b/e2e/pages/Browser/TestDApp.js @@ -119,6 +119,13 @@ class TestDApp { ); } + get erc1155BatchTransferButton() { + return Matchers.getElementByWebID( + BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, + TestDappSelectorsWebIDs.BATCH_TRANSFER_ERC1155_BUTTON_ID, + ); + } + async connect() { await this.tapButton(this.DappConnectButton); } @@ -181,6 +188,10 @@ class TestDApp { await Gestures.tap(this.approveButtonText, 0); } + async tapERC1155BatchTransferButton() { + await this.tapButton(this.erc1155BatchTransferButton); + } + async tapButton(elementId) { await Gestures.scrollToWebViewPort(elementId); await Gestures.tapWebElement(elementId); diff --git a/e2e/selectors/Browser/ContractApprovalBottomSheet.selectors.js b/e2e/selectors/Browser/ContractApprovalBottomSheet.selectors.js index 39ee13229f7..d469fbfed79 100644 --- a/e2e/selectors/Browser/ContractApprovalBottomSheet.selectors.js +++ b/e2e/selectors/Browser/ContractApprovalBottomSheet.selectors.js @@ -6,6 +6,7 @@ export const ContractApprovalBottomSheetSelectorsText = { APPROVE: enContent.transactions.tx_review_approve, REJECT: enContent.transaction.reject, NEXT: enContent.transaction.next, + CONFIRM: enContent.transaction.confirm, }; export const ContractApprovalBottomSheetSelectorsIDs = { diff --git a/e2e/selectors/Browser/TestDapp.selectors.js b/e2e/selectors/Browser/TestDapp.selectors.js index 4725a436c92..e0995aa10f0 100644 --- a/e2e/selectors/Browser/TestDapp.selectors.js +++ b/e2e/selectors/Browser/TestDapp.selectors.js @@ -13,4 +13,5 @@ export const TestDappSelectorsWebIDs = { SIGN_TYPE_DATA_V4: 'signTypedDataV4', ETHEREUM_SIGN: 'siwe', ADD_TOKENS_TO_WALLET_BUTTON: 'watchAssets', + BATCH_TRANSFER_ERC1155_BUTTON_ID: 'batchTransferFromButton', }; diff --git a/e2e/specs/confirmations/batch-transfer-erc1155.spec.js b/e2e/specs/confirmations/batch-transfer-erc1155.spec.js new file mode 100644 index 00000000000..b7ed8494d16 --- /dev/null +++ b/e2e/specs/confirmations/batch-transfer-erc1155.spec.js @@ -0,0 +1,73 @@ +'use strict'; + +import { SmokeConfirmations } from '../../tags'; +import TestHelpers from '../../helpers'; +import { loginToApp } from '../../viewHelper'; + +import TabBarComponent from '../../pages/wallet/TabBarComponent'; +import TestDApp from '../../pages/Browser/TestDApp'; +import FixtureBuilder from '../../fixtures/fixture-builder'; +import { + withFixtures, + defaultGanacheOptions, +} from '../../fixtures/fixture-helper'; +import { SMART_CONTRACTS } from '../../../app/util/test/smart-contracts'; +import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors'; +import Assertions from '../../utils/Assertions'; +import { ContractApprovalBottomSheetSelectorsText } from '../../selectors/Browser/ContractApprovalBottomSheet.selectors'; +import ContractApprovalBottomSheet from '../../pages/Browser/ContractApprovalBottomSheet'; + +describe(SmokeConfirmations('ERC1155 token'), () => { + const ERC1155_CONTRACT = SMART_CONTRACTS.ERC1155; + + beforeAll(async () => { + await TestHelpers.reverseServerPort(); + }); + + it('batch transfer ERC1155 tokens', async () => { + await withFixtures( + { + dapp: true, + fixture: new FixtureBuilder() + .withGanacheNetwork() + .withPermissionControllerConnectedToTestDapp() + .build(), + restartDevice: true, + ganacheOptions: defaultGanacheOptions, + smartContract: ERC1155_CONTRACT, + }, + async ({ contractRegistry }) => { + const erc1155Address = await contractRegistry.getContractAddress( + ERC1155_CONTRACT, + ); + await loginToApp(); + + // Navigate to the browser screen + await TabBarComponent.tapBrowser(); + await TestDApp.navigateToTestDappWithContract({ + contractAddress: erc1155Address, + }); + + // Send batch transfer for ERC1155 tokens + await TestDApp.tapERC1155BatchTransferButton(); + await Assertions.checkIfTextIsDisplayed( + ContractApprovalBottomSheetSelectorsText.CONFIRM, + ); + + // Tap confirm button + await ContractApprovalBottomSheet.tapConfirmButton(); + + // Navigate to the activity screen + await TabBarComponent.tapActivity(); + + // Assert that the ERC1155 activity is an smart contract interaction and it is confirmed + await Assertions.checkIfTextIsDisplayed( + ActivitiesViewSelectorsText.SMART_CONTRACT_INTERACTION, + ); + await Assertions.checkIfTextIsDisplayed( + ActivitiesViewSelectorsText.CONFIRM_TEXT, + ); + }, + ); + }); +}); From 89b61d359a4193c5b6916dcec3161e826591b183 Mon Sep 17 00:00:00 2001 From: EtherWizard33 <165834542+EtherWizard33@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:07:19 +0300 Subject: [PATCH 18/19] chore(tests): move multichain assets test so it runs as part of the assets bitrise workflow (#12807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This moves the mutlichain assets test to the assets workflow. The multichain workflow still has 6 more tests to add and already reached over 20 minutes run, which is close to or over the expected length, while assets workflow was using only 7min of it's workflow. ## **Related issues** Fixes: ## **Manual testing steps** ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../assets => assets/multichain}/asset-list.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename e2e/specs/{multichain/assets => assets/multichain}/asset-list.spec.js (98%) diff --git a/e2e/specs/multichain/assets/asset-list.spec.js b/e2e/specs/assets/multichain/asset-list.spec.js similarity index 98% rename from e2e/specs/multichain/assets/asset-list.spec.js rename to e2e/specs/assets/multichain/asset-list.spec.js index d33360a7f6f..042723d44d9 100644 --- a/e2e/specs/multichain/assets/asset-list.spec.js +++ b/e2e/specs/assets/multichain/asset-list.spec.js @@ -1,6 +1,6 @@ // 'persists the preferred asset list preference when changing networks' -import { SmokeMultiChain } from '../../../tags'; +import { SmokeAssets } from '../../../tags'; import WalletView from '../../../pages/wallet/WalletView'; import FixtureBuilder from '../../../fixtures/fixture-builder'; import { @@ -26,7 +26,7 @@ const ETHEREUM_NAME = 'Ethereum'; const AVAX_NAME = 'AVAX'; const BNB_NAME = 'BNB'; -describe(SmokeMultiChain('Import Tokens'), () => { +describe(SmokeAssets('Import Tokens'), () => { beforeAll(async () => { await TestHelpers.reverseServerPort(); const fixture = new FixtureBuilder().withPopularNetworks().build(); From bea70f16d3bc1d7d78049ce5a21433735cc395b3 Mon Sep 17 00:00:00 2001 From: Curtis David Date: Fri, 20 Dec 2024 10:15:39 -0500 Subject: [PATCH 19/19] test: Add ramps URL scheme deeplinking e2e (#12747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** The purpose of this PR is to add e2e coverage for the URL schema deeplinks in the ramps flow. The goal here is to ensure that we properly test the URL scheme deep linking in these flows. ### Sell Deep link ``` Scenario 1 Given I open a sell ETH deeplink for mainnet Then the app should launch with the ramps build quotes page And the token I want to sell is displayed correctly And the amount i want to send is displayed correctly Scenario 2 Given I open a sell deeplink on an unsupported network Then the app should launch with the ramps build quotes page And I am prompted to add the network When I add the network Then the Quotes page should be displayed ``` ### Buy deeplink Flow ``` Scenario 3 Given i deeplink to the buy eth flow on mainnet Then the app should launch with the ramps build quotes page And the token I want to sell is displayed correctly And the amount i want to send is displayed correctly Scenario 4 Given i deeplink to the buy eth flow on a popular network Then the app should launch with the ramps build quotes page And the token I want to sell is displayed correctly And the amount i want to send is displayed correctly ``` ## **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 Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- e2e/fixtures/fixture-builder.js | 65 ++++++++++---- e2e/pages/Ramps/BuildQuoteView.js | 4 + e2e/pages/Ramps/TokenSelectBottomSheet.js | 12 +++ e2e/resources/blacklistURLs.json | 6 +- e2e/specs/networks/add-custom-rpc.spec.js | 2 +- ...-buy-flow-with-unsupported-network.spec.js | 57 ++++++++++++ e2e/specs/ramps/deeplink-to-buy-flow.spec.js | 55 ++++++++++++ e2e/specs/ramps/deeplink-to-sell-flow.spec.js | 89 +++++++++++++++++++ 8 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 e2e/pages/Ramps/TokenSelectBottomSheet.js create mode 100644 e2e/specs/ramps/deeplink-to-buy-flow-with-unsupported-network.spec.js create mode 100644 e2e/specs/ramps/deeplink-to-buy-flow.spec.js create mode 100644 e2e/specs/ramps/deeplink-to-sell-flow.spec.js diff --git a/e2e/fixtures/fixture-builder.js b/e2e/fixtures/fixture-builder.js index 024f90a6c1a..01da16759e2 100644 --- a/e2e/fixtures/fixture-builder.js +++ b/e2e/fixtures/fixture-builder.js @@ -5,7 +5,8 @@ import { merge } from 'lodash'; import { CustomNetworks, PopularNetworksList } from '../resources/networks.e2e'; import { CHAIN_IDS } from '@metamask/transaction-controller'; -export const DEFAULT_FIXTURE_ACCOUNT = '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3'; +export const DEFAULT_FIXTURE_ACCOUNT = + '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3'; const DAPP_URL = 'localhost'; @@ -627,6 +628,7 @@ class FixtureBuilder { selectedRegionAgg: null, selectedPaymentMethodAgg: null, getStartedAgg: false, + getStartedSell: false, authenticationUrls: [], activationKeys: [], }, @@ -696,8 +698,9 @@ class FixtureBuilder { const { providerConfig } = data; // Generate a unique key for the new network client ID - const newNetworkClientId = `networkClientId${Object.keys(networkController.networkConfigurationsByChainId).length + 1 - }`; + const newNetworkClientId = `networkClientId${ + Object.keys(networkController.networkConfigurationsByChainId).length + 1 + }`; // Define the network configuration const networkConfig = { @@ -776,6 +779,30 @@ class FixtureBuilder { return this; } + withRampsSelectedRegion(region = null) { + const defaultRegion = { + currencies: ['/currencies/fiat/xcd'], + emoji: '🇱🇨', + id: '/regions/lc', + name: 'Saint Lucia', + support: { buy: true, sell: true, recurringBuy: true }, + unsupported: false, + recommended: false, + detected: false, + }; + + // Use the provided region or fallback to the default + this.fixture.state.fiatOrders.selectedRegionAgg = region ?? defaultRegion; + return this; + } + withRampsSelectedPaymentMethod() { + const paymentType = '/payments/debit-credit-card'; + + // Use the provided region or fallback to the default + this.fixture.state.fiatOrders.selectedPaymentMethodAgg = paymentType; + return this; + } + /** * Adds chain switching permission for specific chains. * @param {string[]} chainIds - Array of chain IDs to permit (defaults to ['0x1']), other nexts like linea mainnet 0xe708 @@ -818,9 +845,10 @@ class FixtureBuilder { const fixtures = this.fixture.state.engine.backgroundState; // Generate a unique key for the new network client ID - const newNetworkClientId = `networkClientId${Object.keys(fixtures.NetworkController.networkConfigurationsByChainId) - .length + 1 - }`; + const newNetworkClientId = `networkClientId${ + Object.keys(fixtures.NetworkController.networkConfigurationsByChainId) + .length + 1 + }`; // Define the Ganache network configuration const ganacheNetworkConfig = { @@ -856,9 +884,10 @@ class FixtureBuilder { const sepoliaConfig = CustomNetworks.Sepolia.providerConfig; // Generate a unique key for the new network client ID - const newNetworkClientId = `networkClientId${Object.keys(fixtures.NetworkController.networkConfigurationsByChainId) - .length + 1 - }`; + const newNetworkClientId = `networkClientId${ + Object.keys(fixtures.NetworkController.networkConfigurationsByChainId) + .length + 1 + }`; // Define the Sepolia network configuration const sepoliaNetworkConfig = { @@ -908,8 +937,9 @@ class FixtureBuilder { } = network.providerConfig; // Generate a unique key for the new network client ID - const newNetworkClientId = `networkClientId${Object.keys(networkConfigurationsByChainId).length + 1 - }`; + const newNetworkClientId = `networkClientId${ + Object.keys(networkConfigurationsByChainId).length + 1 + }`; // Define the network configuration const networkConfig = { @@ -988,19 +1018,16 @@ class FixtureBuilder { allTokens: { [CHAIN_IDS.MAINNET]: { [DEFAULT_FIXTURE_ACCOUNT]: tokens, - } - } + }, + }, }); return this; } withIncomingTransactionPreferences(incomingTransactionPreferences) { - merge( - this.fixture.state.engine.backgroundState.PreferencesController, - { - showIncomingTransactions: incomingTransactionPreferences, - }, - ); + merge(this.fixture.state.engine.backgroundState.PreferencesController, { + showIncomingTransactions: incomingTransactionPreferences, + }); return this; } diff --git a/e2e/pages/Ramps/BuildQuoteView.js b/e2e/pages/Ramps/BuildQuoteView.js index 407abb8177d..e7bcd2f1ba1 100644 --- a/e2e/pages/Ramps/BuildQuoteView.js +++ b/e2e/pages/Ramps/BuildQuoteView.js @@ -22,6 +22,10 @@ class BuildQuoteView { async tapCancelButton() { await Gestures.waitAndTap(this.cancelButton); } + async tapDefaultToken(token) { + const tokenName = await Matchers.getElementByText(token); + await Gestures.waitAndTap(tokenName); + } } export default new BuildQuoteView(); diff --git a/e2e/pages/Ramps/TokenSelectBottomSheet.js b/e2e/pages/Ramps/TokenSelectBottomSheet.js new file mode 100644 index 00000000000..0625e6bcf0c --- /dev/null +++ b/e2e/pages/Ramps/TokenSelectBottomSheet.js @@ -0,0 +1,12 @@ +import Matchers from '../../utils/Matchers'; +import Gestures from '../../utils/Gestures'; + +class TokenSelectBottomSheet { + async tapTokenByName(token) { + const tokenName = await Matchers.getElementByText(token); + + await Gestures.waitAndTap(tokenName); + } +} + +export default new TokenSelectBottomSheet(); diff --git a/e2e/resources/blacklistURLs.json b/e2e/resources/blacklistURLs.json index e8b620c8ca8..f337d71278b 100644 --- a/e2e/resources/blacklistURLs.json +++ b/e2e/resources/blacklistURLs.json @@ -14,6 +14,10 @@ ".*phishing-detection.cx.metamask.io/.*", ".*eth.llamarpc.com/.*", ".*token-api.metaswap.codefi.network/.*", - ".*gas.api.cx.metamask.io/networks/*" + ".*gas.api.cx.metamask.io/networks/.*", + ".*clients3.google.com/generate_204.*", + ".*pulse.walletconnect.org/batch.*", + ".*accounts.api.cx.metamask.io/v2/accounts/.*", + ".*exp.host/--/api/v2/development-sessions/.*" ] } diff --git a/e2e/specs/networks/add-custom-rpc.spec.js b/e2e/specs/networks/add-custom-rpc.spec.js index cd8269aa2d2..d0d53ce26a5 100644 --- a/e2e/specs/networks/add-custom-rpc.spec.js +++ b/e2e/specs/networks/add-custom-rpc.spec.js @@ -20,7 +20,7 @@ import { CustomNetworks } from '../../resources/networks.e2e'; const fixtureServer = new FixtureServer(); -describe(SmokeCore('Custom RPC Tests'), () => { +describe('Custom RPC Tests', () => { beforeAll(async () => { await TestHelpers.reverseServerPort(); const fixture = new FixtureBuilder().build(); diff --git a/e2e/specs/ramps/deeplink-to-buy-flow-with-unsupported-network.spec.js b/e2e/specs/ramps/deeplink-to-buy-flow-with-unsupported-network.spec.js new file mode 100644 index 00000000000..a999cb28548 --- /dev/null +++ b/e2e/specs/ramps/deeplink-to-buy-flow-with-unsupported-network.spec.js @@ -0,0 +1,57 @@ +'use strict'; +import TestHelpers from '../../helpers'; + +import { loginToApp } from '../../viewHelper'; +import { withFixtures } from '../../fixtures/fixture-helper'; +import { SmokeCore } from '../../tags'; +import FixtureBuilder from '../../fixtures/fixture-builder'; + +import SellGetStartedView from '../../pages/Ramps/SellGetStartedView'; +import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView'; + +import Assertions from '../../utils/Assertions'; +import NetworkAddedBottomSheet from '../../pages/Network/NetworkAddedBottomSheet'; +import NetworkApprovalBottomSheet from '../../pages/Network/NetworkApprovalBottomSheet'; +import NetworkEducationModal from '../../pages/Network/NetworkEducationModal'; + +describe(SmokeCore('Buy Crypto Deeplinks'), () => { + beforeAll(async () => { + await TestHelpers.reverseServerPort(); + }); + + beforeEach(async () => { + jest.setTimeout(150000); + }); + + it('should deep link to onramp on Base network', async () => { + const BuyDeepLink = + 'metamask://buy?chainId=8453&address=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913&amount=12'; + + await withFixtures( + { + fixture: new FixtureBuilder().withRampsSelectedRegion().build(), + restartDevice: true, + }, + async () => { + await loginToApp(); + await device.sendToHome(); + await device.launchApp({ + url: BuyDeepLink, + }); + + await Assertions.checkIfVisible( + await SellGetStartedView.getStartedButton, + ); + + await BuyGetStartedView.tapGetStartedButton(); + + await Assertions.checkIfVisible(NetworkApprovalBottomSheet.container); + await NetworkApprovalBottomSheet.tapApproveButton(); + await NetworkAddedBottomSheet.tapSwitchToNetwork(); + await Assertions.checkIfVisible(NetworkEducationModal.container); + await NetworkEducationModal.tapGotItButton(); + await Assertions.checkIfTextIsDisplayed('USD Coin'); + }, + ); + }); +}); diff --git a/e2e/specs/ramps/deeplink-to-buy-flow.spec.js b/e2e/specs/ramps/deeplink-to-buy-flow.spec.js new file mode 100644 index 00000000000..0817a1949fa --- /dev/null +++ b/e2e/specs/ramps/deeplink-to-buy-flow.spec.js @@ -0,0 +1,55 @@ +'use strict'; +import TestHelpers from '../../helpers'; + +import { loginToApp } from '../../viewHelper'; +import { withFixtures } from '../../fixtures/fixture-helper'; +import { SmokeCore } from '../../tags'; +import FixtureBuilder from '../../fixtures/fixture-builder'; + +import SellGetStartedView from '../../pages/Ramps/SellGetStartedView'; +import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView'; + +import BuildQuoteView from '../../pages/Ramps/BuildQuoteView'; +import TokenSelectBottomSheet from '../../pages/Ramps/TokenSelectBottomSheet'; +import Assertions from '../../utils/Assertions'; + +describe(SmokeCore('Buy Crypto Deeplinks'), () => { + beforeAll(async () => { + await TestHelpers.reverseServerPort(); + }); + + beforeEach(async () => { + jest.setTimeout(150000); + }); + it('should deep link to onramp ETH', async () => { + const buyLink = 'metamask://buy?chainId=1&amount=275'; + + await withFixtures( + { + fixture: new FixtureBuilder() + .withRampsSelectedPaymentMethod() + .withRampsSelectedRegion() + .build(), + restartDevice: true, + }, + async () => { + await loginToApp(); + await device.sendToHome(); + await device.launchApp({ + url: buyLink, + }); + await Assertions.checkIfVisible( + await SellGetStartedView.getStartedButton, + ); + + await BuyGetStartedView.tapGetStartedButton(); + await Assertions.checkIfVisible(BuildQuoteView.getQuotesButton); + await BuildQuoteView.tapDefaultToken('Ethereum'); + + await TokenSelectBottomSheet.tapTokenByName('DAI'); + await Assertions.checkIfTextIsDisplayed('Dai Stablecoin'); + await Assertions.checkIfTextIsDisplayed('$275'); + }, + ); + }); +}); diff --git a/e2e/specs/ramps/deeplink-to-sell-flow.spec.js b/e2e/specs/ramps/deeplink-to-sell-flow.spec.js new file mode 100644 index 00000000000..49b43b58cf2 --- /dev/null +++ b/e2e/specs/ramps/deeplink-to-sell-flow.spec.js @@ -0,0 +1,89 @@ +'use strict'; +import { loginToApp } from '../../viewHelper'; + +import FixtureBuilder from '../../fixtures/fixture-builder'; +import { withFixtures } from '../../fixtures/fixture-helper'; + +import TestHelpers from '../../helpers'; +import SellGetStartedView from '../../pages/Ramps/SellGetStartedView'; +import { SmokeCore } from '../../tags'; + +import BuildQuoteView from '../../pages/Ramps/BuildQuoteView'; +import Assertions from '../../utils/Assertions'; +import NetworkApprovalBottomSheet from '../../pages/Network/NetworkApprovalBottomSheet'; +import NetworkAddedBottomSheet from '../../pages/Network/NetworkAddedBottomSheet'; +import NetworkEducationModal from '../../pages/Network/NetworkEducationModal'; + +describe(SmokeCore('Sell Crypto Deeplinks'), () => { + beforeAll(async () => { + await TestHelpers.reverseServerPort(); + }); + + beforeEach(async () => { + jest.setTimeout(150000); + }); + it('should deep link to offramp ETH', async () => { + const sellDeepLinkURL = + 'metamask://sell?chainId=1&address=0x0000000000000000000000000000000000000000&amount=50'; + const franceRegion = { + currencies: ['/currencies/fiat/eur'], + emoji: '🇫🇷', + id: '/regions/fr', + name: 'France', + support: { buy: true, sell: true, recurringBuy: true }, + unsupported: false, + recommended: false, + detected: false, + }; + await withFixtures( + { + fixture: new FixtureBuilder() + .withRampsSelectedRegion(franceRegion) + .build(), + restartDevice: true, + }, + async () => { + await loginToApp(); + + await device.openURL({ + url: sellDeepLinkURL, + }); + await Assertions.checkIfVisible( + await SellGetStartedView.getStartedButton, + ); + + await SellGetStartedView.tapGetStartedButton(); + await Assertions.checkIfVisible(BuildQuoteView.getQuotesButton); + + await Assertions.checkIfTextIsDisplayed('50 ETH'); + }, + ); + }); + it('Should deep link to an unsupported network in the off-ramp flow', async () => { + const unsupportedNetworkSellDeepLink = 'metamask://sell?chainId=56'; + + await withFixtures( + { + fixture: new FixtureBuilder().withRampsSelectedRegion().build(), + restartDevice: true, + }, + async () => { + await loginToApp(); + + await device.openURL({ + url: unsupportedNetworkSellDeepLink, + }); + await Assertions.checkIfVisible( + await SellGetStartedView.getStartedButton, + ); + + await SellGetStartedView.tapGetStartedButton(); + + await NetworkApprovalBottomSheet.tapApproveButton(); + await NetworkAddedBottomSheet.tapSwitchToNetwork(); + await Assertions.checkIfVisible(NetworkEducationModal.container); + await NetworkEducationModal.tapGotItButton(); + }, + ); + }); +});