diff --git a/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch b/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch
deleted file mode 100644
index 82d2534242c5..000000000000
--- a/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch
+++ /dev/null
@@ -1,82 +0,0 @@
-diff --git a/dist/token-prices-service/codefi-v2.cjs b/dist/token-prices-service/codefi-v2.cjs
-index ba0f0c1bcbf0f231549b1ca9d3be2d1137a0d732..fd4851471fa0c2f07efbb527a3eea55cbfbc4743 100644
---- a/dist/token-prices-service/codefi-v2.cjs
-+++ b/dist/token-prices-service/codefi-v2.cjs
-@@ -220,43 +222,43 @@ exports.getNativeTokenAddress = getNativeTokenAddress;
- // Source: https://github.com/consensys-vertical-apps/va-mmcx-price-api/blob/main/src/constants/slip44.ts
- // We can only support PricesAPI V3 for EVM chains that have a CAIP-19 native asset mapping.
- exports.SPOT_PRICES_SUPPORT_INFO = {
-- '0x1': 'eip155:1/slip44:60', // Ethereum Mainnet - Native symbol: ETH
-- '0xa': 'eip155:10/slip44:60', // OP Mainnet - Native symbol: ETH
-- '0x19': 'eip155:25/slip44:394', // Cronos Mainnet - Native symbol: CRO
-- '0x38': 'eip155:56/slip44:714', // BNB Smart Chain Mainnet - Native symbol: BNB
-- '0x39': 'eip155:57/erc20:0x0000000000000000000000000000000000000000', // 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS
-+ '0x1': null, //'eip155:1/slip44:60', // Ethereum Mainnet - Native symbol: ETH
-+ '0xa': null, //'eip155:10/slip44:60', // OP Mainnet - Native symbol: ETH
-+ '0x19': null, //'eip155:25/slip44:394', // Cronos Mainnet - Native symbol: CRO
-+ '0x38': null, //'eip155:56/slip44:714', // BNB Smart Chain Mainnet - Native symbol: BNB
-+ '0x39': null, //'eip155:57/erc20:0x0000000000000000000000000000000000000000', // 'eip155:57/slip44:57', // Syscoin Mainnet - Native symbol: SYS
- '0x52': null, // 'eip155:82/slip44:18000', // Meter Mainnet - Native symbol: MTR
-- '0x58': 'eip155:88/erc20:0x0000000000000000000000000000000000000000', // 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO
-- '0x64': 'eip155:100/slip44:700', // Gnosis (formerly xDAI Chain) - Native symbol: xDAI
-- '0x6a': 'eip155:106/erc20:0x0000000000000000000000000000000000000000', // 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX
-- '0x80': 'eip155:128/erc20:0x0000000000000000000000000000000000000000', // 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT
-- '0x89': 'eip155:137/slip44:966', // Polygon Mainnet - Native symbol: POL
-+ '0x58': null, //'eip155:88/erc20:0x0000000000000000000000000000000000000000', // 'eip155:88/slip44:889', // TomoChain - Native symbol: TOMO
-+ '0x64': null, //'eip155:100/slip44:700', // Gnosis (formerly xDAI Chain) - Native symbol: xDAI
-+ '0x6a': null, //'eip155:106/erc20:0x0000000000000000000000000000000000000000', // 'eip155:106/slip44:5655640', // Velas EVM Mainnet - Native symbol: VLX
-+ '0x80': null, //'eip155:128/erc20:0x0000000000000000000000000000000000000000', // 'eip155:128/slip44:1010', // Huobi ECO Chain Mainnet - Native symbol: HT
-+ '0x89': null, //'eip155:137/slip44:966', // Polygon Mainnet - Native symbol: POL
- '0x8f': null, // 'eip155:143/slip44:268435779', // Monad Mainnet - Native symbol: MON
-- '0x92': 'eip155:146/slip44:10007', // Sonic Mainnet - Native symbol: S
-- '0xfa': 'eip155:250/slip44:1007', // Fantom Opera - Native symbol: FTM
-- '0x141': 'eip155:321/erc20:0x0000000000000000000000000000000000000000', // 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS
-- '0x144': 'eip155:324/slip44:60', // zkSync Era Mainnet (Ethereum L2) - Native symbol: ETH
-- '0x169': 'eip155:361/erc20:0x0000000000000000000000000000000000000000', // 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL
-- '0x3e7': 'eip155:999/slip44:2457', // HyperEVM - Native symbol: ETH
-- '0x440': 'eip155:1088/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // 'eip155:1088/slip44:XXX', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: METIS
-- '0x44d': 'eip155:1101/slip44:60', // Polygon zkEVM mainnet - Native symbol: ETH
-- '0x504': 'eip155:1284/slip44:1284', // Moonbeam - Native symbol: GLMR
-- '0x505': 'eip155:1285/slip44:1285', // Moonriver - Native symbol: MOVR
-- '0x531': 'eip155:1329/slip44:19000118', // Sei Mainnet - Native symbol: SEI
-- '0x1388': 'eip155:5000/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT
-- '0x2105': 'eip155:8453/slip44:60', // Base - Native symbol: ETH
-- '0x2710': 'eip155:10000/erc20:0x0000000000000000000000000000000000000000', // 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH
-- '0xa4b1': 'eip155:42161/slip44:60', // Arbitrum One - Native symbol: ETH
-- '0xa4ec': 'eip155:42220/slip44:52752', // Celo Mainnet - Native symbol: CELO
-- '0xa516': 'eip155:42262/erc20:0x0000000000000000000000000000000000000000', // 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE
-- '0xa86a': 'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX
-- '0xe708': 'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH
-- '0x13c31': 'eip155:81457/erc20:0x0000000000000000000000000000000000000000', // 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH
-- '0x17dcd': 'eip155:97741/erc20:0x0000000000000000000000000000000000000000', // 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU
-+ '0x92': null, //'eip155:146/slip44:10007', // Sonic Mainnet - Native symbol: S
-+ '0xfa': null, //'eip155:250/slip44:1007', // Fantom Opera - Native symbol: FTM
-+ '0x141': null, //'eip155:321/erc20:0x0000000000000000000000000000000000000000', // 'eip155:321/slip44:641', // KCC Mainnet - Native symbol: KCS
-+ '0x144': null, //'eip155:324/slip44:60', // zkSync Era Mainnet (Ethereum L2) - Native symbol: ETH
-+ '0x169': null, //'eip155:361/erc20:0x0000000000000000000000000000000000000000', // 'eip155:361/slip44:589', // Theta Mainnet - Native symbol: TFUEL
-+ '0x3e7': null, //'eip155:999/slip44:2457', // HyperEVM - Native symbol: ETH
-+ '0x440': null, //'eip155:1088/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // 'eip155:1088/slip44:XXX', // Metis Andromeda Mainnet (Ethereum L2) - Native symbol: METIS
-+ '0x44d': null, //'eip155:1101/slip44:60', // Polygon zkEVM mainnet - Native symbol: ETH
-+ '0x504': null, //'eip155:1284/slip44:1284', // Moonbeam - Native symbol: GLMR
-+ '0x505': null, //'eip155:1285/slip44:1285', // Moonriver - Native symbol: MOVR
-+ '0x531': null, //'eip155:1329/slip44:19000118', // Sei Mainnet - Native symbol: SEI
-+ '0x1388': null, //'eip155:5000/erc20:0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000', // 'eip155:5000/slip44:XXX', // Mantle - Native symbol: MNT
-+ '0x2105': null, //'eip155:8453/slip44:60', // Base - Native symbol: ETH
-+ '0x2710': null, //'eip155:10000/erc20:0x0000000000000000000000000000000000000000', // 'eip155:10000/slip44:145', // Smart Bitcoin Cash - Native symbol: BCH
-+ '0xa4b1': null, //'eip155:42161/slip44:60', // Arbitrum One - Native symbol: ETH
-+ '0xa4ec': null, //'eip155:42220/slip44:52752', // Celo Mainnet - Native symbol: CELO
-+ '0xa516': null, //'eip155:42262/erc20:0x0000000000000000000000000000000000000000', // 'eip155:42262/slip44:474', // Oasis Emerald - Native symbol: ROSE
-+ '0xa86a': null, //'eip155:43114/slip44:9005', // Avalanche C-Chain - Native symbol: AVAX
-+ '0xe708': null, //'eip155:59144/slip44:60', // Linea Mainnet - Native symbol: ETH
-+ '0x13c31': null, //'eip155:81457/erc20:0x0000000000000000000000000000000000000000', // 'eip155:81457/slip44:60', // Blast Mainnet - Native symbol: ETH
-+ '0x17dcd': null, //'eip155:97741/erc20:0x0000000000000000000000000000000000000000', // 'eip155:97741/slip44:XXX', // Pepe Unchained Mainnet - Native symbol: PEPU
- '0x518af': null, // 'eip155:333999/slip44:1997', // Polis Mainnet - Native symbol: POLIS
-- '0x82750': 'eip155:534352/slip44:60', // Scroll Mainnet - Native symbol: ETH
-- '0x4e454152': 'eip155:60/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH
-- '0x63564c40': 'eip155:1666600000/slip44:1023', // Harmony Mainnet Shard 0 - Native symbol: ONE
-+ '0x82750': null, //'eip155:534352/slip44:60', // Scroll Mainnet - Native symbol: ETH
-+ '0x4e454152': null, //'eip155:60/slip44:60', // Aurora Mainnet (Ethereum L2 on NEAR) - Native symbol: ETH
-+ '0x63564c40': null, //'eip155:1666600000/slip44:1023', // Harmony Mainnet Shard 0 - Native symbol: ONE
- };
- /**
- * The list of chain IDs that can be supplied in the URL for the `/spot-prices`
diff --git a/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts b/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts
index ecd746b6f76e..f3e1a29f33c3 100644
--- a/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts
+++ b/app/components/UI/Bridge/_mocks_/bridgeReducerState.ts
@@ -33,4 +33,5 @@ export const mockBridgeReducerState: BridgeState = {
isGasIncluded7702Supported: false,
bridgeViewMode: BridgeViewMode.Bridge,
isSelectingRecipient: false,
+ isDestTokenManuallySet: false,
};
diff --git a/app/components/UI/Bridge/components/BridgeDestTokenSelector/BridgeDestTokenSelector.test.tsx b/app/components/UI/Bridge/components/BridgeDestTokenSelector/BridgeDestTokenSelector.test.tsx
index 2d7affb06777..390395a76663 100644
--- a/app/components/UI/Bridge/components/BridgeDestTokenSelector/BridgeDestTokenSelector.test.tsx
+++ b/app/components/UI/Bridge/components/BridgeDestTokenSelector/BridgeDestTokenSelector.test.tsx
@@ -9,6 +9,7 @@ import Routes from '../../../../../constants/navigation/Routes';
import {
selectBridgeViewMode,
setDestToken,
+ setIsDestTokenManuallySet,
} from '../../../../../core/redux/slices/bridge';
import { cloneDeep } from 'lodash';
import { BridgeViewMode } from '../../types';
@@ -42,6 +43,7 @@ jest.mock('../../../../../core/redux/slices/bridge', () => {
...actual,
default: actual.default,
setDestToken: jest.fn(actual.setDestToken),
+ setIsDestTokenManuallySet: jest.fn(actual.setIsDestTokenManuallySet),
selectBridgeViewMode: jest.fn().mockReturnValue('Bridge'),
};
});
@@ -304,7 +306,8 @@ describe('BridgeDestTokenSelector', () => {
// TODO: Fix flaky test - timing issue with debounced token selection (500ms)
// Test fails intermittently due to race condition between waitFor and debounce
- it.skip('handles token selection correctly', async () => {
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('handles token selection correctly and marks dest token as manually set', async () => {
// Arrange
const { getByText } = renderScreen(
BridgeDestTokenSelector,
@@ -325,7 +328,7 @@ describe('BridgeDestTokenSelector', () => {
jest.advanceTimersByTime(500);
});
- // Assert - check that actions were called
+ // Assert - check that setDestToken was called with the selected token
expect(setDestToken).toHaveBeenCalledWith(
expect.objectContaining({
address: ethToken2Address,
@@ -337,6 +340,8 @@ describe('BridgeDestTokenSelector', () => {
symbol: 'HELLO',
}),
);
+ // Also verify the manual flag was set
+ expect(setIsDestTokenManuallySet).toHaveBeenCalledWith(true);
expect(mockGoBack).toHaveBeenCalled();
});
diff --git a/app/components/UI/Bridge/components/BridgeDestTokenSelector/index.tsx b/app/components/UI/Bridge/components/BridgeDestTokenSelector/index.tsx
index b9054a8b70f2..8c8100a324dc 100644
--- a/app/components/UI/Bridge/components/BridgeDestTokenSelector/index.tsx
+++ b/app/components/UI/Bridge/components/BridgeDestTokenSelector/index.tsx
@@ -10,6 +10,7 @@ import {
selectSelectedDestChainId,
selectSourceToken,
setDestToken,
+ setIsDestTokenManuallySet,
} from '../../../../../core/redux/slices/bridge';
import { getNetworkImageSource } from '../../../../../util/networks';
import { TokenSelectorItem } from '../TokenSelectorItem';
@@ -78,7 +79,9 @@ export const BridgeDestTokenSelector: React.FC = React.memo(() => {
const handleTokenPress = useCallback(
(token: BridgeToken) => {
+ // Mark as manually set to prevent auto-updating dest when source chain changes
dispatch(setDestToken(token));
+ dispatch(setIsDestTokenManuallySet(true));
navigation.goBack();
},
[dispatch, navigation],
diff --git a/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/BridgeSourceNetworkSelector.test.tsx b/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/BridgeSourceNetworkSelector.test.tsx
index 7af160165bcf..6caaaf26f64d 100644
--- a/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/BridgeSourceNetworkSelector.test.tsx
+++ b/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/BridgeSourceNetworkSelector.test.tsx
@@ -6,7 +6,10 @@ import { BridgeSourceNetworkSelector } from '.';
import Routes from '../../../../../constants/navigation/Routes';
import { strings } from '../../../../../../locales/i18n';
import { Hex } from '@metamask/utils';
-import { setSelectedSourceChainIds } from '../../../../../core/redux/slices/bridge';
+import {
+ setSelectedSourceChainIds,
+ setSourceToken,
+} from '../../../../../core/redux/slices/bridge';
import { BridgeSourceNetworkSelectorSelectorsIDs } from '../../../../../../e2e/selectors/Bridge/BridgeSourceNetworkSelector.selectors';
import { cloneDeep } from 'lodash';
import { MultichainNetwork } from '@metamask/multichain-transactions-controller';
@@ -35,19 +38,34 @@ jest.mock('../../../../../core/redux/slices/bridge', () => {
};
});
+const mockOnSetRpcTarget = jest.fn().mockResolvedValue(undefined);
+const mockOnNonEvmNetworkChange = jest.fn().mockResolvedValue(undefined);
+
jest.mock('../../../../Views/NetworkSelector/useSwitchNetworks', () => ({
useSwitchNetworks: jest.fn(() => ({
- onSetRpcTarget: jest.fn().mockResolvedValue(undefined),
+ onSetRpcTarget: mockOnSetRpcTarget,
onNetworkChange: jest.fn(),
+ onNonEvmNetworkChange: mockOnNonEvmNetworkChange,
})),
}));
+const mockAutoUpdateDestToken = jest.fn();
+
+jest.mock('../../hooks/useAutoUpdateDestToken', () => ({
+ useAutoUpdateDestToken: () => ({
+ autoUpdateDestToken: mockAutoUpdateDestToken,
+ }),
+}));
+
describe('BridgeSourceNetworkSelector', () => {
const mockChainId = '0x1' as Hex;
const optimismChainId = '0xa' as Hex;
beforeEach(() => {
jest.clearAllMocks();
+ mockOnSetRpcTarget.mockResolvedValue(undefined);
+ mockOnNonEvmNetworkChange.mockResolvedValue(undefined);
+ mockAutoUpdateDestToken.mockClear();
});
it('renders with initial state and displays networks', async () => {
@@ -223,6 +241,183 @@ describe('BridgeSourceNetworkSelector', () => {
expect(applyButton.props.disabled).toBe(true);
});
+ describe('handleApply', () => {
+ it('calls onApply callback when provided instead of dispatching actions', async () => {
+ const mockOnApply = jest.fn();
+ const { getAllByTestId, getByText } = renderScreen(
+ () => ,
+ {
+ name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
+ },
+ { state: initialState },
+ );
+
+ // Uncheck Ethereum to have only Optimism selected
+ const ethereumCheckbox = getAllByTestId(`checkbox-${mockChainId}`)[0];
+ fireEvent.press(ethereumCheckbox);
+
+ // Click Apply button
+ const applyButton = getByText('Apply');
+ fireEvent.press(applyButton);
+
+ await waitFor(() => {
+ expect(mockOnApply).toHaveBeenCalledWith([
+ optimismChainId,
+ SolScope.Mainnet,
+ BtcScope.Mainnet,
+ TrxScope.Mainnet,
+ ]);
+ expect(mockGoBack).not.toHaveBeenCalled();
+ expect(setSelectedSourceChainIds).not.toHaveBeenCalled();
+ });
+ });
+
+ it('sets source token to native token when single EVM network is selected', async () => {
+ const { getAllByTestId, getByText } = renderScreen(
+ BridgeSourceNetworkSelector,
+ {
+ name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
+ },
+ { state: initialState },
+ );
+
+ // Deselect all first
+ const deselectAllButton = getByText('Deselect all');
+ fireEvent.press(deselectAllButton);
+
+ // Select only Ethereum
+ const ethereumCheckbox = getAllByTestId(`checkbox-${mockChainId}`)[0];
+ fireEvent.press(ethereumCheckbox);
+
+ // Click Apply
+ const applyButton = getByText('Apply');
+ fireEvent.press(applyButton);
+
+ await waitFor(() => {
+ expect(setSourceToken).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chainId: mockChainId,
+ symbol: 'ETH',
+ }),
+ );
+ });
+ });
+
+ it('calls autoUpdateDestToken when single network is selected', async () => {
+ const { getAllByTestId, getByText } = renderScreen(
+ BridgeSourceNetworkSelector,
+ {
+ name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
+ },
+ { state: initialState },
+ );
+
+ // Deselect all first
+ const deselectAllButton = getByText('Deselect all');
+ fireEvent.press(deselectAllButton);
+
+ // Select only Optimism
+ const optimismCheckbox = getAllByTestId(`checkbox-${optimismChainId}`)[0];
+ fireEvent.press(optimismCheckbox);
+
+ // Click Apply
+ const applyButton = getByText('Apply');
+ fireEvent.press(applyButton);
+
+ await waitFor(() => {
+ expect(mockAutoUpdateDestToken).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chainId: optimismChainId,
+ }),
+ );
+ });
+ });
+
+ it('calls onSetRpcTarget when single EVM network is selected', async () => {
+ const { getAllByTestId, getByText } = renderScreen(
+ BridgeSourceNetworkSelector,
+ {
+ name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
+ },
+ { state: initialState },
+ );
+
+ // Deselect all first
+ const deselectAllButton = getByText('Deselect all');
+ fireEvent.press(deselectAllButton);
+
+ // Select only Ethereum
+ const ethereumCheckbox = getAllByTestId(`checkbox-${mockChainId}`)[0];
+ fireEvent.press(ethereumCheckbox);
+
+ // Click Apply
+ const applyButton = getByText('Apply');
+ fireEvent.press(applyButton);
+
+ await waitFor(() => {
+ expect(mockOnSetRpcTarget).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chainId: mockChainId,
+ }),
+ );
+ });
+ });
+
+ it('calls onNonEvmNetworkChange when single non-EVM network is selected', async () => {
+ const { getAllByTestId, getByText } = renderScreen(
+ BridgeSourceNetworkSelector,
+ {
+ name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
+ },
+ { state: initialState },
+ );
+
+ // Deselect all first
+ const deselectAllButton = getByText('Deselect all');
+ fireEvent.press(deselectAllButton);
+
+ // Select only Solana
+ const solanaCheckbox = getAllByTestId(`checkbox-${SolScope.Mainnet}`)[0];
+ fireEvent.press(solanaCheckbox);
+
+ // Click Apply
+ const applyButton = getByText('Apply');
+ fireEvent.press(applyButton);
+
+ await waitFor(() => {
+ expect(mockOnNonEvmNetworkChange).toHaveBeenCalledWith(
+ SolScope.Mainnet,
+ );
+ expect(mockOnSetRpcTarget).not.toHaveBeenCalled();
+ });
+ });
+
+ it('does not set source token or switch network when multiple networks are selected', async () => {
+ const { getByText } = renderScreen(
+ BridgeSourceNetworkSelector,
+ {
+ name: Routes.BRIDGE.MODALS.SOURCE_NETWORK_SELECTOR,
+ },
+ { state: initialState },
+ );
+
+ // Keep all networks selected and click Apply
+ const applyButton = getByText('Apply');
+ fireEvent.press(applyButton);
+
+ await waitFor(() => {
+ expect(setSelectedSourceChainIds).toHaveBeenCalled();
+ expect(mockGoBack).toHaveBeenCalled();
+ });
+
+ // These should NOT be called when multiple networks are selected
+ expect(setSourceToken).not.toHaveBeenCalled();
+ expect(mockAutoUpdateDestToken).not.toHaveBeenCalled();
+ expect(mockOnSetRpcTarget).not.toHaveBeenCalled();
+ expect(mockOnNonEvmNetworkChange).not.toHaveBeenCalled();
+ });
+ });
+
it('networks should be sorted by fiat value in descending order', async () => {
const { getAllByTestId } = renderScreen(
BridgeSourceNetworkSelector,
diff --git a/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/index.tsx b/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/index.tsx
index b2fb7741cc63..04bf46f26ee4 100644
--- a/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/index.tsx
+++ b/app/components/UI/Bridge/components/BridgeSourceNetworkSelector/index.tsx
@@ -34,6 +34,7 @@ import { selectEvmNetworkConfigurationsByChainId } from '../../../../../selector
import { getNativeSourceToken } from '../../utils/tokenUtils';
import { getGasFeesSponsoredNetworkEnabled } from '../../../../../selectors/featureFlagController/gasFeesSponsored';
import { NETWORK_TO_SHORT_NETWORK_NAME_MAP } from '../../../../../constants/bridge';
+import { useAutoUpdateDestToken } from '../../hooks/useAutoUpdateDestToken';
const createStyles = () =>
StyleSheet.create({
@@ -96,6 +97,7 @@ export const BridgeSourceNetworkSelector: React.FC<
const isGasFeesSponsoredNetworkEnabled = useSelector(
getGasFeesSponsoredNetworkEnabled,
);
+ const { autoUpdateDestToken } = useAutoUpdateDestToken();
// Local state for candidate network selections
const [candidateSourceChainIds, setCandidateSourceChainIds] = useState<
@@ -141,14 +143,16 @@ export const BridgeSourceNetworkSelector: React.FC<
// If there's only 1 network selected, set the source token to native token of that chain and switch chains
if (newSelectedSourceChainids.length === 1) {
+ const newSourceChainId = newSelectedSourceChainids[0] as
+ | Hex
+ | CaipChainId;
+ const newSourceToken = getNativeSourceToken(newSourceChainId);
+
// Reset the source token
- dispatch(
- setSourceToken(
- getNativeSourceToken(
- newSelectedSourceChainids[0] as Hex | CaipChainId,
- ),
- ),
- );
+ dispatch(setSourceToken(newSourceToken));
+
+ // Auto-update destination token when source chain changes AND dest wasn't manually set
+ autoUpdateDestToken(newSourceToken);
const evmNetworkConfiguration =
evmNetworkConfigurations[newSelectedSourceChainids[0] as Hex];
@@ -171,6 +175,7 @@ export const BridgeSourceNetworkSelector: React.FC<
enabledSourceChainIds,
evmNetworkConfigurations,
onSetRpcTarget,
+ autoUpdateDestToken,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
onNonEvmNetworkChange,
///: END:ONLY_INCLUDE_IF
diff --git a/app/components/UI/Bridge/components/BridgeSourceTokenSelector/index.tsx b/app/components/UI/Bridge/components/BridgeSourceTokenSelector/index.tsx
index 12a621d3d9dd..c1ede3f1eff6 100644
--- a/app/components/UI/Bridge/components/BridgeSourceTokenSelector/index.tsx
+++ b/app/components/UI/Bridge/components/BridgeSourceTokenSelector/index.tsx
@@ -34,6 +34,7 @@ import { BridgeToken, BridgeViewMode } from '../../types';
import { useSwitchNetworks } from '../../../../Views/NetworkSelector/useSwitchNetworks';
import { useNetworkInfo } from '../../../../../selectors/selectedNetworkController';
import { NETWORK_TO_SHORT_NETWORK_NAME_MAP } from '../../../../../constants/bridge';
+import { useAutoUpdateDestToken } from '../../hooks/useAutoUpdateDestToken';
export const BridgeSourceTokenSelector: React.FC = React.memo(() => {
const dispatch = useDispatch();
@@ -50,6 +51,7 @@ export const BridgeSourceTokenSelector: React.FC = React.memo(() => {
const selectedSourceToken = useSelector(selectSourceToken);
const selectedDestToken = useSelector(selectDestToken);
const selectedChainId = useSelector(selectChainId);
+ const { autoUpdateDestToken } = useAutoUpdateDestToken();
const {
chainId: selectedEvmChainId, // Will be the most recently selected EVM chain if you are on Solana
@@ -99,6 +101,8 @@ export const BridgeSourceTokenSelector: React.FC = React.memo(() => {
// and also the next time you open up the token selector to fetch top tokens for the right chain
navigation.goBack();
dispatch(setSourceToken(token));
+ // Auto-update destination token when source chain changes AND dest wasn't manually set
+ autoUpdateDestToken(token);
// Switch to the chain of the selected token
const evmNetworkConfiguration =
@@ -119,6 +123,7 @@ export const BridgeSourceTokenSelector: React.FC = React.memo(() => {
dispatch,
evmNetworkConfigurations,
onSetRpcTarget,
+ autoUpdateDestToken,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
onNonEvmNetworkChange,
///: END:ONLY_INCLUDE_IF
diff --git a/app/components/UI/Bridge/hooks/useAutoUpdateDestToken/index.ts b/app/components/UI/Bridge/hooks/useAutoUpdateDestToken/index.ts
new file mode 100644
index 000000000000..e4e1275e5731
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useAutoUpdateDestToken/index.ts
@@ -0,0 +1,102 @@
+import { useCallback } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { BtcScope, EthScope } from '@metamask/keyring-api';
+import { formatChainIdToCaip } from '@metamask/bridge-controller';
+import {
+ setDestToken,
+ selectDestToken,
+ selectIsDestTokenManuallySet,
+} from '../../../../../core/redux/slices/bridge';
+import {
+ getDefaultDestToken,
+ getNativeSourceToken,
+} from '../../utils/tokenUtils';
+import { areAddressesEqual } from '../../../../../util/address';
+import { BridgeToken } from '../../types';
+
+/**
+ * Hook that provides a function to auto-update the destination token when the source changes.
+ * Assume same-chain swap when user hasn't explicitly set dest.
+ *
+ * The dest token is updated if:
+ * 1. The source chain has changed (new source is on different chain than current dest)
+ * 2. Same-chain but dest needs correction (e.g., dest is native fallback but source no longer conflicts with default dest, or source now conflicts with current dest)
+ * 3. The user hasn't manually selected a destination token
+ *
+ * Special cases:
+ * - Bitcoin source: defaults to ETH on Ethereum (same-chain swaps don't make sense for BTC)
+ * - If default dest token equals source token: falls back to native token
+ * - If no default dest exists for chain: falls back to native token
+ */
+export const useAutoUpdateDestToken = () => {
+ const dispatch = useDispatch();
+ const selectedDestToken = useSelector(selectDestToken);
+ const isDestTokenManuallySet = useSelector(selectIsDestTokenManuallySet);
+
+ const autoUpdateDestToken = useCallback(
+ (newSourceToken: BridgeToken) => {
+ // Never auto-update if dest was manually set by user
+ if (isDestTokenManuallySet) {
+ return;
+ }
+
+ // No dest token set yet, nothing to update from
+ if (!selectedDestToken?.chainId) {
+ return;
+ }
+
+ // For Bitcoin source, default to ETH on Ethereum
+ // (same-chain swaps don't make sense for BTC)
+ if (newSourceToken.chainId === BtcScope.Mainnet) {
+ if (
+ !areAddressesEqual(
+ selectedDestToken.address,
+ getNativeSourceToken(EthScope.Mainnet).address,
+ ) ||
+ formatChainIdToCaip(selectedDestToken.chainId) !== EthScope.Mainnet
+ ) {
+ dispatch(setDestToken(getNativeSourceToken(EthScope.Mainnet)));
+ }
+ return;
+ }
+
+ // Calculate what the dest token should be based on the new source
+ const defaultDestToken = getDefaultDestToken(newSourceToken.chainId);
+ const nativeToken = getNativeSourceToken(newSourceToken.chainId);
+
+ let expectedDestToken: BridgeToken | undefined;
+
+ // Determine expected dest token
+ if (
+ defaultDestToken &&
+ !areAddressesEqual(newSourceToken.address, defaultDestToken.address)
+ ) {
+ // Default dest is valid (doesn't conflict with source)
+ expectedDestToken = defaultDestToken;
+ } else if (
+ !areAddressesEqual(newSourceToken.address, nativeToken.address)
+ ) {
+ // Fall back to native token if:
+ // 1. No default dest token exists for this chain, OR
+ // 2. Default dest equals source token
+ expectedDestToken = nativeToken;
+ }
+
+ // Only dispatch if expected dest is different from current dest
+ if (
+ expectedDestToken &&
+ (!areAddressesEqual(
+ selectedDestToken.address,
+ expectedDestToken.address,
+ ) ||
+ formatChainIdToCaip(selectedDestToken.chainId) !==
+ formatChainIdToCaip(expectedDestToken.chainId))
+ ) {
+ dispatch(setDestToken(expectedDestToken));
+ }
+ },
+ [dispatch, selectedDestToken, isDestTokenManuallySet],
+ );
+
+ return { autoUpdateDestToken };
+};
diff --git a/app/components/UI/Bridge/hooks/useAutoUpdateDestToken/useAutoUpdateDestToken.test.ts b/app/components/UI/Bridge/hooks/useAutoUpdateDestToken/useAutoUpdateDestToken.test.ts
new file mode 100644
index 000000000000..603c21aa959f
--- /dev/null
+++ b/app/components/UI/Bridge/hooks/useAutoUpdateDestToken/useAutoUpdateDestToken.test.ts
@@ -0,0 +1,463 @@
+import {
+ initialState,
+ ethChainId,
+ optimismChainId,
+} from '../../_mocks_/initialState';
+import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
+import { useAutoUpdateDestToken } from '.';
+import { BridgeToken } from '../../types';
+import { Hex } from '@metamask/utils';
+import { BtcScope, SolScope } from '@metamask/keyring-api';
+// eslint-disable-next-line import/no-namespace
+import * as bridgeSlice from '../../../../../core/redux/slices/bridge';
+// eslint-disable-next-line import/no-namespace
+import * as tokenUtils from '../../utils/tokenUtils';
+
+describe('useAutoUpdateDestToken', () => {
+ const mockEthToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000001',
+ symbol: 'TOKEN',
+ name: 'Test Token',
+ decimals: 18,
+ chainId: ethChainId,
+ };
+
+ const mockOptimismToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000002',
+ symbol: 'OP_TOKEN',
+ name: 'Optimism Token',
+ decimals: 18,
+ chainId: optimismChainId,
+ };
+
+ const mockPolygonToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000003',
+ symbol: 'MATIC_TOKEN',
+ name: 'Polygon Token',
+ decimals: 18,
+ chainId: '0x89' as Hex,
+ };
+
+ const mockBtcToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'BTC',
+ name: 'Bitcoin',
+ decimals: 8,
+ chainId: BtcScope.Mainnet,
+ };
+
+ const mockSolToken: BridgeToken = {
+ address: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
+ symbol: 'SOL',
+ name: 'Solana',
+ decimals: 9,
+ chainId: SolScope.Mainnet,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when source chain changes and dest was not manually set', () => {
+ it('updates dest token to default for new source chain', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mockEthToken,
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to Optimism (different chain from dest)
+ result.current.autoUpdateDestToken(mockOptimismToken);
+
+ // Dest should be updated to Optimism's default (USDC)
+ expect(setDestTokenSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chainId: optimismChainId,
+ }),
+ );
+ });
+
+ it('sets ETH on Ethereum as dest when source is Bitcoin', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mockSolToken, // Currently on Solana
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to Bitcoin
+ result.current.autoUpdateDestToken(mockBtcToken);
+
+ // Dest should be ETH on Ethereum (0x1)
+ expect(setDestTokenSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ symbol: 'ETH',
+ chainId: '0x1',
+ address: '0x0000000000000000000000000000000000000000',
+ }),
+ );
+ });
+ });
+
+ describe('when dest was manually set', () => {
+ it('does not update dest token when source chain changes', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mockEthToken,
+ isDestTokenManuallySet: true, // User manually selected dest
+ },
+ },
+ },
+ );
+
+ // Change source to Optimism (different chain from dest)
+ result.current.autoUpdateDestToken(mockOptimismToken);
+
+ // Dest should NOT be updated because it was manually set
+ expect(setDestTokenSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('same-chain source token changes', () => {
+ it('does not update dest when current dest already matches expected dest', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ // Dest is already the default (mUSD for Ethereum)
+ const mUsdToken: BridgeToken = {
+ address: '0xaca92e438df0b2401ff60da7e4337b687a2435da',
+ symbol: 'mUSD',
+ name: 'MetaMask USD',
+ decimals: 6,
+ chainId: ethChainId,
+ };
+
+ const anotherEthToken: BridgeToken = {
+ ...mockEthToken,
+ address: '0x0000000000000000000000000000000000000099',
+ symbol: 'ANOTHER',
+ };
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mUsdToken, // Already the expected default
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to another token on same chain (Ethereum)
+ result.current.autoUpdateDestToken(anotherEthToken);
+
+ // Dest should NOT be updated because it already matches expected (mUSD)
+ expect(setDestTokenSpy).not.toHaveBeenCalled();
+ });
+
+ it('updates dest from native fallback to default when source no longer conflicts', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ // Current dest is native ETH (set as fallback because source was mUSD)
+ const nativeEthToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ decimals: 18,
+ chainId: ethChainId,
+ };
+
+ // New source is a different token that doesn't conflict with default (mUSD)
+ const nonConflictingSource: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000099',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ decimals: 6,
+ chainId: ethChainId,
+ };
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: nativeEthToken, // Native fallback
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to a non-conflicting token on same chain
+ result.current.autoUpdateDestToken(nonConflictingSource);
+
+ // Dest should update to the default (mUSD) since source no longer conflicts
+ expect(setDestTokenSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ symbol: 'mUSD',
+ chainId: ethChainId,
+ address: '0xaca92e438df0b2401ff60da7e4337b687a2435da',
+ }),
+ );
+ });
+
+ it('updates dest to native when source changes to match default dest', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ // Current dest is the default mUSD
+ const mUsdToken: BridgeToken = {
+ address: '0xaca92e438df0b2401ff60da7e4337b687a2435da',
+ symbol: 'mUSD',
+ name: 'MetaMask USD',
+ decimals: 6,
+ chainId: ethChainId,
+ };
+
+ // New source is also mUSD (conflicts with dest)
+ const mUsdSourceToken: BridgeToken = {
+ address: '0xaca92e438df0b2401ff60da7e4337b687a2435da',
+ symbol: 'mUSD',
+ name: 'MetaMask USD',
+ decimals: 6,
+ chainId: ethChainId,
+ };
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mUsdToken,
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to mUSD (same as current dest)
+ result.current.autoUpdateDestToken(mUsdSourceToken);
+
+ // Dest should update to native ETH since source now conflicts with default
+ expect(setDestTokenSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ symbol: 'ETH',
+ chainId: ethChainId,
+ address: '0x0000000000000000000000000000000000000000',
+ }),
+ );
+ });
+ });
+
+ describe('when there is no current dest token', () => {
+ it('does not update dest token when destToken is undefined', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: undefined,
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ result.current.autoUpdateDestToken(mockOptimismToken);
+
+ // Dest should NOT be updated because there's no current dest to compare
+ expect(setDestTokenSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when default dest token equals source token', () => {
+ it('falls back to native token for the chain', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ // Mock a source token that matches the default dest token address
+ // For Ethereum mainnet, the default dest is mUSD at 0xaca92e438df0b2401ff60da7e4337b687a2435da
+ const sourceTokenMatchingDefault: BridgeToken = {
+ address: '0xaca92e438df0b2401ff60da7e4337b687a2435da',
+ symbol: 'mUSD',
+ name: 'MetaMask USD',
+ decimals: 6,
+ chainId: ethChainId,
+ };
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mockPolygonToken, // Currently on Polygon
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to Ethereum with a token that matches default dest
+ result.current.autoUpdateDestToken(sourceTokenMatchingDefault);
+
+ // Should fall back to native ETH since default dest (mUSD) matches source
+ expect(setDestTokenSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ symbol: 'ETH',
+ chainId: ethChainId,
+ address: '0x0000000000000000000000000000000000000000',
+ }),
+ );
+ });
+ });
+
+ describe('when chain has no default dest token configured', () => {
+ it('falls back to native token when getDefaultDestToken returns undefined', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ // Mock getDefaultDestToken to return undefined
+ const getDefaultDestTokenSpy = jest
+ .spyOn(tokenUtils, 'getDefaultDestToken')
+ .mockReturnValue(undefined);
+
+ // Mock getNativeSourceToken to return a predictable native token
+ const mockNativeToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'NATIVE',
+ name: 'Native Token',
+ decimals: 18,
+ chainId: optimismChainId,
+ };
+ const getNativeSourceTokenSpy = jest
+ .spyOn(tokenUtils, 'getNativeSourceToken')
+ .mockReturnValue(mockNativeToken);
+
+ const mockSourceToken: BridgeToken = {
+ address: '0x0000000000000000000000000000000000000001',
+ symbol: 'SOME_TOKEN',
+ name: 'Some Token',
+ decimals: 18,
+ chainId: optimismChainId,
+ };
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mockEthToken, // Currently on Ethereum (different chain)
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to Optimism where default returns undefined
+ result.current.autoUpdateDestToken(mockSourceToken);
+
+ // Should fall back to native token since no default exists
+ expect(setDestTokenSpy).toHaveBeenCalledWith(mockNativeToken);
+
+ // Cleanup
+ getDefaultDestTokenSpy.mockRestore();
+ getNativeSourceTokenSpy.mockRestore();
+ });
+ });
+
+ describe('cross-chain scenarios', () => {
+ it('updates dest when switching from EVM to Solana source', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mockEthToken, // Currently on Ethereum
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to Solana
+ result.current.autoUpdateDestToken(mockSolToken);
+
+ // Dest should be updated to Solana's default (USDC)
+ expect(setDestTokenSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chainId: SolScope.Mainnet,
+ }),
+ );
+ });
+
+ it('updates dest when switching from Solana to EVM source', () => {
+ const setDestTokenSpy = jest.spyOn(bridgeSlice, 'setDestToken');
+
+ const { result } = renderHookWithProvider(
+ () => useAutoUpdateDestToken(),
+ {
+ state: {
+ ...initialState,
+ bridge: {
+ ...initialState.bridge,
+ destToken: mockSolToken, // Currently on Solana
+ isDestTokenManuallySet: false,
+ },
+ },
+ },
+ );
+
+ // Change source to Ethereum
+ result.current.autoUpdateDestToken(mockEthToken);
+
+ // Dest should be updated to Ethereum's default (mUSD)
+ expect(setDestTokenSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chainId: ethChainId,
+ }),
+ );
+ });
+ });
+});
diff --git a/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/index.ts b/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/index.ts
index 4c867162b941..c7b7960d6830 100644
--- a/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/index.ts
+++ b/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/index.ts
@@ -24,6 +24,7 @@ import {
selectIsBridgeEnabledSourceFactory,
setSourceToken,
setDestToken,
+ setIsDestTokenManuallySet,
} from '../../../../../core/redux/slices/bridge';
import { trace, TraceName } from '../../../../../util/trace';
import { useCurrentNetworkInfo } from '../../../../hooks/useCurrentNetworkInfo';
@@ -127,6 +128,11 @@ export const useSwapBridgeNavigation = ({
sourceToken = getNativeSourceToken(EthScope.Mainnet);
}
+ // Reset the manual dest token flag on navigation so auto-update works correctly
+ // This ensures if user previously manually set dest, then closed and reopened the app,
+ // changing source token will still auto-update the dest token
+ dispatch(setIsDestTokenManuallySet(false));
+
// Pre-populate Redux state before navigation to prevent empty button flash
dispatch(setSourceToken(sourceToken));
diff --git a/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/useSwapBridgeNavigation.test.ts b/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/useSwapBridgeNavigation.test.ts
index b809cba2d90e..5401ed8ed5f5 100644
--- a/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/useSwapBridgeNavigation.test.ts
+++ b/app/components/UI/Bridge/hooks/useSwapBridgeNavigation/useSwapBridgeNavigation.test.ts
@@ -38,12 +38,20 @@ jest.mock('../../../../hooks/useMetrics', () => {
});
const mockGetIsBridgeEnabledSource = jest.fn(() => true);
-jest.mock('../../../../../core/redux/slices/bridge', () => ({
- ...jest.requireActual('../../../../../core/redux/slices/bridge'),
- selectIsBridgeEnabledSourceFactory: jest.fn(
- () => mockGetIsBridgeEnabledSource,
- ),
-}));
+const mockSetIsDestTokenManuallySet = jest.fn();
+jest.mock('../../../../../core/redux/slices/bridge', () => {
+ const actual = jest.requireActual('../../../../../core/redux/slices/bridge');
+ return {
+ ...actual,
+ selectIsBridgeEnabledSourceFactory: jest.fn(
+ () => mockGetIsBridgeEnabledSource,
+ ),
+ setIsDestTokenManuallySet: (...args: unknown[]) => {
+ mockSetIsDestTokenManuallySet(...args);
+ return actual.setIsDestTokenManuallySet(...args);
+ },
+ };
+});
const mockGoToPortfolioBridge = jest.fn();
jest.mock('../useGoToPortfolioBridge', () => ({
@@ -146,6 +154,9 @@ describe('useSwapBridgeNavigation', () => {
// Reset bridge enabled mock to default (enabled)
mockGetIsBridgeEnabledSource.mockReturnValue(true);
+
+ // Reset setIsDestTokenManuallySet mock
+ mockSetIsDestTokenManuallySet.mockClear();
});
it('uses native token when no token is provided', () => {
@@ -317,6 +328,21 @@ describe('useSwapBridgeNavigation', () => {
});
});
+ it('resets isDestTokenManuallySet flag when navigating to swaps', () => {
+ const { result } = renderHookWithProvider(
+ () =>
+ useSwapBridgeNavigation({
+ location: mockLocation,
+ sourcePage: mockSourcePage,
+ }),
+ { state: initialState },
+ );
+
+ result.current.goToSwaps();
+
+ expect(mockSetIsDestTokenManuallySet).toHaveBeenCalledWith(false);
+ });
+
it('uses home page filter network when no token is provided', () => {
// Mock home page filter network as Polygon
mockUseCurrentNetworkInfo.mockReturnValue({
diff --git a/app/components/UI/Bridge/hooks/useSwitchTokens/index.ts b/app/components/UI/Bridge/hooks/useSwitchTokens/index.ts
index be5c28e6b681..814f7b7fe91b 100644
--- a/app/components/UI/Bridge/hooks/useSwitchTokens/index.ts
+++ b/app/components/UI/Bridge/hooks/useSwitchTokens/index.ts
@@ -6,6 +6,7 @@ import {
selectSourceToken,
setSelectedDestChainId,
setSourceAmount,
+ setIsDestTokenManuallySet,
} from '../../../../../core/redux/slices/bridge';
import { useNetworkInfo } from '../../../../../selectors/selectedNetworkController';
import { useSwitchNetworks } from '../../../../Views/NetworkSelector/useSwitchNetworks';
@@ -48,6 +49,8 @@ export const useSwitchTokens = () => {
if (sourceToken && destToken) {
dispatch(setSourceToken(destToken));
dispatch(setDestToken(sourceToken));
+ // Mark dest as manually set since user explicitly chose to flip
+ dispatch(setIsDestTokenManuallySet(true));
// Swap amounts
dispatch(setSourceAmount(destTokenAmount));
diff --git a/app/components/UI/DeleteWalletModal/styles.ts b/app/components/UI/DeleteWalletModal/styles.ts
index dfe2b0452da5..6b43ec4567c9 100644
--- a/app/components/UI/DeleteWalletModal/styles.ts
+++ b/app/components/UI/DeleteWalletModal/styles.ts
@@ -80,7 +80,7 @@ export const createStyles = (colors: any) =>
warningText: {
textAlign: 'left',
width: '100%',
- fontFamily: 'Geist Medium',
+ fontFamily: 'Geist-Medium',
},
warningTextContainer: {
flexDirection: 'column',
diff --git a/app/components/UI/Earn/Views/EarnMusdConversionEducationView/EarnMusdConversionEducationView.styles.ts b/app/components/UI/Earn/Views/EarnMusdConversionEducationView/EarnMusdConversionEducationView.styles.ts
index 60beddd55867..57b62dbe8a04 100644
--- a/app/components/UI/Earn/Views/EarnMusdConversionEducationView/EarnMusdConversionEducationView.styles.ts
+++ b/app/components/UI/Earn/Views/EarnMusdConversionEducationView/EarnMusdConversionEducationView.styles.ts
@@ -1,4 +1,4 @@
-import { Platform, StyleSheet } from 'react-native';
+import { StyleSheet } from 'react-native';
import type { Theme } from '../../../../../util/theme/models';
export const styleSheet = (params: { theme: Theme }) => {
@@ -24,8 +24,7 @@ export const styleSheet = (params: { theme: Theme }) => {
},
heading: {
marginBottom: 8,
- fontFamily:
- Platform.OS === 'android' ? 'MM Sans Regular' : 'MMSans-Regular',
+ fontFamily: 'MMSans-Regular',
},
bodyText: {
marginBottom: 32,
diff --git a/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.styles.ts b/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.styles.ts
index 463708ac0dbe..8e81a501843b 100644
--- a/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.styles.ts
+++ b/app/components/UI/Perps/components/PerpsGTMModal/PerpsGTMModal.styles.ts
@@ -78,9 +78,7 @@ const createStyles = (
? Platform.OS === 'ios'
? 'System'
: 'Roboto'
- : Platform.OS === 'ios'
- ? 'MM Poly'
- : 'MM Poly Regular',
+ : 'MMPoly-Regular',
fontWeight: useSystemFont
? '700'
: Platform.OS === 'ios'
diff --git a/app/components/UI/Predict/components/PredictGTMModal/PredictGTMModal.styles.ts b/app/components/UI/Predict/components/PredictGTMModal/PredictGTMModal.styles.ts
index 126904a6e68b..2927e7d86453 100644
--- a/app/components/UI/Predict/components/PredictGTMModal/PredictGTMModal.styles.ts
+++ b/app/components/UI/Predict/components/PredictGTMModal/PredictGTMModal.styles.ts
@@ -75,7 +75,7 @@ const createStyles = (theme: Theme) =>
flex: 1,
},
title: {
- fontFamily: Platform.OS === 'ios' ? 'MM Poly' : 'MM Poly Regular',
+ fontFamily: 'MMPoly-Regular',
fontWeight: '400',
// make it smaller on smaller screens
fontSize:
diff --git a/app/components/UI/ReviewModal/styles.ts b/app/components/UI/ReviewModal/styles.ts
index 78f6b5b93322..28156e4c40bf 100644
--- a/app/components/UI/ReviewModal/styles.ts
+++ b/app/components/UI/ReviewModal/styles.ts
@@ -20,7 +20,7 @@ export const createStyles = (colors: any) =>
optionIcon: { fontSize: 24 },
optionLabel: {
fontSize: 14,
- fontFamily: 'Geist Regular',
+ fontFamily: 'Geist-Regular',
color: colors.primary.default,
},
helpOption: { marginVertical: 12 },
@@ -29,14 +29,14 @@ export const createStyles = (colors: any) =>
questionLabel: {
fontSize: 18,
paddingHorizontal: 30,
- fontFamily: 'Geist Bold',
+ fontFamily: 'Geist-Bold',
textAlign: 'center',
color: colors.text.default,
lineHeight: 26,
},
description: {
fontSize: 14,
- fontFamily: 'Geist Regular',
+ fontFamily: 'Geist-Regular',
color: colors.text.alternative,
textAlign: 'center',
lineHeight: 20,
diff --git a/app/components/UI/Rewards/components/Onboarding/OnboardingIntroStep.tsx b/app/components/UI/Rewards/components/Onboarding/OnboardingIntroStep.tsx
index 3e88afcce09b..122fda0b7f8f 100644
--- a/app/components/UI/Rewards/components/Onboarding/OnboardingIntroStep.tsx
+++ b/app/components/UI/Rewards/components/Onboarding/OnboardingIntroStep.tsx
@@ -5,7 +5,7 @@ import React, {
useRef,
useState,
} from 'react';
-import { Image, ImageBackground, Platform, Text as RNText } from 'react-native';
+import { Image, ImageBackground, Text as RNText } from 'react-native';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { useDispatch, useSelector } from 'react-redux';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
@@ -318,7 +318,7 @@ const OnboardingIntroStep: React.FC<{
tw.style('text-center text-white text-12 leading-1 pt-1'),
// eslint-disable-next-line react-native/no-inline-styles
{
- fontFamily: Platform.OS === 'ios' ? 'MM Poly' : 'MM Poly Regular',
+ fontFamily: 'MMPoly-Regular',
},
]}
>
diff --git a/app/components/Views/Onboarding/styles.ts b/app/components/Views/Onboarding/styles.ts
index 575142b3e2e7..aa81963a718b 100644
--- a/app/components/Views/Onboarding/styles.ts
+++ b/app/components/Views/Onboarding/styles.ts
@@ -1,4 +1,4 @@
-import { StyleSheet, Platform } from 'react-native';
+import { StyleSheet } from 'react-native';
import Device from '../../../util/device';
import { colors as importedColors } from '../../../styles/common';
import type { Theme } from '../../../util/theme/models';
@@ -64,8 +64,7 @@ export const createStyles = (colors: Theme['colors']) =>
lineHeight: Device.isMediumDevice() ? 30 : 40,
textAlign: 'center',
paddingHorizontal: Device.isMediumDevice() ? 40 : 60,
- fontFamily:
- Platform.OS === 'android' ? 'MM Sans Regular' : 'MMSans-Regular',
+ fontFamily: 'MMSans-Regular',
color: importedColors.gettingStartedTextColor,
width: '100%',
marginVertical: 16,
diff --git a/app/components/Views/OnboardingSuccess/index.styles.ts b/app/components/Views/OnboardingSuccess/index.styles.ts
index 993d88f85a68..7445e3b02a90 100644
--- a/app/components/Views/OnboardingSuccess/index.styles.ts
+++ b/app/components/Views/OnboardingSuccess/index.styles.ts
@@ -1,4 +1,4 @@
-import { StyleSheet, Platform } from 'react-native';
+import { StyleSheet } from 'react-native';
import { ThemeColors } from '@metamask/design-tokens';
const createStyles = (colors: ThemeColors) =>
@@ -26,8 +26,7 @@ const createStyles = (colors: ThemeColors) =>
marginBottom: 16,
marginHorizontal: 16,
textAlign: 'center',
- fontFamily:
- Platform.OS === 'android' ? 'MM Sans Regular' : 'MMSans-Regular',
+ fontFamily: 'MMSans-Regular',
},
footerLink: {
paddingVertical: 8,
diff --git a/app/components/Views/ResetPassword/index.js b/app/components/Views/ResetPassword/index.js
index e7595bf24c3d..45241b19b117 100644
--- a/app/components/Views/ResetPassword/index.js
+++ b/app/components/Views/ResetPassword/index.js
@@ -542,6 +542,7 @@ class ResetPassword extends PureComponent {
// Set biometrics for new password
await Authentication.resetPassword();
+
try {
// compute and store the new authentication method
const authData = await Authentication.componentAuthenticationType(
@@ -549,6 +550,14 @@ class ResetPassword extends PureComponent {
this.state.rememberMe,
);
await Authentication.storePasswordWithFallback(password, authData);
+ if (
+ Authentication.authData.currentAuthType ===
+ AUTHENTICATION_TYPE.BIOMETRIC
+ ) {
+ await updateAuthTypeStorageFlags(this.state.biometryChoice);
+ } else {
+ await updateAuthTypeStorageFlags(false);
+ }
} catch (error) {
Logger.error(error);
}
@@ -624,7 +633,6 @@ class ResetPassword extends PureComponent {
};
updateBiometryChoice = async (biometryChoice) => {
- await updateAuthTypeStorageFlags(biometryChoice);
this.setState({ biometryChoice });
};
diff --git a/app/components/Views/ResetPassword/index.test.tsx b/app/components/Views/ResetPassword/index.test.tsx
index 6b1115fa5c77..e9d8fb05d021 100644
--- a/app/components/Views/ResetPassword/index.test.tsx
+++ b/app/components/Views/ResetPassword/index.test.tsx
@@ -47,6 +47,13 @@ jest.mock('../../../core/Engine', () => ({
jest.mock('lottie-react-native', () => 'LottieView');
+const mockUpdateAuthTypeStorageFlags = jest.fn().mockResolvedValue(undefined);
+jest.mock('../../../util/authentication', () => ({
+ ...jest.requireActual('../../../util/authentication'),
+ updateAuthTypeStorageFlags: (biometryChoice: boolean) =>
+ mockUpdateAuthTypeStorageFlags(biometryChoice),
+}));
+
jest.mock('../../../store/storage-wrapper', () => ({
setItem: jest.fn(),
getItem: jest.fn().mockResolvedValue(null), // Mock to return null to avoid biometrics interference
@@ -65,6 +72,7 @@ jest.mock('../../../core/Authentication', () => ({
getPassword: jest.fn().mockResolvedValue(null),
resetPassword: jest.fn().mockResolvedValue(undefined),
storePassword: jest.fn().mockResolvedValue(undefined),
+ storePasswordWithFallback: jest.fn().mockResolvedValue(undefined),
newWalletAndKeychain: jest
.fn()
.mockImplementation(
@@ -77,6 +85,9 @@ jest.mock('../../../core/Authentication', () => ({
),
checkIsSeedlessPasswordOutdated: jest.fn().mockResolvedValue(false),
lockApp: jest.fn().mockResolvedValue(undefined),
+ authData: {
+ currentAuthType: 'passcode',
+ },
}));
jest.mock('../../../core/NavigationService', () => ({
@@ -1004,4 +1015,120 @@ describe('ResetPassword', () => {
);
});
});
+
+ describe('biometry choice storage', () => {
+ it('saves biometry choice as false when auth type is not biometric', async () => {
+ mockUpdateAuthTypeStorageFlags.mockClear();
+
+ const component = await renderConfirmPasswordView();
+
+ const newPasswordInput = component.getByTestId(
+ ChoosePasswordSelectorsIDs.NEW_PASSWORD_INPUT_ID,
+ );
+
+ await act(async () => {
+ fireEvent.changeText(newPasswordInput, 'NewPassword123');
+ });
+
+ const confirmPasswordInput = component.getByTestId(
+ ChoosePasswordSelectorsIDs.CONFIRM_PASSWORD_INPUT_ID,
+ );
+
+ await act(async () => {
+ fireEvent.changeText(confirmPasswordInput, 'NewPassword123');
+ });
+
+ const submitButton = component.getByTestId(
+ ChoosePasswordSelectorsIDs.SUBMIT_BUTTON_ID,
+ );
+
+ await act(async () => {
+ fireEvent.press(submitButton);
+ });
+
+ await waitFor(() => {
+ expect(NavigationService.navigation.navigate).toHaveBeenCalledWith(
+ Routes.MODAL.ROOT_MODAL_FLOW,
+ expect.objectContaining({
+ screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
+ }),
+ );
+ });
+
+ const navigationCall = (
+ NavigationService.navigation.navigate as jest.Mock
+ ).mock.calls[0];
+ const onPrimaryButtonPress =
+ navigationCall[1].params.onPrimaryButtonPress;
+
+ await act(async () => {
+ await onPrimaryButtonPress();
+ });
+
+ await waitFor(() => {
+ expect(mockUpdateAuthTypeStorageFlags).toHaveBeenCalledWith(false);
+ });
+ });
+
+ it('saves biometry choice when auth type is biometric', async () => {
+ mockUpdateAuthTypeStorageFlags.mockClear();
+
+ const mockAuthModule = jest.requireMock('../../../core/Authentication');
+ const originalAuthData = mockAuthModule.authData;
+ mockAuthModule.authData = {
+ currentAuthType: AUTHENTICATION_TYPE.BIOMETRIC,
+ };
+
+ const component = await renderConfirmPasswordView();
+
+ const newPasswordInput = component.getByTestId(
+ ChoosePasswordSelectorsIDs.NEW_PASSWORD_INPUT_ID,
+ );
+
+ await act(async () => {
+ fireEvent.changeText(newPasswordInput, 'NewPassword123');
+ });
+
+ const confirmPasswordInput = component.getByTestId(
+ ChoosePasswordSelectorsIDs.CONFIRM_PASSWORD_INPUT_ID,
+ );
+
+ await act(async () => {
+ fireEvent.changeText(confirmPasswordInput, 'NewPassword123');
+ });
+
+ const submitButton = component.getByTestId(
+ ChoosePasswordSelectorsIDs.SUBMIT_BUTTON_ID,
+ );
+
+ await act(async () => {
+ fireEvent.press(submitButton);
+ });
+
+ await waitFor(() => {
+ expect(NavigationService.navigation.navigate).toHaveBeenCalledWith(
+ Routes.MODAL.ROOT_MODAL_FLOW,
+ expect.objectContaining({
+ screen: Routes.SHEET.SUCCESS_ERROR_SHEET,
+ }),
+ );
+ });
+
+ const navigationCall = (
+ NavigationService.navigation.navigate as jest.Mock
+ ).mock.calls[0];
+ const onPrimaryButtonPress =
+ navigationCall[1].params.onPrimaryButtonPress;
+
+ await act(async () => {
+ await onPrimaryButtonPress();
+ });
+
+ await waitFor(() => {
+ expect(mockUpdateAuthTypeStorageFlags).toHaveBeenCalled();
+ });
+
+ mockAuthModule.authData = originalAuthData;
+ });
+ });
});
diff --git a/app/components/Views/confirmations/components/gas/gas-option/gas-option.styles.ts b/app/components/Views/confirmations/components/gas/gas-option/gas-option.styles.ts
index 8d6ff9e6545d..166c0f33d5ff 100644
--- a/app/components/Views/confirmations/components/gas/gas-option/gas-option.styles.ts
+++ b/app/components/Views/confirmations/components/gas/gas-option/gas-option.styles.ts
@@ -35,11 +35,7 @@ const styleSheet = (params: { theme: Theme }) => {
leftSection: {
flexDirection: 'row',
alignItems: 'center',
- },
- emoji: {
- fontSize: 24,
- marginRight: 12,
- marginLeft: 6,
+ marginLeft: 8,
},
optionTextContainer: {
justifyContent: 'center',
diff --git a/app/components/Views/confirmations/components/gas/gas-option/gas-option.test.tsx b/app/components/Views/confirmations/components/gas/gas-option/gas-option.test.tsx
index b8a1380de623..78e30dd5cbb6 100644
--- a/app/components/Views/confirmations/components/gas/gas-option/gas-option.test.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-option/gas-option.test.tsx
@@ -4,7 +4,6 @@ import { noop } from 'lodash';
import { GasOption } from './gas-option';
const mockGasOption = {
- emoji: '🚀',
estimatedTime: '',
isSelected: false,
key: 'fast',
@@ -21,7 +20,6 @@ describe('GasOption', () => {
);
expect(getByTestId('gas-option-fast')).toBeOnTheScreen();
- expect(getByText('🚀')).toBeOnTheScreen();
expect(getByText('Test gas option')).toBeOnTheScreen();
expect(getByText('< 0.0001')).toBeOnTheScreen();
expect(getByText('0.05')).toBeOnTheScreen();
diff --git a/app/components/Views/confirmations/components/gas/gas-option/gas-option.tsx b/app/components/Views/confirmations/components/gas/gas-option/gas-option.tsx
index 107048d97a02..0a6e1d04765a 100644
--- a/app/components/Views/confirmations/components/gas/gas-option/gas-option.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-option/gas-option.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { View, TouchableOpacity, Text as RNText } from 'react-native';
+import { View, TouchableOpacity } from 'react-native';
import { useStyles } from '../../../../../../component-library/hooks';
import Text, {
@@ -9,16 +9,8 @@ import { type GasOption as GasOptionType } from '../../../types/gas';
import styleSheet from './gas-option.styles';
export const GasOption = ({ option }: { option: GasOptionType }) => {
- const {
- onSelect,
- name,
- estimatedTime,
- valueInFiat,
- value,
- emoji,
- isSelected,
- key,
- } = option;
+ const { onSelect, name, estimatedTime, valueInFiat, value, isSelected, key } =
+ option;
const { styles } = useStyles(styleSheet, {});
@@ -33,7 +25,6 @@ export const GasOption = ({ option }: { option: GasOptionType }) => {
onPress={() => onSelect()}
>
- {emoji}
{name}
diff --git a/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.test.tsx b/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.test.tsx
index 7cbb36c0e27c..7e73d0023e63 100644
--- a/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.test.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.test.tsx
@@ -30,33 +30,10 @@ describe('GasSpeed', () => {
jest.clearAllMocks();
});
- it('renders null when transaction metadata has no userFeeLevel', () => {
- const stateWithoutUserFeeLevel = merge({}, transferTransactionStateMock, {
- engine: {
- backgroundState: {
- TransactionController: {
- transactions: [
- {
- id: '56f60ff0-2bef-11f0-80ce-2f66f7fbd577',
- userFeeLevel: undefined,
- },
- ],
- },
- },
- },
- });
-
- const { queryByText } = renderWithProvider(, {
- state: stateWithoutUserFeeLevel,
- });
-
- expect(queryByText(/🐢|🦊|🦍|🌐|⚙️/)).toBeNull();
- });
-
it.each([
- [GasFeeEstimateLevel.Low, 'Low', /🐢.*Low.*~ 30 sec/],
- [GasFeeEstimateLevel.Medium, 'Medium', /🦊.*Market.*~ 20 sec/],
- [GasFeeEstimateLevel.High, 'High', /🦍.*Aggressive.*~ 10 sec/],
+ [GasFeeEstimateLevel.Low, 'Low', /.*Low.*~ 30 sec/],
+ [GasFeeEstimateLevel.Medium, 'Medium', /.*Market.*~ 20 sec/],
+ [GasFeeEstimateLevel.High, 'High', /.*Aggressive.*~ 10 sec/],
])(
'renders correct content for %s gas fee estimate level',
(userFeeLevel, _levelName, expectedPattern) => {
@@ -104,7 +81,7 @@ describe('GasSpeed', () => {
state: stateWithDappSuggested,
});
- expect(getByText('🌐 Site suggested')).toBeTruthy();
+ expect(getByText('Site suggested')).toBeTruthy();
});
it('renders correct content for CUSTOM user fee level', () => {
@@ -126,7 +103,7 @@ describe('GasSpeed', () => {
state: stateWithCustom,
});
- expect(getByText('⚙️ Advanced')).toBeTruthy();
+ expect(getByText('Advanced')).toBeTruthy();
});
it('does not show estimated time for gas price estimate when Medium level is selected', () => {
@@ -151,7 +128,7 @@ describe('GasSpeed', () => {
state: stateWithGasPriceEstimate,
});
- expect(getByText('🦊 Market')).toBeTruthy();
+ expect(getByText('Market')).toBeTruthy();
expect(queryByText(/sec/)).toBeNull();
});
@@ -177,7 +154,7 @@ describe('GasSpeed', () => {
state: stateWithFeeMarketEstimate,
});
- expect(getByText(/🦍.*Aggressive.*~ 10 sec/)).toBeTruthy();
+ expect(getByText(/.*Aggressive.*~ 10 sec/)).toBeTruthy();
});
it('handles unknown user fee level by defaulting to advanced', () => {
@@ -199,7 +176,7 @@ describe('GasSpeed', () => {
state: stateWithUnknownFeeLevel,
});
- expect(getByText('⚙️ Advanced')).toBeTruthy();
+ expect(getByText('Advanced')).toBeTruthy();
});
it('calls useGasFeeEstimates with correct networkClientId', () => {
diff --git a/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.tsx b/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.tsx
index cccff3b5a51e..13a146575174 100644
--- a/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-speed/gas-speed.tsx
@@ -9,26 +9,9 @@ import {
import Text from '../../../../../../component-library/components/Texts/Text/Text';
import { strings } from '../../../../../../../locales/i18n';
import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
-import { GasOptionIcon } from '../../../constants/gas';
import { useGasFeeEstimates } from '../../../hooks/gas/useGasFeeEstimates';
import { toHumanSeconds } from '../../../utils/time';
-const getEmoji = (userFeeLevel: UserFeeLevel | GasFeeEstimateLevel) => {
- switch (userFeeLevel) {
- case GasFeeEstimateLevel.Low:
- return GasOptionIcon.LOW;
- case GasFeeEstimateLevel.Medium:
- return GasOptionIcon.MEDIUM;
- case GasFeeEstimateLevel.High:
- return GasOptionIcon.HIGH;
- case UserFeeLevel.DAPP_SUGGESTED:
- return GasOptionIcon.SITE_SUGGESTED;
- case UserFeeLevel.CUSTOM:
- default:
- return GasOptionIcon.ADVANCED;
- }
-};
-
const getText = (userFeeLevel: UserFeeLevel | GasFeeEstimateLevel) => {
switch (userFeeLevel) {
case UserFeeLevel.DAPP_SUGGESTED:
@@ -86,7 +69,6 @@ export const GasSpeed = () => {
transactionMeta.gasFeeEstimates?.type === GasFeeEstimateType.GasPrice &&
userFeeLevel === GasFeeEstimateLevel.Medium;
- const emoji = getEmoji(userFeeLevel);
const text = getText(userFeeLevel);
const estimatedTime = getEstimatedTime(
userFeeLevel,
@@ -94,6 +76,5 @@ export const GasSpeed = () => {
isGasPriceEstimateSelected,
);
- // Intentionally no space between text and estimated time
- return {`${emoji} ${text}${estimatedTime}`};
+ return {`${text}${estimatedTime}`};
};
diff --git a/app/components/Views/confirmations/components/modals/estimates-modal/estimates-modal.test.tsx b/app/components/Views/confirmations/components/modals/estimates-modal/estimates-modal.test.tsx
index 82796eed0e78..f158881fd8a2 100644
--- a/app/components/Views/confirmations/components/modals/estimates-modal/estimates-modal.test.tsx
+++ b/app/components/Views/confirmations/components/modals/estimates-modal/estimates-modal.test.tsx
@@ -10,7 +10,6 @@ jest.mock('../../../hooks/gas/useGasOptions', () => ({
return {
options: [
{
- emoji: '🚀',
estimatedTime: '',
isSelected: false,
key: 'fast',
@@ -44,9 +43,7 @@ describe('EstimatesModal', () => {
// Header
expect(getByText('Edit network fee')).toBeOnTheScreen();
- // Gas option expected to be rendered
expect(getByTestId('gas-option-fast')).toBeOnTheScreen();
- expect(getByText('🚀')).toBeOnTheScreen();
expect(getByText('Test gas option')).toBeOnTheScreen();
expect(getByText('< 0.0001')).toBeOnTheScreen();
expect(getByText('0.05')).toBeOnTheScreen();
diff --git a/app/components/Views/confirmations/components/send/amount/amount.styles.ts b/app/components/Views/confirmations/components/send/amount/amount.styles.ts
index 85bf4053f242..55e3536effd1 100644
--- a/app/components/Views/confirmations/components/send/amount/amount.styles.ts
+++ b/app/components/Views/confirmations/components/send/amount/amount.styles.ts
@@ -64,7 +64,7 @@ export const styleSheet = (params: {
inputText: {
fontSize: getFontSizeForInputLength(contentLength),
lineHeight: 75,
- fontFamily: 'Geist Medium',
+ fontFamily: 'Geist-Medium',
},
inputWrapper: {
alignItems: 'center',
diff --git a/app/components/Views/confirmations/constants/gas.ts b/app/components/Views/confirmations/constants/gas.ts
index a6e5912c28be..203c8d98a2b3 100644
--- a/app/components/Views/confirmations/constants/gas.ts
+++ b/app/components/Views/confirmations/constants/gas.ts
@@ -4,13 +4,4 @@ export enum GasModalType {
ADVANCED_GAS_PRICE = 'advancedGasPriceModal',
}
-export enum GasOptionIcon {
- ADVANCED = '⚙️',
- GAS_PRICE = '⛓️',
- HIGH = '🦍',
- LOW = '🐢',
- MEDIUM = '🦊',
- SITE_SUGGESTED = '🌐',
-}
-
export const EMPTY_VALUE_STRING = '--';
diff --git a/app/components/Views/confirmations/hooks/gas/useAdvancedGasFeeOption.ts b/app/components/Views/confirmations/hooks/gas/useAdvancedGasFeeOption.ts
index b19243631f26..6b7183babcfc 100644
--- a/app/components/Views/confirmations/hooks/gas/useAdvancedGasFeeOption.ts
+++ b/app/components/Views/confirmations/hooks/gas/useAdvancedGasFeeOption.ts
@@ -9,11 +9,7 @@ import {
import { strings } from '../../../../../../locales/i18n';
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
import { useFeeCalculations } from './useFeeCalculations';
-import {
- EMPTY_VALUE_STRING,
- GasModalType,
- GasOptionIcon,
-} from '../../constants/gas';
+import { EMPTY_VALUE_STRING, GasModalType } from '../../constants/gas';
import { type GasOption } from '../../types/gas';
const HEX_ZERO = '0x0';
@@ -118,7 +114,6 @@ export const useAdvancedGasFeeOption = ({
const memoizedGasOption = useMemo(
() => [
{
- emoji: GasOptionIcon.ADVANCED,
estimatedTime: '',
isSelected: isAdvancedGasFeeSelected,
key: 'advanced',
diff --git a/app/components/Views/confirmations/hooks/gas/useDappSuggestedGasFeeOption.ts b/app/components/Views/confirmations/hooks/gas/useDappSuggestedGasFeeOption.ts
index d5068ad61aee..bbd6028de1ed 100644
--- a/app/components/Views/confirmations/hooks/gas/useDappSuggestedGasFeeOption.ts
+++ b/app/components/Views/confirmations/hooks/gas/useDappSuggestedGasFeeOption.ts
@@ -7,7 +7,7 @@ import {
import { strings } from '../../../../../../locales/i18n';
import { updateTransactionGasFees } from '../../../../../util/transaction-controller';
import { type GasOption } from '../../types/gas';
-import { EMPTY_VALUE_STRING, GasOptionIcon } from '../../constants/gas';
+import { EMPTY_VALUE_STRING } from '../../constants/gas';
import { MMM_ORIGIN } from '../../constants/confirmations';
import { useTransactionMetadataRequest } from '../transactions/useTransactionMetadataRequest';
import { useFeeCalculations } from './useFeeCalculations';
@@ -71,7 +71,6 @@ export const useDappSuggestedGasFeeOption = ({
});
options.push({
- emoji: GasOptionIcon.SITE_SUGGESTED,
estimatedTime: undefined,
isSelected: isDappSuggestedGasFeeSelected,
key: 'site_suggested',
diff --git a/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts b/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts
index a75daffe6d7b..0e2a7b278476 100644
--- a/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts
+++ b/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts
@@ -176,15 +176,12 @@ describe('useGasFeeEstimateLevelOptions', () => {
expect(result.current.length).toEqual(3);
- // Check low option
const lowOption = result.current.find((option) => option.key === 'low');
expect(lowOption).toBeDefined();
expect(lowOption?.isSelected).toEqual(false);
expect(lowOption?.value).toEqual('5');
expect(lowOption?.valueInFiat).toEqual('$5');
- expect(lowOption?.emoji).toEqual('🐢');
- // Check medium option
const mediumOption = result.current.find(
(option) => option.key === 'medium',
);
@@ -192,15 +189,12 @@ describe('useGasFeeEstimateLevelOptions', () => {
expect(mediumOption?.isSelected).toEqual(false);
expect(mediumOption?.value).toEqual('10');
expect(mediumOption?.valueInFiat).toEqual('$10');
- expect(mediumOption?.emoji).toEqual('🦊');
- // Check high option
const highOption = result.current.find((option) => option.key === 'high');
expect(highOption).toBeDefined();
expect(highOption?.isSelected).toEqual(true);
expect(highOption?.value).toEqual('15');
expect(highOption?.valueInFiat).toEqual('$15');
- expect(highOption?.emoji).toEqual('🦍');
expect(mockCalculateGasEstimate).toHaveBeenCalledWith(
expect.objectContaining({
@@ -507,4 +501,86 @@ describe('useGasFeeEstimateLevelOptions', () => {
});
expect(mockHandleCloseModals).toHaveBeenCalled();
});
+
+ it('excludes high option when medium and high have identical fees for FeeMarket estimates', () => {
+ const mockHandleCloseModals = jest.fn();
+
+ const transactionWithFeeMarketEstimates = {
+ ...simpleSendTransaction,
+ id: 'test-id',
+ userFeeLevel: 'medium',
+ txParams: {
+ ...simpleSendTransaction.txParams,
+ type: TransactionEnvelopeType.feeMarket,
+ },
+ gasFeeEstimates: {
+ type: GasFeeEstimateType.FeeMarket,
+ low: {
+ maxFeePerGas: '0x1',
+ maxPriorityFeePerGas: '0x1',
+ },
+ medium: {
+ maxFeePerGas: '0x2',
+ maxPriorityFeePerGas: '0x2',
+ },
+ high: {
+ maxFeePerGas: '0x2',
+ maxPriorityFeePerGas: '0x2',
+ },
+ },
+ } as unknown as TransactionMeta;
+
+ mockUseTransactionMetadataRequest.mockReturnValue(
+ transactionWithFeeMarketEstimates,
+ );
+
+ mockUseGasFeeEstimates.mockReturnValue({
+ gasFeeEstimates: {
+ low: {
+ minWaitTimeEstimate: 60000,
+ maxWaitTimeEstimate: 120000,
+ suggestedMaxPriorityFeePerGas: '1',
+ },
+ medium: {
+ minWaitTimeEstimate: 30000,
+ maxWaitTimeEstimate: 60000,
+ suggestedMaxPriorityFeePerGas: '2',
+ },
+ high: {
+ minWaitTimeEstimate: 15000,
+ maxWaitTimeEstimate: 30000,
+ suggestedMaxPriorityFeePerGas: '2',
+ },
+ } as unknown as GasFeeEstimates,
+ });
+
+ mockCalculateGasEstimate.mockImplementation(({ feePerGas }) => {
+ const value = feePerGas === '0x1' ? '5' : '10';
+ const valueInFiat = feePerGas === '0x1' ? '$5' : '$10';
+
+ return {
+ currentCurrencyFee: valueInFiat,
+ preciseNativeCurrencyFee: value,
+ };
+ });
+
+ const { result } = renderHook(() =>
+ useGasFeeEstimateLevelOptions({
+ handleCloseModals: mockHandleCloseModals,
+ }),
+ );
+
+ expect(result.current.length).toEqual(2);
+
+ const lowOption = result.current.find((option) => option.key === 'low');
+ expect(lowOption).toBeDefined();
+
+ const mediumOption = result.current.find(
+ (option) => option.key === 'medium',
+ );
+ expect(mediumOption).toBeDefined();
+
+ const highOption = result.current.find((option) => option.key === 'high');
+ expect(highOption).toBeUndefined();
+ });
});
diff --git a/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts b/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts
index cb8634139d31..272cd66b5d6a 100644
--- a/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts
+++ b/app/components/Views/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts
@@ -14,16 +14,10 @@ import { toHumanEstimatedTimeRange } from '../../utils/time';
import { useFeeCalculations } from './useFeeCalculations';
import { updateTransactionGasFees } from '../../../../../util/transaction-controller';
import { type GasOption } from '../../types/gas';
-import { EMPTY_VALUE_STRING, GasOptionIcon } from '../../constants/gas';
+import { EMPTY_VALUE_STRING } from '../../constants/gas';
const HEX_ZERO = '0x0';
-const GasEstimateFeeLevelEmojis = {
- [GasFeeEstimateLevel.Low]: GasOptionIcon.LOW,
- [GasFeeEstimateLevel.Medium]: GasOptionIcon.MEDIUM,
- [GasFeeEstimateLevel.High]: GasOptionIcon.HIGH,
-};
-
export const useGasFeeEstimateLevelOptions = ({
handleCloseModals,
}: {
@@ -64,6 +58,26 @@ export const useGasFeeEstimateLevelOptions = ({
if (shouldIncludeGasFeeEstimateLevelOptions) {
Object.values(GasFeeEstimateLevel).forEach((level) => {
+ // Skip adding the high option if it has the same fees as the medium option
+ if (
+ level === GasFeeEstimateLevel.High &&
+ transactionGasFeeEstimates?.type === GasFeeEstimateType.FeeMarket
+ ) {
+ const mediumEstimates =
+ transactionGasFeeEstimates[GasFeeEstimateLevel.Medium];
+ const highEstimates =
+ transactionGasFeeEstimates[GasFeeEstimateLevel.High];
+
+ const hasSameFees =
+ mediumEstimates?.maxFeePerGas === highEstimates?.maxFeePerGas &&
+ mediumEstimates?.maxPriorityFeePerGas ===
+ highEstimates?.maxPriorityFeePerGas;
+
+ if (hasSameFees) {
+ return;
+ }
+ }
+
const estimatedTime = toHumanEstimatedTimeRange(
networkGasFeeEstimates[level].minWaitTimeEstimate,
networkGasFeeEstimates[level].maxWaitTimeEstimate,
@@ -101,7 +115,6 @@ export const useGasFeeEstimateLevelOptions = ({
});
options.push({
- emoji: GasEstimateFeeLevelEmojis[level],
estimatedTime,
isSelected: userFeeLevel === level,
key: level,
diff --git a/app/components/Views/confirmations/hooks/gas/useGasOptions.test.ts b/app/components/Views/confirmations/hooks/gas/useGasOptions.test.ts
index c52a5602fbdd..9873ee34b39b 100644
--- a/app/components/Views/confirmations/hooks/gas/useGasOptions.test.ts
+++ b/app/components/Views/confirmations/hooks/gas/useGasOptions.test.ts
@@ -23,7 +23,6 @@ describe('useGasOptions', () => {
);
const mockAdvancedOption: GasOption = {
- emoji: '⚙️',
estimatedTime: '',
isSelected: false,
key: 'advanced',
@@ -34,7 +33,6 @@ describe('useGasOptions', () => {
};
const mockLowLevelOption: GasOption = {
- emoji: '🐢',
estimatedTime: '~1 min',
isSelected: false,
key: 'low',
@@ -45,7 +43,6 @@ describe('useGasOptions', () => {
};
const mockMediumLevelOption: GasOption = {
- emoji: '🦊',
estimatedTime: '~30 sec',
isSelected: true,
key: 'medium',
@@ -56,7 +53,6 @@ describe('useGasOptions', () => {
};
const mockGasPriceOption: GasOption = {
- emoji: '⛽️',
estimatedTime: '',
isSelected: false,
key: 'gasPrice',
@@ -67,7 +63,6 @@ describe('useGasOptions', () => {
};
const mockDappSuggestedOption: GasOption = {
- emoji: '🌐',
estimatedTime: '',
isSelected: false,
key: 'site_suggested',
diff --git a/app/components/Views/confirmations/hooks/gas/useGasPriceEstimateOption.ts b/app/components/Views/confirmations/hooks/gas/useGasPriceEstimateOption.ts
index 622017c37a8c..1b0e7ce2d498 100644
--- a/app/components/Views/confirmations/hooks/gas/useGasPriceEstimateOption.ts
+++ b/app/components/Views/confirmations/hooks/gas/useGasPriceEstimateOption.ts
@@ -12,7 +12,7 @@ import { useTransactionMetadataRequest } from '../transactions/useTransactionMet
import { useGasFeeEstimates } from './useGasFeeEstimates';
import { useFeeCalculations } from './useFeeCalculations';
import { type GasOption } from '../../types/gas';
-import { EMPTY_VALUE_STRING, GasOptionIcon } from '../../constants/gas';
+import { EMPTY_VALUE_STRING } from '../../constants/gas';
const HEX_ZERO = '0x0';
@@ -109,7 +109,6 @@ export const useGasPriceEstimateOption = ({
return [
{
- emoji: GasOptionIcon.GAS_PRICE,
estimatedTime: undefined,
isSelected: isGasPriceEstimateSelected,
key: 'gasPrice',
diff --git a/app/components/Views/confirmations/types/gas.ts b/app/components/Views/confirmations/types/gas.ts
index ccb347d4a6b9..8a747f95aeb3 100644
--- a/app/components/Views/confirmations/types/gas.ts
+++ b/app/components/Views/confirmations/types/gas.ts
@@ -1,5 +1,4 @@
export interface GasOption {
- emoji: string;
estimatedTime?: string;
isSelected: boolean;
key: string;
diff --git a/app/core/redux/slices/bridge/index.test.ts b/app/core/redux/slices/bridge/index.test.ts
index 1b81e2953385..a6568e8a1ebf 100644
--- a/app/core/redux/slices/bridge/index.test.ts
+++ b/app/core/redux/slices/bridge/index.test.ts
@@ -9,6 +9,8 @@ import reducer, {
setBridgeViewMode,
selectBridgeViewMode,
setDestToken,
+ setIsDestTokenManuallySet,
+ selectIsDestTokenManuallySet,
selectBip44DefaultPair,
selectGasIncludedQuoteParams,
} from '.';
@@ -44,7 +46,7 @@ describe('bridge slice', () => {
};
describe('initial state', () => {
- it('should have the correct initial state', () => {
+ it('has correct initial state', () => {
expect(initialState).toEqual({
bridgeViewMode: undefined,
sourceAmount: undefined,
@@ -60,6 +62,7 @@ describe('bridge slice', () => {
isSubmittingTx: false,
isSelectingRecipient: false,
isMaxSourceAmount: false,
+ isDestTokenManuallySet: false,
});
});
});
@@ -170,13 +173,71 @@ describe('bridge slice', () => {
});
describe('setDestToken', () => {
- it('should set the destination token and update selectedDestChainId', () => {
+ it('sets the destination token and updates selectedDestChainId', () => {
const action = setDestToken(mockDestToken);
const state = reducer(initialState, action);
expect(state.destToken).toBe(mockDestToken);
expect(state.selectedDestChainId).toBe(mockDestToken.chainId);
});
+
+ it('does not modify isDestTokenManuallySet flag', () => {
+ const stateWithManualFlag = {
+ ...initialState,
+ isDestTokenManuallySet: true,
+ };
+
+ const action = setDestToken(mockDestToken);
+ const state = reducer(stateWithManualFlag, action);
+
+ expect(state.isDestTokenManuallySet).toBe(true);
+ });
+ });
+
+ describe('setIsDestTokenManuallySet', () => {
+ it('sets the flag to true', () => {
+ const action = setIsDestTokenManuallySet(true);
+ const state = reducer(initialState, action);
+
+ expect(state.isDestTokenManuallySet).toBe(true);
+ });
+
+ it('sets the flag to false', () => {
+ const stateWithManualFlag = {
+ ...initialState,
+ isDestTokenManuallySet: true,
+ };
+
+ const action = setIsDestTokenManuallySet(false);
+ const state = reducer(stateWithManualFlag, action);
+
+ expect(state.isDestTokenManuallySet).toBe(false);
+ });
+ });
+
+ describe('selectIsDestTokenManuallySet', () => {
+ it('returns false from initial state', () => {
+ const mockState = {
+ bridge: initialState,
+ } as RootState;
+
+ const result = selectIsDestTokenManuallySet(mockState);
+
+ expect(result).toBe(false);
+ });
+
+ it('returns true when flag is set', () => {
+ const mockState = {
+ bridge: {
+ ...initialState,
+ isDestTokenManuallySet: true,
+ },
+ } as RootState;
+
+ const result = selectIsDestTokenManuallySet(mockState);
+
+ expect(result).toBe(true);
+ });
});
describe('resetBridgeState', () => {
diff --git a/app/core/redux/slices/bridge/index.ts b/app/core/redux/slices/bridge/index.ts
index 007f94250c9d..c5d19317c7fe 100644
--- a/app/core/redux/slices/bridge/index.ts
+++ b/app/core/redux/slices/bridge/index.ts
@@ -59,6 +59,12 @@ export interface BridgeState {
isSelectingRecipient: boolean;
isGasIncludedSTXSendBundleSupported: boolean;
isGasIncluded7702Supported: boolean;
+ /**
+ * Tracks whether the user has manually selected a destination token.
+ * When true, changing the source token to a different network won't auto-update the dest token.
+ * When false, changing source network will update dest to the default for that network.
+ */
+ isDestTokenManuallySet: boolean;
}
export const initialState: BridgeState = {
@@ -76,6 +82,7 @@ export const initialState: BridgeState = {
isSelectingRecipient: false,
isGasIncludedSTXSendBundleSupported: false,
isGasIncluded7702Supported: false,
+ isDestTokenManuallySet: false,
};
const name = 'bridge';
@@ -133,6 +140,13 @@ const slice = createSlice({
// Update selectedDestChainId to match the destination token's chain ID
state.selectedDestChainId = action.payload.chainId;
},
+ /**
+ * Sets whether the destination token was manually selected by the user.
+ * When true, auto-updates to dest token are prevented when source chain changes.
+ */
+ setIsDestTokenManuallySet: (state, action: PayloadAction) => {
+ state.isDestTokenManuallySet = action.payload;
+ },
setDestAddress: (state, action: PayloadAction) => {
state.destAddress = action.payload;
},
@@ -565,6 +579,11 @@ export const selectIsSelectingRecipient = createSelector(
(bridgeState) => bridgeState.isSelectingRecipient,
);
+export const selectIsDestTokenManuallySet = createSelector(
+ selectBridgeState,
+ (bridgeState) => bridgeState.isDestTokenManuallySet,
+);
+
export const selectIsGaslessSwapEnabled = createSelector(
selectIsSwap,
selectBridgeFeatureFlags,
@@ -625,6 +644,7 @@ export const {
resetBridgeState,
setSourceToken,
setDestToken,
+ setIsDestTokenManuallySet,
setSelectedSourceChainIds,
setSelectedDestChainId,
setSlippage,
diff --git a/package.json b/package.json
index ebe450ad581e..b4083ca3ef35 100644
--- a/package.json
+++ b/package.json
@@ -198,7 +198,7 @@
"@metamask/address-book-controller": "^7.0.0",
"@metamask/app-metadata-controller": "^2.0.0",
"@metamask/approval-controller": "^8.0.0",
- "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A93.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch",
+ "@metamask/assets-controllers": "^93.0.0",
"@metamask/base-controller": "^9.0.0",
"@metamask/bitcoin-wallet-snap": "^1.8.0",
"@metamask/bridge-controller": "patch:@metamask/bridge-controller@npm%3A61.0.0#~/.yarn/patches/@metamask-bridge-controller-npm-61.0.0-8c413c463f.patch",
diff --git a/yarn.lock b/yarn.lock
index 4f9321be1c22..c1e09248db82 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7132,61 +7132,7 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/assets-controllers@npm:93.0.0":
- version: 93.0.0
- resolution: "@metamask/assets-controllers@npm:93.0.0"
- dependencies:
- "@ethereumjs/util": "npm:^9.1.0"
- "@ethersproject/abi": "npm:^5.7.0"
- "@ethersproject/address": "npm:^5.7.0"
- "@ethersproject/bignumber": "npm:^5.7.0"
- "@ethersproject/contracts": "npm:^5.7.0"
- "@ethersproject/providers": "npm:^5.7.0"
- "@metamask/abi-utils": "npm:^2.0.3"
- "@metamask/account-tree-controller": "npm:^4.0.0"
- "@metamask/accounts-controller": "npm:^35.0.0"
- "@metamask/approval-controller": "npm:^8.0.0"
- "@metamask/base-controller": "npm:^9.0.0"
- "@metamask/contract-metadata": "npm:^2.4.0"
- "@metamask/controller-utils": "npm:^11.16.0"
- "@metamask/core-backend": "npm:^5.0.0"
- "@metamask/eth-query": "npm:^4.0.0"
- "@metamask/keyring-api": "npm:^21.0.0"
- "@metamask/keyring-controller": "npm:^25.0.0"
- "@metamask/messenger": "npm:^0.3.0"
- "@metamask/metamask-eth-abis": "npm:^3.1.1"
- "@metamask/multichain-account-service": "npm:^4.0.0"
- "@metamask/network-controller": "npm:^27.0.0"
- "@metamask/permission-controller": "npm:^12.1.1"
- "@metamask/phishing-controller": "npm:^16.1.0"
- "@metamask/polling-controller": "npm:^16.0.0"
- "@metamask/preferences-controller": "npm:^22.0.0"
- "@metamask/profile-sync-controller": "npm:^27.0.0"
- "@metamask/rpc-errors": "npm:^7.0.2"
- "@metamask/snaps-controllers": "npm:^14.0.1"
- "@metamask/snaps-sdk": "npm:^9.0.0"
- "@metamask/snaps-utils": "npm:^11.0.0"
- "@metamask/transaction-controller": "npm:^62.4.0"
- "@metamask/utils": "npm:^11.8.1"
- "@types/bn.js": "npm:^5.1.5"
- "@types/uuid": "npm:^8.3.0"
- async-mutex: "npm:^0.5.0"
- bitcoin-address-validation: "npm:^2.2.3"
- bn.js: "npm:^5.2.1"
- immer: "npm:^9.0.6"
- lodash: "npm:^4.17.21"
- multiformats: "npm:^9.9.0"
- reselect: "npm:^5.1.1"
- single-call-balance-checker-abi: "npm:^1.0.0"
- uuid: "npm:^8.3.2"
- peerDependencies:
- "@metamask/providers": ^22.0.0
- webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/35b4294bacc2a2123d99af181c2482674386f21d89ae102934573810101ba97b71e5ff5a24038a09f3dd8ad07d12b5cabe7a29c9aa04bdd5847a83284240d217
- languageName: node
- linkType: hard
-
-"@metamask/assets-controllers@npm:^93.1.0":
+"@metamask/assets-controllers@npm:^93.0.0, @metamask/assets-controllers@npm:^93.1.0":
version: 93.1.0
resolution: "@metamask/assets-controllers@npm:93.1.0"
dependencies:
@@ -7240,60 +7186,6 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A93.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch":
- version: 93.0.0
- resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A93.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch::version=93.0.0&hash=e4c733"
- dependencies:
- "@ethereumjs/util": "npm:^9.1.0"
- "@ethersproject/abi": "npm:^5.7.0"
- "@ethersproject/address": "npm:^5.7.0"
- "@ethersproject/bignumber": "npm:^5.7.0"
- "@ethersproject/contracts": "npm:^5.7.0"
- "@ethersproject/providers": "npm:^5.7.0"
- "@metamask/abi-utils": "npm:^2.0.3"
- "@metamask/account-tree-controller": "npm:^4.0.0"
- "@metamask/accounts-controller": "npm:^35.0.0"
- "@metamask/approval-controller": "npm:^8.0.0"
- "@metamask/base-controller": "npm:^9.0.0"
- "@metamask/contract-metadata": "npm:^2.4.0"
- "@metamask/controller-utils": "npm:^11.16.0"
- "@metamask/core-backend": "npm:^5.0.0"
- "@metamask/eth-query": "npm:^4.0.0"
- "@metamask/keyring-api": "npm:^21.0.0"
- "@metamask/keyring-controller": "npm:^25.0.0"
- "@metamask/messenger": "npm:^0.3.0"
- "@metamask/metamask-eth-abis": "npm:^3.1.1"
- "@metamask/multichain-account-service": "npm:^4.0.0"
- "@metamask/network-controller": "npm:^27.0.0"
- "@metamask/permission-controller": "npm:^12.1.1"
- "@metamask/phishing-controller": "npm:^16.1.0"
- "@metamask/polling-controller": "npm:^16.0.0"
- "@metamask/preferences-controller": "npm:^22.0.0"
- "@metamask/profile-sync-controller": "npm:^27.0.0"
- "@metamask/rpc-errors": "npm:^7.0.2"
- "@metamask/snaps-controllers": "npm:^14.0.1"
- "@metamask/snaps-sdk": "npm:^9.0.0"
- "@metamask/snaps-utils": "npm:^11.0.0"
- "@metamask/transaction-controller": "npm:^62.4.0"
- "@metamask/utils": "npm:^11.8.1"
- "@types/bn.js": "npm:^5.1.5"
- "@types/uuid": "npm:^8.3.0"
- async-mutex: "npm:^0.5.0"
- bitcoin-address-validation: "npm:^2.2.3"
- bn.js: "npm:^5.2.1"
- immer: "npm:^9.0.6"
- lodash: "npm:^4.17.21"
- multiformats: "npm:^9.9.0"
- reselect: "npm:^5.1.1"
- single-call-balance-checker-abi: "npm:^1.0.0"
- uuid: "npm:^8.3.2"
- peerDependencies:
- "@metamask/providers": ^22.0.0
- webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0
- checksum: 10/9409c903dcdb56e1126a7f8777de82a88bc64b0470d73ddfffd0d165649df5191597c3bdbead31466dcccc023803c7fbecdbbdc9c027f45044ba5a278a5f1c82
- languageName: node
- linkType: hard
-
"@metamask/auth-network-utils@npm:^0.3.0":
version: 0.3.1
resolution: "@metamask/auth-network-utils@npm:0.3.1"
@@ -34312,7 +34204,7 @@ __metadata:
"@metamask/address-book-controller": "npm:^7.0.0"
"@metamask/app-metadata-controller": "npm:^2.0.0"
"@metamask/approval-controller": "npm:^8.0.0"
- "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A93.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-93.0.0-ea998cb0bd.patch"
+ "@metamask/assets-controllers": "npm:^93.0.0"
"@metamask/auto-changelog": "npm:^5.3.0"
"@metamask/base-controller": "npm:^9.0.0"
"@metamask/bitcoin-wallet-snap": "npm:^1.8.0"