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/.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="" 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; 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/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, ]); 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 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/__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`] = ` StyleSheet.create({ @@ -168,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 = { @@ -240,14 +248,34 @@ 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; }); 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 +315,7 @@ class Asset extends PureComponent { txParams: { from, to }, isTransfer, transferInformation, + type, } = tx; if ( @@ -295,10 +324,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; @@ -482,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 && @@ -493,7 +529,6 @@ class Asset extends PureComponent { const displayBuyButton = asset.isETH ? this.props.isNetworkBuyNativeTokenSupported : this.props.isNetworkRampSupported; - return ( {loading ? ( @@ -559,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 a7461510901..23ab4e6da7b 100644 --- a/app/components/Views/Asset/index.test.js +++ b/app/components/Views/Asset/index.test.js @@ -1,10 +1,26 @@ 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 './'; 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 +32,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,14 +112,65 @@ describe('Asset', () => { it('should render correctly', () => { const { toJSON } = renderWithProvider( null }} - route={{ params: { symbol: 'ETH', address: 'something', isETH: true } }} + navigation={{ setOptions: jest.fn() }} + route={{ + params: { + symbol: 'ETH', + address: 'something', + isETH: true, + chainId: '0x1', + }, + }} + />, + { + state: mockInitialState, + }, + ); + expect(toJSON()).toMatchSnapshot(); + }); + + it('should call navigation.setOptions on mount', () => { + const mockSetOptions = jest.fn(); + renderWithProvider( + , { state: mockInitialState, }, ); + + 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(); }); }); 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), 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(), ); 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 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 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/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 cdf89c571f8..a9bc65d01d6 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" @@ -89,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, ); } @@ -100,12 +112,30 @@ class TestDApp { ); } + get erc1155SetApprovalForAllButton() { + return Matchers.getElementByWebID( + BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, + TestDappSelectorsWebIDs.SET_APPROVAL_FOR_ALL_ERC1155_BUTTON_ID, + ); + } + + get erc1155BatchTransferButton() { + return Matchers.getElementByWebID( + BrowserViewSelectorsIDs.BROWSER_WEBVIEW_ID, + TestDappSelectorsWebIDs.BATCH_TRANSFER_ERC1155_BUTTON_ID, + ); + } + async connect() { 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() { @@ -146,10 +176,22 @@ class TestDApp { await this.tapButton(this.nftSetApprovalForAllButton); } + async tapERC1155SetApprovalForAllButton() { + await this.tapButton(this.erc1155SetApprovalForAllButton); + } + async tapConfirmButton() { await Gestures.tap(this.confirmButtonText, 0); } + async tapApproveButton() { + 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/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/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 0f2b7cad254..e0995aa10f0 100644 --- a/e2e/selectors/Browser/TestDapp.selectors.js +++ b/e2e/selectors/Browser/TestDapp.selectors.js @@ -1,14 +1,17 @@ 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', 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', ETHEREUM_SIGN: 'siwe', ADD_TOKENS_TO_WALLET_BUTTON: 'watchAssets', + BATCH_TRANSFER_ERC1155_BUTTON_ID: 'batchTransferFromButton', }; diff --git a/e2e/selectors/Transactions/ActivitiesView.selectors.js b/e2e/selectors/Transactions/ActivitiesView.selectors.js index 50a3cb95384..1aba946d5ec 100644 --- a/e2e/selectors/Transactions/ActivitiesView.selectors.js +++ b/e2e/selectors/Transactions/ActivitiesView.selectors.js @@ -10,10 +10,12 @@ 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), 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/multichain/assets/asset-list.spec.js b/e2e/specs/assets/multichain/asset-list.spec.js similarity index 97% rename from e2e/specs/multichain/assets/asset-list.spec.js rename to e2e/specs/assets/multichain/asset-list.spec.js index 5399688a711..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 { SmokeMultiChainPermissions } 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(SmokeMultiChainPermissions('Import Tokens'), () => { +describe(SmokeAssets('Import Tokens'), () => { beforeAll(async () => { await TestHelpers.reverseServerPort(); const fixture = new FixtureBuilder().withPopularNetworks().build(); 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, + ); + }, + ); + }); +}); 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, + ); + }, + ); + }); +}); 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, + ); + }, + ); + }); +}); 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, + ); + }, + ); + }); +}); 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(); + }, + ); + }); +}); diff --git a/package.json b/package.json index db621739117..b6b74ef38a3 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", @@ -200,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", @@ -211,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", diff --git a/yarn.lock b/yarn.lock index 1b9e5218c5a..1c291169aa3 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== @@ -5265,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" @@ -5280,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" @@ -5470,7 +5406,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 +13619,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 +16282,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 +16313,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 +16538,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 +20346,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 +26034,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 +27859,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"