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 },