diff --git a/app/component-library/components/Icons/Icon/assets/ai.svg b/app/component-library/components/Icons/Icon/assets/ai.svg index be69d0b22f5..4c3e04053f3 100644 --- a/app/component-library/components/Icons/Icon/assets/ai.svg +++ b/app/component-library/components/Icons/Icon/assets/ai.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/components/UI/Bridge/components/BridgeNetworkSelectorBase.tsx b/app/components/UI/Bridge/components/BridgeNetworkSelectorBase.tsx deleted file mode 100644 index c270c93c6b8..00000000000 --- a/app/components/UI/Bridge/components/BridgeNetworkSelectorBase.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { ScrollView } from 'react-native'; - -import BottomSheetHeader from '../../../../component-library/components/BottomSheets/BottomSheetHeader'; -import BottomSheet from '../../../../component-library/components/BottomSheets/BottomSheet'; -import { strings } from '../../../../../locales/i18n'; - -import { useNavigation } from '@react-navigation/native'; - -interface BridgeNetworkSelectorBaseProps { - children: React.ReactNode; -} - -export const BridgeNetworkSelectorBase: React.FC< - BridgeNetworkSelectorBaseProps -> = ({ children }) => { - const navigation = useNavigation(); - - return ( - - navigation.goBack()} - closeButtonProps={{ - testID: 'bridge-network-selector-close-button', - }} - > - {strings('bridge.select_network')} - - - {children} - - ); -}; diff --git a/app/components/UI/Bridge/components/BridgeTokenSelector/NetworkListModal.tsx b/app/components/UI/Bridge/components/BridgeTokenSelector/NetworkListModal.tsx index 474a8048cf9..7509ecb510a 100644 --- a/app/components/UI/Bridge/components/BridgeTokenSelector/NetworkListModal.tsx +++ b/app/components/UI/Bridge/components/BridgeTokenSelector/NetworkListModal.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useRef } from 'react'; -import { ScrollView } from 'react-native'; +import { ScrollView } from 'react-native-gesture-handler'; // Must use this to make sure scroll works inside a bottom sheet on Android import { useSelector, useDispatch } from 'react-redux'; import { Icon, IconName, IconSize } from '@metamask/design-system-react-native'; import { IconName as ComponentLibraryIconName } from '../../../../../component-library/components/Icons/Icon'; diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx index 62a7fee2fb7..2ae3c5ccef7 100644 --- a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx +++ b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx @@ -602,6 +602,45 @@ describe('QuoteDetailsCard', () => { }); }); + it('does not navigate when price impact is below warning threshold', () => { + // priceImpact 0.04 < warning threshold 0.05 → priceImpactIsSafe = true → no navigation + const mockModule = jest.requireMock('../../hooks/useBridgeQuoteData'); + mockModule.useBridgeQuoteData.mockImplementationOnce(() => ({ + quoteFetchError: null, + activeQuote: { + ...mockQuotes[0], + quote: { + ...mockQuotes[0].quote, + priceData: { ...mockQuotes[0].quote.priceData, priceImpact: '0.04' }, + gasIncluded: false, + gasIncluded7702: false, + }, + }, + destTokenAmount: '24.44', + isLoading: false, + formattedQuoteData: { + networkFee: '0.01', + estimatedTime: '1 min', + rate: '1 ETH = 24.4 USDC', + priceImpact: '0.04%', + slippage: '0.5%', + }, + })); + + const { getByTestId } = renderScreen( + QuoteDetailsCardTestScreen, + { name: Routes.BRIDGE.ROOT }, + { state: testState }, + ); + + fireEvent.press(getByTestId('price-impact-info-button')); + + expect(mockNavigate).not.toHaveBeenCalledWith(Routes.BRIDGE.MODALS.ROOT, { + screen: Routes.BRIDGE.MODALS.PRICE_IMPACT_MODAL, + params: expect.anything(), + }); + }); + it('opens rate tooltip modal when rate info icon is pressed', () => { const { getByLabelText } = renderScreen( QuoteDetailsCardTestScreen, diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx index 278a8cc6896..d04f7956846 100644 --- a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx +++ b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { TouchableOpacity, Platform, UIManager, Pressable } from 'react-native'; +import { TouchableOpacity, Platform, UIManager } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { strings } from '../../../../../../locales/i18n'; import { useTheme } from '../../../../../util/theme'; @@ -26,6 +26,7 @@ import { selectSourceAmount, selectDestToken, selectSourceToken, + selectBridgeFeatureFlags, } from '../../../../../core/redux/slices/bridge'; import { getNativeSourceToken } from '../../utils/tokenUtils'; import { formatMinimumReceived } from '../../utils/currencyUtils'; @@ -53,6 +54,7 @@ import { PriceImpactModalType } from '../PriceImpactModal/constants'; import { formatPriceImpact } from '../../utils/formatPriceImpact'; import KeyValueRowLabel from '../../../../../component-library/components-temp/KeyValueRow/KeyValueLabel/KeyValueLabel'; import { usePriceImpactViewData } from '../../hooks/usePriceImpactViewData'; +import AppConstants from '../../../../../core/AppConstants'; if ( Platform.OS === 'android' && @@ -65,6 +67,7 @@ const QuoteDetailsCard: React.FC = ({ hasInsufficientBalance, location, }) => { + const bridgeFeatureFlags = useSelector(selectBridgeFeatureFlags); const tw = useTailwind(); const theme = useTheme(); const navigation = useNavigation(); @@ -90,6 +93,13 @@ const QuoteDetailsCard: React.FC = ({ isQuoteLoading, }); + const priceImpactIsSafe = + !activeQuote?.quote.priceData?.priceImpact || + Number(activeQuote.quote.priceData.priceImpact) <= + // @ts-expect-error TODO: remove comment after changes to core are published. + (bridgeFeatureFlags?.priceImpactThreshold?.warning ?? + AppConstants.BRIDGE.PRICE_IMPACT_WARNING_THRESHOLD); + const nativeTokenName = useMemo(() => { const chainId = sourceToken?.chainId; if (!chainId) return undefined; @@ -117,6 +127,10 @@ const QuoteDetailsCard: React.FC = ({ }; const handlePriceImpactPress = () => { + if (priceImpactIsSafe) { + return; + } + navigation.navigate(Routes.BRIDGE.MODALS.ROOT, { screen: Routes.BRIDGE.MODALS.PRICE_IMPACT_MODAL, params: { @@ -133,7 +147,7 @@ const QuoteDetailsCard: React.FC = ({ activeQuote?.minToTokenAmount?.amount || '0', ); - const priceImactViewData = usePriceImpactViewData( + const priceImpactViewData = usePriceImpactViewData( activeQuote?.quote.priceData?.priceImpact, ); @@ -178,28 +192,33 @@ const QuoteDetailsCard: React.FC = ({ iconName: IconNameLegacy.Info, }} /> - - - {formattedQuoteData?.rate} - - - - - + + + {formattedQuoteData?.rate} + + + + {shouldShowGasSponsored ? ( = ({ }} value={{ label: ( - - - {priceImactViewData.icon && ( + {priceImpactViewData.icon && ( )} - - - {formatPriceImpact(formattedQuoteData.priceImpact)} - - + + {formatPriceImpact(formattedQuoteData.priceImpact)} + + + ), }} /> diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap b/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap index bb672526750..f846b5cff78 100644 --- a/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap +++ b/app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap @@ -454,95 +454,70 @@ exports[`QuoteDetailsCard renders initial state 1`] = ` /> - - - 1 ETH = 24.4 USDC - - - - - + > + + 1 ETH = 24.4 USDC + + + + - - - - 0% - - + + 0% + + + diff --git a/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.test.tsx b/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.test.tsx index 296207ebf47..d5c6ef23660 100644 --- a/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.test.tsx +++ b/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.test.tsx @@ -129,11 +129,11 @@ describe('PredictMarketSportCardWrapper', () => { beforeEach(() => { jest.clearAllMocks(); mockUsePredictMarket.mockReturnValue({ - market: null, - isFetching: false, + data: null, + isLoading: false, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); }); afterEach(() => { @@ -143,11 +143,11 @@ describe('PredictMarketSportCardWrapper', () => { describe('loading state', () => { it('returns null when fetching market data', () => { mockUsePredictMarket.mockReturnValue({ - market: null, - isFetching: true, + data: null, + isLoading: true, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); const { toJSON } = renderWithProvider( , @@ -161,11 +161,11 @@ describe('PredictMarketSportCardWrapper', () => { describe('error state', () => { it('returns null when error occurs', () => { mockUsePredictMarket.mockReturnValue({ - market: null, - isFetching: false, - error: 'Failed to fetch market', + data: null, + isLoading: false, + error: new Error('Failed to fetch market'), refetch: jest.fn(), - }); + } as unknown as ReturnType); const { toJSON } = renderWithProvider( , @@ -179,11 +179,11 @@ describe('PredictMarketSportCardWrapper', () => { describe('no market data', () => { it('returns null when market is null', () => { mockUsePredictMarket.mockReturnValue({ - market: null, - isFetching: false, + data: null, + isLoading: false, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); const { toJSON } = renderWithProvider( , @@ -197,11 +197,11 @@ describe('PredictMarketSportCardWrapper', () => { describe('successful render', () => { beforeEach(() => { mockUsePredictMarket.mockReturnValue({ - market: mockMarket, - isFetching: false, + data: mockMarket, + isLoading: false, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); }); it('renders PredictMarketSportCard when market data is available', () => { @@ -334,11 +334,11 @@ describe('PredictMarketSportCardWrapper', () => { it('calls onLoad when market data is available', () => { const mockOnLoad = jest.fn(); mockUsePredictMarket.mockReturnValue({ - market: mockMarket, - isFetching: false, + data: mockMarket, + isLoading: false, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); renderWithProvider( { it('does not call onLoad when fetching', () => { const mockOnLoad = jest.fn(); mockUsePredictMarket.mockReturnValue({ - market: null, - isFetching: true, + data: null, + isLoading: true, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); renderWithProvider( { it('does not call onLoad when error occurs', () => { const mockOnLoad = jest.fn(); mockUsePredictMarket.mockReturnValue({ - market: null, - isFetching: false, - error: 'Failed to fetch', + data: null, + isLoading: false, + error: new Error('Failed to fetch'), refetch: jest.fn(), - }); + } as unknown as ReturnType); renderWithProvider( { it('does not call onLoad when market is null', () => { const mockOnLoad = jest.fn(); mockUsePredictMarket.mockReturnValue({ - market: null, - isFetching: false, + data: null, + isLoading: false, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); renderWithProvider( { it('does not call onLoad when onLoad is not provided', () => { mockUsePredictMarket.mockReturnValue({ - market: mockMarket, - isFetching: false, + data: mockMarket, + isLoading: false, error: null, refetch: jest.fn(), - }); + } as unknown as ReturnType); renderWithProvider( , diff --git a/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.tsx b/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.tsx index 0b84ef2db27..5310e63cc3c 100644 --- a/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.tsx +++ b/app/components/UI/Predict/components/PredictMarketSportCard/PredictMarketSportCardWrapper.tsx @@ -15,20 +15,24 @@ interface PredictMarketSportCardWrapperProps { const PredictMarketSportCardWrapper: React.FC< PredictMarketSportCardWrapperProps > = ({ marketId, testID, entryPoint, onDismiss, onLoad }) => { - const { market, isFetching, error } = usePredictMarket({ + const { + data: market, + isLoading, + error, + } = usePredictMarket({ id: marketId, enabled: Boolean(marketId), }); const hasCalledOnLoad = useRef(false); useEffect(() => { - if (!isFetching && !error && market && onLoad && !hasCalledOnLoad.current) { + if (!isLoading && !error && market && onLoad && !hasCalledOnLoad.current) { hasCalledOnLoad.current = true; onLoad(); } - }, [isFetching, error, market, onLoad]); + }, [isLoading, error, market, onLoad]); - if (isFetching || error || !market) { + if (isLoading || error || !market) { return null; } diff --git a/app/components/UI/Predict/hooks/usePredictMarket.test.tsx b/app/components/UI/Predict/hooks/usePredictMarket.test.tsx index a9a6088e671..2002060db02 100644 --- a/app/components/UI/Predict/hooks/usePredictMarket.test.tsx +++ b/app/components/UI/Predict/hooks/usePredictMarket.test.tsx @@ -1,245 +1,211 @@ -import { renderHook, act } from '@testing-library/react-hooks'; -import Engine from '../../../../core/Engine'; +import React from 'react'; +import { renderHook, waitFor, act } from '@testing-library/react-native'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { usePredictMarket } from './usePredictMarket'; import { PredictMarket, Recurrence } from '../types'; - import { POLYMARKET_PROVIDER_ID } from '../providers/polymarket/constants'; -// Mock dependencies + +jest.mock('../../../../util/Logger', () => ({ + __esModule: true, + default: { + error: jest.fn(), + }, +})); + +const mockGetMarket = jest.fn(); jest.mock('../../../../core/Engine', () => ({ context: { PredictController: { - getMarket: jest.fn(), + getMarket: (...args: unknown[]) => mockGetMarket(...args), }, }, })); -describe('usePredictMarket', () => { - const mockGetMarket = jest.fn(); - - const mockMarket: PredictMarket = { - id: 'market-1', - providerId: POLYMARKET_PROVIDER_ID, - slug: 'bitcoin-price-prediction', - title: 'Will Bitcoin reach $200k by end of 2025?', - description: 'Bitcoin price prediction market', - endDate: '2025-12-31T23:59:59Z', - image: 'https://example.com/btc.png', - status: 'open', - recurrence: Recurrence.NONE, - category: 'crypto', - tags: ['trending'], - outcomes: [ - { - id: 'outcome-1', - providerId: POLYMARKET_PROVIDER_ID, - marketId: 'market-1', - title: 'Yes', - description: 'Bitcoin will reach $200k', - image: '', - status: 'open', - tokens: [ - { - id: 'token-1', - title: 'Yes', - price: 0.65, - }, - ], - volume: 1000000, - groupItemTitle: 'Yes/No', - }, - { - id: 'outcome-2', - providerId: POLYMARKET_PROVIDER_ID, - marketId: 'market-1', - title: 'No', - description: 'Bitcoin will not reach $200k', - image: '', - status: 'open', - tokens: [ - { - id: 'token-2', - title: 'No', - price: 0.35, - }, - ], - volume: 1000000, - groupItemTitle: 'Yes/No', - }, - ], - liquidity: 1000000, - volume: 1000000, - }; +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const Wrapper = ({ children }: { children: React.ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + return { Wrapper, queryClient }; +}; + +const mockMarket: PredictMarket = { + id: 'market-1', + providerId: POLYMARKET_PROVIDER_ID, + slug: 'bitcoin-price-prediction', + title: 'Will Bitcoin reach $200k by end of 2025?', + description: 'Bitcoin price prediction market', + endDate: '2025-12-31T23:59:59Z', + image: 'https://example.com/btc.png', + status: 'open', + recurrence: Recurrence.NONE, + category: 'crypto', + tags: ['trending'], + outcomes: [ + { + id: 'outcome-1', + providerId: POLYMARKET_PROVIDER_ID, + marketId: 'market-1', + title: 'Yes', + description: 'Bitcoin will reach $200k', + image: '', + status: 'open', + tokens: [ + { + id: 'token-1', + title: 'Yes', + price: 0.65, + }, + ], + volume: 1000000, + groupItemTitle: 'Yes/No', + }, + { + id: 'outcome-2', + providerId: POLYMARKET_PROVIDER_ID, + marketId: 'market-1', + title: 'No', + description: 'Bitcoin will not reach $200k', + image: '', + status: 'open', + tokens: [ + { + id: 'token-2', + title: 'No', + price: 0.35, + }, + ], + volume: 1000000, + groupItemTitle: 'Yes/No', + }, + ], + liquidity: 1000000, + volume: 1000000, +}; +describe('usePredictMarket', () => { beforeEach(() => { jest.clearAllMocks(); - (Engine.context.PredictController.getMarket as jest.Mock) = mockGetMarket; }); afterEach(() => { - jest.clearAllMocks(); + jest.restoreAllMocks(); }); describe('initial state', () => { - it('returns null market and not fetching when no id provided', () => { - const { result } = renderHook(() => usePredictMarket()); - - expect(result.current.market).toBe(null); - expect(result.current.isFetching).toBe(false); - expect(result.current.error).toBe(null); - expect(typeof result.current.refetch).toBe('function'); - }); - - it('returns null market and not fetching when id is undefined', () => { - const { result } = renderHook(() => usePredictMarket({ id: undefined })); - - expect(result.current.market).toBe(null); - expect(result.current.isFetching).toBe(false); - expect(result.current.error).toBe(null); - }); - - it('returns null market and not fetching when id is empty string', () => { - const { result } = renderHook(() => usePredictMarket({ id: '' })); + it('does not fetch when id is empty string', () => { + const { Wrapper } = createWrapper(); + const { result } = renderHook(() => usePredictMarket({ id: '' }), { + wrapper: Wrapper, + }); - expect(result.current.market).toBe(null); + expect(result.current.data).toBeUndefined(); expect(result.current.isFetching).toBe(false); expect(result.current.error).toBe(null); }); }); describe('successful market fetching', () => { - it('fetches market data successfully with string id', async () => { + it('returns market data when given a valid id', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockResolvedValue(mockMarket); - const { result, waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 'market-1' }), + const { result } = renderHook( + () => usePredictMarket({ id: 'market-1' }), + { wrapper: Wrapper }, ); - // Initially loading + // Initially fetching expect(result.current.isFetching).toBe(true); - expect(result.current.market).toBe(null); - expect(result.current.error).toBe(null); - - // Wait for data to load - await waitForNextUpdate(); - - expect(result.current.isFetching).toBe(false); - expect(result.current.market).toEqual(mockMarket); + expect(result.current.data).toBeUndefined(); expect(result.current.error).toBe(null); - expect(mockGetMarket).toHaveBeenCalledWith({ - marketId: 'market-1', - providerId: undefined, - }); - }); - - it('fetches market data successfully with number id', async () => { - mockGetMarket.mockResolvedValue(mockMarket); - - const { result, waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 123 }), - ); - - await waitForNextUpdate(); - expect(result.current.market).toEqual(mockMarket); - expect(mockGetMarket).toHaveBeenCalledWith({ - marketId: '123', + await waitFor(() => { + expect(result.current.isFetching).toBe(false); }); - }); - - it('fetches market data with string id', async () => { - mockGetMarket.mockResolvedValue(mockMarket); - - const { result, waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 'market-1' }), - ); - - await waitForNextUpdate(); - expect(result.current.market).toEqual(mockMarket); + expect(result.current.data).toEqual(mockMarket); + expect(result.current.error).toBe(null); expect(mockGetMarket).toHaveBeenCalledWith({ marketId: 'market-1', }); }); - it('handles null market response', async () => { + it('returns null data when API responds with null', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockResolvedValue(null); - const { result, waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 'market-1' }), + const { result } = renderHook( + () => usePredictMarket({ id: 'market-1' }), + { wrapper: Wrapper }, ); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); - expect(result.current.isFetching).toBe(false); - expect(result.current.market).toBe(null); + expect(result.current.data).toBe(null); expect(result.current.error).toBe(null); }); }); describe('error handling', () => { - it('handles API error with Error instance', async () => { + it('exposes Error instance when API rejects with Error', async () => { + const { Wrapper } = createWrapper(); const errorMessage = 'Network error occurred'; mockGetMarket.mockRejectedValue(new Error(errorMessage)); - const { result, waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 'market-1' }), + const { result } = renderHook( + () => usePredictMarket({ id: 'market-1' }), + { wrapper: Wrapper }, ); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); - expect(result.current.isFetching).toBe(false); - expect(result.current.market).toBe(null); - expect(result.current.error).toBe(errorMessage); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBeInstanceOf(Error); + expect(result.current.error?.message).toBe(errorMessage); }); - it('handles API error with non-Error instance', async () => { + it('exposes raw value when API rejects with non-Error', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockRejectedValue('String error'); - const { result, waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 'market-1' }), + const { result } = renderHook( + () => usePredictMarket({ id: 'market-1' }), + { wrapper: Wrapper }, ); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); - expect(result.current.isFetching).toBe(false); - expect(result.current.market).toBe(null); - expect(result.current.error).toBe('Failed to fetch market'); + expect(result.current.data).toBeUndefined(); + expect(result.current.error).toBe('String error'); }); }); describe('enabled option', () => { it('does not fetch when enabled is false', () => { - renderHook(() => usePredictMarket({ id: 'market-1', enabled: false })); + const { Wrapper } = createWrapper(); + renderHook(() => usePredictMarket({ id: 'market-1', enabled: false }), { + wrapper: Wrapper, + }); expect(mockGetMarket).not.toHaveBeenCalled(); }); - it('clears state when disabled after being enabled', async () => { - mockGetMarket.mockResolvedValue(mockMarket); - - const { result, rerender, waitForNextUpdate } = renderHook( - ({ enabled }) => usePredictMarket({ id: 'market-1', enabled }), - { initialProps: { enabled: true } }, - ); - - await waitForNextUpdate(); - - expect(result.current.market).toEqual(mockMarket); - - // Disable the hook - rerender({ enabled: false }); - - expect(result.current.market).toBe(null); - expect(result.current.error).toBe(null); - expect(result.current.isFetching).toBe(false); - }); - it('fetches when enabled changes from false to true', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockResolvedValue(mockMarket); - const { result, rerender, waitForNextUpdate } = renderHook( - ({ enabled }) => usePredictMarket({ id: 'market-1', enabled }), - { initialProps: { enabled: false } }, + const { result, rerender } = renderHook( + ({ enabled }: { enabled: boolean }) => + usePredictMarket({ id: 'market-1', enabled }), + { wrapper: Wrapper, initialProps: { enabled: false } }, ); expect(mockGetMarket).not.toHaveBeenCalled(); @@ -247,9 +213,11 @@ describe('usePredictMarket', () => { // Enable the hook rerender({ enabled: true }); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); - expect(result.current.market).toEqual(mockMarket); + expect(result.current.data).toEqual(mockMarket); expect(mockGetMarket).toHaveBeenCalledWith({ marketId: 'market-1', }); @@ -258,13 +226,17 @@ describe('usePredictMarket', () => { describe('refetch functionality', () => { it('refetches data when calling refetch', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockResolvedValue(mockMarket); - const { result, waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 'market-1' }), + const { result } = renderHook( + () => usePredictMarket({ id: 'market-1' }), + { wrapper: Wrapper }, ); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); expect(mockGetMarket).toHaveBeenCalledTimes(1); @@ -276,69 +248,46 @@ describe('usePredictMarket', () => { expect(mockGetMarket).toHaveBeenCalledTimes(2); }); - it('maintains stable refetch function reference', () => { + it('maintains a callable refetch function across rerenders', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockResolvedValue(mockMarket); - const { result, rerender } = renderHook(() => - usePredictMarket({ id: 'market-1' }), + const { result, rerender } = renderHook( + () => usePredictMarket({ id: 'market-1' }), + { wrapper: Wrapper }, ); - const firstRefetch = result.current.refetch; + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); // Trigger a re-render - rerender(); + rerender({}); - expect(result.current.refetch).toBe(firstRefetch); - }); - - it('does not refetch when disabled', async () => { - const { result } = renderHook(() => - usePredictMarket({ id: 'market-1', enabled: false }), - ); + expect(typeof result.current.refetch).toBe('function'); + // Ensure refetch still works after rerender await act(async () => { await result.current.refetch(); }); - expect(mockGetMarket).not.toHaveBeenCalled(); + expect(mockGetMarket).toHaveBeenCalledTimes(2); }); }); describe('dependency changes', () => { it('refetches when id changes', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockResolvedValue(mockMarket); - const { rerender, waitForNextUpdate } = renderHook( - ({ id }) => usePredictMarket({ id }), - { initialProps: { id: 'market-1' } }, + const { result, rerender } = renderHook( + ({ id }: { id: string }) => usePredictMarket({ id }), + { wrapper: Wrapper, initialProps: { id: 'market-1' } }, ); - await waitForNextUpdate(); - - expect(mockGetMarket).toHaveBeenCalledWith({ - marketId: 'market-1', - }); - - // Change id - rerender({ id: 'market-2' }); - - await waitForNextUpdate(); - - expect(mockGetMarket).toHaveBeenCalledWith({ - marketId: 'market-2', + await waitFor(() => { + expect(result.current.isFetching).toBe(false); }); - expect(mockGetMarket).toHaveBeenCalledTimes(2); - }); - - it('refetches when id changes across rerenders', async () => { - mockGetMarket.mockResolvedValue(mockMarket); - - const { rerender, waitForNextUpdate } = renderHook( - ({ id }) => usePredictMarket({ id }), - { initialProps: { id: 'market-1' } }, - ); - - await waitForNextUpdate(); expect(mockGetMarket).toHaveBeenCalledWith({ marketId: 'market-1', @@ -347,7 +296,9 @@ describe('usePredictMarket', () => { // Change id rerender({ id: 'market-2' }); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); expect(mockGetMarket).toHaveBeenCalledWith({ marketId: 'market-2', @@ -356,119 +307,24 @@ describe('usePredictMarket', () => { }); }); - describe('component unmounting', () => { - it('does not update state after component unmounts', async () => { - mockGetMarket.mockImplementation( - () => - new Promise((resolve) => { - setTimeout(() => resolve(mockMarket), 100); - }), - ); - - const { result, unmount } = renderHook(() => - usePredictMarket({ id: 'market-1' }), - ); - - // Start the fetch - expect(result.current.isFetching).toBe(true); - - // Unmount before fetch completes - unmount(); - - // Wait for the promise to resolve - await new Promise((resolve) => setTimeout(resolve, 150)); - - // The hook should not have updated state after unmount - // We can't test this directly since the hook is unmounted, - // but we can verify the mock was called - expect(mockGetMarket).toHaveBeenCalled(); - }); - }); - - describe('edge cases', () => { - it('handles id conversion from number to string', async () => { - mockGetMarket.mockResolvedValue(mockMarket); - - const { waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: 0 }), - ); - - await waitForNextUpdate(); - - expect(mockGetMarket).toHaveBeenCalledWith({ - marketId: '0', - }); - }); - - it('handles id conversion from negative number to string', async () => { - mockGetMarket.mockResolvedValue(mockMarket); - - const { waitForNextUpdate } = renderHook(() => - usePredictMarket({ id: -1 }), - ); - - await waitForNextUpdate(); - - expect(mockGetMarket).toHaveBeenCalledWith({ - marketId: '-1', - }); - }); - - it('handles multiple rapid id changes', async () => { - mockGetMarket.mockResolvedValue(mockMarket); - - const { rerender } = renderHook(({ id }) => usePredictMarket({ id }), { - initialProps: { id: 'market-1' }, - }); - - // Rapidly change id multiple times - rerender({ id: 'market-2' }); - rerender({ id: 'market-3' }); - rerender({ id: 'market-4' }); - - // Wait for all promises to settle - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - - // Should have been called for each change - expect(mockGetMarket).toHaveBeenCalledTimes(4); - }); - }); - - describe('integration with controller', () => { + describe('integration', () => { it('calls getMarket with correct parameters', async () => { + const { Wrapper } = createWrapper(); mockGetMarket.mockResolvedValue(mockMarket); - const { waitForNextUpdate } = renderHook(() => - usePredictMarket({ - id: 'test-market-id', - }), + const { result } = renderHook( + () => usePredictMarket({ id: 'test-market-id' }), + { wrapper: Wrapper }, ); - await waitForNextUpdate(); + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); expect(mockGetMarket).toHaveBeenCalledWith({ marketId: 'test-market-id', }); expect(mockGetMarket).toHaveBeenCalledTimes(1); }); - - it('handles controller method throwing synchronously', async () => { - mockGetMarket.mockImplementation(() => { - throw new Error('Synchronous error'); - }); - - const { result } = renderHook(() => usePredictMarket({ id: 'market-1' })); - - // Wait a bit for the error to be processed - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 10)); - }); - - expect(result.current.isFetching).toBe(false); - expect(result.current.market).toBe(null); - expect(result.current.error).toBe('Synchronous error'); - }); }); }); diff --git a/app/components/UI/Predict/hooks/usePredictMarket.tsx b/app/components/UI/Predict/hooks/usePredictMarket.tsx index 849f8990dc0..063b4c121aa 100644 --- a/app/components/UI/Predict/hooks/usePredictMarket.tsx +++ b/app/components/UI/Predict/hooks/usePredictMarket.tsx @@ -1,126 +1,44 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import Engine from '../../../../core/Engine'; +import { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; import Logger from '../../../../util/Logger'; import { PREDICT_CONSTANTS } from '../constants/errors'; import { ensureError } from '../utils/predictErrorHandler'; -import { PredictMarket } from '../types'; - -export interface UsePredictMarketOptions { - id?: string | number; - enabled?: boolean; -} - -export interface UsePredictMarketResult { - market: PredictMarket | null; - isFetching: boolean; - error: string | null; - refetch: () => Promise; -} +import { predictQueries } from '../queries'; /** * Hook to fetch detailed Predict market information */ -export const usePredictMarket = ( - options: UsePredictMarketOptions = {}, -): UsePredictMarketResult => { - const { id, enabled = true } = options; - const [market, setMarket] = useState(null); - const [isFetching, setIsFetching] = useState(false); - const [error, setError] = useState(null); - - const isMountedRef = useRef(true); - useEffect( - () => () => { - isMountedRef.current = false; - }, - [], - ); +export const usePredictMarket = ({ + id, + enabled = true, +}: { + id: string; + enabled?: boolean; +}) => { + const query = useQuery({ + ...predictQueries.market.options({ marketId: id }), + enabled: enabled && !!id, + }); useEffect(() => { - if (!enabled && isMountedRef.current) { - setMarket(null); - setError(null); - setIsFetching(false); - } - }, [enabled]); - - const fetchMarket = useCallback(async () => { - if (!enabled) { - return; - } - - const marketId = id !== undefined && id !== null ? String(id) : ''; - if (!marketId) { - if (isMountedRef.current) { - setMarket(null); - setError(null); - setIsFetching(false); - } - return; - } - - if (isMountedRef.current) { - setIsFetching(true); - setError(null); - } - - try { - if (!Engine || !Engine.context) { - throw new Error('Engine not initialized'); - } - - const controller = Engine.context.PredictController; - if (!controller) { - throw new Error('Predict controller not available'); - } - - const marketData = await controller.getMarket({ - marketId, - }); - - if (isMountedRef.current) { - setMarket(marketData ?? null); - } - } catch (err) { - const errorMessage = - err instanceof Error ? err.message : 'Failed to fetch market'; - - // Capture exception with market loading context - Logger.error(ensureError(err), { - tags: { - feature: PREDICT_CONSTANTS.FEATURE_NAME, - component: 'usePredictMarket', + if (!query.error) return; + + Logger.error(ensureError(query.error), { + tags: { + feature: PREDICT_CONSTANTS.FEATURE_NAME, + component: 'usePredictMarket', + }, + context: { + name: 'usePredictMarket', + data: { + method: 'queryFn', + action: 'market_load', + operation: 'data_fetching', + marketId: id, }, - context: { - name: 'usePredictMarket', - data: { - method: 'loadMarket', - action: 'market_load', - operation: 'data_fetching', - marketId: id, - }, - }, - }); - - if (isMountedRef.current) { - setError(errorMessage); - setMarket(null); - } - } finally { - if (isMountedRef.current) { - setIsFetching(false); - } - } - }, [enabled, id]); - - useEffect(() => { - fetchMarket(); - }, [fetchMarket]); + }, + }); + }, [query.error, id]); - return { - market, - isFetching, - error, - refetch: fetchMarket, - }; + return query; }; diff --git a/app/components/UI/Predict/queries/index.ts b/app/components/UI/Predict/queries/index.ts index 541f7618040..490802e6a86 100644 --- a/app/components/UI/Predict/queries/index.ts +++ b/app/components/UI/Predict/queries/index.ts @@ -1,5 +1,6 @@ import { predictActivityKeys, predictActivityOptions } from './activity'; import { predictBalanceKeys, predictBalanceOptions } from './balance'; +import { predictMarketKeys, predictMarketOptions } from './market'; import { predictOrderPreviewKeys, predictOrderPreviewOptions, @@ -19,6 +20,10 @@ export const predictQueries = { keys: predictBalanceKeys, options: predictBalanceOptions, }, + market: { + keys: predictMarketKeys, + options: predictMarketOptions, + }, orderPreview: { keys: predictOrderPreviewKeys, options: predictOrderPreviewOptions, diff --git a/app/components/UI/Predict/queries/market.ts b/app/components/UI/Predict/queries/market.ts new file mode 100644 index 00000000000..6df69c9239a --- /dev/null +++ b/app/components/UI/Predict/queries/market.ts @@ -0,0 +1,25 @@ +import { queryOptions } from '@tanstack/react-query'; +import Engine from '../../../../core/Engine'; +import type { PredictMarket } from '../types'; + +/** + * Query key factory for Predict single-market queries. + * + * - `all()` — prefix key for invalidating every market entry at once. + * - `detail(marketId)` — unique key for a specific market. + */ +export const predictMarketKeys = { + all: () => ['predict', 'market'] as const, + detail: (marketId: string) => [...predictMarketKeys.all(), marketId] as const, +}; + +export const predictMarketOptions = ({ marketId }: { marketId: string }) => + queryOptions({ + queryKey: predictMarketKeys.detail(marketId), + queryFn: async (): Promise => { + const controller = Engine.context.PredictController; + const marketData = await controller.getMarket({ marketId }); + return marketData ?? null; + }, + staleTime: 10_000, + }); diff --git a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx index eb3528509d7..bc0f90f0dcc 100644 --- a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx +++ b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx @@ -183,7 +183,8 @@ jest.mock('../../utils/format', () => ({ jest.mock('../../hooks/usePredictMarket', () => ({ usePredictMarket: jest.fn(() => ({ - market: null, + data: null, + isLoading: false, isFetching: false, refetch: jest.fn(), })), @@ -528,7 +529,8 @@ function setupPredictMarketDetailsTest( }); usePredictMarket.mockReturnValue({ - market: mockMarket, + data: mockMarket, + isLoading: false, isFetching: false, refetch: jest.fn(), ...hookOverrides.market, @@ -710,7 +712,7 @@ describe('PredictMarketDetails', () => { setupPredictMarketDetailsTest( {}, {}, - { market: { isFetching: true, market: null } }, + { market: { isLoading: true, isFetching: true, data: null } }, ); // Check that skeleton loaders appear @@ -732,7 +734,7 @@ describe('PredictMarketDetails', () => { }); it('displays fallback title when market data is unavailable', () => { - setupPredictMarketDetailsTest({}, {}, { market: { market: null } }); + setupPredictMarketDetailsTest({}, {}, { market: { data: null } }); // Screen renders without a title; other sections may still show loading keys expect( @@ -772,7 +774,7 @@ describe('PredictMarketDetails', () => { setupPredictMarketDetailsTest( {}, {}, - { market: { isFetching: true, market: null } }, + { market: { isLoading: true, isFetching: true, data: null } }, ); expect( @@ -811,7 +813,9 @@ describe('PredictMarketDetails', () => { expect( screen.getByText('predict.market_details.end_date'), ).toBeOnTheScreen(); - expect(screen.getByText('12/31/2024')).toBeOnTheScreen(); + expect( + screen.getByText(new Date('2024-12-31T23:59:59Z').toLocaleDateString()), + ).toBeOnTheScreen(); }); it('displays resolution details information', () => { @@ -3499,7 +3503,8 @@ describe('PredictMarketDetails', () => { }); usePredictMarket.mockReturnValue({ - market: marketWithoutWaivedTag, + data: marketWithoutWaivedTag, + isLoading: false, isFetching: false, refetch: jest.fn(), }); diff --git a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.tsx b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.tsx index 0c54e356e5d..3333bf0c62f 100644 --- a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.tsx +++ b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.tsx @@ -76,13 +76,15 @@ const PredictMarketDetails: React.FC = () => { }); const { - market, + data: marketData, + isLoading: isMarketLoading, isFetching: isMarketFetching, refetch: refetchMarket, } = usePredictMarket({ - id: resolvedMarketId, + id: resolvedMarketId ?? '', enabled: Boolean(resolvedMarketId), }); + const market = marketData ?? null; // Track screen load performance (market details + chart) usePredictMeasurement({ @@ -97,11 +99,11 @@ const PredictMarketDetails: React.FC = () => { // calculate sticky header indices based on content structure const stickyHeaderIndices = useMemo(() => { - if (isMarketFetching && !market) { + if (isMarketLoading) { return []; } return [1]; - }, [isMarketFetching, market]); + }, [isMarketLoading]); const titleLineCount = useMemo( () => estimateLineCount(title ?? market?.title), @@ -116,7 +118,7 @@ const PredictMarketDetails: React.FC = () => { } = usePredictPositions({ marketId: resolvedMarketId, claimable: false, - enabled: !isMarketFetching && Boolean(resolvedMarketId), + enabled: !isMarketLoading && Boolean(resolvedMarketId), }); // "claimable" positions @@ -127,7 +129,7 @@ const PredictMarketDetails: React.FC = () => { } = usePredictPositions({ marketId: resolvedMarketId, claimable: true, - enabled: !isMarketFetching && Boolean(resolvedMarketId), + enabled: !isMarketLoading && Boolean(resolvedMarketId), }); const feeCollectionConfig = useSelector(selectPredictFeeCollectionFlag); @@ -139,10 +141,10 @@ const PredictMarketDetails: React.FC = () => { // Tabs become ready when both market and positions queries have resolved const tabsReady = useMemo( () => - !isMarketFetching && + !isMarketLoading && !isActivePositionsLoading && !isClaimablePositionsLoading, - [isMarketFetching, isActivePositionsLoading, isClaimablePositionsLoading], + [isMarketLoading, isActivePositionsLoading, isClaimablePositionsLoading], ); const { @@ -362,7 +364,7 @@ const PredictMarketDetails: React.FC = () => { > = () => { {/* Show content skeleton while initial market data is fetching */} - {isMarketFetching && !market ? ( + {isMarketLoading ? ( @@ -423,7 +425,7 @@ const PredictMarketDetails: React.FC = () => { )} {/* Tab content - only show when market is loaded */} - {!isMarketFetching && market && ( + {!isMarketLoading && market && ( = () => { hasPositivePnl={hasPositivePnl} marketStatus={market?.status as PredictMarketStatus | undefined} singleOutcomeMarket={singleOutcomeMarket} - isMarketFetching={isMarketFetching} + isMarketLoading={isMarketLoading} market={market} openOutcomes={openOutcomes} yesPercentage={yesPercentage} diff --git a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx index 272da826523..c66e46f96c1 100644 --- a/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx +++ b/app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.view.test.tsx @@ -52,9 +52,11 @@ describe('PredictMarketDetails', () => { const screen = await findByTestId( PredictMarketDetailsSelectorsIDs.SCREEN, ); - expect( - within(screen).getByText(MOCK_PREDICT_MARKET.title), - ).toBeOnTheScreen(); + await waitFor(() => { + expect( + within(screen).getByText(MOCK_PREDICT_MARKET.title), + ).toBeOnTheScreen(); + }); expect(await findByText(/Yes.*¢/)).toBeOnTheScreen(); expect(await findByText(/No.*¢/)).toBeOnTheScreen(); diff --git a/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.test.tsx b/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.test.tsx index c5168678e78..af915968fef 100644 --- a/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.test.tsx +++ b/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.test.tsx @@ -54,7 +54,7 @@ const createProps = ( hasPositivePnl: false, marketStatus: PredictMarketStatus.OPEN, singleOutcomeMarket: true, - isMarketFetching: false, + isMarketLoading: false, market: createMarket(), openOutcomes: [createOutcome()], yesPercentage: 65, @@ -130,7 +130,7 @@ describe('PredictMarketDetailsActions', () => { it('renders skeleton while market details are loading', () => { const props = createProps({ - isMarketFetching: true, + isMarketLoading: true, market: null, marketStatus: PredictMarketStatus.CLOSED, singleOutcomeMarket: false, @@ -149,7 +149,7 @@ describe('PredictMarketDetailsActions', () => { singleOutcomeMarket: false, hasPositivePnl: false, isClaimablePositionsLoading: false, - isMarketFetching: false, + isMarketLoading: false, }); renderWithProvider(); diff --git a/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.tsx b/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.tsx index 55914f7a6b7..4a7bc6d246c 100644 --- a/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.tsx +++ b/app/components/UI/Predict/views/PredictMarketDetails/components/PredictMarketDetailsActions/PredictMarketDetailsActions.tsx @@ -28,7 +28,7 @@ export interface PredictMarketDetailsActionsProps { hasPositivePnl: boolean; marketStatus: PredictMarketStatus | undefined; singleOutcomeMarket: boolean; - isMarketFetching: boolean; + isMarketLoading: boolean; market: PredictMarket | null; openOutcomes: PredictOutcome[]; yesPercentage: number; @@ -43,7 +43,7 @@ const PredictMarketDetailsActions = memo( hasPositivePnl, marketStatus, singleOutcomeMarket, - isMarketFetching, + isMarketLoading, market, openOutcomes, yesPercentage, @@ -125,7 +125,7 @@ const PredictMarketDetailsActions = memo( } // Show skeleton buttons while loading - if (isMarketFetching && !market) { + if (isMarketLoading) { return ; } diff --git a/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.test.tsx b/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.test.tsx index 1eff1cfc055..1c8bb9c31e0 100644 --- a/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.test.tsx +++ b/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.test.tsx @@ -257,7 +257,7 @@ describe('PaymentSelectionModal', () => { }); }); - it('does not navigate to provider selection when change provider is pressed and payment methods are loading', async () => { + it('navigates to provider selection when change provider is pressed while payment methods are loading', () => { const loadingState = { ...defaultControllerReturn, selectedProvider: mockSelectedProvider, @@ -266,13 +266,14 @@ describe('PaymentSelectionModal', () => { selectedPaymentMethod: null, }; mockUseRampsController.mockImplementation(() => loadingState); + mockUseParams.mockReturnValue({ amount: 100 }); const { getByText } = renderWithProvider(PaymentSelectionModal); const changeProviderLink = getByText('fiat_on_ramp.change_provider'); fireEvent.press(changeProviderLink); - await waitFor(() => { - expect(getByText('fiat_on_ramp.pay_with')).toBeOnTheScreen(); + expect(mockNavigate).toHaveBeenCalledWith('RampProviderSelectionModal', { + amount: 100, }); }); diff --git a/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.tsx b/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.tsx index f283c5fba98..8a1faf7af30 100644 --- a/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.tsx @@ -268,14 +268,12 @@ function PaymentSelectionModal() { {strings('fiat_on_ramp.change_provider')} diff --git a/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.test.tsx b/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.test.tsx index a1c9c1d0c61..e970e514222 100644 --- a/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.test.tsx +++ b/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.test.tsx @@ -29,9 +29,11 @@ const mockNavigationState = { stale: false as const, }; +const mockNavigate = jest.fn(); + jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), - useNavigation: () => ({ goBack: mockGoBack, navigate: jest.fn() }), + useNavigation: () => ({ goBack: mockGoBack, navigate: mockNavigate }), useNavigationState: ( selector: (state: typeof mockNavigationState) => unknown, ) => selector(mockNavigationState), @@ -125,15 +127,26 @@ jest.mock('../../../hooks/useRampAccountAddress', () => ({ default: () => '0x123', })); +let capturedOnClose: ((hasPendingAction?: boolean) => void) | undefined; + jest.mock( '../../../../../../component-library/components/BottomSheets/BottomSheet', () => { const ReactActual = jest.requireActual('react'); return ReactActual.forwardRef( ( - { children }: { children: React.ReactNode }, + { + children, + onClose, + }: { + children: React.ReactNode; + onClose?: (hasPendingAction?: boolean) => void; + }, _ref: React.Ref, - ) => <>{children}, + ) => { + capturedOnClose = onClose; + return <>{children}; + }, ); }, ); @@ -261,4 +274,27 @@ describe('ProviderSelectionModal', () => { expect(getByText('MoonPay')).toBeOnTheScreen(); expect(queryByText('Other')).toBeNull(); }); + + it('navigates to token selection when dismissed without action and skipQuotes is true', () => { + mockUseParams.mockReturnValue({ + assetId: 'eip155:1/slip44:60', + skipQuotes: true, + }); + renderWithProvider(ProviderSelectionModal); + + capturedOnClose?.(false); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.RAMP.TOKEN_SELECTION, { + screen: Routes.RAMP.TOKEN_SELECTION, + }); + }); + + it('does not navigate to token selection when dismissed without action and skipQuotes is false', () => { + mockUseParams.mockReturnValue({ amount: 100 }); + renderWithProvider(ProviderSelectionModal); + + capturedOnClose?.(false); + + expect(mockNavigate).not.toHaveBeenCalled(); + }); }); diff --git a/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx b/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx index f6260452ae6..b6ef67590ed 100644 --- a/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx @@ -122,6 +122,17 @@ function ProviderSelectionModal() { error: quotesError, } = useRampsQuotes(quoteFetchParams); + const handleDismiss = useCallback( + (hasPendingAction?: boolean) => { + if (!hasPendingAction && skipQuotes) { + navigation.navigate(Routes.RAMP.TOKEN_SELECTION, { + screen: Routes.RAMP.TOKEN_SELECTION, + }); + } + }, + [navigation, skipQuotes], + ); + const handleBack = useCallback(() => { navigation.goBack(); }, [navigation]); @@ -151,7 +162,7 @@ function ProviderSelectionModal() { ); return ( - + ({ name: 'test-event' })); +const mockCreateEventBuilder = jest.fn(() => ({ + addProperties: mockAddProperties, + build: mockBuild, +})); + +jest.mock('../../../../../hooks/useAnalytics/useAnalytics', () => ({ + useAnalytics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder: mockCreateEventBuilder, + }), +})); const MOCK_ASSET_ID = 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; @@ -53,6 +69,8 @@ const mockOnCloseBottomSheet = jest.fn((callback?: () => void) => { if (callback) callback(); }); +let capturedOnClose: ((hasPendingAction?: boolean) => void) | undefined; + jest.mock( '../../../../../../component-library/components/BottomSheets/BottomSheet', () => { @@ -61,11 +79,14 @@ jest.mock( ( { children, + onClose, }: { children: React.ReactNode; + onClose?: (hasPendingAction?: boolean) => void; }, ref: React.Ref<{ onCloseBottomSheet: (cb?: () => void) => void }>, ) => { + capturedOnClose = onClose; ReactActual.useImperativeHandle(ref, () => ({ onCloseBottomSheet: mockOnCloseBottomSheet, })); @@ -138,13 +159,31 @@ describe('TokenNotAvailableModal', () => { ); }); - it('closes the modal when the close button is pressed', () => { + it('closes the bottom sheet when the close button is pressed', () => { const { getByTestId } = render(TokenNotAvailableModal); const closeButton = getByTestId('bottomsheetheader-close-button'); fireEvent.press(closeButton); - expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1); + expect(mockOnCloseBottomSheet).toHaveBeenCalled(); + }); + + it('navigates to token selection when modal is dismissed without a pending action', () => { + render(TokenNotAvailableModal); + + capturedOnClose?.(false); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.RAMP.TOKEN_SELECTION, { + screen: Routes.RAMP.TOKEN_SELECTION, + }); + }); + + it('does not navigate on dismiss when there is a pending action', () => { + render(TokenNotAvailableModal); + + capturedOnClose?.(true); + + expect(mockNavigate).not.toHaveBeenCalled(); }); it('matches snapshot with missing provider and token names', () => { @@ -155,4 +194,54 @@ describe('TokenNotAvailableModal', () => { expect(toJSON()).toMatchSnapshot(); }); + + it('fires RAMPS_SCREEN_VIEWED analytics event on mount', () => { + render(TokenNotAvailableModal); + + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.RAMPS_SCREEN_VIEWED, + ); + expect(mockAddProperties).toHaveBeenCalledWith({ + location: 'Token Unavailable Modal', + ramp_type: 'UNIFIED_BUY_2', + }); + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + }); + + it('fires RAMPS_CHANGE_TOKEN_BUTTON_CLICKED analytics event when Change token is pressed', () => { + const { getByText } = render(TokenNotAvailableModal); + mockTrackEvent.mockClear(); + mockCreateEventBuilder.mockClear(); + mockAddProperties.mockClear(); + + fireEvent.press(getByText('Change token')); + + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.RAMPS_CHANGE_TOKEN_BUTTON_CLICKED, + ); + expect(mockAddProperties).toHaveBeenCalledWith({ + current_provider: 'Transak', + location: 'Token Unavailable Modal', + ramp_type: 'UNIFIED_BUY_2', + }); + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + }); + + it('fires RAMPS_CLOSE_BUTTON_CLICKED analytics event when close button is pressed', () => { + const { getByTestId } = render(TokenNotAvailableModal); + mockTrackEvent.mockClear(); + mockCreateEventBuilder.mockClear(); + mockAddProperties.mockClear(); + + fireEvent.press(getByTestId('bottomsheetheader-close-button')); + + expect(mockCreateEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.RAMPS_CLOSE_BUTTON_CLICKED, + ); + expect(mockAddProperties).toHaveBeenCalledWith({ + location: 'Token Unavailable Modal', + ramp_type: 'UNIFIED_BUY_2', + }); + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + }); }); diff --git a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx index 5f972a1222f..7776134fd78 100644 --- a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import Text, { @@ -49,13 +49,33 @@ function TokenNotAvailableModal() { const tokenName = selectedToken?.name ?? ''; const providerName = selectedProvider?.name ?? ''; + useEffect(() => { + trackEvent( + createEventBuilder(MetaMetricsEvents.RAMPS_SCREEN_VIEWED) + .addProperties({ + location: 'Token Unavailable Modal', + ramp_type: 'UNIFIED_BUY_2', + }) + .build(), + ); + }, [trackEvent, createEventBuilder]); + const handleChangeToken = useCallback(() => { + trackEvent( + createEventBuilder(MetaMetricsEvents.RAMPS_CHANGE_TOKEN_BUTTON_CLICKED) + .addProperties({ + current_provider: selectedProvider?.name, + location: 'Token Unavailable Modal', + ramp_type: 'UNIFIED_BUY_2', + }) + .build(), + ); sheetRef.current?.onCloseBottomSheet(() => { navigation.navigate(Routes.RAMP.TOKEN_SELECTION, { screen: Routes.RAMP.TOKEN_SELECTION, }); }); - }, [navigation]); + }, [navigation, selectedProvider?.name, trackEvent, createEventBuilder]); const handleChangeProvider = useCallback(() => { trackEvent( @@ -84,14 +104,33 @@ function TokenNotAvailableModal() { ]); const handleClose = useCallback(() => { + trackEvent( + createEventBuilder(MetaMetricsEvents.RAMPS_CLOSE_BUTTON_CLICKED) + .addProperties({ + location: 'Token Unavailable Modal', + ramp_type: 'UNIFIED_BUY_2', + }) + .build(), + ); sheetRef.current?.onCloseBottomSheet(); - }, []); + }, [trackEvent, createEventBuilder]); + + const handleDismiss = useCallback( + (hasPendingAction?: boolean) => { + if (!hasPendingAction) { + navigation.navigate(Routes.RAMP.TOKEN_SELECTION, { + screen: Routes.RAMP.TOKEN_SELECTION, + }); + } + }, + [navigation], + ); return ( { transactionMetadata && hasTransactionType(transactionMetadata, [TransactionType.predictClaim]) ) { - return ; + return ; } return ( diff --git a/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.test.tsx b/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.test.tsx index a9d819e3f0e..44717775388 100644 --- a/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.test.tsx +++ b/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.test.tsx @@ -9,12 +9,17 @@ import { otherControllersMock, } from '../../../__mocks__/controllers/other-controllers-mock'; import { strings } from '../../../../../../../locales/i18n'; -import { fireEvent } from '@testing-library/react-native'; +import { fireEvent, waitFor } from '@testing-library/react-native'; function render({ onPress, + onError, singlePosition, -}: { onPress?: () => void; singlePosition?: boolean } = {}) { +}: { + onPress?: () => void; + onError?: (error?: Error) => void; + singlePosition?: boolean; +} = {}) { const state = merge( {}, simpleSendTransactionControllerMock, @@ -31,9 +36,12 @@ function render({ }; } - return renderWithProvider(, { - state, - }); + return renderWithProvider( + , + { + state, + }, + ); } describe('PredictClaimFooter', () => { @@ -67,9 +75,10 @@ describe('PredictClaimFooter', () => { expect(onPressMock).toHaveBeenCalled(); }); - it('uses fallback address when selectedAddress is undefined', () => { - // Arrange - state with no selected account address - const stateWithNoAddress = merge( + it('calls onError when there are no won positions', async () => { + // Arrange - state with transaction from address that has no claimable positions + const onErrorMock = jest.fn(); + const state = merge( {}, simpleSendTransactionControllerMock, transactionApprovalControllerMock, @@ -77,10 +86,12 @@ describe('PredictClaimFooter', () => { { engine: { backgroundState: { - AccountsController: { - internalAccounts: { - selectedAccount: undefined, - }, + TransactionController: { + transactions: [ + { + txParams: { from: '0xunknown' }, + }, + ], }, }, }, @@ -88,13 +99,18 @@ describe('PredictClaimFooter', () => { ); // Act - const { getByTestId } = renderWithProvider( - , - { state: stateWithNoAddress }, + const { queryByTestId } = renderWithProvider( + , + { state }, ); - // Assert - component renders without crashing - expect(getByTestId('predict-claim-footer')).toBeDefined(); + // Assert - component returns null and calls onError + expect(queryByTestId('predict-claim-footer')).toBeNull(); + await waitFor(() => { + expect(onErrorMock).toHaveBeenCalledWith( + new Error('Tried to claim but no positions were won'), + ); + }); }); it('renders extra info for single win', () => { diff --git a/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.tsx b/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.tsx index 5ad13515ff6..ba6d30405f0 100644 --- a/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.tsx +++ b/app/components/Views/confirmations/components/predict-confirmations/predict-claim-footer/predict-claim-footer.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { strings } from '../../../../../../../locales/i18n'; import Avatar, { @@ -15,31 +15,43 @@ import { Box } from '../../../../../UI/Box/Box'; import { PredictClaimConfirmationSelectorsIDs } from '../../../../../UI/Predict/Predict.testIds'; import styleSheet from './predict-claim-footer.styles'; import { selectPredictWonPositions } from '../../../../../UI/Predict/selectors/predictController'; -import { selectSelectedInternalAccountAddress } from '../../../../../../selectors/accountsController'; import { PredictPosition } from '../../../../../UI/Predict'; import { AlignItems, FlexDirection } from '../../../../../UI/Box/box.types'; import useFiatFormatter from '../../../../../UI/SimulationDetails/FiatDisplay/useFiatFormatter'; import { BigNumber } from 'bignumber.js'; import ButtonHero from '../../../../../../component-library/components-temp/Buttons/ButtonHero'; import { ButtonBaseSize } from '@metamask/design-system-react-native'; +import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest'; export interface PredictClaimFooterProps { onPress: () => void; + onError: (error?: Error) => void; } -export function PredictClaimFooter({ onPress }: PredictClaimFooterProps) { +export function PredictClaimFooter({ + onPress, + onError, +}: PredictClaimFooterProps) { + const transactionMetadata = useTransactionMetadataRequest(); const { styles } = useStyles(styleSheet, {}); - const selectedAddress = - useSelector(selectSelectedInternalAccountAddress) ?? '0x0'; + const address = transactionMetadata?.txParams.from; const wonPositions = useSelector( selectPredictWonPositions({ - address: selectedAddress, + address: address ?? '0x', }), ); - if (!wonPositions?.length) { + const hasNoPositions = !address || !wonPositions?.length; + + useEffect(() => { + if (hasNoPositions) { + onError(new Error('Tried to claim but no positions were won')); + } + }, [hasNoPositions, onError]); + + if (hasNoPositions) { return null; } diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index 832d8247937..53387cc656f 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -318,6 +318,7 @@ enum EVENT_NAME { RAMPS_PAYMENT_METHOD_SELECTOR_CLICKED = 'Ramps Payment Method Selector Clicked', RAMPS_QUICK_AMOUNT_CLICKED = 'Ramps Quick Amount Clicked', RAMPS_CHANGE_PROVIDER_BUTTON_CLICKED = 'Ramps Change Provider Button Clicked', + RAMPS_CHANGE_TOKEN_BUTTON_CLICKED = 'Ramps Change Token Button Clicked', RAMPS_PROVIDER_SELECTED = 'Ramps Provider Selected', RAMPS_CONTINUE_BUTTON_CLICKED = 'Ramps Continue Button Clicked', RAMPS_TERMS_CONSENT_CLICKED = 'Ramps Terms Consent Clicked', @@ -1117,6 +1118,9 @@ const events = { RAMPS_CHANGE_PROVIDER_BUTTON_CLICKED: generateOpt( EVENT_NAME.RAMPS_CHANGE_PROVIDER_BUTTON_CLICKED, ), + RAMPS_CHANGE_TOKEN_BUTTON_CLICKED: generateOpt( + EVENT_NAME.RAMPS_CHANGE_TOKEN_BUTTON_CLICKED, + ), RAMPS_PROVIDER_SELECTED: generateOpt(EVENT_NAME.RAMPS_PROVIDER_SELECTED), RAMPS_CONTINUE_BUTTON_CLICKED: generateOpt( EVENT_NAME.RAMPS_CONTINUE_BUTTON_CLICKED, diff --git a/app/core/AppConstants.ts b/app/core/AppConstants.ts index 595ba62d267..744f23ba4f9 100644 --- a/app/core/AppConstants.ts +++ b/app/core/AppConstants.ts @@ -95,6 +95,7 @@ export default { }, }, MM_UNIVERSAL_LINK_HOST: 'metamask.app.link', + MM_UNIVERSAL_LINK_HOST_ALTERNATE: 'metamask-alternate.app.link', MM_IO_UNIVERSAL_LINK_HOST: 'link.metamask.io', MM_IO_UNIVERSAL_LINK_TEST_HOST: 'link-test.metamask.io', MM_DEEP_ITMS_APP_LINK: 'https://metamask.app.link/skAH3BaF99', diff --git a/app/core/DeeplinkManager/DeeplinkManager.test.ts b/app/core/DeeplinkManager/DeeplinkManager.test.ts index 5aa1055957f..f77f417a66f 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.test.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.test.ts @@ -2,7 +2,11 @@ import { NavigationProp, ParamListBase } from '@react-navigation/native'; import { waitFor } from '@testing-library/react-native'; import FCMService from '../../util/notifications/services/FCMService'; import NavigationService from '../NavigationService'; -import SharedDeeplinkManager, { DeeplinkManager } from './DeeplinkManager'; +import SharedDeeplinkManager, { + DeeplinkManager, + rewriteBranchUri, +} from './DeeplinkManager'; +import type { BranchParams } from './types/deepLinkAnalytics.types'; import { handleDeeplink } from './handlers/legacy/handleDeeplink'; import switchNetwork from '../../util/networks/switchNetwork'; import parseDeeplink from './utils/parseDeeplink'; @@ -282,6 +286,34 @@ describe('SharedDeeplinkManager', () => { }); }); +describe('rewriteBranchUri', () => { + it('rewrites host and path to link.metamask.io and preserves query when +clicked_branch_link and $deeplink_path are set', () => { + const uri = + 'https://metamask-alternate.app.link/1WkF6GmE40b?amount=100&from=0x'; + const params: BranchParams = { + '+clicked_branch_link': true, + $deeplink_path: 'swap', + }; + expect(rewriteBranchUri(uri, params)).toBe( + 'https://link.metamask.io/swap?amount=100&from=0x', + ); + }); + + it('returns uri unchanged when +clicked_branch_link is false', () => { + const uri = 'https://metamask.app.link/swap'; + expect( + rewriteBranchUri(uri, { '+clicked_branch_link': false } as BranchParams), + ).toBe(uri); + }); + + it('returns uri unchanged when $deeplink_path is missing', () => { + const uri = 'https://metamask.app.link/swap'; + expect( + rewriteBranchUri(uri, { '+clicked_branch_link': true } as BranchParams), + ).toBe(uri); + }); +}); + describe('DeeplinkManager.start Branch deeplink handling', () => { beforeEach(() => { jest.clearAllMocks(); @@ -304,6 +336,31 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockDeeplink }); }); + it('rewrites cold start Branch link using $deeplink_path from getLatestReferringParams', async () => { + (branch.getLatestReferringParams as jest.Mock).mockResolvedValue({ + '+clicked_branch_link': true, + $deeplink_path: 'swap', + '~referring_link': + 'https://metamask-alternate.app.link/1WkF6GmE40b?amount=500', + }); + DeeplinkManager.start(); + await new Promise((resolve) => setImmediate(resolve)); + expect(handleDeeplink).toHaveBeenCalledWith({ + uri: 'https://link.metamask.io/swap?amount=500', + }); + }); + + it('falls back to +non_branch_link on cold start when +clicked_branch_link is false', async () => { + const mockDeeplink = 'https://link.metamask.io/home'; + (branch.getLatestReferringParams as jest.Mock).mockResolvedValue({ + '+clicked_branch_link': false, + '+non_branch_link': mockDeeplink, + }); + DeeplinkManager.start(); + await new Promise((resolve) => setImmediate(resolve)); + expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockDeeplink }); + }); + it('subscribes to Branch deeplink events', async () => { DeeplinkManager.start(); expect(branch.subscribe).toHaveBeenCalled(); @@ -318,4 +375,68 @@ describe('DeeplinkManager.start Branch deeplink handling', () => { await new Promise((resolve) => setImmediate(resolve)); expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri }); }); + + it('rewrites Branch short link to link.metamask.io when +clicked_branch_link and $deeplink_path are present', async () => { + DeeplinkManager.start(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + + callback({ + uri: 'https://metamask-alternate.app.link/1WkF6GmE40b?amount=1000000&from=eip155%3A1%2Ferc20%3A0xabc', + params: { + '+clicked_branch_link': true, + $deeplink_path: 'swap', + }, + }); + + await new Promise((resolve) => setImmediate(resolve)); + expect(handleDeeplink).toHaveBeenCalledWith({ + uri: 'https://link.metamask.io/swap?amount=1000000&from=eip155%3A1%2Ferc20%3A0xabc', + }); + }); + + it('passes URI through unchanged when +clicked_branch_link is false', async () => { + DeeplinkManager.start(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + const mockUri = 'https://metamask.app.link/swap?amount=100'; + + callback({ + uri: mockUri, + params: { '+clicked_branch_link': false }, + }); + + await new Promise((resolve) => setImmediate(resolve)); + expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri }); + }); + + it('passes URI through unchanged when $deeplink_path is missing', async () => { + DeeplinkManager.start(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + const mockUri = 'https://metamask.app.link/swap?amount=100'; + + callback({ + uri: mockUri, + params: { '+clicked_branch_link': true }, + }); + + await new Promise((resolve) => setImmediate(resolve)); + expect(handleDeeplink).toHaveBeenCalledWith({ uri: mockUri }); + }); + + it('strips leading slash from $deeplink_path when rewriting', async () => { + DeeplinkManager.start(); + const callback = (branch.subscribe as jest.Mock).mock.calls[0][0]; + + callback({ + uri: 'https://metamask-alternate.app.link/ABC123', + params: { + '+clicked_branch_link': true, + $deeplink_path: '/swap/token', + }, + }); + + await new Promise((resolve) => setImmediate(resolve)); + expect(handleDeeplink).toHaveBeenCalledWith({ + uri: 'https://link.metamask.io/swap/token', + }); + }); }); diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index 82592138411..1c145b39b6d 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -7,6 +7,33 @@ import Logger from '../../util/Logger'; import { handleDeeplink } from './handlers/legacy/handleDeeplink'; import FCMService from '../../util/notifications/services/FCMService'; import AppConstants from '../AppConstants'; +import { BranchParams } from './types/deepLinkAnalytics.types'; + +/** + * When Branch resolves a short link (e.g. metamask-alternate.app.link/1WkF6GmE40b), + * the URI path may be link ID, not an in-app route. If the resolved params indicate + * a clicked Branch link with a $deeplink_path, replace the host and path segment + * with link.metamask.io/$deeplink_path while preserving the original query string. + */ +export function rewriteBranchUri( + uri: string | undefined, + params: BranchParams | undefined, +): string | undefined { + try { + if (!uri || !params?.['+clicked_branch_link']) return uri; + const rawPath = params.$deeplink_path; + if (typeof rawPath !== 'string') return uri; + + const parsed = new URL(uri); + parsed.host = AppConstants.MM_IO_UNIVERSAL_LINK_HOST; + // Set the pathname to the sanitized $deeplink_path + parsed.pathname = `/${rawPath.replace(/^\//, '')}`; + return parsed.toString(); + } catch (error) { + Logger.error(error as Error, `Error rewriting Branch URI: ${uri}`); + return uri; + } +} export class DeeplinkManager { // singleton instance @@ -66,6 +93,17 @@ export class DeeplinkManager { try { const latestParams = await branch.getLatestReferringParams(); + + // Cold start: params may contain a resolved Branch link with $deeplink_path. + const rewritten = rewriteBranchUri( + latestParams?.['~referring_link'] as string | undefined, + latestParams as Record | undefined, + ); + if (rewritten) { + handleDeeplink({ uri: rewritten }); + return; + } + const deeplink = latestParams?.['+non_branch_link'] as string; if (deeplink) { handleDeeplink({ uri: deeplink }); @@ -117,12 +155,11 @@ export class DeeplinkManager { const branchError = new Error(error); Logger.error(branchError, 'Error subscribing to branch.'); } - getBranchDeeplink(opts.uri); - //TODO: that async call in the subscribe doesn't look good to me - branch.getLatestReferringParams().then((val) => { - const deeplink = opts.uri || (val['+non_branch_link'] as string); - handleDeeplink({ uri: deeplink }); - }); + const rewritten = rewriteBranchUri( + opts.uri, + opts.params as Record | undefined, + ); + getBranchDeeplink(rewritten ?? opts.uri); }); } } diff --git a/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts b/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts index 7a4a84d625c..e82b3472119 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleUniversalLink.ts @@ -57,6 +57,7 @@ import Logger from '../../../../util/Logger'; const { MM_UNIVERSAL_LINK_HOST, + MM_UNIVERSAL_LINK_HOST_ALTERNATE, MM_IO_UNIVERSAL_LINK_HOST, MM_IO_UNIVERSAL_LINK_TEST_HOST, } = AppConstants; @@ -216,6 +217,7 @@ async function handleUniversalLink({ const isSupportedDomain = urlObj.hostname === MM_UNIVERSAL_LINK_HOST || + urlObj.hostname === MM_UNIVERSAL_LINK_HOST_ALTERNATE || urlObj.hostname === MM_IO_UNIVERSAL_LINK_HOST || urlObj.hostname === MM_IO_UNIVERSAL_LINK_TEST_HOST; diff --git a/app/core/DeeplinkManager/util/deeplinks/index.ts b/app/core/DeeplinkManager/util/deeplinks/index.ts index 86965010401..530628cb4e3 100644 --- a/app/core/DeeplinkManager/util/deeplinks/index.ts +++ b/app/core/DeeplinkManager/util/deeplinks/index.ts @@ -2,6 +2,7 @@ import AppConstants from '../../../AppConstants'; const { MM_UNIVERSAL_LINK_HOST, + MM_UNIVERSAL_LINK_HOST_ALTERNATE, MM_IO_UNIVERSAL_LINK_HOST, MM_IO_UNIVERSAL_LINK_TEST_HOST, } = AppConstants; @@ -10,6 +11,7 @@ const METAMASK_HOSTS = [ ...new Set( [ MM_UNIVERSAL_LINK_HOST || 'link.metamask.io', + MM_UNIVERSAL_LINK_HOST_ALTERNATE || 'metamask-alternate.app.link', MM_IO_UNIVERSAL_LINK_HOST || 'link.metamask.io', MM_IO_UNIVERSAL_LINK_TEST_HOST || 'link-test.metamask.io', 'metamask.app.link', diff --git a/tests/framework/fixtures/FixtureBuilder.ts b/tests/framework/fixtures/FixtureBuilder.ts index 433486b0e6f..3278bcb4b66 100644 --- a/tests/framework/fixtures/FixtureBuilder.ts +++ b/tests/framework/fixtures/FixtureBuilder.ts @@ -44,7 +44,20 @@ import { MOCK_ENTROPY_SOURCE_3, } from '../../../app/util/test/keyringControllerTestUtils.ts'; import { NetworkEnablementControllerState } from '@metamask/network-enablement-controller'; +import { RpcEndpointType } from '@metamask/network-controller'; import { USDC_MAINNET, MUSD_MAINNET } from '../../constants/musd-mainnet.ts'; +import type { + Fixture, + ProviderConfig, + PermissionControllerState, + SnapControllerState, + TokenInfo, + UserKeyringState, + UserSnapState, + UserPermissionState, +} from './types.ts'; +import type { PreferencesState } from '@metamask/preferences-controller'; +import type { AccountTreeControllerState } from '@metamask/account-tree-controller'; export const DEFAULT_FIXTURE_ACCOUNT_CHECKSUM = '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3'; @@ -91,14 +104,12 @@ export interface MusdFixtureOptions { * FixtureBuilder class provides a fluent interface for building fixture data. */ class FixtureBuilder { - // We currently have no type representation of the whole fixture state - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private fixture: any; + private fixture!: Fixture; /** * Create a new instance of FixtureBuilder. - * @param {Object} options - Options for the fixture builder. - * @param {boolean} options.onboarding - Flag indicating if onboarding fixture should be used. + * @param options - Options for the fixture builder. + * @param options.onboarding - Flag indicating if onboarding fixture should be used. */ constructor({ onboarding = false } = {}) { // Initialize the fixture based on the onboarding flag @@ -109,27 +120,17 @@ class FixtureBuilder { /** * Set the asyncState property of the fixture. - * @param {any} asyncState - The value to set for asyncState. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param asyncState - The value to set for asyncState. + * @returns - The FixtureBuilder instance for method chaining. */ - withAsyncState(asyncState: Record) { + withAsyncState(asyncState: Record) { this.fixture.asyncState = asyncState; return this; } - /** - * Set the state property of the fixture. - * @param {any} state - The value to set for state. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. - */ - withState(state: Record) { - this.fixture.state = state; - return this; - } - /** * Ensures that the Solana feature modal is suppressed by adding the appropriate flag to asyncState. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ ensureSolanaModalSuppressed() { if (!this.fixture.asyncState) { @@ -141,11 +142,11 @@ class FixtureBuilder { /** * Ensures that the multichain accounts intro modal is suppressed by setting the appropriate flag. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ ensureMultichainIntroModalSuppressed() { if (!this.fixture?.state?.user) { - this.fixture.state.user = {}; + this.fixture.state.user = {} as typeof this.fixture.state.user; } this.fixture.state.user.multichainAccountsIntroModalSeen = true; return this; @@ -155,8 +156,7 @@ class FixtureBuilder { * Defines a Perps profile for E2E mocks. * The value is stored in the PerpsController state so that the mocks can read it. * @param profile Profile, e.g.: 'no-funds', 'default'. - * @returns {FixtureBuilder} - */ + * @returns */ withPerpsProfile(profile: string) { merge(this.fixture.state.engine.backgroundState.PerpsController, { // Field only for E2E; read by the mocks mixin @@ -195,7 +195,7 @@ class FixtureBuilder { /** * Set the showTestNetworks property of the fixture to false. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withTestNetworksOff() { this.fixture.state.engine.backgroundState.PreferencesController.showTestNetworks = false; @@ -205,7 +205,7 @@ class FixtureBuilder { /** * Set the default fixture values. * Uses JSON-based fixture with runtime-injected dynamic values. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withDefaultFixture() { // Deep clone the JSON fixture to avoid mutations @@ -237,7 +237,7 @@ class FixtureBuilder { { networkClientId, name: 'Localhost default RPC', - type: 'custom', + type: RpcEndpointType.Custom, url: '', }, ], @@ -251,56 +251,49 @@ class FixtureBuilder { /** * Merges provided data into the background state of the PermissionController. - * @param {object} data - Data to merge into the PermissionController's state. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param data - Data to merge into the PermissionController's state. + * @returns - The FixtureBuilder instance for method chaining. */ - withPermissionController(data: Record) { + withPermissionController(data: Partial) { merge(this.fixture.state.engine.backgroundState.PermissionController, data); return this; } /** - * Merges provided data into the background state of the NetworkController. - * @param {object} data - Data to merge into the NetworkController's state. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * Configures the NetworkController with a custom network using a provider config. + * @param providerConfig - The provider configuration for the new network. + * @returns - The FixtureBuilder instance for method chaining. */ - withNetworkController(data: Record) { + withNetworkController(providerConfig: ProviderConfig) { const networkController = this.fixture.state.engine.backgroundState.NetworkController; - // Extract providerConfig data - const { providerConfig } = data as { - providerConfig: Record; - }; - - // Generate a unique key for the new network client ID const newNetworkClientId = `networkClientId${ Object.keys(networkController.networkConfigurationsByChainId).length + 1 }`; - // Define the network configuration - const networkConfig = { + // NetworkConfiguration type is more specific than our ProviderConfig; cast is safe here + ( + networkController.networkConfigurationsByChainId as Record< + string, + unknown + > + )[providerConfig.chainId] = { chainId: providerConfig.chainId, rpcEndpoints: [ { networkClientId: newNetworkClientId, url: providerConfig.rpcUrl, - type: providerConfig.type, + type: providerConfig.type as 'custom' | 'infura', name: providerConfig.nickname, }, ], defaultRpcEndpointIndex: 0, blockExplorerUrls: [], - name: providerConfig.nickname, - nativeCurrency: providerConfig.ticker, + name: providerConfig.nickname ?? '', + nativeCurrency: providerConfig.ticker ?? 'ETH', }; - // Add the new network configuration to the object - networkController.networkConfigurationsByChainId[ - providerConfig.chainId as string - ] = networkConfig; - - // Update selectedNetworkClientId to the new network client ID networkController.selectedNetworkClientId = newNetworkClientId; return this; } @@ -308,8 +301,8 @@ class FixtureBuilder { /** * Private helper method to create permission controller configuration * @private - * @param {Object} additionalPermissions - Additional permissions to merge with permission - * @returns {Object} Permission controller configuration object + * @param additionalPermissions - Additional permissions to merge with permission + * @returns Permission controller configuration object */ createPermissionControllerConfig( additionalPermissions: Record = {}, @@ -373,8 +366,8 @@ class FixtureBuilder { /** * Connects the PermissionController to a test dapp with specific accounts permissions and origins. - * @param {Object} additionalPermissions - Additional permissions to merge. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param additionalPermissions - Additional permissions to merge. + * @returns - The FixtureBuilder instance for method chaining. */ withPermissionControllerConnectedToTestDapp( additionalPermissions = {}, @@ -399,8 +392,8 @@ class FixtureBuilder { } /** - * @param {RampsRegion | null} region - The region to set, or null for default (Saint Lucia). - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param region - The region to set, or null for default (Saint Lucia). + * @returns - The FixtureBuilder instance for method chaining. * @example * new FixtureBuilder() * .withRampsSelectedRegion(RampsRegions[RampsRegionsEnum.UNITED_STATES]) @@ -495,7 +488,7 @@ class FixtureBuilder { /** * Sets the selected payment method for the fiat orders. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withRampsSelectedPaymentMethod() { const paymentType = '/payments/debit-credit-card'; @@ -508,8 +501,8 @@ class FixtureBuilder { /** * Sets detected geolocation (e.g. for RWA/Stocks section visibility in Trending). * Use a non-restricted country code so RWA data is shown when not in __DEV__ (e.g. CI). - * @param {string} countryCode - ISO 3166-2 location code (e.g. 'AR', 'US-NY'). - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param countryCode - ISO country code (e.g. 'AR' for Argentina). + * @returns - The FixtureBuilder instance for method chaining. */ withDetectedGeolocation(countryCode: string) { merge(this.fixture.state.engine.backgroundState, { @@ -525,8 +518,8 @@ class FixtureBuilder { /** * 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 - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param chainIds - Array of chain IDs to permit (defaults to ['0x1']), other nexts like linea mainnet 0xe708 + * @returns - The FixtureBuilder instance for method chaining. */ withChainPermission(chainIds: `0x${string}`[] = ['0x1']) { const optionalScopes = chainIds @@ -573,7 +566,7 @@ class FixtureBuilder { /** * Adds Solana account permissions for default fixture account. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withSolanaAccountPermission() { const caveatValue = { @@ -610,19 +603,23 @@ class FixtureBuilder { /** * Sets the user profile key ring in the fixture's background state. - * @param {object} userState - The user state to set. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param userState - The user state to set. + * @returns - The FixtureBuilder instance for method chaining. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - withUserProfileKeyRing(userState: any) { + withUserProfileKeyRing(userState: UserKeyringState) { merge( this.fixture.state.engine.backgroundState.KeyringController, userState.KEYRING_CONTROLLER_STATE, ); // Add accounts controller with the first account selected - const firstAccountAddress = - userState.KEYRING_CONTROLLER_STATE.keyrings[0].accounts[0]; + const keyrings = userState.KEYRING_CONTROLLER_STATE.keyrings; + if (!keyrings?.[0]?.accounts?.[0]) { + throw new Error( + 'withUserProfileKeyRing: userState must contain at least one keyring with one account', + ); + } + const firstAccountAddress = keyrings[0].accounts[0]; const accountId = '4d7a5e0b-b261-4aed-8126-43972b0fa0a1'; merge(this.fixture.state.engine.backgroundState.AccountsController, { @@ -659,11 +656,10 @@ class FixtureBuilder { /** * Sets the user profile snap unencrypted state in the fixture's background state. - * @param {object} userState - The user state to set. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param userState - The user state to set. + * @returns - The FixtureBuilder instance for method chaining. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - withUserProfileSnapUnencryptedState(userState: any) { + withUserProfileSnapUnencryptedState(userState: UserSnapState) { merge( this.fixture.state.engine.backgroundState.SnapController, userState.SNAPS_CONTROLLER_STATE, @@ -674,11 +670,10 @@ class FixtureBuilder { /** * Sets the user profile snap permissions in the fixture's background state. - * @param {object} userState - The user state to set. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param userState - The user state to set. + * @returns - The FixtureBuilder instance for method chaining. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - withUserProfileSnapPermissions(userState: any) { + withUserProfileSnapPermissions(userState: UserPermissionState) { merge( this.fixture.state.engine.backgroundState.PermissionController, userState.PERMISSION_CONTROLLER_STATE, @@ -690,12 +685,11 @@ class FixtureBuilder { * Sets the tokens for all popular networks in the fixture's background state. * @param tokens - The tokens to set. * @param userState - The user state to set. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withTokensForAllPopularNetworks( - tokens: Record[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - userState: any = null, + tokens: TokenInfo[], + userState: UserKeyringState | null = null, ) { // Get all popular network chain IDs using proper constants const popularChainIds = [ @@ -712,30 +706,27 @@ class FixtureBuilder { // Use userState accounts if provided, otherwise fall back to MULTIPLE_ACCOUNTS_ACCOUNTS_CONTROLLER let allAccountAddresses: string[] = []; - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - if (userState && userState.KEYRING_CONTROLLER_STATE) { + if (userState?.KEYRING_CONTROLLER_STATE) { // Extract all account addresses from the user state keyring - allAccountAddresses = userState.KEYRING_CONTROLLER_STATE.keyrings.flatMap( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (keyring: any) => keyring.accounts, - ); + allAccountAddresses = ( + userState.KEYRING_CONTROLLER_STATE.keyrings ?? [] + ).flatMap((keyring) => keyring.accounts); } else { // Fallback to the hardcoded accounts const accountsData = MULTIPLE_ACCOUNTS_ACCOUNTS_CONTROLLER.internalAccounts.accounts; allAccountAddresses = Object.values(accountsData).map( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (account: any) => account.address, + (account: { address: string }) => account.address, ); } // Create tokens object for all accounts - const accountTokens: Record[]> = {}; + const accountTokens: Record = {}; allAccountAddresses.forEach((address) => { accountTokens[address] = tokens; }); - const allTokens: Record> = {}; + const allTokens: Record> = {}; // Add tokens to each popular network popularChainIds.forEach((chainId) => { @@ -762,8 +753,7 @@ class FixtureBuilder { popularChainIds.forEach((chainId) => { tokenBalances[accountAddress][chainId] = {}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - tokens.forEach((token: any, tokenIndex: number) => { + tokens.forEach((token, tokenIndex) => { // Generate realistic but varied balances for testing // Using different multipliers to create variety across accounts and tokens const baseBalance = (accountIndex + 1) * (tokenIndex + 1) * 1000; @@ -790,7 +780,7 @@ class FixtureBuilder { /** * Set the fixture to an empty object for onboarding. * Uses JSON-based fixture for consistency. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withOnboardingFixture() { // Deep clone the JSON fixture to avoid mutations @@ -820,7 +810,7 @@ class FixtureBuilder { { networkClientId: newNetworkClientId, url: `http://localhost:${port}`, - type: 'custom', + type: RpcEndpointType.Custom, name: 'Localhost', }, ], @@ -832,8 +822,12 @@ class FixtureBuilder { }; // Add the new Ganache network configuration - fixtures.NetworkController.networkConfigurationsByChainId[chainId] = - ganacheNetworkConfig; + ( + fixtures.NetworkController.networkConfigurationsByChainId as Record< + string, + unknown + > + )[chainId] = ganacheNetworkConfig; // Update selectedNetworkClientId to the new network client ID fixtures.NetworkController.selectedNetworkClientId = newNetworkClientId; @@ -861,7 +855,7 @@ class FixtureBuilder { { networkClientId: newNetworkClientId, url: sepoliaConfig.rpcUrl, - type: 'custom', + type: RpcEndpointType.Custom, name: sepoliaConfig.nickname, }, ], @@ -872,9 +866,12 @@ class FixtureBuilder { }; // Add the new Sepolia network configuration - fixtures.NetworkController.networkConfigurationsByChainId[ - sepoliaConfig.chainId - ] = sepoliaNetworkConfig; + ( + fixtures.NetworkController.networkConfigurationsByChainId as Record< + string, + unknown + > + )[sepoliaConfig.chainId] = sepoliaNetworkConfig; // Update selectedNetworkClientId to the new network client ID fixtures.NetworkController.selectedNetworkClientId = newNetworkClientId; @@ -904,7 +901,7 @@ class FixtureBuilder { { networkClientId: newNetworkClientId, url: `http://localhost:${getMockServerPortForFixture()}/proxy?url=https://polygon-mainnet.infura.io/v3/${infuraProjectId}`, - type: 'custom', + type: RpcEndpointType.Custom, name: 'Polygon Localhost', }, ], @@ -915,8 +912,12 @@ class FixtureBuilder { nativeCurrency: 'MATIC', }; - fixtures.NetworkController.networkConfigurationsByChainId[chainId] = - polygonNetworkConfig; + ( + fixtures.NetworkController.networkConfigurationsByChainId as Record< + string, + unknown + > + )[chainId] = polygonNetworkConfig; fixtures.NetworkController.selectedNetworkClientId = newNetworkClientId; @@ -952,7 +953,7 @@ class FixtureBuilder { { networkClientId: newNetworkClientId, url: rpcTarget, - type: 'custom', + type: RpcEndpointType.Custom, name: nickname, }, ], @@ -963,7 +964,8 @@ class FixtureBuilder { }; // Add the new network configuration to the object - networkConfigurationsByChainId[chainId] = networkConfig; + (networkConfigurationsByChainId as Record)[chainId] = + networkConfig; } // Assign networkConfigurationsByChainId object to NetworkController in fixtures @@ -980,7 +982,7 @@ class FixtureBuilder { * Sets the privacy mode preferences in the fixture's asyncState. * This indicates that the user has agreed to MetaMetrics data collection. * - * @returns {FixtureBuilder} The current instance for method chaining. + * @returns The current instance for method chaining. */ withPrivacyModePreferences(privacyMode: boolean) { merge(this.fixture.state.engine.backgroundState.PreferencesController, { @@ -1000,7 +1002,12 @@ class FixtureBuilder { return this; } - withPreferencesController(data: Record) { + /** + * Merges provided data into the background state of the PreferencesController. + * @param data - Data to merge into the PreferencesController's state. + * @returns - The FixtureBuilder instance for method chaining. + */ + withPreferencesController(data: Partial) { merge( this.fixture.state.engine.backgroundState.PreferencesController, data, @@ -1012,8 +1019,9 @@ class FixtureBuilder { * Merges provided data into the KeyringController's state with a random imported account. * and also includes the default HD Key Tree fixture account. * - * @param {Object} account - ethers.Wallet object containing address and privateKey. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param address - The account address to import. + * @param privateKey - The private key for the imported account. + * @returns - The FixtureBuilder instance for method chaining. */ withRandomImportedAccountKeyringController( address: string, @@ -1306,7 +1314,7 @@ class FixtureBuilder { /** * Enables profile syncing in the fixture. - * @returns {this} The current instance for method chaining. + * @returns The current instance for method chaining. */ withKeyringControllerOfMultipleAccounts() { merge(this.fixture.state.engine.backgroundState.KeyringController, { @@ -1390,7 +1398,7 @@ class FixtureBuilder { /** * Enables profile syncing in the fixture. - * @returns {this} The current instance for method chaining. + * @returns The current instance for method chaining. */ withProfileSyncingEnabled() { // Enable AuthenticationController - user must be signed in for profile syncing @@ -1417,7 +1425,7 @@ class FixtureBuilder { /** * Disables profile syncing in the fixture. - * @returns {this} The current instance for method chaining. + * @returns The current instance for method chaining. */ withProfileSyncingDisabled() { merge(this.fixture.state.engine.backgroundState.UserStorageController, { @@ -1485,7 +1493,7 @@ class FixtureBuilder { * and enables the AnalyticsController. * This indicates that the user has agreed to MetaMetrics data collection. * - * @returns {this} The current instance for method chaining. + * @returns The current instance for method chaining. */ withMetaMetricsOptIn() { if (!this.fixture.asyncState) { @@ -1505,8 +1513,8 @@ class FixtureBuilder { * Adds multiple test dapp tabs to the browser state. * This is intended to be used for testing multiple dapps concurrently. * The dapps are opened in the order they are added. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. - * @param {number} extraTabs - The amount of extra tabs to open. + * @returns - The FixtureBuilder instance for method chaining. + * @param extraTabs - The amount of extra tabs to open. */ withExtraTabs(extraTabs = 1) { if (!this.fixture.state.browser.tabs) { @@ -1527,12 +1535,13 @@ class FixtureBuilder { /** * Sets ETH as the primary currency for both currency rate controller and settings. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withETHAsPrimaryCurrency() { this.fixture.state.engine.backgroundState.CurrencyRateController.currentCurrency = 'ETH'; - this.fixture.state.settings.primaryCurrency = 'ETH'; + (this.fixture.state.settings as Record).primaryCurrency = + 'ETH'; return this; } @@ -1564,7 +1573,7 @@ class FixtureBuilder { * Disables the seedphraseBackedUp flag in the user state. * This is useful for testing scenarios where the user hasn't backed up their seedphrase. * - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining + * @returns - The FixtureBuilder instance for method chaining */ withSeedphraseBackedUpDisabled() { this.fixture.state.user.seedphraseBackedUp = false; @@ -1577,10 +1586,10 @@ class FixtureBuilder { * with pre-defined grouping rules. Uses existing entropy sources (MOCK_ENTROPY_SOURCE), * real keyring types (KeyringTypes.hd, .qr, .simple), and actual Snap IDs from the codebase. * If custom wallets are provided, they completely replace the defaults. - * @param {object} data - Data to merge into the AccountTreeController's state. Optional. - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @param data - Data to merge into the AccountTreeController's state. Optional. + * @returns - The FixtureBuilder instance for method chaining. */ - withAccountTreeController(data: Record = {}) { + withAccountTreeController(data: Partial = {}) { // Define a comprehensive default state following @metamask/account-tree-controller specs // Leverages existing keyring types, entropy sources (MOCK_ENTROPY_SOURCE*), and real Snap IDs from the codebase const defaultAccountTreeState = { @@ -1844,7 +1853,7 @@ class FixtureBuilder { return this; } - withSnapController(data: Record = {}) { + withSnapController(data: Partial = {}) { merge(this.fixture.state.engine.backgroundState.SnapController, data); return this; } @@ -1948,7 +1957,7 @@ class FixtureBuilder { * Call after withNetworkController, withTokensForAllPopularNetworks([ETH, USDC, MUSD?]), and withTokenRates. * * @param options - mUSD conversion options (education seen, USDC/MUSD balances). - * @returns {FixtureBuilder} - The FixtureBuilder instance for method chaining. + * @returns - The FixtureBuilder instance for method chaining. */ withMusdConversion(options: MusdFixtureOptions) { const USDC_DECIMALS = 6; @@ -2036,7 +2045,7 @@ class FixtureBuilder { /** * Build and return the fixture object. - * @returns {Object} - The built fixture object. + * @returns - The built fixture object. */ build() { return this.fixture; diff --git a/tests/framework/fixtures/FixtureHelper.ts b/tests/framework/fixtures/FixtureHelper.ts index 58e643d0e2a..7f48216e647 100644 --- a/tests/framework/fixtures/FixtureHelper.ts +++ b/tests/framework/fixtures/FixtureHelper.ts @@ -48,6 +48,7 @@ import { createLogger } from '../logger'; import { mockNotificationServices } from '../../smoke/notifications/utils/mocks'; import PortManager, { ResourceType } from '../PortManager'; import { DEFAULT_MOCKS } from '../../api-mocking/mock-responses/defaults'; +import type { Fixture } from './types'; import CommandQueueServer from './CommandQueueServer'; import DappServer from '../DappServer'; import { PlatformDetector } from '../PlatformLocator'; @@ -292,9 +293,7 @@ async function handleDappCleanup( * @param state - The fixture state to update * @returns The updated fixture state */ -function updateRpcUrlsWithAllocatedPorts( - state: FixtureBuilder['fixture'], -): FixtureBuilder { +function updateRpcUrlsWithAllocatedPorts(state: Fixture): Fixture { const portManager = PortManager.getInstance(); const actualAnvilPort = portManager.getPort(ResourceType.ANVIL); @@ -305,7 +304,7 @@ function updateRpcUrlsWithAllocatedPorts( ?.networkConfigurationsByChainId; if (networkConfigs) { for (const chainId of Object.keys(networkConfigs)) { - const config = networkConfigs[chainId]; + const config = networkConfigs[chainId as `0x${string}`]; if (config.rpcEndpoints) { for (const endpoint of config.rpcEndpoints) { if (endpoint.url) { @@ -334,9 +333,7 @@ function updateRpcUrlsWithAllocatedPorts( * Updates dapp URLs in PermissionController with actual allocated ports by index. * Replaces all occurrences of dapp URLs (by index) with their actual allocated ports. */ -function updateDappUrlsWithAllocatedPorts( - state: FixtureBuilder['fixture'], -): FixtureBuilder { +function updateDappUrlsWithAllocatedPorts(state: Fixture): Fixture { const portManager = PortManager.getInstance(); const permissionController = state.state?.engine?.backgroundState?.PermissionController; @@ -377,9 +374,7 @@ function updateDappUrlsWithAllocatedPorts( * Replaces all occurrences of localhost:8000 with the actual mock server port. * This affects browser tabs and RPC endpoints that proxy through mock server. */ -function updateMockServerUrlsInFixture( - state: FixtureBuilder['fixture'], -): FixtureBuilder { +function updateMockServerUrlsInFixture(state: Fixture): Fixture { const portManager = PortManager.getInstance(); const actualPort = portManager.getPort(ResourceType.MOCK_SERVER); @@ -405,11 +400,13 @@ function updateMockServerUrlsInFixture( */ export const loadFixture = async ( fixtureServer: FixtureServer, - { fixture }: { fixture: FixtureBuilder }, + { fixture }: { fixture: FixtureBuilder | Fixture }, ) => { - // If no fixture is provided, the `onboarding` option is set to `true` by default, which means - // the app will be loaded without any fixtures and will start and go through the onboarding process. - let state = fixture || new FixtureBuilder({ onboarding: true }).build(); + // Normalize FixtureBuilder → Fixture; fall back to onboarding fixture if nothing provided. + let state: Fixture = + fixture instanceof FixtureBuilder + ? fixture.build() + : (fixture ?? new FixtureBuilder({ onboarding: true }).build()); // Update RPC URLs with actual allocated ports from PortManager state = updateRpcUrlsWithAllocatedPorts(state); @@ -577,7 +574,7 @@ export async function withFixtures( mockServerInstance = mockServerResult.mockServerInstance; mockServerPort = mockServerResult.mockServerPort; // Resolve fixture after local nodes are started so dynamic ports are known - let resolvedFixture: FixtureBuilder; + let resolvedFixture: FixtureBuilder | Fixture; if (typeof fixtureOption === 'function') { resolvedFixture = await fixtureOption({ localNodes }); } else { diff --git a/tests/framework/fixtures/FixtureServer.ts b/tests/framework/fixtures/FixtureServer.ts index 76d761601c8..6bfd6e52eaa 100644 --- a/tests/framework/fixtures/FixtureServer.ts +++ b/tests/framework/fixtures/FixtureServer.ts @@ -2,6 +2,7 @@ import { getLocalHost } from './FixtureUtils.ts'; import Koa, { Context } from 'koa'; import { isObject, mapValues } from 'lodash'; import FixtureBuilder from './FixtureBuilder.ts'; +import type { Fixture } from './types.ts'; import { createLogger } from '../logger.ts'; import { Resource, ServerStatus } from '../types.ts'; import PortManager, { ResourceType } from '../PortManager.ts'; @@ -238,7 +239,7 @@ class FixtureServer implements Resource { } // Load JSON state into the server loadJsonState( - rawState: FixtureBuilder, + rawState: Fixture | FixtureBuilder, contractRegistry: ContractRegistry | null, ) { logger.debug('Loading JSON state...'); diff --git a/tests/framework/fixtures/types.ts b/tests/framework/fixtures/types.ts new file mode 100644 index 00000000000..1cd419c4a68 --- /dev/null +++ b/tests/framework/fixtures/types.ts @@ -0,0 +1,266 @@ +// ─── Layer 1: Imported from MetaMask packages (and re-exported) ─────────────── +// Imported for use in Layer 3 composed types; re-exported so consumers can +// import controller state types from this single file rather than each package. +import type { NetworkState } from '@metamask/network-controller'; +import type { AccountsControllerState } from '@metamask/accounts-controller'; +import type { PreferencesState } from '@metamask/preferences-controller'; +import type { AccountTreeControllerState } from '@metamask/account-tree-controller'; + +export type { + NetworkState, + AccountsControllerState, + PreferencesState, + AccountTreeControllerState, +}; + +// ─── Layer 2: Minimal hand-written interfaces ───────────────────────────────── +// Written for controllers that do not export a clean state type. +// These match the shape used in default-fixture.json. Keep them narrow — +// only include the fields FixtureBuilder methods actually read or write. + +export interface KeyringEntry { + type: string; + accounts: string[]; + metadata?: { id: string; name: string }; +} + +export interface KeyringControllerState { + keyrings: KeyringEntry[]; + vault?: string; + isUnlocked?: boolean; + encryptionKey?: string; + encryptionSalt?: string; +} + +// A single caveat inside a permission grant. +export interface PermissionCaveat { + type: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; +} + +// One permission granted to an origin. +export interface PermissionEntry { + id: string; + parentCapability: string; + invoker: string; + caveats: PermissionCaveat[] | null; + date: number; +} + +// A subject (origin) that holds one or more permissions. +export interface PermissionSubject { + origin: string; + permissions: Record; +} + +export interface PermissionControllerState { + subjects: Record; +} + +// Snaps controller — only the fields we set in fixtures. +export interface SnapEntry { + id: string; + enabled: boolean; + blocked: boolean; + status: string; + version: string; + // Allow additional snap manifest fields without listing them all. + [key: string]: unknown; +} + +export interface SnapControllerState { + snaps?: Record; // absent in default fixture; injected at runtime +} + +// Token entry used in TokensController.allTokens +export interface TokenInfo { + address: string; + symbol: string; + decimals: number; + name?: string; + image?: string; + [key: string]: unknown; +} + +export interface TokensControllerState { + // allTokens[chainId][accountAddress] = TokenInfo[] + allTokens: Record>; +} + +export interface TokenBalancesControllerState { + // tokenBalances[accountAddress][chainId][tokenAddress] = hex string + tokenBalances: Record>>; +} + +export interface TokenRatesControllerState { + // marketData[chainId][tokenAddress] = { tokenAddress, price, ... } + marketData: Record>>; +} + +export interface CurrencyRateEntry { + conversionDate: number; + conversionRate: number; + usdConversionRate: number; +} + +export interface CurrencyRateControllerState { + currentCurrency: string; + currencyRates: Record; +} + +export interface AccountBalance { + balance: string; +} + +export interface AccountTrackerControllerState { + accounts?: Record; // legacy field; absent in current fixture + accountsByChainId: Record>; +} + +export interface RampsRegionCountry { + isoCode: string; + name: string; + flag: string; + currency?: string; + phone: { prefix: string; placeholder: string; template: string }; + supported: { buy: boolean; sell: boolean }; +} + +export interface RampsRegionState { + country: RampsRegionCountry; + state: { + stateId: string; + name: string; + supported: { buy: boolean; sell: boolean }; + } | null; + regionCode: string; +} + +export interface RampsControllerState { + userRegion: RampsRegionState | null; + [key: string]: unknown; +} + +// ─── Redux slice shapes ─────────────────────────────────────────────────────── + +export interface BrowserTab { + id: number; + url: string; + isArchived?: boolean; +} + +export interface BrowserState { + activeTab: number | null; + tabs: BrowserTab[]; + history: string[]; + favicons: unknown[]; + isFullscreen: boolean; + visitedDappsByHostname: Record; + whitelist: string[]; +} + +export interface UserState { + seedphraseBackedUp: boolean; + backUpSeedphraseVisible: boolean; + passwordSet: boolean; + importTime?: number; + musdConversionEducationSeen?: boolean; + [key: string]: unknown; +} + +export interface FiatOrdersState { + orders: unknown[]; + customOrderIds: unknown[]; + selectedRegionAgg?: unknown; + selectedPaymentMethodAgg?: string; + detectedGeolocation?: string; + rampRoutingDecision?: string; + networks?: unknown[]; + [key: string]: unknown; +} + +export interface LegalNoticesState { + newPrivacyPolicyToastShownDate: number; + isPna25Acknowledged?: boolean; + [key: string]: unknown; +} + +// ─── Layer 3: Composed types ────────────────────────────────────────────────── + +export interface EngineBackgroundState { + NetworkController: NetworkState; + AccountsController: AccountsControllerState; + PreferencesController: PreferencesState; + AccountTreeController: AccountTreeControllerState; + KeyringController: KeyringControllerState; + PermissionController: PermissionControllerState; + SnapController: SnapControllerState; + TokensController: TokensControllerState; + TokenBalancesController: TokenBalancesControllerState; + TokenRatesController: TokenRatesControllerState; + CurrencyRateController: CurrencyRateControllerState; + AccountTrackerController: AccountTrackerControllerState; + RampsController: RampsControllerState; + // PerpsController has an exotic shape that changes frequently; keep loose. + PerpsController: Record; + // Allow other controllers that fixtures may optionally set. + [key: string]: unknown; +} + +export interface FixtureState { + engine: { backgroundState: EngineBackgroundState }; + browser: BrowserState; + user: UserState; + fiatOrders: FiatOrdersState; + legalNotices: LegalNoticesState; + // Other Redux slices we don't need to narrow further. + [key: string]: unknown; +} + +export interface Fixture { + state: FixtureState; + asyncState: Record; +} + +// ─── Method parameter types ─────────────────────────────────────────────────── + +/** + * The network provider config passed to withNetworkController(). + * Matches the shape of providerConfig objects in tests/resources/networks.e2e.js. + */ +export interface ProviderConfig { + chainId: string; + rpcUrl: string; + type: string; + nickname?: string; + ticker?: string; +} + +/** + * Shape of user-profile objects exported from profile fixtures. + * Used by withUserProfileKeyRing(). + */ +export interface UserKeyringState { + KEYRING_CONTROLLER_STATE: Partial; +} + +/** + * Shape of user-profile objects containing snap controller state. + * Used by withUserProfileSnapUnencryptedState(). + */ +export interface UserSnapState { + SNAPS_CONTROLLER_STATE: Partial; +} + +/** + * Shape of user-profile objects containing permission controller state. + * Used by withUserProfileSnapPermissions(). + */ +export interface UserPermissionState { + PERMISSION_CONTROLLER_STATE: Partial; +} + +// ─── Utility ────────────────────────────────────────────────────────────────── + +export type DeepPartial = { [P in keyof T]?: DeepPartial }; diff --git a/tests/framework/types.ts b/tests/framework/types.ts index ae1355292e8..d8fe0aafe1c 100644 --- a/tests/framework/types.ts +++ b/tests/framework/types.ts @@ -7,6 +7,7 @@ import ContractAddressRegistry from '../../app/util/test/contract-address-regist import Ganache from '../../app/util/test/ganache'; import { Mockttp } from 'mockttp'; import FixtureBuilder from './fixtures/FixtureBuilder.ts'; +import type { Fixture } from './fixtures/types.ts'; import CommandQueueServer from './fixtures/CommandQueueServer.ts'; /* @@ -327,9 +328,10 @@ export type TestSpecificMock = (mockServer: Mockttp) => Promise; export interface WithFixturesOptions { fixture: | FixtureBuilder + | Fixture | ((ctx: { localNodes?: LocalNode[]; - }) => FixtureBuilder | Promise); + }) => FixtureBuilder | Fixture | Promise); restartDevice?: boolean; smartContracts?: string[]; disableLocalNodes?: boolean; diff --git a/tests/regression/accounts/error-boundary-srp-backup.spec.ts b/tests/regression/accounts/error-boundary-srp-backup.spec.ts index b9adc1f1b8e..8ac580623e6 100644 --- a/tests/regression/accounts/error-boundary-srp-backup.spec.ts +++ b/tests/regression/accounts/error-boundary-srp-backup.spec.ts @@ -50,13 +50,11 @@ describe(RegressionAccounts('Error Boundary Screen'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/regression/assets/import-custom-token.spec.ts b/tests/regression/assets/import-custom-token.spec.ts index da5e7db79cc..81c9e71ee58 100644 --- a/tests/regression/assets/import-custom-token.spec.ts +++ b/tests/regression/assets/import-custom-token.spec.ts @@ -30,13 +30,11 @@ describe(RegressionAssets('Import custom token'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withNetworkEnabledMap({ eip155: { '0x539': true }, diff --git a/tests/regression/assets/import-tokens-via-asset-watcher.spec.ts b/tests/regression/assets/import-tokens-via-asset-watcher.spec.ts index a064dfa2c87..0ea103038bc 100644 --- a/tests/regression/assets/import-tokens-via-asset-watcher.spec.ts +++ b/tests/regression/assets/import-tokens-via-asset-watcher.spec.ts @@ -62,13 +62,11 @@ describe(RegressionNetworkAbstractions('Asset Watch:'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildERC20PermsForAddress(), diff --git a/tests/regression/assets/nft-details.spec.ts b/tests/regression/assets/nft-details.spec.ts index f1c24a584ac..874a72d9d2d 100644 --- a/tests/regression/assets/nft-details.spec.ts +++ b/tests/regression/assets/nft-details.spec.ts @@ -36,13 +36,11 @@ describe.skip(RegressionAssets('NFT Details page'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/regression/assets/transaction.spec.ts b/tests/regression/assets/transaction.spec.ts index f705fa00439..4fb9823f75c 100644 --- a/tests/regression/assets/transaction.spec.ts +++ b/tests/regression/assets/transaction.spec.ts @@ -40,13 +40,11 @@ describe(RegressionAssets('Transaction'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withNetworkEnabledMap({ eip155: { '0x539': true }, diff --git a/tests/regression/confirmations/new-networks-signatures.spec.ts b/tests/regression/confirmations/new-networks-signatures.spec.ts index 86a09cd7e01..af6f8d8b2e3 100644 --- a/tests/regression/confirmations/new-networks-signatures.spec.ts +++ b/tests/regression/confirmations/new-networks-signatures.spec.ts @@ -64,9 +64,7 @@ describe.skip(RegressionConfirmations('Signature Requests'), () => { }, ], fixture: new FixtureBuilder() - .withNetworkController({ - providerConfig: networkConfig.providerConfig, - }) + .withNetworkController(networkConfig.providerConfig) .withPermissionControllerConnectedToTestDapp( buildPermissions(networkConfig.permissions), ) diff --git a/tests/regression/networks/add-custom-rpc.spec.ts b/tests/regression/networks/add-custom-rpc.spec.ts index 71f66fe2460..85f8406a855 100644 --- a/tests/regression/networks/add-custom-rpc.spec.ts +++ b/tests/regression/networks/add-custom-rpc.spec.ts @@ -159,7 +159,7 @@ describe.skip(RegressionAssets('Custom RPC Tests'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withNetworkController(CustomNetworks.Gnosis) + .withNetworkController(CustomNetworks.Gnosis.providerConfig) .build(), restartDevice: true, }, diff --git a/tests/regression/ramps/onramp-parameters.spec.ts b/tests/regression/ramps/onramp-parameters.spec.ts index 3c8322a3e4f..ce41d537690 100644 --- a/tests/regression/ramps/onramp-parameters.spec.ts +++ b/tests/regression/ramps/onramp-parameters.spec.ts @@ -30,7 +30,7 @@ const setupOnRampTest = async (testFn: () => Promise) => { await withFixtures( { fixture: new FixtureBuilder() - .withNetworkController(CustomNetworks.Tenderly.Mainnet) + .withNetworkController(CustomNetworks.Tenderly.Mainnet.providerConfig) .withRampsSelectedRegion(selectedRegion) .withMetaMetricsOptIn() .build(), diff --git a/tests/regression/swap/swap-action-regression.spec.ts b/tests/regression/swap/swap-action-regression.spec.ts index 901d9d34836..0ffe6002202 100644 --- a/tests/regression/swap/swap-action-regression.spec.ts +++ b/tests/regression/swap/swap-action-regression.spec.ts @@ -31,13 +31,11 @@ describe(RegressionTrade('Swap ETH <-> WETH from Actions'), (): void => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x1', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId: '0x1', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withDisabledSmartTransactions() .build(); diff --git a/tests/regression/swap/swap-token-chart.spec.ts b/tests/regression/swap/swap-token-chart.spec.ts index 4a550c59989..4026e88a93d 100644 --- a/tests/regression/swap/swap-token-chart.spec.ts +++ b/tests/regression/swap/swap-token-chart.spec.ts @@ -38,13 +38,11 @@ describe(RegressionTrade('Swap from Token view'), (): void => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withDisabledSmartTransactions() .build(); diff --git a/tests/regression/swap/swap-token-rwa.spec.ts b/tests/regression/swap/swap-token-rwa.spec.ts index 01f4f7059b5..98c4adfb379 100644 --- a/tests/regression/swap/swap-token-rwa.spec.ts +++ b/tests/regression/swap/swap-token-rwa.spec.ts @@ -35,13 +35,11 @@ describe(RegressionTrade('Swap RWA'), (): void => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withDisabledSmartTransactions() .build(); diff --git a/tests/regression/wallet/balance-privacy-toggle.spec.ts b/tests/regression/wallet/balance-privacy-toggle.spec.ts index c4d48bee38e..fe621dacec8 100644 --- a/tests/regression/wallet/balance-privacy-toggle.spec.ts +++ b/tests/regression/wallet/balance-privacy-toggle.spec.ts @@ -28,13 +28,11 @@ describe(RegressionWalletPlatform('Balance Privacy Toggle'), (): void => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withETHAsPrimaryCurrency() // Set primary currency to ETH .build(); diff --git a/tests/regression/wallet/send-ERC-token.spec.ts b/tests/regression/wallet/send-ERC-token.spec.ts index 9dbecde0220..e6998f3f1be 100644 --- a/tests/regression/wallet/send-ERC-token.spec.ts +++ b/tests/regression/wallet/send-ERC-token.spec.ts @@ -39,13 +39,11 @@ describe(RegressionWalletPlatform('Send ERC Token'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withNetworkEnabledMap({ eip155: { '0x539': true }, diff --git a/tests/smoke/card/card-button.spec.ts b/tests/smoke/card/card-button.spec.ts index a714a2abbc2..a84a5fcc44d 100644 --- a/tests/smoke/card/card-button.spec.ts +++ b/tests/smoke/card/card-button.spec.ts @@ -21,7 +21,7 @@ describe(SmokeCard('Card NavBar Button'), () => { { fixture: new FixtureBuilder() .withMetaMetricsOptIn() - .withNetworkController(CustomNetworks.Tenderly.Linea) + .withNetworkController(CustomNetworks.Tenderly.Linea.providerConfig) .withAccountTreeController() .withTokens( [ diff --git a/tests/smoke/card/card-home-add-funds.spec.ts b/tests/smoke/card/card-home-add-funds.spec.ts index 68b42879d9b..786bafef50a 100644 --- a/tests/smoke/card/card-home-add-funds.spec.ts +++ b/tests/smoke/card/card-home-add-funds.spec.ts @@ -21,7 +21,7 @@ describe(SmokeCard('CardHome - Add Funds'), () => { { fixture: new FixtureBuilder() .withMetaMetricsOptIn() - .withNetworkController(CustomNetworks.Tenderly.Linea) + .withNetworkController(CustomNetworks.Tenderly.Linea.providerConfig) .withAccountTreeController() .withTokens( [ diff --git a/tests/smoke/card/card-home-manage-card.spec.ts b/tests/smoke/card/card-home-manage-card.spec.ts index 0d66717417d..0419257bdd8 100644 --- a/tests/smoke/card/card-home-manage-card.spec.ts +++ b/tests/smoke/card/card-home-manage-card.spec.ts @@ -21,7 +21,7 @@ describe(SmokeCard('CardHome - Manage Card'), () => { { fixture: new FixtureBuilder() .withMetaMetricsOptIn() - .withNetworkController(CustomNetworks.Tenderly.Linea) + .withNetworkController(CustomNetworks.Tenderly.Linea.providerConfig) .withAccountTreeController() .withTokens( [ diff --git a/tests/smoke/confirmations/send/send-erc20-token.spec.ts b/tests/smoke/confirmations/send/send-erc20-token.spec.ts index be6874b8b15..55bad325fd6 100644 --- a/tests/smoke/confirmations/send/send-erc20-token.spec.ts +++ b/tests/smoke/confirmations/send/send-erc20-token.spec.ts @@ -289,13 +289,11 @@ describe(SmokeConfirmations('Send ERC20 asset'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withTokensForAllPopularNetworks([ { @@ -355,13 +353,11 @@ describe(SmokeConfirmations('Send ERC20 asset'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withTokensForAllPopularNetworks([ { @@ -421,13 +417,11 @@ describe(SmokeConfirmations('Send ERC20 asset'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withTokensForAllPopularNetworks([ { diff --git a/tests/smoke/confirmations/send/send-native-token.spec.ts b/tests/smoke/confirmations/send/send-native-token.spec.ts index 55fb48ce0d3..1d18facfabf 100644 --- a/tests/smoke/confirmations/send/send-native-token.spec.ts +++ b/tests/smoke/confirmations/send/send-native-token.spec.ts @@ -28,13 +28,11 @@ describe(SmokeConfirmations('Send native asset'), () => { ], fixture: new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: LOCAL_NODE_RPC_URL, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: LOCAL_NODE_RPC_URL, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withMetaMetricsOptIn() .withPreferencesController({}) diff --git a/tests/smoke/confirmations/signatures/signatures-typed.spec.ts b/tests/smoke/confirmations/signatures/signatures-typed.spec.ts index 9f427ab2aea..a7fd4abe88f 100644 --- a/tests/smoke/confirmations/signatures/signatures-typed.spec.ts +++ b/tests/smoke/confirmations/signatures/signatures-typed.spec.ts @@ -82,13 +82,11 @@ describe(SmokeConfirmations('Typed Signature Requests'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/smoke/confirmations/signatures/signatures.spec.ts b/tests/smoke/confirmations/signatures/signatures.spec.ts index 252033438f5..d7495d40b1c 100644 --- a/tests/smoke/confirmations/signatures/signatures.spec.ts +++ b/tests/smoke/confirmations/signatures/signatures.spec.ts @@ -76,13 +76,11 @@ describe(SmokeConfirmations('Signature Requests'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/smoke/confirmations/transactions/7702/batch-transaction.spec.ts b/tests/smoke/confirmations/transactions/7702/batch-transaction.spec.ts index 07b31f83e8d..7e1a312fb7a 100644 --- a/tests/smoke/confirmations/transactions/7702/batch-transaction.spec.ts +++ b/tests/smoke/confirmations/transactions/7702/batch-transaction.spec.ts @@ -119,13 +119,11 @@ describe(SmokeConfirmations('7702 - smart account'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), @@ -216,13 +214,11 @@ describe(SmokeConfirmations('7702 - smart account'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .build(); }, diff --git a/tests/smoke/confirmations/transactions/contract-deployment.spec.ts b/tests/smoke/confirmations/transactions/contract-deployment.spec.ts index abed4e7fe41..6b6e7ddb70b 100644 --- a/tests/smoke/confirmations/transactions/contract-deployment.spec.ts +++ b/tests/smoke/confirmations/transactions/contract-deployment.spec.ts @@ -59,13 +59,11 @@ describe(SmokeConfirmations('Contract Deployment'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/smoke/confirmations/transactions/contract-interaction.spec.ts b/tests/smoke/confirmations/transactions/contract-interaction.spec.ts index 883b6a30f27..146f3bd74c4 100644 --- a/tests/smoke/confirmations/transactions/contract-interaction.spec.ts +++ b/tests/smoke/confirmations/transactions/contract-interaction.spec.ts @@ -60,13 +60,11 @@ describe(SmokeConfirmations('Contract Interaction'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/smoke/confirmations/transactions/dapp-initiated-transfer.spec.ts b/tests/smoke/confirmations/transactions/dapp-initiated-transfer.spec.ts index 98ab85f07b1..c5f9cc12b0e 100644 --- a/tests/smoke/confirmations/transactions/dapp-initiated-transfer.spec.ts +++ b/tests/smoke/confirmations/transactions/dapp-initiated-transfer.spec.ts @@ -117,13 +117,11 @@ describe(SmokeConfirmations('DApp Initiated Transfer'), () => { ], fixture: new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${DEFAULT_ANVIL_PORT}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${DEFAULT_ANVIL_PORT}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withNetworkEnabledMap({ eip155: { '0x539': true }, diff --git a/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts index 57e2f772660..1bfae467617 100644 --- a/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts +++ b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702-sponsored.spec.ts @@ -130,13 +130,11 @@ const createFixture = ({ localNodes }: { localNodes?: LocalNode[] }) => { node instanceof AnvilManager ? (node.getPort() ?? AnvilPort()) : undefined; return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withDisabledSmartTransactions() .build(); diff --git a/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts index f9a86587c3a..84cfa3205ee 100644 --- a/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts +++ b/tests/smoke/confirmations/transactions/gas-fee-tokens-eip-7702.spec.ts @@ -184,13 +184,11 @@ describe( : undefined; return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withDisabledSmartTransactions() .build(); diff --git a/tests/smoke/confirmations/transactions/per-dapp-selected-network.spec.ts b/tests/smoke/confirmations/transactions/per-dapp-selected-network.spec.ts index 3fffcf08f35..0d669c16f4e 100644 --- a/tests/smoke/confirmations/transactions/per-dapp-selected-network.spec.ts +++ b/tests/smoke/confirmations/transactions/per-dapp-selected-network.spec.ts @@ -60,13 +60,11 @@ describe(SmokeConfirmations('Dapp Network Switching'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: LOCAL_CHAIN_ID, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: LOCAL_CHAIN_NAME, - ticker: 'ETH', - }, + chainId: LOCAL_CHAIN_ID, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: LOCAL_CHAIN_NAME, + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions([LOCAL_CHAIN_ID]), diff --git a/tests/smoke/confirmations/transactions/token-approve/approve.spec.ts b/tests/smoke/confirmations/transactions/token-approve/approve.spec.ts index ea2dce8b248..c23efe09c10 100644 --- a/tests/smoke/confirmations/transactions/token-approve/approve.spec.ts +++ b/tests/smoke/confirmations/transactions/token-approve/approve.spec.ts @@ -59,13 +59,11 @@ describe(SmokeConfirmations('Token Approve - approve method'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), @@ -155,13 +153,11 @@ describe(SmokeConfirmations('Token Approve - approve method'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/smoke/confirmations/transactions/token-approve/increase-allowance.spec.ts b/tests/smoke/confirmations/transactions/token-approve/increase-allowance.spec.ts index b858e77c9da..3058906a814 100644 --- a/tests/smoke/confirmations/transactions/token-approve/increase-allowance.spec.ts +++ b/tests/smoke/confirmations/transactions/token-approve/increase-allowance.spec.ts @@ -58,13 +58,11 @@ describe(SmokeConfirmations('Token Approve - increaseAllowance method'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/smoke/confirmations/transactions/token-approve/set-approval-for-all.spec.ts b/tests/smoke/confirmations/transactions/token-approve/set-approval-for-all.spec.ts index 27c53f98953..73fe3e1d659 100644 --- a/tests/smoke/confirmations/transactions/token-approve/set-approval-for-all.spec.ts +++ b/tests/smoke/confirmations/transactions/token-approve/set-approval-for-all.spec.ts @@ -59,13 +59,11 @@ describe(SmokeConfirmations('Token Approve - setApprovalForAll method'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), @@ -167,13 +165,11 @@ describe(SmokeConfirmations('Token Approve - setApprovalForAll method'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), @@ -244,13 +240,11 @@ describe(SmokeConfirmations('Token Approve - setApprovalForAll method'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withPermissionControllerConnectedToTestDapp( buildPermissions(['0x539']), diff --git a/tests/smoke/identity/utils/withIdentityFixtures.ts b/tests/smoke/identity/utils/withIdentityFixtures.ts index 0f22f3c5a7b..e4876a7c6d0 100644 --- a/tests/smoke/identity/utils/withIdentityFixtures.ts +++ b/tests/smoke/identity/utils/withIdentityFixtures.ts @@ -1,5 +1,6 @@ import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import type { Fixture } from '../../../framework/fixtures/types'; import { createUserStorageController, setupAccountMockedBalances, @@ -17,7 +18,7 @@ import { } from '@metamask/account-tree-controller'; export interface IdentityFixtureOptions { - fixture?: object; + fixture?: FixtureBuilder | Fixture; restartDevice?: boolean; userStorageFeatures?: (keyof typeof pathRegexps)[]; userStorageOverrides?: Partial< diff --git a/tests/smoke/multichain/permissions/accounts/permission-system-remove.failing.ts b/tests/smoke/multichain/permissions/accounts/permission-system-remove.failing.ts index 4f41882458e..bf6122297d5 100644 --- a/tests/smoke/multichain/permissions/accounts/permission-system-remove.failing.ts +++ b/tests/smoke/multichain/permissions/accounts/permission-system-remove.failing.ts @@ -37,7 +37,7 @@ describe(SmokeNetworkAbstractions('Chain Permission Management'), () => { }, ], fixture: new FixtureBuilder() - .withNetworkController(PopularNetworksList.Polygon) + .withNetworkController(PopularNetworksList.Polygon.providerConfig) .build(), restartDevice: true, }, diff --git a/tests/smoke/multichain/permissions/chains/permission-system-dapp-chain-switch-grant.spec.js b/tests/smoke/multichain/permissions/chains/permission-system-dapp-chain-switch-grant.spec.js index d086ba1a580..20043a1c79c 100644 --- a/tests/smoke/multichain/permissions/chains/permission-system-dapp-chain-switch-grant.spec.js +++ b/tests/smoke/multichain/permissions/chains/permission-system-dapp-chain-switch-grant.spec.js @@ -28,8 +28,10 @@ describe(SmokeNetworkAbstractions('Chain Permission System'), () => { }, ], fixture: new FixtureBuilder() - .withNetworkController(CustomNetworks.ElysiumTestnet) - .withNetworkController(CustomNetworks.EthereumMainCustom) + .withNetworkController(CustomNetworks.ElysiumTestnet.providerConfig) + .withNetworkController( + CustomNetworks.EthereumMainCustom.providerConfig, + ) .withPermissionController() .build(), restartDevice: true, diff --git a/tests/smoke/performance/account-list/dismiss-account-list.spec.ts b/tests/smoke/performance/account-list/dismiss-account-list.spec.ts index 2df06297f80..fc871d54f08 100644 --- a/tests/smoke/performance/account-list/dismiss-account-list.spec.ts +++ b/tests/smoke/performance/account-list/dismiss-account-list.spec.ts @@ -6,6 +6,11 @@ import Assertions from '../../../framework/Assertions'; import TestHelpers from '../../../helpers'; import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import type { + UserKeyringState, + UserSnapState, + UserPermissionState, +} from '../../../framework/fixtures/types'; import { toChecksumAddress } from 'ethereumjs-util'; import { CORE_USER_STATE, @@ -65,9 +70,9 @@ describe(SmokePerformance('Switching Accounts to Dismiss Load Testing'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withUserProfileKeyRing(_userState) - .withUserProfileSnapUnencryptedState(_userState) - .withUserProfileSnapPermissions(_userState) + .withUserProfileKeyRing(_userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(_userState as UserSnapState) + .withUserProfileSnapPermissions(_userState as UserPermissionState) .withTokens(minimalTokens) .build(), restartDevice: true, diff --git a/tests/smoke/performance/account-list/render-account-list.spec.ts b/tests/smoke/performance/account-list/render-account-list.spec.ts index 3782e635150..d5bb16c27e7 100644 --- a/tests/smoke/performance/account-list/render-account-list.spec.ts +++ b/tests/smoke/performance/account-list/render-account-list.spec.ts @@ -6,6 +6,11 @@ import Assertions from '../../../framework/Assertions'; import TestHelpers from '../../../helpers'; import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import type { + UserKeyringState, + UserSnapState, + UserPermissionState, +} from '../../../framework/fixtures/types'; import { toChecksumAddress } from 'ethereumjs-util'; import { CORE_USER_STATE, @@ -52,9 +57,9 @@ describe(SmokePerformance('Account List Load Testing'), () => { { fixture: new FixtureBuilder() .withPopularNetworks() - .withUserProfileKeyRing(userState) - .withUserProfileSnapUnencryptedState(userState) - .withUserProfileSnapPermissions(userState) + .withUserProfileKeyRing(userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(userState as UserSnapState) + .withUserProfileSnapPermissions(userState as UserPermissionState) .build(), restartDevice: true, }, @@ -140,11 +145,14 @@ describe(SmokePerformance('Account List Load Testing'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withUserProfileKeyRing(userState) - .withUserProfileSnapUnencryptedState(userState) - .withUserProfileSnapPermissions(userState) + .withUserProfileKeyRing(userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(userState as UserSnapState) + .withUserProfileSnapPermissions(userState as UserPermissionState) .withPopularNetworks() - .withTokensForAllPopularNetworks(heavyTokenLoad, userState) + .withTokensForAllPopularNetworks( + heavyTokenLoad, + userState as UserKeyringState, + ) .build(), restartDevice: true, }, @@ -230,9 +238,9 @@ describe(SmokePerformance('Account List Load Testing'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withUserProfileKeyRing(_userState) - .withUserProfileSnapUnencryptedState(_userState) - .withUserProfileSnapPermissions(_userState) + .withUserProfileKeyRing(_userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(_userState as UserSnapState) + .withUserProfileSnapPermissions(_userState as UserPermissionState) .withTokens(minimalTokens) .build(), restartDevice: true, diff --git a/tests/smoke/performance/network-list/dismiss-network-list.spec.ts b/tests/smoke/performance/network-list/dismiss-network-list.spec.ts index 4b5fe7965d7..65b3ff2228e 100644 --- a/tests/smoke/performance/network-list/dismiss-network-list.spec.ts +++ b/tests/smoke/performance/network-list/dismiss-network-list.spec.ts @@ -5,6 +5,11 @@ import Assertions from '../../../framework/Assertions'; import TestHelpers from '../../../helpers'; import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import type { + UserKeyringState, + UserSnapState, + UserPermissionState, +} from '../../../framework/fixtures/types'; import NetworkManager from '../../../page-objects/wallet/NetworkManager'; import { CORE_USER_STATE, @@ -62,9 +67,9 @@ describe(SmokePerformance('Network List Load Testing'), () => { fixture: new FixtureBuilder() .withTokens(minimalTokens) .withPopularNetworks() - .withUserProfileKeyRing(userState) - .withUserProfileSnapUnencryptedState(userState) - .withUserProfileSnapPermissions(userState) + .withUserProfileKeyRing(userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(userState as UserSnapState) + .withUserProfileSnapPermissions(userState as UserPermissionState) .build(), restartDevice: true, }, diff --git a/tests/smoke/performance/network-list/render-network-list.spec.ts b/tests/smoke/performance/network-list/render-network-list.spec.ts index a073b388568..959ac6a5ac0 100644 --- a/tests/smoke/performance/network-list/render-network-list.spec.ts +++ b/tests/smoke/performance/network-list/render-network-list.spec.ts @@ -5,6 +5,11 @@ import Assertions from '../../../framework/Assertions'; import TestHelpers from '../../../helpers'; import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import type { + UserKeyringState, + UserSnapState, + UserPermissionState, +} from '../../../framework/fixtures/types'; import NetworkManager from '../../../page-objects/wallet/NetworkManager'; import { toChecksumAddress } from 'ethereumjs-util'; import { @@ -53,9 +58,9 @@ describe(SmokePerformance('Network List Load Testing'), () => { { fixture: new FixtureBuilder() .withPopularNetworks() - .withUserProfileKeyRing(userState) - .withUserProfileSnapUnencryptedState(userState) - .withUserProfileSnapPermissions(userState) + .withUserProfileKeyRing(userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(userState as UserSnapState) + .withUserProfileSnapPermissions(userState as UserPermissionState) .build(), restartDevice: true, }, @@ -146,9 +151,9 @@ describe(SmokePerformance('Network List Load Testing'), () => { { fixture: new FixtureBuilder() .withPopularNetworks() - .withUserProfileKeyRing(userState) - .withUserProfileSnapUnencryptedState(userState) - .withUserProfileSnapPermissions(userState) + .withUserProfileKeyRing(userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(userState as UserSnapState) + .withUserProfileSnapPermissions(userState as UserPermissionState) // eslint-disable-next-line @typescript-eslint/no-explicit-any .withTokensForAllPopularNetworks(heavyTokenLoad, userState as any) .build(), @@ -230,9 +235,9 @@ describe(SmokePerformance('Network List Load Testing'), () => { fixture: new FixtureBuilder() .withTokens(minimalTokens) .withPopularNetworks() - .withUserProfileKeyRing(userState) - .withUserProfileSnapUnencryptedState(userState) - .withUserProfileSnapPermissions(userState) + .withUserProfileKeyRing(userState as UserKeyringState) + .withUserProfileSnapUnencryptedState(userState as UserSnapState) + .withUserProfileSnapPermissions(userState as UserPermissionState) .build(), restartDevice: true, }, diff --git a/tests/smoke/perps/perps-add-funds.spec.ts b/tests/smoke/perps/perps-add-funds.spec.ts index d1c284efcd9..498ad6a3af0 100644 --- a/tests/smoke/perps/perps-add-funds.spec.ts +++ b/tests/smoke/perps/perps-add-funds.spec.ts @@ -37,13 +37,11 @@ describe(SmokePerps('Perps - Add funds (has funds, not first time)'), () => { .withPerpsFirstTimeUser(false) .withKeyringControllerOfMultipleAccounts() .withNetworkController({ - providerConfig: { - type: 'rpc', - chainId: '0xa4b1', - rpcUrl: 'https://arb1.arbitrum.io/rpc', - nickname: 'Arbitrum One', - ticker: 'ETH', - }, + type: 'rpc', + chainId: '0xa4b1', + rpcUrl: 'https://arb1.arbitrum.io/rpc', + nickname: 'Arbitrum One', + ticker: 'ETH', }) .withTokensForAllPopularNetworks([ { diff --git a/tests/smoke/ramps/onramp-unified-buy.spec.ts b/tests/smoke/ramps/onramp-unified-buy.spec.ts index 21dfe6bd988..0495141f788 100644 --- a/tests/smoke/ramps/onramp-unified-buy.spec.ts +++ b/tests/smoke/ramps/onramp-unified-buy.spec.ts @@ -92,7 +92,7 @@ describe(SmokeRamps('Onramp Unified Buy'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withNetworkController(CustomNetworks.Tenderly.Mainnet) + .withNetworkController(CustomNetworks.Tenderly.Mainnet.providerConfig) .withRampsSelectedRegion(selectedRegion) .withMetaMetricsOptIn() .build(), @@ -165,7 +165,7 @@ describe(SmokeRamps('Onramp Unified Buy'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withNetworkController(CustomNetworks.Tenderly.Mainnet) + .withNetworkController(CustomNetworks.Tenderly.Mainnet.providerConfig) .withRampsSelectedRegion(selectedRegion) .withMetaMetricsOptIn() .build(), diff --git a/tests/smoke/snaps/test-snap-name-lookup.spec.ts b/tests/smoke/snaps/test-snap-name-lookup.spec.ts index d63c0292ad3..3a4c6a7ea7a 100644 --- a/tests/smoke/snaps/test-snap-name-lookup.spec.ts +++ b/tests/smoke/snaps/test-snap-name-lookup.spec.ts @@ -27,13 +27,11 @@ describe(FlaskBuildTests('Name Lookup Snap Tests'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x1', - rpcUrl: `http://localhost:${node.getPort() ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x1', + rpcUrl: `http://localhost:${node.getPort() ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .build(); }, diff --git a/tests/smoke/stake/stake-action-smoke.spec.ts b/tests/smoke/stake/stake-action-smoke.spec.ts index 20632a33f5c..808062ee5b3 100644 --- a/tests/smoke/stake/stake-action-smoke.spec.ts +++ b/tests/smoke/stake/stake-action-smoke.spec.ts @@ -40,13 +40,11 @@ describe(SmokeTrade('Stake from Actions'), (): void => { return new FixtureBuilder() .withPolygon() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .build(); }, diff --git a/tests/smoke/swap/bridge-action-smoke.spec.ts b/tests/smoke/swap/bridge-action-smoke.spec.ts index 38a6bb7fb9a..941321f17b9 100644 --- a/tests/smoke/swap/bridge-action-smoke.spec.ts +++ b/tests/smoke/swap/bridge-action-smoke.spec.ts @@ -50,13 +50,11 @@ describe(SmokeTrade('Bridge functionality'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withDisabledSmartTransactions() .build(); diff --git a/tests/smoke/swap/gasless-swap.spec.ts b/tests/smoke/swap/gasless-swap.spec.ts index 22317040f83..8271e72e37b 100644 --- a/tests/smoke/swap/gasless-swap.spec.ts +++ b/tests/smoke/swap/gasless-swap.spec.ts @@ -33,13 +33,11 @@ describe(SmokeTrade('Gasless Swap - '), (): void => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withMetaMetricsOptIn() .build(); diff --git a/tests/smoke/swap/swap-action-smoke.spec.ts b/tests/smoke/swap/swap-action-smoke.spec.ts index 2cbfd3d3c3a..6158818b62b 100644 --- a/tests/smoke/swap/swap-action-smoke.spec.ts +++ b/tests/smoke/swap/swap-action-smoke.spec.ts @@ -53,13 +53,11 @@ describe.skip(SmokeTrade('Swap from Actions'), (): void => { { fixture: new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x1', - rpcUrl: `http://localhost:${DEFAULT_ANVIL_PORT}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId: '0x1', + rpcUrl: `http://localhost:${DEFAULT_ANVIL_PORT}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withDisabledSmartTransactions() .withMetaMetricsOptIn() diff --git a/tests/smoke/swap/swap-deeplink-smoke.spec.ts b/tests/smoke/swap/swap-deeplink-smoke.spec.ts index 6dcd7da4dfc..266e82c85be 100644 --- a/tests/smoke/swap/swap-deeplink-smoke.spec.ts +++ b/tests/smoke/swap/swap-deeplink-smoke.spec.ts @@ -39,13 +39,11 @@ describe( return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withMetaMetricsOptIn() .build(); @@ -106,13 +104,11 @@ describe( return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withMetaMetricsOptIn() .build(); @@ -168,13 +164,11 @@ describe( return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId, - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId, + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withMetaMetricsOptIn() .build(); diff --git a/tests/smoke/swap/swap-trending-tokens.spec.ts b/tests/smoke/swap/swap-trending-tokens.spec.ts index f2b92d929b3..b019d94eea4 100644 --- a/tests/smoke/swap/swap-trending-tokens.spec.ts +++ b/tests/smoke/swap/swap-trending-tokens.spec.ts @@ -133,13 +133,11 @@ const withBridgeFixtures = async (run: () => Promise) => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x1', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Localhost', - ticker: 'ETH', - }, + chainId: '0x1', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Localhost', + ticker: 'ETH', }) .withDisabledSmartTransactions() .build(); diff --git a/tests/smoke/wallet/helpers/musd-fixture.ts b/tests/smoke/wallet/helpers/musd-fixture.ts index 7f95644f107..6141e1c3605 100644 --- a/tests/smoke/wallet/helpers/musd-fixture.ts +++ b/tests/smoke/wallet/helpers/musd-fixture.ts @@ -65,13 +65,11 @@ export async function createMusdFixture( return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: CHAIN_IDS.MAINNET, - rpcUrl: `http://localhost:${rpcPort}`, - type: 'custom', - nickname: 'Ethereum Mainnet', - ticker: 'ETH', - }, + chainId: CHAIN_IDS.MAINNET, + rpcUrl: `http://localhost:${rpcPort}`, + type: 'custom', + nickname: 'Ethereum Mainnet', + ticker: 'ETH', }) .withNetworkEnabledMap({ eip155: { [CHAIN_IDS.MAINNET]: true } }) .withMetaMetricsOptIn() diff --git a/tests/smoke/wallet/incoming-transactions.spec.ts b/tests/smoke/wallet/incoming-transactions.spec.ts index 34c680f04a2..63c5128a855 100644 --- a/tests/smoke/wallet/incoming-transactions.spec.ts +++ b/tests/smoke/wallet/incoming-transactions.spec.ts @@ -9,6 +9,7 @@ import FixtureBuilder, { DEFAULT_FIXTURE_ACCOUNT, ENTROPY_WALLET_1_ID, } from '../../framework/fixtures/FixtureBuilder'; +import type { AccountTreeControllerState } from '../../framework/fixtures/types'; import TabBarComponent from '../../page-objects/wallet/TabBarComponent'; import ToastModal from '../../page-objects/wallet/ToastModal'; import { MockApiEndpoint, TestSpecificMock } from '../../framework/types'; @@ -130,7 +131,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withAccountTreeController(EVM_ONLY_ACCOUNT_TREE) + .withAccountTreeController( + EVM_ONLY_ACCOUNT_TREE as unknown as Partial, + ) .withNetworkEnabledMap({ eip155: { '0x1': true } }) .withPrivacyModePreferences(false) .build(), @@ -152,7 +155,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withAccountTreeController(EVM_ONLY_ACCOUNT_TREE) + .withAccountTreeController( + EVM_ONLY_ACCOUNT_TREE as unknown as Partial, + ) .withNetworkEnabledMap({ eip155: { '0x1': true } }) .withTokens([ { @@ -181,7 +186,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withAccountTreeController(EVM_ONLY_ACCOUNT_TREE) + .withAccountTreeController( + EVM_ONLY_ACCOUNT_TREE as unknown as Partial, + ) .withNetworkEnabledMap({ eip155: { '0x1': true } }) .withPrivacyModePreferences(false) .build(), @@ -203,7 +210,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withAccountTreeController(EVM_ONLY_ACCOUNT_TREE) + .withAccountTreeController( + EVM_ONLY_ACCOUNT_TREE as unknown as Partial, + ) .withNetworkEnabledMap({ eip155: { '0x1': true } }) .withPrivacyModePreferences(true) .build(), @@ -223,7 +232,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withAccountTreeController(EVM_ONLY_ACCOUNT_TREE) + .withAccountTreeController( + EVM_ONLY_ACCOUNT_TREE as unknown as Partial, + ) .withNetworkEnabledMap({ eip155: { '0x1': true } }) .withTransactions([ { @@ -253,7 +264,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => { await withFixtures( { fixture: new FixtureBuilder() - .withAccountTreeController(EVM_ONLY_ACCOUNT_TREE) + .withAccountTreeController( + EVM_ONLY_ACCOUNT_TREE as unknown as Partial, + ) .withNetworkEnabledMap({ eip155: { '0x1': true } }) .build(), restartDevice: true, diff --git a/tests/smoke/wallet/settings/addressbook-send-add-contact.spec.ts b/tests/smoke/wallet/settings/addressbook-send-add-contact.spec.ts index eafa1d426ba..06b9ff053ac 100644 --- a/tests/smoke/wallet/settings/addressbook-send-add-contact.spec.ts +++ b/tests/smoke/wallet/settings/addressbook-send-add-contact.spec.ts @@ -82,13 +82,11 @@ describe(RegressionWalletPlatform('Addressbook Tests'), () => { return new FixtureBuilder() .withNetworkController({ - providerConfig: { - chainId: '0x539', - rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, - type: 'custom', - nickname: 'Local RPC', - ticker: 'ETH', - }, + chainId: '0x539', + rpcUrl: `http://localhost:${rpcPort ?? AnvilPort()}`, + type: 'custom', + nickname: 'Local RPC', + ticker: 'ETH', }) .withNetworkEnabledMap({ eip155: { '0x539': true },