diff --git a/test/data/bridge/mock-bridge-store.ts b/test/data/bridge/mock-bridge-store.ts index 35902a7a0f8e..ed40ddf5236a 100644 --- a/test/data/bridge/mock-bridge-store.ts +++ b/test/data/bridge/mock-bridge-store.ts @@ -75,6 +75,47 @@ export const MOCK_SOLANA_ACCOUNT = { }, }; +export const MOCK_BITCOIN_ACCOUNT = { + type: 'bip122:p2wpkh', + scopes: ['bip122:000000000019d6689c085ae165831e93'], + id: '40b25442-ed7e-4b94-9f9f-ea8ff06d03b3', + address: 'bc1q2pxsagdzfdn6k6umvf9gj3eme7a27p7acym9g2', + options: { + entropySource: '01K8NT6Z7XDKEKX9MFSZ5EPFMW', + exportable: false, + entropy: { + type: 'mnemonic', + id: '01K8NT6Z7XDKEKX9MFSZ5EPFMW', + derivationPath: "m/84'/0'/0'", + groupIndex: 0, + }, + }, + methods: [ + 'signPsbt', + 'computeFee', + 'fillPsbt', + 'broadcastPsbt', + 'sendTransfer', + 'getUtxo', + 'listUtxos', + 'publicDescriptor', + 'signMessage', + ], + metadata: { + name: 'Snap Account 9', + importTime: 1763417984346, + keyring: { + type: 'Snap Keyring', + }, + snap: { + id: 'npm:@metamask/bitcoin-wallet-snap', + name: 'Bitcoin', + enabled: true, + }, + lastSelected: 1764203245474, + }, +}; + export const MOCK_EVM_ACCOUNT = { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', @@ -163,6 +204,7 @@ export const createBridgeMockStore = ({ ...(internalAccountsOverrides?.accounts ?? {}), [MOCK_LEDGER_ACCOUNT.id]: MOCK_LEDGER_ACCOUNT, [MOCK_SOLANA_ACCOUNT.id]: MOCK_SOLANA_ACCOUNT, + [MOCK_BITCOIN_ACCOUNT.id]: MOCK_BITCOIN_ACCOUNT, [MOCK_EVM_ACCOUNT.id]: MOCK_EVM_ACCOUNT, [MOCK_EVM_ACCOUNT_2.id]: MOCK_EVM_ACCOUNT_2, }, @@ -259,7 +301,11 @@ export const createBridgeMockStore = ({ groupIndex: 0, }, }, - accounts: [MOCK_EVM_ACCOUNT.id, MOCK_SOLANA_ACCOUNT.id], + accounts: [ + MOCK_EVM_ACCOUNT.id, + MOCK_SOLANA_ACCOUNT.id, + MOCK_BITCOIN_ACCOUNT.id, + ], }, 'entropy:01K2FF18CTTXJYD34R78X4N1N1/1': { type: 'multichain-account', @@ -325,6 +371,14 @@ export const createBridgeMockStore = ({ name: '', }, }, + { + type: KeyringType.snap, + accounts: [MOCK_BITCOIN_ACCOUNT.address], + metadata: { + id: '01K6GQ6SXDB9GKP6CAPSRV5AJG', + name: '', + }, + }, { type: KeyringType.ledger, accounts: [MOCK_LEDGER_ACCOUNT.address], diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index 167d4748d509..5c0d6b726a8a 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -3,18 +3,12 @@ import { type BridgeController, BridgeUserAction, formatChainIdToCaip, - isNativeAddress, - getNativeAssetForChainId, type RequiredEventContextFromClient, UnifiedSwapBridgeEventName, + formatChainIdToHex, + isCrossChain, } from '@metamask/bridge-controller'; -import { type InternalAccount } from '@metamask/keyring-internal-api'; -import { type CaipChainId } from '@metamask/utils'; -import type { - AddNetworkFields, - NetworkConfiguration, -} from '@metamask/network-controller'; -import { trace, TraceName } from '../../../shared/lib/trace'; +import { selectDefaultNetworkClientIdsByChainId } from '../../../shared/modules/selectors/networks'; import { forceUpdateMetamaskState, setActiveNetworkWithError, @@ -31,11 +25,12 @@ import { setEVMSrcNativeBalance, } from './bridge'; import type { TokenPayload } from './types'; -import { isNetworkAdded, isNonEvmChain } from './utils'; +import { isNonEvmChain } from './utils'; +import { type BridgeAppState, getFromChain } from './selectors'; const { setToChainId, - setFromToken, + setFromToken: setFromTokenAction, setToToken, setFromTokenInputValue, resetInputFields, @@ -50,7 +45,6 @@ export { setToChainId, resetInputFields, setToToken, - setFromToken, setFromTokenInputValue, setDestTokenExchangeRates, setDestTokenUsdExchangeRates, @@ -125,14 +119,6 @@ export const setEVMSrcTokenBalance = ( ) => { return async (dispatch: MetaMaskReduxDispatch) => { if (token) { - trace({ - name: TraceName.BridgeBalancesUpdated, - data: { - srcChainId: formatChainIdToCaip(token.chainId), - isNative: isNativeAddress(token.address), - }, - startTime: Date.now(), - }); await dispatch( setEVMSrcTokenBalance_({ selectedAddress, @@ -144,71 +130,33 @@ export const setEVMSrcTokenBalance = ( }; }; -export const setFromChain = ({ - networkConfig, - selectedAccount, - token = null, -}: { - networkConfig?: - | NetworkConfiguration - | AddNetworkFields - | (Omit & { chainId: CaipChainId }); - selectedAccount: InternalAccount | null; - token?: TokenPayload['payload']; -}) => { - return async (dispatch: MetaMaskReduxDispatch) => { - if (!networkConfig) { - return; - } - - // Check for ALL non-EVM chains - const isNonEvm = isNonEvmChain(networkConfig.chainId); +export const setFromToken = (token: NonNullable) => { + return async ( + dispatch: MetaMaskReduxDispatch, + getState: () => BridgeAppState, + ) => { + const { chainId } = token; + const isNonEvm = isNonEvmChain(chainId); + const currentChainId = getFromChain(getState())?.chainId; + const shouldSetNetwork = currentChainId + ? isCrossChain(currentChainId, chainId) + : true; // Set the src network - if (isNonEvm) { - dispatch(setActiveNetworkWithError(networkConfig.chainId)); - } else { - const networkId = isNetworkAdded(networkConfig) - ? networkConfig.rpcEndpoints?.[networkConfig.defaultRpcEndpointIndex] - ?.networkClientId - : null; - if (networkId) { - dispatch(setActiveNetworkWithError(networkId)); - } - } - - // Set the src token - if no token provided, set native token for non-EVM chains - if (token) { - dispatch(setFromToken(token)); - } else if (isNonEvm) { - // Auto-select native token for non-EVM chains when switching - const nativeAsset = getNativeAssetForChainId(networkConfig.chainId); - if (nativeAsset) { - dispatch( - setFromToken({ - ...nativeAsset, - chainId: networkConfig.chainId, - }), - ); + if (shouldSetNetwork) { + if (isNonEvm) { + const caipChainId = formatChainIdToCaip(chainId); + dispatch(setActiveNetworkWithError(caipChainId)); + } else { + const hexChainId = formatChainIdToHex(chainId); + const networkId = + selectDefaultNetworkClientIdsByChainId(getState())[hexChainId]; + if (networkId && shouldSetNetwork) { + dispatch(setActiveNetworkWithError(networkId)); + } } } - - // Fetch the native balance (EVM only) - if (selectedAccount && !isNonEvm) { - trace({ - name: TraceName.BridgeBalancesUpdated, - data: { - srcChainId: formatChainIdToCaip(networkConfig.chainId), - isNative: true, - }, - startTime: Date.now(), - }); - await dispatch( - setEVMSrcNativeBalance({ - selectedAddress: selectedAccount.address, - chainId: networkConfig.chainId, - }), - ); - } + // Set the fromToken + dispatch(setFromTokenAction(token)); }; }; diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 7d1d004856f8..a32069f9c10f 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -10,10 +10,11 @@ import { type GenericQuoteRequest, type QuoteResponse, isBitcoinChainId, + isNativeAddress, } from '@metamask/bridge-controller'; import { zeroAddress } from 'ethereumjs-util'; import { fetchTxAlerts } from '../../../shared/modules/bridge-utils/security-alerts-api.util'; -import { endTrace, TraceName } from '../../../shared/lib/trace'; +import { trace, TraceName } from '../../../shared/lib/trace'; import { SlippageValue } from '../../pages/bridge/utils/slippage-service'; import { getTokenExchangeRate, toBridgeToken } from './utils'; import type { BridgeState, ChainIdPayload, TokenPayload } from './types'; @@ -67,14 +68,25 @@ const getBalanceAmount = async ({ if (isNonEvmChainId(chainId) || !selectedAddress) { return null; } - return ( - await calcLatestSrcBalance( - global.ethereumProvider, - selectedAddress, - tokenAddress, - formatChainIdToHex(chainId), - ) - )?.toString(); + return await trace( + { + name: TraceName.BridgeBalancesUpdated, + data: { + srcChainId: formatChainIdToCaip(chainId), + isNative: isNativeAddress(tokenAddress), + }, + startTime: Date.now(), + }, + async () => + ( + await calcLatestSrcBalance( + global.ethereumProvider, + selectedAddress, + tokenAddress, + formatChainIdToHex(chainId), + ) + )?.toString(), + ); }; export const setEVMSrcNativeBalance = createAsyncThunk( @@ -216,29 +228,17 @@ const bridgeSlice = createSlice({ ? action.meta.arg.tokenAddress === state.fromToken.address : true ) { - state.fromTokenBalance = action.payload?.toString() ?? null; + state.fromTokenBalance = action.payload ?? null; } - endTrace({ - name: TraceName.BridgeBalancesUpdated, - }); }); builder.addCase(setEVMSrcTokenBalance.rejected, (state) => { state.fromTokenBalance = null; - endTrace({ - name: TraceName.BridgeBalancesUpdated, - }); }); builder.addCase(setEVMSrcNativeBalance.fulfilled, (state, action) => { - state.fromNativeBalance = action.payload?.toString() ?? null; - endTrace({ - name: TraceName.BridgeBalancesUpdated, - }); + state.fromNativeBalance = action.payload ?? null; }); builder.addCase(setEVMSrcNativeBalance.rejected, (state) => { state.fromNativeBalance = null; - endTrace({ - name: TraceName.BridgeBalancesUpdated, - }); }); }, }); diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 3b02e7957494..bff98a168823 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -9,7 +9,10 @@ import { } from '@metamask/bridge-controller'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { SolAccountType, SolScope } from '@metamask/keyring-api'; -import { createBridgeMockStore } from '../../../test/data/bridge/mock-bridge-store'; +import { + createBridgeMockStore, + MOCK_BITCOIN_ACCOUNT, +} from '../../../test/data/bridge/mock-bridge-store'; import { CHAIN_IDS, FEATURED_RPCS } from '../../../shared/constants/network'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { mockNetworkState } from '../../../test/stub/networks'; @@ -466,7 +469,7 @@ describe('Bridge selectors', () => { it('returns selected toToken', () => { const state = createBridgeMockStore({ bridgeSliceOverrides: { - fromToken: { address: '0x123', symbol: 'TEST' }, + fromToken: { address: '0x123', symbol: 'TEST', chainId: '0x1' }, toChainId: formatChainIdToCaip(1), toToken: { address: '0x567', symbol: 'DEST' }, }, @@ -487,7 +490,7 @@ describe('Bridge selectors', () => { it('returns default token if toToken is not set', () => { const state = createBridgeMockStore({ bridgeSliceOverrides: { - fromToken: { address: '0x123', symbol: 'TEST' }, + fromToken: { address: '0x123', symbol: 'TEST', chainId: '0x1' }, toChainId: formatChainIdToCaip(1), }, featureFlagOverrides: { @@ -575,6 +578,10 @@ describe('Bridge selectors', () => { isActiveSrc: true, isActiveDest: true, }, + [MultichainNetworks.BITCOIN]: { + isActiveSrc: true, + isActiveDest: true, + }, }, bip44DefaultPairs: { bip122: { @@ -587,6 +594,18 @@ describe('Bridge selectors', () => { }, }, }, + metamaskStateOverrides: { + internalAccounts: { + selectedAccount: MOCK_BITCOIN_ACCOUNT.id, + }, + balances: { + [MOCK_BITCOIN_ACCOUNT.id]: { + [getNativeAssetForChainId(ChainId.BTC).assetId]: { + amount: '2', + }, + }, + }, + }, }); const result = getToToken(state as never); @@ -1919,6 +1938,7 @@ describe('Bridge selectors', () => { fromToken: { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: 6, + chainId: '0x1', }, }, featureFlagOverrides: { @@ -1954,6 +1974,7 @@ describe('Bridge selectors', () => { fromToken: { address: zeroAddress(), decimals: 18, + chainId: '0x1', }, }, featureFlagOverrides: { @@ -2009,6 +2030,9 @@ describe('Bridge selectors', () => { fromToken: { address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6, + assetId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + chainId: MultichainNetworks.SOLANA, }, }, featureFlagOverrides: { @@ -2062,6 +2086,7 @@ describe('Bridge selectors', () => { fromToken: { address: zeroAddress(), decimals: 18, + chainId: MultichainNetworks.SOLANA, }, }, featureFlagOverrides: { diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 4228f46ba389..b71bc3ff230e 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -74,7 +74,6 @@ import { HardwareKeyringNames, HardwareKeyringType, } from '../../../shared/constants/hardware-wallets'; -import { toAssetId } from '../../../shared/lib/asset-utils'; import { MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 } from '../../../shared/constants/multichain/assets'; import { Numeric } from '../../../shared/modules/Numeric'; import { getIsSmartTransaction } from '../../../shared/modules/selectors'; @@ -316,16 +315,53 @@ export const getLastSelectedChainId = createSelector( ); // This returns undefined if the selected chain is not supported by swap/bridge (i.e, testnets) -export const getFromChain = createDeepEqualSelector( - [getFromChains, getMultichainProviderConfig], - (fromChains, providerConfig) => { +export const getFromToken = createSelector( + [ + (state: BridgeAppState) => state.bridge.fromToken, + getFromChains, + getMultichainProviderConfig, + ], + (fromToken, fromChains, providerConfig) => { // When the page loads the global network always matches the network filter // Because useBridging checks whether the lastSelectedNetwork matches the provider config // Then useBridgeQueryParams sets the global network to lastSelectedNetwork as needed // TODO remove providerConfig references and just use getLastSelectedChainId - return fromChains.find( + const fromChain = fromChains.find( ({ chainId }) => chainId === providerConfig?.chainId, ); + if (!fromChain) { + return null; + } + const fromChainId = fromChain.chainId; + if ( + fromToken && + fromToken.address && + !isCrossChain(fromToken.chainId, fromChainId) + ) { + return fromToken; + } + const { iconUrl, ...nativeAsset } = getNativeAssetForChainId(fromChainId); + const newToToken = toBridgeToken(nativeAsset); + return newToToken + ? { + ...newToToken, + chainId: formatChainIdToCaip(fromChainId), + } + : newToToken; + }, +); + +// Return's the chain matching the fromToken's chainId +export const getFromChain = createSelector( + [getFromToken, getFromChains], + (fromToken, fromChains) => { + if (!fromToken) { + return undefined; + } + return fromChains.find( + ({ chainId }) => + formatChainIdToCaip(chainId) === formatChainIdToCaip(fromToken.chainId), + ); }, ); @@ -419,29 +455,6 @@ export const getToChain = createSelector( }, ); -export const getFromToken = createSelector( - [ - (state: BridgeAppState) => state.bridge.fromToken, - (state) => getFromChain(state)?.chainId, - ], - (fromToken, fromChainId) => { - if (!fromChainId) { - return null; - } - if (fromToken?.address) { - return fromToken; - } - const { iconUrl, ...nativeAsset } = getNativeAssetForChainId(fromChainId); - const newToToken = toBridgeToken(nativeAsset); - return newToToken - ? { - ...newToToken, - chainId: formatChainIdToCaip(fromChainId), - } - : newToToken; - }, -); - export const getToToken = createSelector( [getFromToken, getToChain, (state) => state.bridge.toToken], (fromToken, toChain, toToken) => { @@ -628,10 +641,7 @@ export const getFromTokenConversionRate = createSelector( const nativeAssetId = getNativeAssetForChainId( fromChain.chainId, )?.assetId; - const tokenAssetId = toAssetId( - fromToken.address, - formatChainIdToCaip(fromChain.chainId), - ); + const tokenAssetId = fromToken.assetId; const nativeToCurrencyRate = isNonEvmChain(fromChain.chainId) ? Number( rates?.[fromChain.nativeCurrency?.toLowerCase()]?.conversionRate ?? @@ -731,10 +741,7 @@ export const getToTokenConversionRate = createDeepEqualSelector( } if (toChain?.chainId && toToken) { const nativeAssetId = getNativeAssetForChainId(toChain.chainId)?.assetId; - const tokenAssetId = toAssetId( - toToken.address, - formatChainIdToCaip(toChain.chainId), - ); + const tokenAssetId = toToken.assetId; // For non-EVM tokens (Solana, Bitcoin, Tron), we use the conversion rates provided by the multichain rates controller if (isNonEvmChain(toChain.chainId) && nativeAssetId && tokenAssetId) { diff --git a/ui/ducks/bridge/types.ts b/ui/ducks/bridge/types.ts index cec78727bf4c..3982e51455e0 100644 --- a/ui/ducks/bridge/types.ts +++ b/ui/ducks/bridge/types.ts @@ -11,7 +11,7 @@ import { type TxAlert } from '../../../shared/types/security-alerts-api'; export type BridgeToken = { address: string; - assetId?: CaipAssetType; + assetId: CaipAssetType; symbol: string; image: string; decimals: number; diff --git a/ui/ducks/bridge/utils.ts b/ui/ducks/bridge/utils.ts index abe27b9979d4..7fe31777f478 100644 --- a/ui/ducks/bridge/utils.ts +++ b/ui/ducks/bridge/utils.ts @@ -159,17 +159,10 @@ export const getTxGasEstimates = async ({ }; const fetchTokenExchangeRates = async ( - chainId: Hex | CaipChainId | ChainId, currency: string, signal?: AbortSignal, - ...tokenAddresses: string[] + ...assetIds: CaipAssetType[] ) => { - const assetIds = tokenAddresses - .map((address) => toAssetId(address, formatChainIdToCaip(chainId))) - .filter(Boolean); - if (assetIds.length === 0) { - return {}; - } const queryParams = new URLSearchParams({ assetIds: assetIds.join(','), includeMarketData: 'true', @@ -197,19 +190,16 @@ const fetchTokenExchangeRates = async ( // rate is not available in the TokenRatesController, which happens when the selected token has not been // imported into the wallet export const getTokenExchangeRate = async (request: { - chainId: Hex | CaipChainId | ChainId; - tokenAddress: string; + assetId: CaipAssetType; currency: string; signal?: AbortSignal; }) => { - const { chainId, tokenAddress, currency, signal } = request; + const { assetId, currency, signal } = request; const exchangeRates = await fetchTokenExchangeRates( - chainId, currency, signal, - tokenAddress, + assetId, ); - const assetId = toAssetId(tokenAddress, formatChainIdToCaip(chainId)); return assetId ? exchangeRates?.[assetId] : undefined; }; @@ -303,14 +293,17 @@ export const toBridgeToken = ( return null; } const caipChainId = formatChainIdToCaip(payload.chainId); - return { - ...payload, - balance: payload.balance ?? '0', - string: payload.string ?? '0', - chainId: payload.chainId, - image: getTokenImage(payload), - assetId: payload.assetId ?? toAssetId(payload.address, caipChainId), - }; + const assetId = payload.assetId ?? toAssetId(payload.address, caipChainId); + return assetId + ? { + ...payload, + balance: payload.balance ?? '0', + string: payload.string ?? '0', + chainId: payload.chainId, + image: getTokenImage(payload), + assetId, + } + : null; }; const createBridgeTokenPayload = ( tokenData: { diff --git a/ui/hooks/bridge/useBridgeExchangeRates.ts b/ui/hooks/bridge/useBridgeExchangeRates.ts index 95ff724d7541..986e00dc4f48 100644 --- a/ui/hooks/bridge/useBridgeExchangeRates.ts +++ b/ui/hooks/bridge/useBridgeExchangeRates.ts @@ -1,132 +1,50 @@ import { useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - getBridgeQuotes, - getFromToken, - getQuoteRequest, - getToChain, - getToToken, -} from '../../ducks/bridge/selectors'; -import { getMarketData, getParticipateInMetaMetrics } from '../../selectors'; +import { getFromToken } from '../../ducks/bridge/selectors'; +import { getMarketData } from '../../selectors'; import { getCurrentCurrency } from '../../ducks/metamask/metamask'; -import { - setDestTokenExchangeRates, - setDestTokenUsdExchangeRates, - setSrcTokenExchangeRates, -} from '../../ducks/bridge/bridge'; +import { setSrcTokenExchangeRates } from '../../ducks/bridge/bridge'; import { exchangeRateFromMarketData } from '../../ducks/bridge/utils'; -import { useMultichainSelector } from '../useMultichainSelector'; -import { getMultichainCurrentChainId } from '../../selectors/multichain'; export const useBridgeExchangeRates = () => { - const { srcTokenAddress, destTokenAddress } = useSelector(getQuoteRequest); - const { activeQuote } = useSelector(getBridgeQuotes); - const fromChainId = useMultichainSelector(getMultichainCurrentChainId); - const toChain = useSelector(getToChain); - const toChainId = toChain?.chainId; - - const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics); - const dispatch = useDispatch(); const currency = useSelector(getCurrentCurrency); - // Only use token address from quote as a fallback if there is no token address in the store - const fromTokenAddressFromQuote = activeQuote - ? activeQuote.quote.srcAsset.address - : srcTokenAddress; - const fromTokenFromStore = useSelector(getFromToken); - const fromTokenAddress = - fromTokenFromStore?.address ?? fromTokenAddressFromQuote; - - // Only use token address from quote as a fallback if there is no token address in the store - const toTokenAddressFromQuote = activeQuote - ? activeQuote.quote.destAsset.address - : destTokenAddress; - const toTokenFromStore = useSelector(getToToken); - const toTokenAddress = toTokenFromStore?.address ?? toTokenAddressFromQuote; + const fromToken = useSelector(getFromToken); const marketData = useSelector(getMarketData); const fromAbortController = useRef( new AbortController(), ); - const toAbortController = useRef( - new AbortController(), - ); - // Cleanup abort controller on unmount useEffect(() => { return () => { fromAbortController.current?.abort(); fromAbortController.current = null; - toAbortController.current?.abort(); - toAbortController.current = null; }; }, []); + const cachedFromTokenExchangeRate = fromToken + ? exchangeRateFromMarketData( + fromToken.chainId, + fromToken.address, + marketData, + ) + : undefined; + // Fetch exchange rates for selected src token if not found in marketData useEffect(() => { fromAbortController.current?.abort(); fromAbortController.current = new AbortController(); - if (fromChainId && fromTokenAddress) { - const exchangeRate = exchangeRateFromMarketData( - fromChainId, - fromTokenAddress, - marketData, - ); - - if (!exchangeRate) { - dispatch( - setSrcTokenExchangeRates({ - chainId: fromChainId, - tokenAddress: fromTokenAddress, - currency, - signal: fromAbortController.current.signal, - }), - ); - } - } - }, [currency, dispatch, fromChainId, fromTokenAddress, marketData]); - - // Fetch exchange rates for selected dest token if not found in marketData - useEffect(() => { - toAbortController.current?.abort(); - toAbortController.current = new AbortController(); - if (toChainId && toTokenAddress) { - const exchangeRate = exchangeRateFromMarketData( - toChainId, - toTokenAddress, - marketData, + if (fromToken?.assetId && !cachedFromTokenExchangeRate) { + dispatch( + setSrcTokenExchangeRates({ + assetId: fromToken.assetId, + currency, + signal: fromAbortController.current.signal, + }), ); - - if (!exchangeRate) { - dispatch( - setDestTokenExchangeRates({ - chainId: toChainId, - tokenAddress: toTokenAddress, - currency, - signal: toAbortController.current.signal, - }), - ); - // If the selected currency is not USD, fetch the USD exchange rate for metrics - if (isMetaMetricsEnabled && currency !== 'usd') { - dispatch( - setDestTokenUsdExchangeRates({ - chainId: toChainId, - tokenAddress: toTokenAddress, - currency: 'usd', - signal: toAbortController.current.signal, - }), - ); - } - } } - }, [ - currency, - dispatch, - isMetaMetricsEnabled, - marketData, - toChainId, - toTokenAddress, - ]); + }, [currency, dispatch, fromToken?.assetId, cachedFromTokenExchangeRate]); }; diff --git a/ui/hooks/bridge/useBridgeQueryParams.test.ts b/ui/hooks/bridge/useBridgeQueryParams.test.ts index 8eaa05118a9a..5048a25c5164 100644 --- a/ui/hooks/bridge/useBridgeQueryParams.test.ts +++ b/ui/hooks/bridge/useBridgeQueryParams.test.ts @@ -59,6 +59,10 @@ describe('useBridgeQueryParams', () => { featureFlagOverrides: { bridgeConfig: { chains: { + [CHAIN_IDS.MAINNET]: { + isActiveSrc: true, + isActiveDest: true, + }, [ChainId.SOLANA]: { isActiveSrc: true, isActiveDest: true, @@ -66,6 +70,19 @@ describe('useBridgeQueryParams', () => { }, }, }, + metamaskStateOverrides: { + internalAccounts: { + selectedAccount: 'bf13d52c-d6e8-40ea-9726-07d7149a3ca5', + }, + balances: { + 'bf13d52c-d6e8-40ea-9726-07d7149a3ca5': { + [bridgeControllerUtils.getNativeAssetForChainId(ChainId.SOLANA) + .assetId]: { + amount: '2', + }, + }, + }, + }, }); const searchParams = new URLSearchParams({ diff --git a/ui/hooks/bridge/useBridgeQueryParams.ts b/ui/hooks/bridge/useBridgeQueryParams.ts index dde7bda93b01..990d04aef78d 100644 --- a/ui/hooks/bridge/useBridgeQueryParams.ts +++ b/ui/hooks/bridge/useBridgeQueryParams.ts @@ -7,13 +7,10 @@ import { parseCaipAssetType, } from '@metamask/utils'; import { - formatChainIdToCaip, getNativeAssetForChainId, isCrossChain, isNativeAddress, } from '@metamask/bridge-controller'; -import { type InternalAccount } from '@metamask/keyring-internal-api'; -import { type NetworkConfiguration } from '@metamask/network-controller'; import { type AssetMetadata, fetchAssetMetadataForAssetIds, @@ -23,7 +20,6 @@ import { calcTokenAmount } from '../../../shared/lib/transactions-controller-uti import { setEVMSrcTokenBalance, setEVMSrcNativeBalance, - setFromChain, setFromToken, setFromTokenInputValue, setToToken, @@ -169,15 +165,7 @@ export const useBridgeQueryParams = () => { // Set fromChain and fromToken const setFromChainAndToken = useCallback( - ( - fromTokenMetadata, - fromAsset, - networks: NetworkConfiguration[], - account: InternalAccount | null, - network?: NetworkConfiguration, - ) => { - const { chainId: assetChainId } = fromAsset; - + (fromTokenMetadata: AssetMetadata) => { if (fromTokenMetadata) { const { chainId, assetReference } = parseCaipAssetType( fromTokenMetadata.assetId, @@ -193,24 +181,7 @@ export const useBridgeQueryParams = () => { ? (nativeAsset?.address ?? '') : assetReference, }; - // If asset's chain is the same as fromChain, only set the fromToken - if (network && assetChainId === formatChainIdToCaip(network.chainId)) { - dispatch(setFromToken(token)); - } else { - // Find the chain matching the srcAsset's chainId - const targetChain = networks.find( - (chain) => formatChainIdToCaip(chain.chainId) === assetChainId, - ); - if (targetChain) { - dispatch( - setFromChain({ - networkConfig: targetChain, - selectedAccount: account, - token, - }), - ); - } - } + dispatch(setFromToken(token)); } }, [], @@ -247,20 +218,8 @@ export const useBridgeQueryParams = () => { ]; // Process from chain/token first - setFromChainAndToken( - fromTokenMetadata, - parsedFromAssetId, - fromChains, - selectedAccount, - fromChain, - ); - }, [ - assetMetadataByAssetId, - parsedFromAssetId, - fromChains, - fromChain, - selectedAccount, - ]); + setFromChainAndToken(fromTokenMetadata); + }, [assetMetadataByAssetId, parsedFromAssetId, fromChains]); // Set toChainId and toToken useEffect(() => { diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts index bc3cbe385462..4a9783e0779f 100644 --- a/ui/hooks/bridge/useBridging.test.ts +++ b/ui/hooks/bridge/useBridging.test.ts @@ -25,7 +25,7 @@ jest.mock('react-redux', () => ({ const mockSetFromChain = jest.fn(); jest.mock('../../ducks/bridge/actions', () => ({ ...jest.requireActual('../../ducks/bridge/actions'), - setFromChain: () => mockSetFromChain(), + setFromToken: () => mockSetFromChain(), })); const MOCK_METAMETRICS_ID = '0xtestMetaMetricsId'; diff --git a/ui/hooks/bridge/useTokenAlerts.test.ts b/ui/hooks/bridge/useTokenAlerts.test.ts index 4b011028bdc4..05826f050bac 100644 --- a/ui/hooks/bridge/useTokenAlerts.test.ts +++ b/ui/hooks/bridge/useTokenAlerts.test.ts @@ -51,12 +51,14 @@ describe('useTokenAlerts', () => { bridgeSliceOverrides: { fromToken: { address: '0x3fa807b6f8d4c407e6e605368f4372d14658b38c', + chainId: CHAIN_IDS.MAINNET, }, fromChain: { chainId: CHAIN_IDS.MAINNET, }, toToken: { address: '6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN', + chainId: MultichainNetworks.SOLANA, }, toChainId: MultichainNetworks.SOLANA, }, diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 57e84312cc26..dd34eabf1e63 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -33,7 +33,6 @@ import { updateQuoteRequestParams, resetBridgeState, trackUnifiedSwapBridgeEvent, - setFromChain, } from '../../../ducks/bridge/actions'; import { getBridgeQuotes, @@ -113,7 +112,6 @@ import { useDestinationAccount } from '../hooks/useDestinationAccount'; import { Toast, ToastContainer } from '../../../components/multichain'; import { useIsTxSubmittable } from '../../../hooks/bridge/useIsTxSubmittable'; import type { BridgeToken } from '../../../ducks/bridge/types'; -import { toAssetId } from '../../../../shared/lib/asset-utils'; import { endTrace, TraceName } from '../../../../shared/lib/trace'; import { FEATURED_NETWORK_CHAIN_IDS, @@ -549,9 +547,6 @@ const PrepareBridgePage = ({ }; dispatch(setFromToken(bridgeToken)); dispatch(setFromTokenInputValue(null)); - if (token.address === toToken?.address) { - dispatch(setToToken(null)); - } }} networkProps={{ network: fromChain, @@ -561,10 +556,7 @@ const PrepareBridgePage = ({ enableMissingNetwork(networkConfig.chainId); } dispatch( - setFromChain({ - networkConfig, - selectedAccount, - }), + setFromToken(getNativeAssetForChainId(networkConfig.chainId)), ); }, header: t('yourNetworks'), @@ -666,19 +658,10 @@ const PrepareBridgePage = ({ token_symbol_destination: fromToken?.symbol ?? null, // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 // eslint-disable-next-line @typescript-eslint/naming-convention - token_address_source: - toAssetId( - toToken.address ?? '', - formatChainIdToCaip(toToken.chainId ?? ''), - ) ?? - getNativeAssetForChainId(toChain.chainId)?.assetId, + token_address_source: toToken?.assetId, // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 // eslint-disable-next-line @typescript-eslint/naming-convention - token_address_destination: - toAssetId( - fromToken.address ?? '', - formatChainIdToCaip(fromToken.chainId ?? ''), - ) ?? null, + token_address_destination: fromToken?.assetId, // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 // eslint-disable-next-line @typescript-eslint/naming-convention chain_id_source: formatChainIdToCaip(toChain.chainId), @@ -696,18 +679,7 @@ const PrepareBridgePage = ({ setRotateSwitchTokens(!rotateSwitchTokens); - if (isSwap) { - dispatch(setFromToken(toToken)); - } else { - // Handle account switching for Solana - dispatch( - setFromChain({ - networkConfig: toChain, - token: toToken, - selectedAccount, - }), - ); - } + toToken && dispatch(setFromToken(toToken)); dispatch(setToToken(fromToken)); }} />