From e46a4c15a3f6b4f4ac1db5fcd3f6f06ad572e271 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 14 Mar 2025 14:29:04 -0700 Subject: [PATCH 01/71] feat: migrate to @metamask/bridge-controller --- app/scripts/metamask-controller.js | 20 +++++- package.json | 2 + shared/types/bridge.ts | 2 +- .../bridge/prepare/bridge-input-group.tsx | 2 +- .../bridge/prepare/prepare-bridge-page.tsx | 6 +- yarn.lock | 67 +++++++++++++++++++ 6 files changed, 91 insertions(+), 8 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ad65c3dcb536..bb1292b83cc4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -124,6 +124,7 @@ import { } from '@metamask/queued-request-controller'; import { UserOperationController } from '@metamask/user-operation-controller'; +import { BridgeController } from '@metamask/bridge-controller'; import { TransactionStatus, @@ -250,6 +251,10 @@ import { BridgeUserAction, BridgeBackgroundAction, } from '../../shared/types/bridge'; +import { + BRIDGE_API_BASE_URL, + BRIDGE_CLIENT_ID, +} from '../../shared/constants/bridge'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) handleMMITransactionUpdate, @@ -349,7 +354,6 @@ import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmM import { isEthAddress } from './lib/multichain/address'; import { decodeTransactionData } from './lib/transaction/decode/util'; -import BridgeController from './controllers/bridge/bridge-controller'; import { BRIDGE_CONTROLLER_NAME } from './controllers/bridge/constants'; import { onPushNotificationClicked, @@ -1771,18 +1775,28 @@ export default class MetamaskController extends EventEmitter { const bridgeControllerMessenger = this.controllerMessenger.getRestricted({ name: BRIDGE_CONTROLLER_NAME, allowedActions: [ - // 'AccountsController:getSelectedAccount', 'AccountsController:getSelectedMultichainAccount', 'SnapController:handleRequest', - 'NetworkController:getSelectedNetworkClient', + 'NetworkController:getState', + 'NetworkController:getNetworkClientById', 'NetworkController:findNetworkClientIdByChainId', ], allowedEvents: [], }); this.bridgeController = new BridgeController({ messenger: bridgeControllerMessenger, + clientId: BRIDGE_CLIENT_ID, // TODO: Remove once TransactionController exports this action type getLayer1GasFee: (...args) => this.txController.getLayer1GasFee(...args), + fetchFn: async (url, { headers, ...requestOptions }) => + await fetchWithCache({ + url, + fetchOptions: { method: 'GET', headers }, + ...requestOptions, + }), + config: { + customBridgeApiBaseUrl: BRIDGE_API_BASE_URL, + }, }); const bridgeStatusControllerMessenger = diff --git a/package.json b/package.json index 8aec2bcd11e6..4ab9507fbd00 100644 --- a/package.json +++ b/package.json @@ -191,6 +191,7 @@ "readable-stream-3@^3.6.2": "npm:readable-stream@^3.6.2", "semver@7.3.7": "^7.5.4", "semver@7.3.8": "^7.5.4", + "@metamask/bridge-controller": "portal:./../core/packages/bridge-controller", "lavamoat-core@npm:^16.2.2": "patch:lavamoat-core@npm%3A16.2.2#~/.yarn/patches/lavamoat-core-npm-16.2.2-e361ff1f8a.patch", "@metamask/snaps-sdk": "^6.19.0", "@swc/types@0.1.5": "^0.1.6", @@ -266,6 +267,7 @@ "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A53.1.1#~/.yarn/patches/@metamask-assets-controllers-npm-53.1.1-644979f986.patch", "@metamask/base-controller": "^8.0.0", "@metamask/bitcoin-wallet-snap": "^0.9.0", + "@metamask/bridge-controller": "*", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.4.0", diff --git a/shared/types/bridge.ts b/shared/types/bridge.ts index 0d818da80974..338da1147a28 100644 --- a/shared/types/bridge.ts +++ b/shared/types/bridge.ts @@ -95,7 +95,7 @@ export type QuoteRequest< srcTokenAddress: TokenAddressType; destTokenAddress: TokenAddressType; srcTokenAmount: string; // This is the amount sent - slippage: number; + slippage?: number; aggIds?: string[]; bridgeIds?: string[]; insufficientBal?: boolean; diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx index 7a98b7a44500..a31094ee299e 100644 --- a/ui/pages/bridge/prepare/bridge-input-group.tsx +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { BigNumber } from 'bignumber.js'; +import { isNativeAddress } from '@metamask/bridge-controller'; import { Text, TextField, @@ -14,7 +15,6 @@ import { TabName } from '../../../components/multichain/asset-picker-amount/asse import { useI18nContext } from '../../../hooks/useI18nContext'; import { getCurrentCurrency } from '../../../ducks/metamask/metamask'; import { formatCurrencyAmount, formatTokenAmount } from '../utils/quote'; -import { isNativeAddress } from '../../../../shared/modules/bridge-utils/caip-formatters'; import { Column, Row } from '../layout'; import { Display, diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 656c4b668254..c6c5a6b09063 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -12,6 +12,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import { BigNumber } from 'bignumber.js'; import { type TokenListMap } from '@metamask/assets-controllers'; import { toChecksumAddress, zeroAddress } from 'ethereumjs-util'; +import { isValidQuoteRequest } from '@metamask/bridge-controller'; import { setFromToken, setFromTokenInputValue, @@ -74,7 +75,6 @@ import { formatTokenAmount, isQuoteExpired as isQuoteExpiredUtil, } from '../utils/quote'; -import { isValidQuoteRequest } from '../../../../shared/modules/bridge-utils/quote'; import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { CrossChainSwapsEventProperties, @@ -325,7 +325,7 @@ const PrepareBridgePage = () => { // This override allows quotes to be returned when the rpcUrl is a tenderly fork // Otherwise quotes get filtered out by the bridge-api when the wallet's real // balance is less than the tenderly balance - insufficientBal: Boolean(providerConfig?.rpcUrl?.includes('tenderly')), + // insufficientBal: Boolean(providerConfig?.rpcUrl?.includes('tenderly')), slippage, walletAddress: selectedAccount?.address ?? '', destWalletAddress: selectedDestinationAccount?.address, @@ -337,7 +337,7 @@ const PrepareBridgePage = () => { fromAmount, fromChain?.chainId, toChain?.chainId, - providerConfig?.rpcUrl, + // providerConfig?.rpcUrl, slippage, selectedAccount?.address, selectedDestinationAccount?.address, diff --git a/yarn.lock b/yarn.lock index c7a18c0cd3a2..cebad3c80f9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5046,6 +5046,31 @@ __metadata: languageName: node linkType: hard +"@metamask/bridge-controller@portal:./../core/packages/bridge-controller::locator=metamask-crx%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@metamask/bridge-controller@portal:./../core/packages/bridge-controller::locator=metamask-crx%40workspace%3A." + dependencies: + "@ethersproject/address": "npm:^5.7.0" + "@ethersproject/bignumber": "npm:^5.7.0" + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/controller-utils": "npm:^11.6.0" + "@metamask/keyring-api": "npm:^17.2.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-network-controller": "npm:^0.1.0" + "@metamask/polling-controller": "npm:^12.0.3" + "@metamask/snaps-controllers": "npm:^10.0.1" + "@metamask/snaps-utils": "npm:^9.0.1" + "@metamask/utils": "npm:^11.2.0" + peerDependencies: + "@metamask/accounts-controller": ^26.0.0 + "@metamask/network-controller": ^22.0.0 + "@metamask/transaction-controller": ^49.0.0 + languageName: node + linkType: soft + "@metamask/browser-passworder@npm:^4.3.0": version: 4.3.0 resolution: "@metamask/browser-passworder@npm:4.3.0" @@ -6280,6 +6305,47 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-controllers@npm:^10.0.1": + version: 10.0.1 + resolution: "@metamask/snaps-controllers@npm:10.0.1" + dependencies: + "@metamask/approval-controller": "npm:^7.1.3" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/json-rpc-engine": "npm:^10.0.2" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.7" + "@metamask/key-tree": "npm:^10.0.2" + "@metamask/object-multiplex": "npm:^2.1.0" + "@metamask/permission-controller": "npm:^11.0.6" + "@metamask/phishing-controller": "npm:^12.4.0" + "@metamask/post-message-stream": "npm:^9.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-registry": "npm:^3.2.3" + "@metamask/snaps-rpc-methods": "npm:^11.13.0" + "@metamask/snaps-sdk": "npm:^6.19.0" + "@metamask/snaps-utils": "npm:^9.0.1" + "@metamask/utils": "npm:^11.2.0" + "@xstate/fsm": "npm:^2.0.0" + async-mutex: "npm:^0.5.0" + browserify-zlib: "npm:^0.2.0" + concat-stream: "npm:^2.0.0" + fast-deep-equal: "npm:^3.1.3" + get-npm-tarball-url: "npm:^2.0.3" + immer: "npm:^9.0.6" + luxon: "npm:^3.5.0" + nanoid: "npm:^3.1.31" + readable-stream: "npm:^3.6.2" + readable-web-to-node-stream: "npm:^3.0.2" + semver: "npm:^7.5.4" + tar-stream: "npm:^3.1.7" + peerDependencies: + "@metamask/snaps-execution-environments": ^7.0.0 + peerDependenciesMeta: + "@metamask/snaps-execution-environments": + optional: true + checksum: 10/aa1e1b3da0edfba50e7c0ae78fa02479b6f345ae6e5e6ebf3fdaf072a52fede176d954ea99c25a468856b91331003920610b214fcf21f8e23328310b516e068b + languageName: node + linkType: hard + "@metamask/snaps-controllers@npm:^11.0.0": version: 11.0.0 resolution: "@metamask/snaps-controllers@npm:11.0.0" @@ -27247,6 +27313,7 @@ __metadata: "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^8.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.9.0" + "@metamask/bridge-controller": "npm:*" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" From a6c731701e81da9f0038c21f55bfa2f7457e379f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 12:24:34 -0700 Subject: [PATCH 02/71] refactor: replace solana conditions with isSolanaChainId --- shared/modules/bridge-utils/bridge.util.ts | 2 +- ui/ducks/bridge/selectors.ts | 20 ++++++++----------- ui/ducks/bridge/utils.ts | 6 +++--- ui/hooks/bridge/useLatestBalance.ts | 8 ++------ ui/hooks/bridge/useTokensWithFiltering.ts | 10 +++++----- .../bridge/hooks/useDestinationAccount.ts | 7 ++----- .../hooks/useSubmitBridgeTransaction.ts | 6 ++---- .../bridge/prepare/prepare-bridge-page.tsx | 16 +++++++-------- .../quotes/multichain-bridge-quote-card.tsx | 5 +++-- 9 files changed, 33 insertions(+), 47 deletions(-) diff --git a/shared/modules/bridge-utils/bridge.util.ts b/shared/modules/bridge-utils/bridge.util.ts index fb36fc4bab3d..112fdc407a28 100644 --- a/shared/modules/bridge-utils/bridge.util.ts +++ b/shared/modules/bridge-utils/bridge.util.ts @@ -197,7 +197,7 @@ export async function fetchBridgeQuotes( srcTokenAddress: formatAddressToString(request.srcTokenAddress), destTokenAddress: formatAddressToString(request.destTokenAddress), srcTokenAmount: request.srcTokenAmount, - ...(ignoreSlippage ? {} : { slippage: request.slippage.toString() }), + ...(ignoreSlippage ? {} : { slippage: request.slippage?.toString() }), insufficientBal: request.insufficientBal ? 'true' : 'false', resetApproval: request.resetApproval ? 'true' : 'false', }; diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index c439da53dbe4..8ed4bbd19b17 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -12,6 +12,7 @@ import type { GasFeeEstimates } from '@metamask/gas-fee-controller'; import { BigNumber } from 'bignumber.js'; import { calcTokenAmount } from '@metamask/notification-services-controller/push-services'; import { CaipChainId, Hex } from '@metamask/utils'; +import { isSolanaChainId } from '@metamask/bridge-controller'; import { MultichainNetworks, ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) @@ -145,8 +146,7 @@ export const getFromChains = createDeepEqualSelector( const filteredNetworks = hasSolanaAccount ? allBridgeableNetworks : allBridgeableNetworks.filter( - // @ts-expect-error: gotta fix type here. - ({ chainId }) => chainId !== MultichainNetworks.SOLANA, + ({ chainId }) => !isSolanaChainId(chainId), ); // Then apply the standard filter for active source chains @@ -302,7 +302,7 @@ export const getFromTokenConversionRate = createSelector( fromTokenExchangeRate, ) => { if (fromChain?.chainId && fromToken) { - if (fromChain.chainId === MultichainNetworks.SOLANA) { + if (isSolanaChainId(fromChain.chainId)) { // For SOLANA tokens, we use the conversion rates provided by the multichain rates controller const tokenToNativeAssetRate = tokenPriceInNativeAsset( assetsRates[fromToken.address]?.rate, @@ -368,7 +368,7 @@ export const getToTokenConversionRate = createDeepEqualSelector( }; } if (toChain?.chainId && toToken) { - if (toChain.chainId === MultichainNetworks.SOLANA) { + if (isSolanaChainId(toChain.chainId)) { // For SOLANA tokens, we use the conversion rates provided by the multichain rates controller const tokenToNativeAssetRate = tokenPriceInNativeAsset( assetsRates[toToken.address]?.rate, @@ -424,8 +424,7 @@ const _getQuotesWithMetadata = createSelector( ): (QuoteResponse & QuoteMetadata)[] => { const newQuotes = quotes.map((quote: QuoteResponse & SolanaFees) => { const isSolanaQuote = - formatChainIdToCaip(quote.quote.srcChainId) === - MultichainNetworks.SOLANA && quote.solanaFeesInLamports; + isSolanaChainId(quote.quote.srcChainId) && quote.solanaFeesInLamports; const toTokenAmount = calcToAmount( quote.quote, @@ -718,8 +717,7 @@ export const needsSolanaAccountForDestination = createDeepEqualSelector( return false; } - const isSolanaDestination = - formatChainIdToCaip(toChain.chainId) === MultichainNetworks.SOLANA; + const isSolanaDestination = isSolanaChainId(toChain.chainId); return isSolanaDestination && !hasSolanaAccount; }, @@ -733,10 +731,8 @@ export const getIsToOrFromSolana = createSelector( return false; } - const fromChainIsSolana = - formatChainIdToCaip(fromChain.chainId) === MultichainNetworks.SOLANA; - const toChainIsSolana = - formatChainIdToCaip(toChain.chainId) === MultichainNetworks.SOLANA; + const fromChainIsSolana = isSolanaChainId(fromChain.chainId); + const toChainIsSolana = isSolanaChainId(toChain.chainId); // Only return true if either chain is Solana and the other is EVM return toChainIsSolana !== fromChainIsSolana; diff --git a/ui/ducks/bridge/utils.ts b/ui/ducks/bridge/utils.ts index d3896431906d..dd4bd9595cd3 100644 --- a/ui/ducks/bridge/utils.ts +++ b/ui/ducks/bridge/utils.ts @@ -6,13 +6,13 @@ import { NetworkConfiguration, } from '@metamask/network-controller'; import { toChecksumAddress } from 'ethereumjs-util'; +import { isSolanaChainId } from '@metamask/bridge-controller'; import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils'; import { Numeric } from '../../../shared/modules/Numeric'; import { ChainId, type TxData } from '../../../shared/types/bridge'; import { getTransaction1559GasFeeEstimates } from '../../pages/swaps/swaps.util'; import { fetchTokenExchangeRates as fetchTokenExchangeRatesUtil } from '../../helpers/utils/util'; import { formatChainIdToHex } from '../../../shared/modules/bridge-utils/caip-formatters'; -import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; import { BRIDGE_CLIENT_ID } from '../../../shared/constants/bridge'; @@ -82,7 +82,7 @@ const fetchTokenExchangeRates = async ( ...tokenAddresses: string[] ) => { let exchangeRates; - if (chainId === MultichainNetworks.SOLANA) { + if (isSolanaChainId(chainId)) { const queryParams = new URLSearchParams({ assetIds: tokenAddresses.join(','), includeMarketData: 'true', @@ -137,7 +137,7 @@ export const getTokenExchangeRate = async (request: { currency, tokenAddress, ); - if (chainId === MultichainNetworks.SOLANA) { + if (isSolanaChainId(chainId)) { return exchangeRates?.[tokenAddress]; } // The exchange rate can be checksummed or not, so we need to check both diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index ff3dd4090780..085dc6b0dcb6 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -1,5 +1,6 @@ import { type Hex, type CaipChainId, isCaipChainId } from '@metamask/utils'; import { useMemo } from 'react'; +import { isSolanaChainId } from '@metamask/bridge-controller'; import { getSelectedInternalAccount } from '../../selectors'; import { calcLatestSrcBalance } from '../../../shared/modules/bridge-utils/balance'; import { useAsyncResult } from '../useAsyncResult'; @@ -10,7 +11,6 @@ import { getMultichainBalances, getMultichainCurrentChainId, } from '../../selectors/multichain'; -import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; /** * Custom hook to fetch and format the latest balance of a given token or native asset. @@ -57,11 +57,7 @@ const useLatestBalance = ( // No need to fetch the balance for non-EVM tokens, use the balance provided by the // multichain balances controller - if ( - isCaipChainId(chainId) && - chainId === MultichainNetworks.SOLANA && - token?.decimals - ) { + if (chainId && isSolanaChainId(chainId) && token?.decimals) { return Numeric.from( nonEvmBalances?.[token.address]?.amount ?? token?.string, 10, diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 854c7dfb093f..802ef1757a36 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { ChainId } from '@metamask/controller-utils'; import { type CaipChainId, isStrictHexString, type Hex } from '@metamask/utils'; import { zeroAddress } from 'ethereumjs-util'; +import { isSolanaChainId } from '@metamask/bridge-controller'; import { getAllDetectedTokensForSelectedAddress, selectERC20TokensByChain, @@ -31,7 +32,6 @@ import { isTokenV3Asset, } from '../../../shared/modules/bridge-utils/bridge.util'; import { MINUTE } from '../../../shared/constants/time'; -import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; import { type BridgeAppState, getTopAssetsFromFeatureFlags, @@ -77,7 +77,7 @@ export const useTokensWithFiltering = ( const { value: tokenList, pending: isTokenListLoading } = useAsyncResult< Record >(async () => { - if (chainId && chainId !== MultichainNetworks.SOLANA) { + if (chainId && !isSolanaChainId(chainId)) { const hexChainId = formatChainIdToHex(chainId); const timestamp = cachedTokens[hexChainId]?.timestamp; // Use cached token data if updated in the last 10 minutes @@ -87,8 +87,8 @@ export const useTokensWithFiltering = ( // Otherwise fetch new token data return await fetchBridgeTokens(hexChainId); } - if (chainId && formatChainIdToCaip(chainId) === MultichainNetworks.SOLANA) { - return await fetchNonEvmTokens(chainId); + if (chainId && isSolanaChainId(chainId)) { + return await fetchNonEvmTokens(formatChainIdToCaip(chainId)); } return {}; }, [chainId, cachedTokens]); @@ -219,7 +219,7 @@ export const useTokensWithFiltering = ( } // Yield tokens for solana from TokenApi V3 then return - if (chainId === MultichainNetworks.SOLANA) { + if (isSolanaChainId(chainId)) { // Yield topTokens from selected chain for (const { address: tokenAddress } of topTokens) { const assetId = `${chainId}/token:${tokenAddress}`; diff --git a/ui/pages/bridge/hooks/useDestinationAccount.ts b/ui/pages/bridge/hooks/useDestinationAccount.ts index 6175e4b024b6..6b12592309e5 100644 --- a/ui/pages/bridge/hooks/useDestinationAccount.ts +++ b/ui/pages/bridge/hooks/useDestinationAccount.ts @@ -1,6 +1,7 @@ import { useSelector } from 'react-redux'; import { useEffect, useState } from 'react'; import type { InternalAccount } from '@metamask/keyring-internal-api'; +import { isSolanaChainId } from '@metamask/bridge-controller'; import { getSelectedInternalAccount, getSelectedEvmInternalAccount, @@ -11,8 +12,6 @@ import { getMultichainIsEvm, } from '../../../selectors/multichain'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; -import { formatChainIdToCaip } from '../../../../shared/modules/bridge-utils/caip-formatters'; -import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; export const useDestinationAccount = (isSwap = false) => { const [selectedDestinationAccount, setSelectedDestinationAccount] = @@ -29,9 +28,7 @@ export const useDestinationAccount = (isSwap = false) => { : selectedMultichainAccount; const toChain = useSelector(getToChain); - const isDestinationSolana = - toChain && - formatChainIdToCaip(toChain.chainId) === MultichainNetworks.SOLANA; + const isDestinationSolana = toChain && isSolanaChainId(toChain.chainId); // Auto-select most recently used account when toChain or account changes useEffect(() => { diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts index a6a1b12bb625..1e45ba7a2d66 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -3,6 +3,7 @@ import { zeroAddress } from 'ethereumjs-util'; import { useHistory } from 'react-router-dom'; import { TransactionMeta } from '@metamask/transaction-controller'; import { createProjectLogger, Hex } from '@metamask/utils'; +import { isSolanaChainId } from '@metamask/bridge-controller'; import type { QuoteMetadata, QuoteResponse, @@ -33,8 +34,6 @@ import { } from '../../../../shared/types/bridge-status'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import { getMultichainIsEvm } from '../../../selectors/multichain'; -import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; -import { formatChainIdToCaip } from '../../../../shared/modules/bridge-utils/caip-formatters'; import useAddToken from './useAddToken'; import useHandleApprovalTx, { APPROVAL_TX_ERROR, @@ -238,8 +237,7 @@ export default function useSubmitBridgeTransaction() { } if ( quoteResponse.quote.destAsset.address !== zeroAddress() && - formatChainIdToCaip(quoteResponse.quote.destChainId) !== - MultichainNetworks.SOLANA + !isSolanaChainId(quoteResponse.quote.destChainId) ) { await addDestToken(quoteResponse); } diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index c6c5a6b09063..04385a5d0109 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -12,7 +12,10 @@ import { useHistory, useLocation } from 'react-router-dom'; import { BigNumber } from 'bignumber.js'; import { type TokenListMap } from '@metamask/assets-controllers'; import { toChecksumAddress, zeroAddress } from 'ethereumjs-util'; -import { isValidQuoteRequest } from '@metamask/bridge-controller'; +import { + isSolanaChainId, + isValidQuoteRequest, +} from '@metamask/bridge-controller'; import { setFromToken, setFromTokenInputValue, @@ -75,7 +78,6 @@ import { formatTokenAmount, isQuoteExpired as isQuoteExpiredUtil, } from '../utils/quote'; -import { getProviderConfig } from '../../../../shared/modules/selectors/networks'; import { CrossChainSwapsEventProperties, useCrossChainSwapsEventTracker, @@ -111,8 +113,6 @@ import { } from '../../../selectors/multichain'; import { MultichainBridgeQuoteCard } from '../quotes/multichain-bridge-quote-card'; import { BridgeQuoteCard } from '../quotes/bridge-quote-card'; -import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; -import { formatChainIdToCaip } from '../../../../shared/modules/bridge-utils/caip-formatters'; import { TokenFeatureType } from '../../../../shared/types/security-alerts-api'; import { useTokenAlerts } from '../../../hooks/bridge/useTokenAlerts'; import { useDestinationAccount } from '../hooks/useDestinationAccount'; @@ -144,7 +144,6 @@ const PrepareBridgePage = () => { const fromAmount = useSelector(getFromAmount); const fromAmountInCurrency = useSelector(getFromAmountInCurrency); - const providerConfig = useSelector(getProviderConfig); const slippage = useSelector(getSlippage); const quoteRequest = useSelector(getQuoteRequest); @@ -302,7 +301,7 @@ const PrepareBridgePage = () => { if (!toChain?.chainId) { return false; } - return formatChainIdToCaip(toChain.chainId) === MultichainNetworks.SOLANA; + return isSolanaChainId(toChain.chainId); }, [toChain?.chainId]); const quoteParams = useMemo( @@ -475,7 +474,7 @@ const PrepareBridgePage = () => { dispatch(setToToken(null)); } if ( - networkConfig.chainId === MultichainNetworks.SOLANA && + isSolanaChainId(networkConfig.chainId) && selectedSolanaAccount ) { dispatch(setSelectedAccount(selectedSolanaAccount.address)); @@ -573,8 +572,7 @@ const PrepareBridgePage = () => { : undefined; if ( toChain?.chainId && - formatChainIdToCaip(toChain.chainId) === - MultichainNetworks.SOLANA && + isSolanaChainId(toChain.chainId) && selectedSolanaAccount ) { // Switch accounts to switch to solana diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx index 7eb8602f63c9..e48b2183133a 100644 --- a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; +import { isSolanaChainId } from '@metamask/bridge-controller'; import { Text, PopoverPosition, @@ -67,7 +68,7 @@ export const MultichainBridgeQuoteCard = () => { const [showAllQuotes, setShowAllQuotes] = useState(false); const getNetworkImage = (chainId: ChainId) => { - if (chainId === 1151111081099710) { + if (isSolanaChainId(chainId)) { return MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA]; } return CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ @@ -78,7 +79,7 @@ export const MultichainBridgeQuoteCard = () => { }; const getNetworkName = (chainId: ChainId) => { - if (chainId === 1151111081099710) { + if (isSolanaChainId(chainId)) { return 'Solana'; } return NETWORK_TO_SHORT_NETWORK_NAME_MAP[ From 2f3c227903935441ce88bf77f65affb5704f24a9 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 12:55:55 -0700 Subject: [PATCH 03/71] refactor: rm BridgeController --- .eslintrc.js | 1 - .../bridge/bridge-controller.test.ts | 697 ------------------ .../controllers/bridge/bridge-controller.ts | 453 ------------ app/scripts/controllers/bridge/constants.ts | 40 - app/scripts/controllers/bridge/types.ts | 56 -- app/scripts/metamask-controller.js | 6 +- test/jest/mock-store.js | 2 +- 7 files changed, 5 insertions(+), 1250 deletions(-) delete mode 100644 app/scripts/controllers/bridge/bridge-controller.test.ts delete mode 100644 app/scripts/controllers/bridge/bridge-controller.ts delete mode 100644 app/scripts/controllers/bridge/constants.ts delete mode 100644 app/scripts/controllers/bridge/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index c8e449a10cbb..74e8093c6963 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -353,7 +353,6 @@ module.exports = { 'app/scripts/controllers/alert-controller.test.ts', 'app/scripts/metamask-controller.actions.test.js', 'app/scripts/detect-multiple-instances.test.js', - 'app/scripts/controllers/bridge.test.ts', 'app/scripts/controllers/swaps/**/*.test.js', 'app/scripts/controllers/swaps/**/*.test.ts', 'app/scripts/controllers/metametrics.test.js', diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts deleted file mode 100644 index 79e3c7e58f53..000000000000 --- a/app/scripts/controllers/bridge/bridge-controller.test.ts +++ /dev/null @@ -1,697 +0,0 @@ -import nock from 'nock'; -import { BigNumber } from 'bignumber.js'; -import { add0x } from '@metamask/utils'; -import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge'; -import { SWAPS_API_V2_BASE_URL } from '../../../../shared/constants/swaps'; -import { flushPromises } from '../../../../test/lib/timer-helpers'; -import * as bridgeUtil from '../../../../shared/modules/bridge-utils/bridge.util'; -import * as balanceUtils from '../../../../shared/modules/bridge-utils/balance'; -import mockBridgeQuotesErc20Native from '../../../../test/data/bridge/mock-quotes-erc20-native.json'; -import mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json'; -import mockBridgeQuotesNativeErc20Eth from '../../../../test/data/bridge/mock-quotes-native-erc20-eth.json'; -import { type QuoteResponse } from '../../../../shared/types/bridge'; -import { decimalToHex } from '../../../../shared/modules/conversion.utils'; -import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; -import BridgeController from './bridge-controller'; -import { BridgeControllerMessenger } from './types'; -import { DEFAULT_BRIDGE_STATE } from './constants'; - -const EMPTY_INIT_STATE = { - bridgeState: { ...DEFAULT_BRIDGE_STATE }, -}; - -const messengerMock = { - call: jest.fn(), - registerActionHandler: jest.fn(), - registerInitialEventPayload: jest.fn(), - publish: jest.fn(), -} as unknown as jest.Mocked; - -jest.mock('@ethersproject/contracts', () => { - return { - Contract: jest.fn(() => ({ - allowance: jest.fn(() => '100000000000000000000'), - })), - }; -}); - -jest.mock('@ethersproject/providers', () => { - return { - Web3Provider: jest.fn(), - }; -}); -const getLayer1GasFeeMock = jest.fn(); - -describe('BridgeController', function () { - let bridgeController: BridgeController; - - beforeAll(function () { - bridgeController = new BridgeController({ - messenger: messengerMock, - getLayer1GasFee: getLayer1GasFeeMock, - }); - }); - - beforeEach(() => { - jest.clearAllMocks(); - jest.clearAllTimers(); - - nock(BRIDGE_API_BASE_URL) - .get('/getAllFeatureFlags') - .reply(200, { - 'extension-config': { - refreshRate: 3, - maxRefreshCount: 3, - support: true, - chains: { - '10': { - isActiveSrc: true, - isActiveDest: false, - }, - '534352': { - isActiveSrc: true, - isActiveDest: false, - }, - '137': { - isActiveSrc: false, - isActiveDest: true, - }, - '42161': { - isActiveSrc: false, - isActiveDest: true, - }, - }, - }, - 'approval-gas-multiplier': { - '137': 1.1, - '42161': 1.2, - '10': 1.3, - '534352': 1.4, - }, - 'bridge-gas-multiplier': { - '137': 2.1, - '42161': 2.2, - '10': 2.3, - '534352': 2.4, - }, - }); - nock(BRIDGE_API_BASE_URL) - .get('/getTokens?chainId=10') - .reply(200, [ - { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - symbol: 'ABC', - decimals: 16, - aggregators: ['lifl', 'socket'], - }, - { - address: '0x1291478912', - symbol: 'DEF', - decimals: 16, - }, - ]); - nock(SWAPS_API_V2_BASE_URL) - .get('/networks/10/topAssets') - .reply(200, [ - { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - symbol: 'ABC', - }, - ]); - bridgeController.resetState(); - }); - - it('constructor should setup correctly', function () { - expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); - }); - - it('setBridgeFeatureFlags should fetch and set the bridge feature flags', async function () { - const expectedFeatureFlagsResponse = { - extensionConfig: { - maxRefreshCount: 3, - refreshRate: 3, - support: true, - chains: { - 'eip155:10': { isActiveSrc: true, isActiveDest: false }, - 'eip155:534352': { isActiveSrc: true, isActiveDest: false }, - 'eip155:137': { isActiveSrc: false, isActiveDest: true }, - 'eip155:42161': { isActiveSrc: false, isActiveDest: true }, - }, - }, - }; - expect(bridgeController.state).toStrictEqual(EMPTY_INIT_STATE); - - const setIntervalLengthSpy = jest.spyOn( - bridgeController, - 'setIntervalLength', - ); - - await bridgeController.setBridgeFeatureFlags(); - expect(bridgeController.state.bridgeState.bridgeFeatureFlags).toStrictEqual( - expectedFeatureFlagsResponse, - ); - expect(setIntervalLengthSpy).toHaveBeenCalledTimes(1); - expect(setIntervalLengthSpy).toHaveBeenCalledWith(3); - - bridgeController.resetState(); - expect(bridgeController.state.bridgeState).toStrictEqual( - expect.objectContaining({ - bridgeFeatureFlags: expectedFeatureFlagsResponse, - quotes: DEFAULT_BRIDGE_STATE.quotes, - quotesLastFetched: DEFAULT_BRIDGE_STATE.quotesLastFetched, - quotesLoadingStatus: DEFAULT_BRIDGE_STATE.quotesLoadingStatus, - }), - ); - }); - - it('updateBridgeQuoteRequestParams should update the quoteRequest state', function () { - bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); - expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ - srcChainId: 1, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - walletAddress: undefined, - }); - - bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); - expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ - destChainId: 10, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - walletAddress: undefined, - }); - - bridgeController.updateBridgeQuoteRequestParams({ destChainId: undefined }); - expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ - destChainId: undefined, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - walletAddress: undefined, - }); - - bridgeController.updateBridgeQuoteRequestParams({ - srcTokenAddress: undefined, - slippage: 0.5, - }); - expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ - slippage: 0.5, - srcTokenAddress: undefined, - walletAddress: undefined, - }); - - bridgeController.updateBridgeQuoteRequestParams({ - srcTokenAmount: '100000', - destTokenAddress: '0x123', - slippage: 0.5, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - walletAddress: undefined, - }); - expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ - srcTokenAmount: '100000', - destTokenAddress: '0x123', - slippage: 0.5, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - walletAddress: undefined, - }); - - bridgeController.updateBridgeQuoteRequestParams({ - srcTokenAddress: '0x2ABC', - }); - expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ - srcTokenAddress: '0x2ABC', - walletAddress: undefined, - }); - - bridgeController.resetState(); - expect(bridgeController.state.bridgeState.quoteRequest).toStrictEqual({ - srcTokenAddress: '0x0000000000000000000000000000000000000000', - walletAddress: undefined, - }); - }); - - it('updateBridgeQuoteRequestParams should trigger quote polling if request is valid', async function () { - jest.useFakeTimers(); - const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); - const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); - const hasSufficientBalanceSpy = jest - .spyOn(balanceUtils, 'hasSufficientBalance') - .mockResolvedValue(true); - messengerMock.call.mockReturnValue({ - address: '0x123', - provider: jest.fn(), - } as never); - - const fetchBridgeQuotesSpy = jest - .spyOn(bridgeUtil, 'fetchBridgeQuotes') - .mockImplementationOnce(async () => { - return await new Promise((resolve) => { - return setTimeout(() => { - resolve(mockBridgeQuotesNativeErc20Eth as never); - }, 5000); - }); - }); - - fetchBridgeQuotesSpy.mockImplementationOnce(async () => { - return await new Promise((resolve) => { - return setTimeout(() => { - resolve([ - ...mockBridgeQuotesNativeErc20Eth, - ...mockBridgeQuotesNativeErc20Eth, - ] as never); - }, 10000); - }); - }); - - fetchBridgeQuotesSpy.mockImplementationOnce(async () => { - return await new Promise((_, reject) => { - return setTimeout(() => { - reject(new Error('Network error')); - }, 10000); - }); - }); - - const quoteParams = { - srcChainId: '0x1', - destChainId: MultichainNetworks.SOLANA, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x123', - srcTokenAmount: '1000000000000000000', - walletAddress: '0x123', - slippage: 0, - }; - const quoteRequest = { - ...quoteParams, - slippage: 0, - }; - await bridgeController.updateBridgeQuoteRequestParams(quoteParams); - - expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); - expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); - expect(startPollingSpy).toHaveBeenCalledTimes(1); - expect(startPollingSpy).toHaveBeenCalledWith({ - networkClientId: expect.anything(), - updatedQuoteRequest: { - ...quoteRequest, - insufficientBal: false, - }, - }); - - expect(bridgeController.state.bridgeState).toStrictEqual( - expect.objectContaining({ - quoteRequest, - quotes: DEFAULT_BRIDGE_STATE.quotes, - quotesLastFetched: DEFAULT_BRIDGE_STATE.quotesLastFetched, - quotesLoadingStatus: DEFAULT_BRIDGE_STATE.quotesLoadingStatus, - }), - ); - - // Loading state - jest.advanceTimersByTime(1000); - await flushPromises(); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( - { - ...quoteRequest, - insufficientBal: false, - }, - expect.any(AbortSignal), - ); - expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( - undefined, - ); - - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: false }, - quotes: [], - quotesLoadingStatus: 0, - }), - ); - - // After first fetch - jest.advanceTimersByTime(10000); - await flushPromises(); - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: false }, - quotes: mockBridgeQuotesNativeErc20Eth, - quotesLoadingStatus: 1, - }), - ); - const firstFetchTime = - bridgeController.state.bridgeState.quotesLastFetched ?? 0; - expect(firstFetchTime).toBeGreaterThan(0); - - // After 2nd fetch - jest.advanceTimersByTime(50000); - await flushPromises(); - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: false }, - quotes: [ - ...mockBridgeQuotesNativeErc20Eth, - ...mockBridgeQuotesNativeErc20Eth, - ], - quotesLoadingStatus: 1, - quoteFetchError: undefined, - quotesRefreshCount: 2, - }), - ); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(2); - const secondFetchTime = - bridgeController.state.bridgeState.quotesLastFetched; - expect(secondFetchTime).toBeGreaterThan(firstFetchTime); - - // After 3nd fetch throws an error - jest.advanceTimersByTime(50000); - await flushPromises(); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(3); - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: false }, - quotes: [], - quotesLoadingStatus: 2, - quoteFetchError: 'Network error', - quotesRefreshCount: 3, - }), - ); - secondFetchTime && - expect( - bridgeController.state.bridgeState.quotesLastFetched, - ).toBeGreaterThan(secondFetchTime); - - expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); - expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); - }); - - it('updateBridgeQuoteRequestParams should only poll once if insufficientBal=true', async function () { - jest.useFakeTimers(); - const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); - const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); - const hasSufficientBalanceSpy = jest - .spyOn(balanceUtils, 'hasSufficientBalance') - .mockResolvedValue(false); - messengerMock.call.mockReturnValue({ - address: '0x123', - provider: jest.fn(), - } as never); - - const fetchBridgeQuotesSpy = jest - .spyOn(bridgeUtil, 'fetchBridgeQuotes') - .mockImplementationOnce(async () => { - return await new Promise((resolve) => { - return setTimeout(() => { - resolve(mockBridgeQuotesNativeErc20Eth as never); - }, 5000); - }); - }); - - fetchBridgeQuotesSpy.mockImplementation(async () => { - return await new Promise((resolve) => { - return setTimeout(() => { - resolve([ - ...mockBridgeQuotesNativeErc20Eth, - ...mockBridgeQuotesNativeErc20Eth, - ] as never); - }, 10000); - }); - }); - - const quoteParams = { - srcChainId: '0x1', - destChainId: '0x10', - srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x123', - srcTokenAmount: '1000000000000000000', - walletAddress: '0x123', - slippage: 0.5, - }; - const quoteRequest = { - ...quoteParams, - slippage: 0.5, - }; - await bridgeController.updateBridgeQuoteRequestParams(quoteParams); - - expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); - expect(startPollingSpy).toHaveBeenCalledTimes(1); - expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); - expect(startPollingSpy).toHaveBeenCalledWith({ - networkClientId: expect.anything(), - updatedQuoteRequest: { - ...quoteRequest, - insufficientBal: true, - }, - }); - - expect(bridgeController.state.bridgeState).toStrictEqual( - expect.objectContaining({ - quoteRequest, - quotes: DEFAULT_BRIDGE_STATE.quotes, - quotesLastFetched: DEFAULT_BRIDGE_STATE.quotesLastFetched, - quotesInitialLoadTime: undefined, - quotesLoadingStatus: DEFAULT_BRIDGE_STATE.quotesLoadingStatus, - }), - ); - - // Loading state - jest.advanceTimersByTime(1000); - await flushPromises(); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( - { - ...quoteRequest, - insufficientBal: true, - }, - expect.any(AbortSignal), - ); - expect(bridgeController.state.bridgeState.quotesLastFetched).toStrictEqual( - undefined, - ); - - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, - quotes: [], - quotesLoadingStatus: 0, - }), - ); - - // After first fetch - jest.advanceTimersByTime(10000); - await flushPromises(); - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, - quotes: mockBridgeQuotesNativeErc20Eth, - quotesLoadingStatus: 1, - quotesRefreshCount: 1, - quotesInitialLoadTime: 11000, - }), - ); - const firstFetchTime = - bridgeController.state.bridgeState.quotesLastFetched ?? 0; - expect(firstFetchTime).toBeGreaterThan(0); - - // After 2nd fetch - jest.advanceTimersByTime(50000); - await flushPromises(); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, - quotes: mockBridgeQuotesNativeErc20Eth, - quotesLoadingStatus: 1, - quotesRefreshCount: 1, - quotesInitialLoadTime: 11000, - }), - ); - const secondFetchTime = - bridgeController.state.bridgeState.quotesLastFetched; - expect(secondFetchTime).toStrictEqual(firstFetchTime); - expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); - }); - - it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', function () { - const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); - const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); - messengerMock.call.mockReturnValue({ - address: '0x123', - provider: jest.fn(), - } as never); - - bridgeController.updateBridgeQuoteRequestParams({ - srcChainId: 1, - destChainId: 10, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x123', - slippage: 0.5, - }); - - expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); - expect(startPollingSpy).not.toHaveBeenCalled(); - - expect(bridgeController.state.bridgeState).toStrictEqual( - expect.objectContaining({ - quoteRequest: { - srcChainId: 1, - slippage: 0.5, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - walletAddress: undefined, - destChainId: 10, - destTokenAddress: '0x123', - }, - quotes: DEFAULT_BRIDGE_STATE.quotes, - quotesLastFetched: DEFAULT_BRIDGE_STATE.quotesLastFetched, - quotesLoadingStatus: DEFAULT_BRIDGE_STATE.quotesLoadingStatus, - }), - ); - }); - - describe('getBridgeERC20Allowance', () => { - it('should return the atomic allowance of the ERC20 token contract', async () => { - messengerMock.call.mockReturnValue({ - address: '0x123', - provider: jest.fn(), - } as never); - - const allowance = await bridgeController.getBridgeERC20Allowance( - '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - '0xa', - ); - expect(allowance).toBe('100000000000000000000'); - }); - }); - - // @ts-expect-error This is missing from the Mocha type definitions - it.each([ - [ - 'should append l1GasFees if srcChain is 10 and srcToken is erc20', - mockBridgeQuotesErc20Native, - add0x(decimalToHex(new BigNumber('2608710388388').mul(2).toFixed())), - 12, - ], - [ - 'should append l1GasFees if srcChain is 10 and srcToken is native', - mockBridgeQuotesNativeErc20, - add0x(decimalToHex(new BigNumber('2608710388388').toFixed())), - 2, - ], - [ - 'should not append l1GasFees if srcChain is not 10', - mockBridgeQuotesNativeErc20Eth, - undefined, - 0, - ], - ])( - 'updateBridgeQuoteRequestParams: %s', - async ( - _: string, - quoteResponse: QuoteResponse[], - l1GasFeesInHexWei: string, - getLayer1GasFeeMockCallCount: number, - ) => { - jest.useFakeTimers(); - const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); - const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); - const hasSufficientBalanceSpy = jest - .spyOn(balanceUtils, 'hasSufficientBalance') - .mockResolvedValue(false); - messengerMock.call.mockReturnValue({ - address: '0x123', - provider: jest.fn(), - } as never); - getLayer1GasFeeMock.mockResolvedValue('0x25F63418AA4'); - - const fetchBridgeQuotesSpy = jest - .spyOn(bridgeUtil, 'fetchBridgeQuotes') - .mockImplementationOnce(async () => { - return await new Promise((resolve) => { - return setTimeout(() => { - resolve(quoteResponse as never); - }, 1000); - }); - }); - - const quoteParams = { - srcChainId: '0x10', - destChainId: '0x1', - srcTokenAddress: '0x4200000000000000000000000000000000000006', - destTokenAddress: '0x0000000000000000000000000000000000000000', - srcTokenAmount: '991250000000000000', - walletAddress: 'eip:id/id:id/0x123', - slippage: 0.5, - }; - const quoteRequest = { - ...quoteParams, - slippage: 0.5, - }; - await bridgeController.updateBridgeQuoteRequestParams(quoteParams); - - expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); - expect(startPollingSpy).toHaveBeenCalledTimes(1); - expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); - expect(startPollingSpy).toHaveBeenCalledWith({ - networkClientId: expect.anything(), - updatedQuoteRequest: { - ...quoteRequest, - insufficientBal: true, - }, - }); - - expect(bridgeController.state.bridgeState).toStrictEqual( - expect.objectContaining({ - quoteRequest, - quotes: DEFAULT_BRIDGE_STATE.quotes, - quotesLastFetched: DEFAULT_BRIDGE_STATE.quotesLastFetched, - quotesLoadingStatus: DEFAULT_BRIDGE_STATE.quotesLoadingStatus, - }), - ); - - // // Loading state - jest.advanceTimersByTime(500); - await flushPromises(); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); - expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith( - { - ...quoteRequest, - insufficientBal: true, - }, - expect.any(AbortSignal), - ); - expect( - bridgeController.state.bridgeState.quotesLastFetched, - ).toStrictEqual(undefined); - - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, - quotes: [], - quotesLoadingStatus: 0, - }), - ); - - // After first fetch - jest.advanceTimersByTime(1500); - await flushPromises(); - const { quotes } = bridgeController.state.bridgeState; - expect(bridgeController.state.bridgeState).toEqual( - expect.objectContaining({ - quoteRequest: { ...quoteRequest, insufficientBal: true }, - quotesLoadingStatus: 1, - quotesRefreshCount: 1, - }), - ); - quotes.forEach((quote) => { - const expectedQuote = l1GasFeesInHexWei - ? { ...quote, l1GasFeesInHexWei } - : quote; - expect(quote).toStrictEqual(expectedQuote); - }); - - const firstFetchTime = - bridgeController.state.bridgeState.quotesLastFetched ?? 0; - expect(firstFetchTime).toBeGreaterThan(0); - - expect(getLayer1GasFeeMock).toHaveBeenCalledTimes( - getLayer1GasFeeMockCallCount, - ); - }, - ); -}); diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts deleted file mode 100644 index 400706b497c0..000000000000 --- a/app/scripts/controllers/bridge/bridge-controller.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { add0x, type Hex } from '@metamask/utils'; -import { StaticIntervalPollingController } from '@metamask/polling-controller'; -import { NetworkClientId } from '@metamask/network-controller'; -import { StateMetadata } from '@metamask/base-controller'; -import { Contract } from '@ethersproject/contracts'; -import { abiERC20 } from '@metamask/metamask-eth-abis'; -import { Web3Provider } from '@ethersproject/providers'; -import { BigNumber } from '@ethersproject/bignumber'; -import { TransactionParams } from '@metamask/transaction-controller'; -import type { ChainId } from '@metamask/controller-utils'; -import { HandlerType } from '@metamask/snaps-utils'; -import type { SnapId } from '@metamask/snaps-sdk'; -import { - fetchBridgeFeatureFlags, - fetchBridgeQuotes, -} from '../../../../shared/modules/bridge-utils/bridge.util'; -import { - decimalToHex, - sumHexes, -} from '../../../../shared/modules/conversion.utils'; -import { - type L1GasFees, - type QuoteResponse, - type TxData, - type BridgeControllerState, - BridgeFeatureFlagsKey, - RequestStatus, - type GenericQuoteRequest, - type SolanaFees, -} from '../../../../shared/types/bridge'; -import { isValidQuoteRequest } from '../../../../shared/modules/bridge-utils/quote'; -import { hasSufficientBalance } from '../../../../shared/modules/bridge-utils/balance'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { REFRESH_INTERVAL_MS } from '../../../../shared/constants/bridge'; -import { - formatAddressToString, - formatChainIdToCaip, - formatChainIdToHex, -} from '../../../../shared/modules/bridge-utils/caip-formatters'; -import { MultichainNetworks } from '../../../../shared/constants/multichain/networks'; -import { - BRIDGE_CONTROLLER_NAME, - DEFAULT_BRIDGE_STATE, - METABRIDGE_CHAIN_TO_ADDRESS_MAP, -} from './constants'; -import type { BridgeControllerMessenger } from './types'; - -const metadata: StateMetadata = { - bridgeState: { - persist: false, - anonymous: false, - }, -}; - -const RESET_STATE_ABORT_MESSAGE = 'Reset controller state'; - -/** The input to start polling for the {@link BridgeController} */ -type BridgePollingInput = { - networkClientId: NetworkClientId; - updatedQuoteRequest: GenericQuoteRequest; -}; - -export default class BridgeController extends StaticIntervalPollingController()< - typeof BRIDGE_CONTROLLER_NAME, - BridgeControllerState, - BridgeControllerMessenger -> { - #abortController: AbortController | undefined; - - #quotesFirstFetched: number | undefined; - - #getLayer1GasFee: (params: { - transactionParams: TransactionParams; - chainId: ChainId; - }) => Promise; - - constructor({ - messenger, - getLayer1GasFee, - }: { - messenger: BridgeControllerMessenger; - getLayer1GasFee: (params: { - transactionParams: TransactionParams; - chainId: ChainId; - }) => Promise; - }) { - super({ - name: BRIDGE_CONTROLLER_NAME, - metadata, - messenger, - state: { - bridgeState: { ...DEFAULT_BRIDGE_STATE }, - }, - }); - - this.setIntervalLength(REFRESH_INTERVAL_MS); - - this.#abortController = new AbortController(); - // Register action handlers - this.messagingSystem.registerActionHandler( - `${BRIDGE_CONTROLLER_NAME}:setBridgeFeatureFlags`, - this.setBridgeFeatureFlags.bind(this), - ); - this.messagingSystem.registerActionHandler( - `${BRIDGE_CONTROLLER_NAME}:updateBridgeQuoteRequestParams`, - this.updateBridgeQuoteRequestParams.bind(this), - ); - this.messagingSystem.registerActionHandler( - `${BRIDGE_CONTROLLER_NAME}:resetState`, - this.resetState.bind(this), - ); - this.messagingSystem.registerActionHandler( - `${BRIDGE_CONTROLLER_NAME}:getBridgeERC20Allowance`, - this.getBridgeERC20Allowance.bind(this), - ); - - this.#getLayer1GasFee = getLayer1GasFee; - } - - _executePoll = async (pollingInput: BridgePollingInput) => { - await this.#fetchBridgeQuotes(pollingInput); - }; - - updateBridgeQuoteRequestParams = async ( - paramsToUpdate: Partial, - ) => { - this.stopAllPolling(); - this.#abortController?.abort('Quote request updated'); - - const { bridgeState } = this.state; - const updatedQuoteRequest = { - ...DEFAULT_BRIDGE_STATE.quoteRequest, - ...paramsToUpdate, - }; - - this.update((_state) => { - _state.bridgeState = { - ...bridgeState, - quoteRequest: updatedQuoteRequest, - quotes: DEFAULT_BRIDGE_STATE.quotes, - quotesLastFetched: DEFAULT_BRIDGE_STATE.quotesLastFetched, - quotesLoadingStatus: DEFAULT_BRIDGE_STATE.quotesLoadingStatus, - quoteFetchError: DEFAULT_BRIDGE_STATE.quoteFetchError, - quotesRefreshCount: DEFAULT_BRIDGE_STATE.quotesRefreshCount, - quotesInitialLoadTime: DEFAULT_BRIDGE_STATE.quotesInitialLoadTime, - }; - }); - - if (isValidQuoteRequest(updatedQuoteRequest)) { - this.#quotesFirstFetched = Date.now(); - const srcChainIdString = updatedQuoteRequest.srcChainId.toString(); - - // Query the balance of the source token if the source chain is an EVM chain - let insufficientBal: boolean | undefined; - if (srcChainIdString === MultichainNetworks.SOLANA) { - insufficientBal = paramsToUpdate.insufficientBal; - } else { - insufficientBal = - paramsToUpdate.insufficientBal || - !(await this.#hasSufficientBalance(updatedQuoteRequest)); - } - - // Set refresh rate based on the source chain before starting polling - this.#setIntervalLength(); - this.startPolling({ - networkClientId: srcChainIdString, - updatedQuoteRequest: { - ...updatedQuoteRequest, - insufficientBal, - }, - }); - } - }; - - #hasSufficientBalance = async (quoteRequest: GenericQuoteRequest) => { - const walletAddress = this.#getMultichainSelectedAccount()?.address; - const srcChainIdInHex = formatChainIdToHex(quoteRequest.srcChainId); - const provider = this.#getSelectedNetworkClient()?.provider; - const srcTokenAddressWithoutPrefix = formatAddressToString( - quoteRequest.srcTokenAddress, - ); - - return ( - provider && - walletAddress && - srcTokenAddressWithoutPrefix && - quoteRequest.srcTokenAmount && - srcChainIdInHex && - (await hasSufficientBalance( - provider, - walletAddress, - srcTokenAddressWithoutPrefix, - quoteRequest.srcTokenAmount, - srcChainIdInHex, - )) - ); - }; - - resetState = () => { - this.stopAllPolling(); - this.#abortController?.abort(RESET_STATE_ABORT_MESSAGE); - - this.update((_state) => { - _state.bridgeState = { - ...DEFAULT_BRIDGE_STATE, - quotes: [], - bridgeFeatureFlags: _state.bridgeState.bridgeFeatureFlags, - }; - }); - }; - - setBridgeFeatureFlags = async () => { - const { bridgeState } = this.state; - const bridgeFeatureFlags = await fetchBridgeFeatureFlags(); - this.update((_state) => { - _state.bridgeState = { ...bridgeState, bridgeFeatureFlags }; - }); - this.#setIntervalLength(); - }; - - /** - * Sets the interval length based on the source chain - */ - #setIntervalLength = () => { - const { bridgeState } = this.state; - const { srcChainId } = bridgeState.quoteRequest; - const refreshRateOverride = srcChainId - ? bridgeState.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG] - .chains[formatChainIdToCaip(srcChainId)]?.refreshRate - : undefined; - const defaultRefreshRate = - bridgeState.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG] - .refreshRate; - this.setIntervalLength(refreshRateOverride ?? defaultRefreshRate); - }; - - #fetchBridgeQuotes = async ({ - networkClientId: _networkClientId, - updatedQuoteRequest, - }: BridgePollingInput) => { - this.#abortController?.abort('New quote request'); - this.#abortController = new AbortController(); - const { bridgeState } = this.state; - this.update((_state) => { - _state.bridgeState = { - ...bridgeState, - quotesLoadingStatus: RequestStatus.LOADING, - quoteRequest: updatedQuoteRequest, - quoteFetchError: DEFAULT_BRIDGE_STATE.quoteFetchError, - }; - }); - - try { - const quotes = await fetchBridgeQuotes( - updatedQuoteRequest, - this.#abortController.signal, - ); - - const quotesWithL1GasFees = await this.#appendL1GasFees(quotes); - const quotesWithSolanaFees = await this.#appendSolanaFees(quotes); - - this.update((_state) => { - _state.bridgeState = { - ..._state.bridgeState, - quotes: quotesWithL1GasFees ?? quotesWithSolanaFees ?? quotes, - quotesLoadingStatus: RequestStatus.FETCHED, - }; - }); - } catch (error) { - const isAbortError = (error as Error).name === 'AbortError'; - const isAbortedDueToReset = error === RESET_STATE_ABORT_MESSAGE; - if (isAbortedDueToReset || isAbortError) { - return; - } - - this.update((_state) => { - _state.bridgeState = { - ...bridgeState, - quotes: DEFAULT_BRIDGE_STATE.quotes, - quoteFetchError: - error instanceof Error ? error.message : 'Unknown error', - quotesLoadingStatus: RequestStatus.ERROR, - }; - }); - console.log('Failed to fetch bridge quotes', error); - } finally { - const { maxRefreshCount } = - bridgeState.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG]; - - const updatedQuotesRefreshCount = bridgeState.quotesRefreshCount + 1; - // Stop polling if the maximum number of refreshes has been reached - if ( - updatedQuoteRequest.insufficientBal || - (!updatedQuoteRequest.insufficientBal && - updatedQuotesRefreshCount >= maxRefreshCount) - ) { - this.stopAllPolling(); - } - - // Update quote fetching stats - const quotesLastFetched = Date.now(); - this.update((_state) => { - _state.bridgeState = { - ..._state.bridgeState, - quotesInitialLoadTime: - updatedQuotesRefreshCount === 1 && this.#quotesFirstFetched - ? quotesLastFetched - this.#quotesFirstFetched - : bridgeState.quotesInitialLoadTime, - quotesLastFetched, - quotesRefreshCount: updatedQuotesRefreshCount, - }; - }); - } - }; - - #appendL1GasFees = async ( - quotes: QuoteResponse[], - ): Promise<(QuoteResponse & L1GasFees)[] | undefined> => { - // Return undefined if some of the quotes are not for optimism or base - if ( - quotes.some(({ quote }) => { - const chainId = formatChainIdToCaip(quote.srcChainId); - return ![CHAIN_IDS.OPTIMISM, CHAIN_IDS.BASE] - .map(formatChainIdToCaip) - .includes(chainId); - }) - ) { - return undefined; - } - - return await Promise.all( - quotes.map(async (quoteResponse) => { - const { quote, trade, approval } = quoteResponse; - const chainId = add0x(decimalToHex(quote.srcChainId)) as ChainId; - - const getTxParams = (txData: TxData) => ({ - from: txData.from, - to: txData.to, - value: txData.value, - data: txData.data, - gasLimit: txData.gasLimit?.toString(), - }); - const approvalL1GasFees = approval - ? await this.#getLayer1GasFee({ - transactionParams: getTxParams(approval), - chainId, - }) - : '0'; - const tradeL1GasFees = await this.#getLayer1GasFee({ - transactionParams: getTxParams(trade), - chainId, - }); - return { - ...quoteResponse, - l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees), - }; - - return quoteResponse; - }), - ); - }; - - #appendSolanaFees = async ( - quotes: QuoteResponse[], - ): Promise<(QuoteResponse & SolanaFees)[] | undefined> => { - // Return undefined if some of the quotes are not for solana - if ( - quotes.some(({ quote }) => { - return ( - formatChainIdToCaip(quote.srcChainId) !== MultichainNetworks.SOLANA - ); - }) - ) { - return undefined; - } - - return await Promise.all( - quotes.map(async (quoteResponse) => { - const { trade } = quoteResponse; - const selectedAccount = this.#getMultichainSelectedAccount(); - - if (selectedAccount?.metadata?.snap?.id && typeof trade === 'string') { - const { value: fees } = (await this.messagingSystem.call( - 'SnapController:handleRequest', - { - snapId: selectedAccount.metadata.snap.id as SnapId, - origin: 'metamask', - handler: HandlerType.OnRpcRequest, - request: { - method: 'getFeeForTransaction', - params: { - transaction: trade, - scope: selectedAccount.options.scope, - }, - }, - }, - )) as { value: string }; - - return { - ...quoteResponse, - solanaFeesInLamports: fees, - }; - } - return quoteResponse; - }), - ); - }; - - #getMultichainSelectedAccount() { - return this.messagingSystem.call( - 'AccountsController:getSelectedMultichainAccount', - ); // ?? this.messagingSystem.call('AccountsController:getSelectedAccount') - } - - #getSelectedNetworkClient() { - return this.messagingSystem.call( - 'NetworkController:getSelectedNetworkClient', - ); - } - - #getSelectedNetworkClientId(chainId: Hex) { - return this.messagingSystem.call( - 'NetworkController:findNetworkClientIdByChainId', - chainId, - ); - } - - /** - * - * @param contractAddress - The address of the ERC20 token contract - * @param chainId - The hex chain ID of the bridge network - * @returns The atomic allowance of the ERC20 token contract - */ - getBridgeERC20Allowance = async ( - contractAddress: string, - chainId: Hex, - ): Promise => { - const provider = this.#getSelectedNetworkClient()?.provider; - if (!provider) { - throw new Error('No provider found'); - } - - const web3Provider = new Web3Provider(provider); - const contract = new Contract(contractAddress, abiERC20, web3Provider); - const { address: walletAddress } = - this.#getMultichainSelectedAccount() ?? {}; - const allowance = await contract.allowance( - walletAddress, - METABRIDGE_CHAIN_TO_ADDRESS_MAP[chainId], - ); - return BigNumber.from(allowance).toString(); - }; -} diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts deleted file mode 100644 index 53fa3df55422..000000000000 --- a/app/scripts/controllers/bridge/constants.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { zeroAddress } from 'ethereumjs-util'; -import type { Hex } from '@metamask/utils'; -import { - DEFAULT_MAX_REFRESH_COUNT, - METABRIDGE_ETHEREUM_ADDRESS, - REFRESH_INTERVAL_MS, -} from '../../../../shared/constants/bridge'; -import { CHAIN_IDS } from '../../../../shared/constants/network'; -import { BridgeFeatureFlagsKey } from '../../../../shared/types/bridge'; -import type { BridgeState } from '../../../../shared/types/bridge'; - -export const BRIDGE_CONTROLLER_NAME = 'BridgeController'; -export const DEFAULT_BRIDGE_STATE: BridgeState = { - bridgeFeatureFlags: { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - refreshRate: REFRESH_INTERVAL_MS, - maxRefreshCount: DEFAULT_MAX_REFRESH_COUNT, - support: false, - chains: {}, - }, - }, - quoteRequest: { - walletAddress: undefined, - srcTokenAddress: zeroAddress() as `0x${string}`, - }, - quotesInitialLoadTime: undefined, - quotes: [], - quotesLastFetched: undefined, - quotesLoadingStatus: undefined, - quoteFetchError: undefined, - quotesRefreshCount: 0, -}; - -export const DEFAULT_BRIDGE_CONTROLLER_STATE = { - bridgeState: { ...DEFAULT_BRIDGE_STATE }, -}; - -export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { - [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, -}; diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts deleted file mode 100644 index 958f98f9073c..000000000000 --- a/app/scripts/controllers/bridge/types.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - ControllerStateChangeEvent, - RestrictedMessenger, -} from '@metamask/base-controller'; -import { - // AccountsControllerGetSelectedAccountAction, - AccountsControllerGetSelectedMultichainAccountAction, -} from '@metamask/accounts-controller'; -import { - NetworkControllerFindNetworkClientIdByChainIdAction, - NetworkControllerGetSelectedNetworkClientAction, -} from '@metamask/network-controller'; -import { HandleSnapRequest } from '@metamask/snaps-controllers'; -import type { - BridgeBackgroundAction, - BridgeControllerState, - BridgeUserAction, -} from '../../../../shared/types/bridge'; -import BridgeController from './bridge-controller'; -import { BRIDGE_CONTROLLER_NAME } from './constants'; - -type BridgeControllerAction = { - type: `${typeof BRIDGE_CONTROLLER_NAME}:${FunctionName}`; - handler: BridgeController[FunctionName]; -}; - -// Maps to BridgeController function names -type BridgeControllerActions = - | BridgeControllerAction - | BridgeControllerAction - | BridgeControllerAction - | BridgeControllerAction; - -type BridgeControllerEvents = ControllerStateChangeEvent< - typeof BRIDGE_CONTROLLER_NAME, - BridgeControllerState ->; - -type AllowedActions = - // | AccountsControllerGetSelectedAccountAction - | AccountsControllerGetSelectedMultichainAccountAction - | HandleSnapRequest - | NetworkControllerGetSelectedNetworkClientAction - | NetworkControllerFindNetworkClientIdByChainIdAction; -type AllowedEvents = never; - -/** - * The messenger for the BridgeController. - */ -export type BridgeControllerMessenger = RestrictedMessenger< - typeof BRIDGE_CONTROLLER_NAME, - BridgeControllerActions | AllowedActions, - BridgeControllerEvents | AllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] ->; diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index bb1292b83cc4..8aeb7b86ae40 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -124,7 +124,10 @@ import { } from '@metamask/queued-request-controller'; import { UserOperationController } from '@metamask/user-operation-controller'; -import { BridgeController } from '@metamask/bridge-controller'; +import { + BridgeController, + BRIDGE_CONTROLLER_NAME, +} from '@metamask/bridge-controller'; import { TransactionStatus, @@ -354,7 +357,6 @@ import createEvmMethodsToNonEvmAccountReqFilterMiddleware from './lib/createEvmM import { isEthAddress } from './lib/multichain/address'; import { decodeTransactionData } from './lib/transaction/decode/util'; -import { BRIDGE_CONTROLLER_NAME } from './controllers/bridge/constants'; import { onPushNotificationClicked, onPushNotificationReceived, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index c005655350aa..a31801cfb458 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -1,9 +1,9 @@ import { EthAccountType, EthScope } from '@metamask/keyring-api'; +import { DEFAULT_BRIDGE_STATE } from '@metamask/bridge-controller'; import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../shared/constants/network'; import { KeyringType } from '../../shared/constants/keyring'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { mockNetworkState } from '../stub/networks'; -import { DEFAULT_BRIDGE_STATE } from '../../app/scripts/controllers/bridge/constants'; import { DEFAULT_BRIDGE_STATUS_STATE } from '../../app/scripts/controllers/bridge-status/constants'; import { BRIDGE_PREFERRED_GAS_ESTIMATE } from '../../shared/constants/bridge'; import { mockTokenData } from '../data/bridge/mock-token-data'; From 2229022c2d848065594168da9b2ed79a35e52f4d Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 14:13:57 -0700 Subject: [PATCH 04/71] refactor: import bridge types, constants and utils from core --- .../controllers/bridge-status/utils.ts | 5 +- app/scripts/lib/bridge-status/metrics.ts | 7 +- app/scripts/metamask-controller.js | 16 +- shared/constants/bridge.ts | 19 +- shared/lib/bridge-status/metrics.ts | 6 +- shared/lib/bridge/metrics.ts | 5 +- shared/modules/bridge-utils/balance.test.ts | 137 ---------- shared/modules/bridge-utils/balance.ts | 53 ---- shared/modules/bridge-utils/bridge.util.ts | 239 +----------------- .../modules/bridge-utils/caip-formatters.ts | 106 -------- shared/modules/bridge-utils/quote.ts | 50 ---- shared/modules/bridge-utils/validators.ts | 74 +----- shared/types/bridge.ts | 236 ----------------- test/e2e/tests/bridge/bridge-test-utils.ts | 10 +- test/e2e/tests/bridge/constants.ts | 2 +- .../onboarding/wallet-created.test.tsx | 2 +- test/jest/mock-store.js | 8 +- ui/ducks/bridge/actions.ts | 2 +- ui/ducks/bridge/bridge.test.ts | 12 +- ui/ducks/bridge/bridge.ts | 6 +- ui/ducks/bridge/selectors.test.ts | 12 +- ui/ducks/bridge/selectors.ts | 40 ++- ui/ducks/bridge/utils.ts | 13 +- .../events/useRequestMetadataProperties.ts | 2 +- .../bridge/events/useRequestProperties.ts | 2 +- ui/hooks/bridge/useBridgeChainInfo.ts | 2 +- ui/hooks/bridge/useBridging.ts | 3 +- .../bridge/useCrossChainSwapsEventTracker.ts | 2 +- ui/hooks/bridge/useLatestBalance.ts | 6 +- .../bridge/useTokensWithFiltering.test.ts | 2 +- ui/hooks/bridge/useTokensWithFiltering.ts | 16 +- ui/pages/bridge/hooks/useAddToken.ts | 2 +- ui/pages/bridge/hooks/useHandleApprovalTx.ts | 4 +- ui/pages/bridge/hooks/useHandleBridgeTx.ts | 2 +- ui/pages/bridge/hooks/useHandleTx.ts | 2 +- .../hooks/useSubmitBridgeTransaction.ts | 5 +- .../bridge/prepare/bridge-cta-button.test.tsx | 6 +- .../bridge/prepare/bridge-input-group.tsx | 2 +- .../bridge-transaction-settings-modal.tsx | 2 +- .../prepare/prepare-bridge-page.stories.tsx | 2 +- .../bridge/prepare/prepare-bridge-page.tsx | 8 +- .../bridge/quotes/bridge-quote-card.test.tsx | 6 +- ui/pages/bridge/quotes/bridge-quote-card.tsx | 6 +- .../quotes/bridge-quotes-modal.stories.tsx | 2 +- .../quotes/bridge-quotes-modal.test.tsx | 2 +- .../bridge/quotes/bridge-quotes-modal.tsx | 10 +- .../quotes/multichain-bridge-quote-card.tsx | 12 +- ui/pages/bridge/utils/quote.ts | 16 +- ui/selectors/selectors.js | 2 +- 49 files changed, 150 insertions(+), 1036 deletions(-) delete mode 100644 shared/modules/bridge-utils/balance.test.ts delete mode 100644 shared/modules/bridge-utils/balance.ts delete mode 100644 shared/modules/bridge-utils/caip-formatters.ts delete mode 100644 shared/modules/bridge-utils/quote.ts delete mode 100644 shared/types/bridge.ts diff --git a/app/scripts/controllers/bridge-status/utils.ts b/app/scripts/controllers/bridge-status/utils.ts index 52de5b737ab8..c1fad0c43d2e 100644 --- a/app/scripts/controllers/bridge-status/utils.ts +++ b/app/scripts/controllers/bridge-status/utils.ts @@ -1,15 +1,14 @@ -import { BRIDGE_CLIENT_ID } from '../../../../shared/constants/bridge'; +import { BridgeClientId, type Quote } from '@metamask/bridge-controller'; import fetchWithCache from '../../../../shared/lib/fetch-with-cache'; import { StatusResponse, StatusRequestWithSrcTxHash, StatusRequestDto, } from '../../../../shared/types/bridge-status'; -import type { Quote } from '../../../../shared/types/bridge'; import { validateResponse, validators } from './validators'; import { BRIDGE_STATUS_BASE_URL } from './constants'; -const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; +const CLIENT_ID_HEADER = { 'X-Client-Id': BridgeClientId.EXTENSION }; export const getStatusRequestDto = ( statusRequest: StatusRequestWithSrcTxHash, diff --git a/app/scripts/lib/bridge-status/metrics.ts b/app/scripts/lib/bridge-status/metrics.ts index d94af0d2d7ab..1dad61d9ba24 100644 --- a/app/scripts/lib/bridge-status/metrics.ts +++ b/app/scripts/lib/bridge-status/metrics.ts @@ -1,7 +1,6 @@ /* eslint-disable camelcase */ import { TransactionControllerTransactionFailedEvent } from '@metamask/transaction-controller'; -// eslint-disable-next-line import/no-restricted-paths -import { ActionType } from '../../../../ui/hooks/bridge/events/types'; +import { formatChainIdToHex, isEthUsdt } from '@metamask/bridge-controller'; // eslint-disable-next-line import/no-restricted-paths import { BridgeStatusControllerBridgeTransactionCompleteEvent, @@ -22,9 +21,9 @@ import { StatusTypes, MetricsBackgroundState, } from '../../../../shared/types/bridge-status'; -import { isEthUsdt } from '../../../../shared/modules/bridge-utils/bridge.util'; import { getCommonProperties } from '../../../../shared/lib/bridge-status/metrics'; -import { formatChainIdToHex } from '../../../../shared/modules/bridge-utils/caip-formatters'; +// eslint-disable-next-line import/no-restricted-paths +import { type ActionType } from '../../../../ui/hooks/bridge/events/types'; import { getTokenUsdValue } from './metrics-utils'; type TrackEvent = ( diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8aeb7b86ae40..1265046106d2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -127,6 +127,9 @@ import { UserOperationController } from '@metamask/user-operation-controller'; import { BridgeController, BRIDGE_CONTROLLER_NAME, + BridgeUserAction, + BridgeBackgroundAction, + BridgeClientId, } from '@metamask/bridge-controller'; import { @@ -247,17 +250,10 @@ import { } from '../../shared/lib/transactions-controller-utils'; import { getProviderConfig } from '../../shared/modules/selectors/networks'; import { endTrace, trace } from '../../shared/lib/trace'; -import { BridgeStatusAction } from '../../shared/types/bridge-status'; import { ENVIRONMENT } from '../../development/build/constants'; import fetchWithCache from '../../shared/lib/fetch-with-cache'; -import { - BridgeUserAction, - BridgeBackgroundAction, -} from '../../shared/types/bridge'; -import { - BRIDGE_API_BASE_URL, - BRIDGE_CLIENT_ID, -} from '../../shared/constants/bridge'; +import { BRIDGE_API_BASE_URL } from '../../shared/constants/bridge'; +import { BridgeStatusAction } from '../../shared/types/bridge-status'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) handleMMITransactionUpdate, @@ -1787,7 +1783,7 @@ export default class MetamaskController extends EventEmitter { }); this.bridgeController = new BridgeController({ messenger: bridgeControllerMessenger, - clientId: BRIDGE_CLIENT_ID, + clientId: BridgeClientId.EXTENSION, // TODO: Remove once TransactionController exports this action type getLayer1GasFee: (...args) => this.txController.getLayer1GasFee(...args), fetchFn: async (url, { headers, ...requestOptions }) => diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index 5943c1fa0863..b4c96030cc4e 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -1,3 +1,7 @@ +import { + BRIDGE_DEV_API_BASE_URL, + BRIDGE_PROD_API_BASE_URL, +} from '@metamask/bridge-controller'; import { MultichainNetworks } from './multichain/networks'; import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './network'; @@ -19,23 +23,11 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; -export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; -export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS ? BRIDGE_DEV_API_BASE_URL : BRIDGE_PROD_API_BASE_URL; -export const BRIDGE_CLIENT_ID = 'extension'; - export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; -export const METABRIDGE_ETHEREUM_ADDRESS = - '0x0439e60F02a8900a951603950d8D4527f400C3f1'; -export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour -export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.35; // if a quote returns in x times less return than the best quote, ignore it - -export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high'; -export const BRIDGE_DEFAULT_SLIPPAGE = 0.5; - export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< AllowedBridgeChainIds, string @@ -59,9 +51,6 @@ export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< [MultichainNetworks.BITCOIN_TESTNET]: 'Bitcoin Testnet', ///: END:ONLY_INCLUDE_IF }; -export const BRIDGE_MM_FEE_RATE = 0.875; -export const REFRESH_INTERVAL_MS = 30 * 1000; -export const DEFAULT_MAX_REFRESH_COUNT = 5; export const STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io'; diff --git a/shared/lib/bridge-status/metrics.ts b/shared/lib/bridge-status/metrics.ts index fd23b91a09f0..61bfdac906b1 100644 --- a/shared/lib/bridge-status/metrics.ts +++ b/shared/lib/bridge-status/metrics.ts @@ -1,4 +1,8 @@ /* eslint-disable import/no-restricted-paths, camelcase */ +import { + formatChainIdToCaip, + BRIDGE_DEFAULT_SLIPPAGE, +} from '@metamask/bridge-controller'; import { getHexGasTotalUsd } from '../../../app/scripts/lib/bridge-status/metrics-utils'; import { MetricsBackgroundState, @@ -8,9 +12,7 @@ import { isHardwareKeyring } from '../../../ui/helpers/utils/hardware'; import { ActionType } from '../../../ui/hooks/bridge/events/types'; import { formatProviderLabel } from '../../../ui/pages/bridge/utils/quote'; import { getCurrentKeyring } from '../../../ui/selectors'; -import { BRIDGE_DEFAULT_SLIPPAGE } from '../../constants/bridge'; import { getIsSmartTransaction } from '../../modules/selectors'; -import { formatChainIdToCaip } from '../../modules/bridge-utils/caip-formatters'; export const getCommonProperties = ( bridgeHistoryItem: BridgeHistoryItem, diff --git a/shared/lib/bridge/metrics.ts b/shared/lib/bridge/metrics.ts index 908e9226cd0e..97f54d48d86c 100644 --- a/shared/lib/bridge/metrics.ts +++ b/shared/lib/bridge/metrics.ts @@ -1,5 +1,8 @@ import { BigNumber } from 'bignumber.js'; -import { QuoteMetadata, QuoteResponse } from '../../types/bridge'; +import { + type QuoteMetadata, + type QuoteResponse, +} from '@metamask/bridge-controller'; export const getConvertedUsdAmounts = ({ activeQuote, diff --git a/shared/modules/bridge-utils/balance.test.ts b/shared/modules/bridge-utils/balance.test.ts deleted file mode 100644 index 03bd0c9500f1..000000000000 --- a/shared/modules/bridge-utils/balance.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { BigNumber } from 'ethers'; -import { zeroAddress } from 'ethereumjs-util'; -import { createTestProviderTools } from '../../../test/stub/provider'; -import { CHAIN_IDS } from '../../constants/network'; -import { Numeric } from '../Numeric'; -import * as tokenutil from '../../lib/token-util'; -import { calcLatestSrcBalance, hasSufficientBalance } from './balance'; - -const mockGetBalance = jest.fn(); -jest.mock('@ethersproject/providers', () => { - return { - Web3Provider: jest.fn().mockImplementation(() => { - return { - getBalance: mockGetBalance, - }; - }), - }; -}); - -describe('balance', () => { - beforeEach(() => { - jest.clearAllMocks(); - const { provider } = createTestProviderTools({ - networkId: 'Ethereum', - chainId: CHAIN_IDS.MAINNET, - }); - - global.ethereumProvider = provider; - }); - - describe('calcLatestSrcBalance', () => { - it('should return the ERC20 token balance', async () => { - const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance'); - mockFetchTokenBalance.mockResolvedValueOnce(BigNumber.from('100')); - - expect( - await calcLatestSrcBalance( - global.ethereumProvider, - '0x123', - '0x456', - '0x789', - ), - ).toStrictEqual(Numeric.from(100, 10)); - expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1); - expect(mockFetchTokenBalance).toHaveBeenCalledWith( - '0x456', - '0x123', - global.ethereumProvider, - ); - }); - - it('should return the native asset balance', async () => { - mockGetBalance.mockImplementation(() => { - return BigNumber.from(100); - }); - - expect( - await calcLatestSrcBalance( - global.ethereumProvider, - '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', - zeroAddress(), - '0x789', - ), - ).toStrictEqual(Numeric.from(100, 10)); - expect(mockGetBalance).toHaveBeenCalledTimes(1); - expect(mockGetBalance).toHaveBeenCalledWith( - '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', - ); - }); - - it('should return undefined if token address and chainId are undefined', async () => { - const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance'); - expect( - await calcLatestSrcBalance( - global.ethereumProvider, - '0x141d32a89a1e0a5Ef360034a2f60a4B917c18838', - undefined as never, - undefined as never, - ), - ).toStrictEqual(undefined); - expect(mockFetchTokenBalance).not.toHaveBeenCalled(); - expect(mockGetBalance).not.toHaveBeenCalled(); - }); - }); - - describe('hasSufficientBalance', () => { - it('should return true if user has sufficient balance', async () => { - mockGetBalance.mockImplementation(() => { - return BigNumber.from('10000000000000000000'); - }); - const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance'); - mockFetchTokenBalance.mockResolvedValueOnce( - BigNumber.from('10000000000000000001'), - ); - - expect( - await hasSufficientBalance( - global.ethereumProvider, - '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', - zeroAddress(), - '10000000000000000000', - '0x1', - ), - ).toBe(true); - - expect( - await hasSufficientBalance( - global.ethereumProvider, - '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', - '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', - '10000000000000000000', - '0x1', - ), - ).toBe(true); - }); - - it('should return false if user has native assets but insufficient ERC20 src tokens', async () => { - mockGetBalance.mockImplementation(() => { - return BigNumber.from('10000000000000000000'); - }); - const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance'); - mockFetchTokenBalance.mockResolvedValueOnce( - BigNumber.from('9000000000000000000'), - ); - - expect( - await hasSufficientBalance( - global.ethereumProvider, - '0x141d32a89a1e0a5ef360034a2f60a4b917c18838', - '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1', - '10000000000000000000', - '0x1', - ), - ).toBe(false); - }); - }); -}); diff --git a/shared/modules/bridge-utils/balance.ts b/shared/modules/bridge-utils/balance.ts deleted file mode 100644 index b768f0ff2b84..000000000000 --- a/shared/modules/bridge-utils/balance.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Web3Provider } from '@ethersproject/providers'; -import type { Provider } from '@metamask/network-controller'; -import type { Hex } from '@metamask/utils'; -import { zeroAddress } from 'ethereumjs-util'; -import { getAddress } from 'ethers/lib/utils'; -import { fetchTokenBalance } from '../../lib/token-util'; -import { Numeric } from '../Numeric'; - -export const calcLatestSrcBalance = async ( - provider: Provider, - selectedAddress: string, - tokenAddress: string, - chainId: Hex, -): Promise => { - if (tokenAddress && chainId) { - if (tokenAddress === zeroAddress()) { - const ethersProvider = new Web3Provider(provider); - return Numeric.from( - ( - await ethersProvider.getBalance(getAddress(selectedAddress)) - ).toString(), - 10, - ); - } - return Numeric.from( - ( - await fetchTokenBalance(tokenAddress, selectedAddress, provider) - ).toString(), - 10, - ); - } - return undefined; -}; - -export const hasSufficientBalance = async ( - provider: Provider, - selectedAddress: string, - tokenAddress: string, - fromTokenAmount: string, - chainId: Hex, -) => { - const srcTokenBalance = await calcLatestSrcBalance( - provider, - selectedAddress, - tokenAddress, - chainId, - ); - - return ( - srcTokenBalance?.greaterThanOrEqualTo(Numeric.from(fromTokenAmount, 10)) ?? - false - ); -}; diff --git a/shared/modules/bridge-utils/bridge.util.ts b/shared/modules/bridge-utils/bridge.util.ts index 112fdc407a28..c9f941da5a10 100644 --- a/shared/modules/bridge-utils/bridge.util.ts +++ b/shared/modules/bridge-utils/bridge.util.ts @@ -1,105 +1,19 @@ -import { Contract } from '@ethersproject/contracts'; -import { CaipChainId, type Hex } from '@metamask/utils'; -import { abiERC20 } from '@metamask/metamask-eth-abis'; -import { - BRIDGE_API_BASE_URL, - BRIDGE_CLIENT_ID, - ETH_USDT_ADDRESS, - METABRIDGE_ETHEREUM_ADDRESS, - REFRESH_INTERVAL_MS, - STATIC_METAMASK_BASE_URL, -} from '../../constants/bridge'; -import { MINUTE } from '../../constants/time'; +import { CaipChainId } from '@metamask/utils'; +import { BridgeClientId } from '@metamask/bridge-controller'; +import { STATIC_METAMASK_BASE_URL } from '../../constants/bridge'; import fetchWithCache from '../../lib/fetch-with-cache'; -import { hexToDecimal } from '../conversion.utils'; -import { - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, - SwapsTokenObject, - TOKEN_API_BASE_URL, -} from '../../constants/swaps'; -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from '../swaps.utils'; -import { CHAIN_IDS } from '../../constants/network'; -import { - type BridgeAsset, - BridgeFlag, - type FeatureFlagResponse, - type FeeData, - type Quote, - type QuoteResponse, - type TxData, - BridgeFeatureFlagsKey, - type BridgeFeatureFlags, - type GenericQuoteRequest, - type TokenV3Asset, - FeeType, -} from '../../types/bridge'; -///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) -import { MultichainNetworks } from '../../constants/multichain/networks'; -///: END:ONLY_INCLUDE_IF +import { TOKEN_API_BASE_URL } from '../../constants/swaps'; -import { - formatAddressToString, - formatChainIdToDec, - formatChainIdToCaip, -} from './caip-formatters'; -import { - FEATURE_FLAG_VALIDATORS, - QUOTE_VALIDATORS, - TX_DATA_VALIDATORS, - TOKEN_VALIDATORS, - validateResponse, - QUOTE_RESPONSE_VALIDATORS, - FEE_DATA_VALIDATORS, - ASSET_VALIDATORS, -} from './validators'; +import { validateResponse, ASSET_VALIDATORS } from './validators'; -const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID }; -const CACHE_REFRESH_TEN_MINUTES = 10 * MINUTE; +const CLIENT_ID_HEADER = { 'X-Client-Id': BridgeClientId.EXTENSION }; -export async function fetchBridgeFeatureFlags(): Promise { - const url = `${BRIDGE_API_BASE_URL}/getAllFeatureFlags`; - const rawFeatureFlags = await fetchWithCache({ - url, - fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, - cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, - functionName: 'fetchBridgeFeatureFlags', - }); - - if ( - validateResponse( - FEATURE_FLAG_VALIDATORS, - rawFeatureFlags, - url, - ) - ) { - return { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - ...rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG], - chains: Object.entries( - rawFeatureFlags[BridgeFlag.EXTENSION_CONFIG].chains, - ).reduce( - (acc, [chainId, value]) => ({ - ...acc, - [formatChainIdToCaip(chainId)]: value, - }), - {}, - ), - }, - }; - } - - return { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - refreshRate: REFRESH_INTERVAL_MS, - maxRefreshCount: 5, - support: false, - chains: {}, - }, - }; -} +type TokenV3Asset = { + assetId: string; + symbol: string; + name: string; + decimals: number; +}; // Returns a list of non-EVM assets export async function fetchNonEvmTokens( @@ -132,132 +46,3 @@ export const getAssetImageUrl = (assetId: string) => ':', '/', )}.png`; - -// Returns a list of enabled (unblocked) tokens -export async function fetchBridgeTokens( - chainId: Hex, -): Promise> { - // TODO make token api v2 call - const url = `${BRIDGE_API_BASE_URL}/getTokens?chainId=${hexToDecimal( - chainId, - )}`; - const tokens = await fetchWithCache({ - url, - fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, - cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, - functionName: 'fetchBridgeTokens', - }); - - const nativeToken = - SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ]; - - const transformedTokens: Record = {}; - if (nativeToken) { - transformedTokens[nativeToken.address] = nativeToken; - } - - tokens.forEach((token: unknown) => { - if ( - validateResponse(TOKEN_VALIDATORS, token, url, false) && - !( - isSwapsDefaultTokenSymbol(token.symbol, chainId) || - isSwapsDefaultTokenAddress(token.address, chainId) - ) - ) { - transformedTokens[token.address] = token; - } - }); - return transformedTokens; -} - -// Returns a list of bridge tx quotes -// Converts the quote request to the format the bridge-api expects prior to fetching quotes -export async function fetchBridgeQuotes( - request: GenericQuoteRequest, - signal: AbortSignal, -): Promise { - // Ignore slippage for solana swaps - let ignoreSlippage = false; - - ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) - ignoreSlippage = - request.srcChainId === request.destChainId && - request.destChainId === MultichainNetworks.SOLANA; - ///: END:ONLY_INCLUDE_IF - - const normalizedRequest = { - walletAddress: formatAddressToString(request.walletAddress), - destWalletAddress: formatAddressToString( - request.destWalletAddress ?? request.walletAddress, - ), - srcChainId: formatChainIdToDec(request.srcChainId).toString(), - destChainId: formatChainIdToDec(request.destChainId).toString(), - srcTokenAddress: formatAddressToString(request.srcTokenAddress), - destTokenAddress: formatAddressToString(request.destTokenAddress), - srcTokenAmount: request.srcTokenAmount, - ...(ignoreSlippage ? {} : { slippage: request.slippage?.toString() }), - insufficientBal: request.insufficientBal ? 'true' : 'false', - resetApproval: request.resetApproval ? 'true' : 'false', - }; - if (request.slippage !== undefined) { - normalizedRequest.slippage = request.slippage.toString(); - } - const queryParams = new URLSearchParams(normalizedRequest); - const url = `${BRIDGE_API_BASE_URL}/getQuote?${queryParams}`; - const quotes = await fetchWithCache({ - url, - fetchOptions: { - method: 'GET', - headers: CLIENT_ID_HEADER, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - const filteredQuotes = quotes.filter((quoteResponse: QuoteResponse) => { - const { quote, approval, trade } = quoteResponse; - return ( - validateResponse( - QUOTE_RESPONSE_VALIDATORS, - quoteResponse, - url, - ) && - validateResponse(QUOTE_VALIDATORS, quote, url) && - validateResponse(TOKEN_VALIDATORS, quote.srcAsset, url) && - validateResponse(TOKEN_VALIDATORS, quote.destAsset, url) && - (typeof trade === 'string' || - validateResponse(TX_DATA_VALIDATORS, trade, url)) && - validateResponse( - FEE_DATA_VALIDATORS, - quote.feeData[FeeType.METABRIDGE], - url, - ) && - (approval - ? validateResponse(TX_DATA_VALIDATORS, approval, url) - : true) - ); - }); - return filteredQuotes; -} -/** - * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum - * - * @returns The txParam data that will reset allowance to 0, combine it with the approval tx params received from Bridge API - */ -export const getEthUsdtResetData = () => { - const UsdtContractInterface = new Contract(ETH_USDT_ADDRESS, abiERC20) - .interface; - const data = UsdtContractInterface.encodeFunctionData('approve', [ - METABRIDGE_ETHEREUM_ADDRESS, - '0', - ]); - - return data; -}; - -export const isEthUsdt = (chainId: Hex, address: string) => - chainId === CHAIN_IDS.MAINNET && - address.toLowerCase() === ETH_USDT_ADDRESS.toLowerCase(); diff --git a/shared/modules/bridge-utils/caip-formatters.ts b/shared/modules/bridge-utils/caip-formatters.ts deleted file mode 100644 index 56c8a045bce7..000000000000 --- a/shared/modules/bridge-utils/caip-formatters.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - type Hex, - type CaipChainId, - isCaipChainId, - isStrictHexString, - parseCaipChainId, - isCaipReference, -} from '@metamask/utils'; -import { zeroAddress, toChecksumAddress } from 'ethereumjs-util'; -import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; -import { MultichainNetworks } from '../../constants/multichain/networks'; -import { ChainId } from '../../types/bridge'; -import { decimalToPrefixedHex, hexToDecimal } from '../conversion.utils'; -import { MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19 } from '../../constants/multichain/assets'; - -// Returns true if the address looka like a native asset -export const isNativeAddress = (address?: string | null) => - address === zeroAddress() || - address === '' || - !address || - Object.values(MULTICHAIN_NATIVE_CURRENCY_TO_CAIP19).some((assetId) => - assetId.includes(address), - ); - -// Converts a chainId to a CaipChainId -export const formatChainIdToCaip = ( - chainId: Hex | number | CaipChainId | string, -): CaipChainId => { - if (isCaipChainId(chainId)) { - return chainId; - } else if (isStrictHexString(chainId)) { - return toEvmCaipChainId(chainId); - } - const chainIdString = chainId.toString(); - if (chainIdString === '1151111081099710') { - return MultichainNetworks.SOLANA; - } - return toEvmCaipChainId(decimalToPrefixedHex(chainIdString)); -}; - -// Converts a chainId to a decimal number that can be used for bridge-api requests -export const formatChainIdToDec = ( - chainId: number | Hex | CaipChainId | string, -) => { - if (isStrictHexString(chainId)) { - return Number(hexToDecimal(chainId)); - } - if (chainId === MultichainNetworks.SOLANA) { - return ChainId.SOLANA; - } - if (isCaipChainId(chainId)) { - return Number(chainId.split(':').at(-1)); - } - if (typeof chainId === 'string') { - return parseInt(chainId, 10); - } - return chainId; -}; - -// Converts a chainId to a hex string used to read controller data within the app -// Hex chainIds are also used for fetching exchange rates -export const formatChainIdToHex = ( - chainId: Hex | CaipChainId | string | number, -) => { - if (isStrictHexString(chainId)) { - return chainId; - } - if (typeof chainId === 'number' || parseInt(chainId, 10)) { - return decimalToPrefixedHex(chainId.toString()); - } - if (isCaipChainId(chainId)) { - const { reference } = parseCaipChainId(chainId); - if (isCaipReference(reference) && !isNaN(Number(reference))) { - return decimalToPrefixedHex(reference); - } - } - // Throw an error if a non-evm chainId is passed to this function - // This should never happen, but it's a sanity check - throw new Error(`Invalid cross-chain swaps chainId: ${chainId}`); -}; - -// Converts an asset or account address to a string that can be used for bridge-api requests -export const formatAddressToString = (address: string) => { - if (isStrictHexString(address)) { - return toChecksumAddress(address); - } - // If the address looks like a native token, return the zero address because it's - // what bridge-api uses to represent a native asset - if (isNativeAddress(address)) { - return zeroAddress(); - } - const addressWithoutPrefix = address.split(':').at(-1); - // If the address is not a valid hex string or CAIP address, throw an error - // This should never happen, but it's a sanity check - if (!addressWithoutPrefix) { - throw new Error('Invalid address'); - } - return addressWithoutPrefix; -}; - -export const formatChainIdToHexOrCaip = (chainId: number) => { - if (chainId === ChainId.SOLANA) { - return MultichainNetworks.SOLANA; - } - return formatChainIdToHex(chainId); -}; diff --git a/shared/modules/bridge-utils/quote.ts b/shared/modules/bridge-utils/quote.ts deleted file mode 100644 index f2dc102d49a2..000000000000 --- a/shared/modules/bridge-utils/quote.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { - BridgeControllerState, - GenericQuoteRequest, -} from '../../types/bridge'; - -export const isValidQuoteRequest = ( - partialRequest: Partial, - requireAmount = true, -): partialRequest is GenericQuoteRequest => { - const STRING_FIELDS = [ - 'srcTokenAddress', - 'destTokenAddress', - 'srcChainId', - 'destChainId', - 'walletAddress', - ]; - if (requireAmount) { - STRING_FIELDS.push('srcTokenAmount'); - } - const NUMBER_FIELDS = []; - - // if slippage is defined, require it to be a number - if (partialRequest.slippage !== undefined) { - NUMBER_FIELDS.push('slippage'); - } - - return ( - STRING_FIELDS.every( - (field) => - field in partialRequest && - typeof partialRequest[field as keyof typeof partialRequest] === - 'string' && - partialRequest[field as keyof typeof partialRequest] !== undefined && - partialRequest[field as keyof typeof partialRequest] !== '' && - partialRequest[field as keyof typeof partialRequest] !== null, - ) && - NUMBER_FIELDS.every( - (field) => - field in partialRequest && - typeof partialRequest[field as keyof typeof partialRequest] === - 'number' && - partialRequest[field as keyof typeof partialRequest] !== undefined && - !isNaN(Number(partialRequest[field as keyof typeof partialRequest])) && - partialRequest[field as keyof typeof partialRequest] !== null, - ) && - (requireAmount - ? Boolean((partialRequest.srcTokenAmount ?? '').match(/^[1-9]\d*$/u)) - : true) - ); -}; diff --git a/shared/modules/bridge-utils/validators.ts b/shared/modules/bridge-utils/validators.ts index 4f81e47c7c55..cdcdf7f3c437 100644 --- a/shared/modules/bridge-utils/validators.ts +++ b/shared/modules/bridge-utils/validators.ts @@ -1,7 +1,4 @@ -import { isStrictHexString } from '@metamask/utils'; -import { isValidHexAddress as isValidHexAddress_ } from '@metamask/controller-utils'; -import { truthyDigitString, validateData } from '../../lib/swaps-utils'; -import { BridgeFlag, FeatureFlagResponse } from '../../types/bridge'; +import { validateData } from '../../lib/swaps-utils'; type Validator = { property: keyof ExpectedResponse | string; @@ -23,33 +20,6 @@ const isValidObject = (v: unknown): v is object => typeof v === 'object' && v !== null; const isValidString = (v: unknown): v is string => typeof v === 'string' && v.length > 0; -const isValidHexAddress = (v: unknown) => - isValidString(v) && isValidHexAddress_(v, { allowNonPrefixed: false }); - -export const FEATURE_FLAG_VALIDATORS = [ - { - property: BridgeFlag.EXTENSION_CONFIG, - type: 'object', - validator: ( - v: unknown, - ): v is Pick => - isValidObject(v) && - 'refreshRate' in v && - isValidNumber(v.refreshRate) && - 'maxRefreshCount' in v && - isValidNumber(v.maxRefreshCount) && - 'chains' in v && - isValidObject(v.chains) && - Object.values(v.chains).every((chain) => isValidObject(chain)) && - Object.values(v.chains).every( - (chain) => - 'isActiveSrc' in chain && - 'isActiveDest' in chain && - typeof chain.isActiveSrc === 'boolean' && - typeof chain.isActiveDest === 'boolean', - ), - }, -]; export const TOKEN_AGGREGATOR_VALIDATORS = [ { @@ -79,45 +49,3 @@ export const TOKEN_VALIDATORS = [ validator: (v: unknown) => isValidString(v) && v.length <= 12, }, ]; - -export const QUOTE_RESPONSE_VALIDATORS = [ - { property: 'quote', type: 'object', validator: isValidObject }, - { property: 'estimatedProcessingTimeInSeconds', type: 'number' }, - { - property: 'approval', - type: 'object|undefined', - validator: (v: unknown) => v === undefined || isValidObject(v), - }, - { - property: 'trade', - type: 'string|object', - validator: (v: unknown) => isValidObject(v) || isValidString(v), - }, -]; - -export const QUOTE_VALIDATORS = [ - { property: 'requestId', type: 'string' }, - { property: 'srcTokenAmount', type: 'string' }, - { property: 'destTokenAmount', type: 'string' }, - { property: 'bridgeId', type: 'string' }, - { property: 'bridges', type: 'object', validator: isValidObject }, - { property: 'srcChainId', type: 'number' }, - { property: 'destChainId', type: 'number' }, - { property: 'srcAsset', type: 'object', validator: isValidObject }, - { property: 'destAsset', type: 'object', validator: isValidObject }, - { property: 'feeData', type: 'object', validator: isValidObject }, -]; - -export const FEE_DATA_VALIDATORS = [ - { property: 'amount', type: 'string', validator: truthyDigitString }, - { property: 'asset', type: 'object', validator: isValidObject }, -]; - -export const TX_DATA_VALIDATORS = [ - { property: 'chainId', type: 'number' }, - { property: 'value', type: 'string', validator: isStrictHexString }, - { property: 'gasLimit', type: 'number' }, - { property: 'to', type: 'string', validator: isValidHexAddress }, - { property: 'from', type: 'string', validator: isValidHexAddress }, - { property: 'data', type: 'string', validator: isStrictHexString }, -]; diff --git a/shared/types/bridge.ts b/shared/types/bridge.ts deleted file mode 100644 index 338da1147a28..000000000000 --- a/shared/types/bridge.ts +++ /dev/null @@ -1,236 +0,0 @@ -import type { - CaipAccountId, - CaipAssetId, - CaipChainId, - Hex, -} from '@metamask/utils'; -import type { BigNumber } from 'bignumber.js'; - -export type ChainConfiguration = { - isActiveSrc: boolean; - isActiveDest: boolean; - refreshRate?: number; - topAssets?: string[]; -}; - -export type L1GasFees = { - l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller -}; - -export type SolanaFees = { - solanaFeesInLamports?: string; // solana fees in lamports, appended by controller -}; - -// Values derived from the quote response -// valueInCurrency values are calculated based on the user's selected currency -export type TokenAmountValues = { - amount: BigNumber; - valueInCurrency: BigNumber | null; - usd: BigNumber | null; -}; -export type QuoteMetadata = { - gasFee: TokenAmountValues; - totalNetworkFee: TokenAmountValues; // estimatedGasFees + relayerFees - totalMaxNetworkFee: TokenAmountValues; // maxGasFees + relayerFees - toTokenAmount: TokenAmountValues; // destTokenAmount - adjustedReturn: Omit; // destTokenAmount - totalNetworkFee - sentAmount: TokenAmountValues; // srcTokenAmount + metabridgeFee - swapRate: BigNumber; // destTokenAmount / sentAmount - cost: Omit; // sentAmount - adjustedReturn -}; -// Sort order set by the user - -export enum SortOrder { - COST_ASC = 'cost_ascending', - ETA_ASC = 'time_descending', -} - -export type BridgeToken = { - address: string; - symbol: string; - image: string; - decimals: number; - chainId: number | Hex | ChainId | CaipChainId; - balance: string; // raw balance - string: string | undefined; // normalized balance as a stringified number - tokenFiatAmount?: number | null; -}; -// Types copied from Metabridge API - -export enum BridgeFlag { - EXTENSION_CONFIG = 'extension-config', -} -type DecimalChainId = string; -export type GasMultiplierByChainId = Record; - -export type FeatureFlagResponse = { - [BridgeFlag.EXTENSION_CONFIG]: { - refreshRate: number; - maxRefreshCount: number; - support: boolean; - chains: Record; - }; -}; - -export type BridgeAsset = { - chainId: ChainId; - address: string; - symbol: string; - name: string; - decimals: number; - icon?: string; -}; - -// Generic types for the quote request -// Only the controller and reducer should be overriding these types to prepare the fetch request -export type QuoteRequest< - ChainIdType = ChainId | number, - TokenAddressType = string, - WalletAddressType = string, -> = { - walletAddress: WalletAddressType; - destWalletAddress?: WalletAddressType; - srcChainId: ChainIdType; - destChainId: ChainIdType; - srcTokenAddress: TokenAddressType; - destTokenAddress: TokenAddressType; - srcTokenAmount: string; // This is the amount sent - slippage?: number; - aggIds?: string[]; - bridgeIds?: string[]; - insufficientBal?: boolean; - resetApproval?: boolean; - refuel?: boolean; -}; - -type Protocol = { - name: string; - displayName?: string; - icon?: string; -}; -enum ActionTypes { - BRIDGE = 'bridge', - SWAP = 'swap', - REFUEL = 'refuel', -} -type Step = { - action: ActionTypes; - srcChainId: ChainId; - destChainId?: ChainId; - srcAsset?: BridgeAsset; - destAsset?: BridgeAsset; - srcAmount: string; - destAmount: string; - protocol: Protocol; -}; -type RefuelData = Step; - -export type Quote = { - requestId: string; - srcChainId: ChainId; - srcAsset: BridgeAsset; - // Some tokens have a fee of 0, so sometimes it's equal to amount sent - srcTokenAmount: string; // Atomic amount, the amount sent - fees - destChainId: ChainId; - destAsset: BridgeAsset; - destTokenAmount: string; // Atomic amount, the amount received - feeData: Record & - Partial>; - bridgeId: string; - bridges: string[]; - steps: Step[]; - refuel?: RefuelData; -}; - -export type QuoteResponse = { - quote: Quote; - approval: TxData | null; - trade: TxData; - estimatedProcessingTimeInSeconds: number; -}; - -export enum ChainId { - ETH = 1, - OPTIMISM = 10, - BSC = 56, - POLYGON = 137, - ZKSYNC = 324, - BASE = 8453, - ARBITRUM = 42161, - AVALANCHE = 43114, - LINEA = 59144, - SOLANA = 1151111081099710, -} - -export enum FeeType { - METABRIDGE = 'metabridge', - REFUEL = 'refuel', -} -export type FeeData = { - amount: string; - asset: BridgeAsset; -}; -export type TxData = { - chainId: ChainId; - to: string; - from: string; - value: string; - data: string; - gasLimit: number | null; -}; -export enum BridgeFeatureFlagsKey { - EXTENSION_CONFIG = 'extensionConfig', -} - -export type BridgeFeatureFlags = { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: { - refreshRate: number; - maxRefreshCount: number; - support: boolean; - chains: Record; - }; -}; -export enum RequestStatus { - LOADING, - FETCHED, - ERROR, -} -export enum BridgeUserAction { - SELECT_DEST_NETWORK = 'selectDestNetwork', - UPDATE_QUOTE_PARAMS = 'updateBridgeQuoteRequestParams', -} -export enum BridgeBackgroundAction { - SET_FEATURE_FLAGS = 'setBridgeFeatureFlags', - RESET_STATE = 'resetState', - GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', -} - -// These are types that components pass in. Since data is a mix of types when coming from the redux store, we need to use a generic type that can cover all the types. -// This is formatted by fetchBridgeQuotes right before fetching quotes to whatever type the bridge-api is expecting. -export type GenericQuoteRequest = QuoteRequest< - Hex | CaipChainId | string | number, // chainIds - Hex | CaipAssetId | string, // assetIds/addresses - Hex | CaipAccountId | string // accountIds/addresses ->; - -export type BridgeState = { - bridgeFeatureFlags: BridgeFeatureFlags; - quoteRequest: Partial; - quotes: (QuoteResponse & L1GasFees & SolanaFees)[]; - quotesInitialLoadTime?: number; - quotesLastFetched?: number; - quotesLoadingStatus?: RequestStatus; - quoteFetchError?: string; - quotesRefreshCount: number; -}; - -export type BridgeControllerState = { - bridgeState: BridgeState; -}; - -export type TokenV3Asset = { - assetId: string; - symbol: string; - name: string; - decimals: number; -}; diff --git a/test/e2e/tests/bridge/bridge-test-utils.ts b/test/e2e/tests/bridge/bridge-test-utils.ts index 594ccf22f539..ffbc13dd5dfe 100644 --- a/test/e2e/tests/bridge/bridge-test-utils.ts +++ b/test/e2e/tests/bridge/bridge-test-utils.ts @@ -1,14 +1,14 @@ import { Mockttp } from 'mockttp'; -import FixtureBuilder from '../../fixture-builder'; +import type { FeatureFlagResponse } from '@metamask/bridge-controller'; import { - BRIDGE_CLIENT_ID, BRIDGE_DEV_API_BASE_URL, BRIDGE_PROD_API_BASE_URL, -} from '../../../../shared/constants/bridge'; + BridgeClientId, +} from '@metamask/bridge-controller'; +import FixtureBuilder from '../../fixture-builder'; import { SMART_CONTRACTS } from '../../seeder/smart-contracts'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { Driver } from '../../webdriver/driver'; -import type { FeatureFlagResponse } from '../../../../shared/types/bridge'; import { emptyHtmlPage } from '../../mock-e2e'; import { DEFAULT_FEATURE_FLAGS_RESPONSE, @@ -86,7 +86,7 @@ const mockServer = async (url) => await mockServer_ .forGet(url) - .withHeaders({ 'X-Client-Id': BRIDGE_CLIENT_ID }) + .withHeaders({ 'X-Client-Id': BridgeClientId.EXTENSION }) .always() .thenCallback(() => { return { diff --git a/test/e2e/tests/bridge/constants.ts b/test/e2e/tests/bridge/constants.ts index 844cec673509..13ef91a47953 100644 --- a/test/e2e/tests/bridge/constants.ts +++ b/test/e2e/tests/bridge/constants.ts @@ -1,4 +1,4 @@ -import type { FeatureFlagResponse } from '../../../../shared/types/bridge'; +import type { FeatureFlagResponse } from '@metamask/bridge-controller'; export const DEFAULT_FEATURE_FLAGS_RESPONSE: FeatureFlagResponse = { 'extension-config': { diff --git a/test/integration/onboarding/wallet-created.test.tsx b/test/integration/onboarding/wallet-created.test.tsx index d4d326fbd528..41ae244f425e 100644 --- a/test/integration/onboarding/wallet-created.test.tsx +++ b/test/integration/onboarding/wallet-created.test.tsx @@ -1,5 +1,6 @@ import { waitFor } from '@testing-library/react'; import nock from 'nock'; +import { BridgeBackgroundAction } from '@metamask/bridge-controller'; import mockMetaMaskState from '../data/onboarding-completion-route.json'; import { integrationTestRender } from '../../lib/render-helpers'; import * as backgroundConnection from '../../../ui/store/background-connection'; @@ -13,7 +14,6 @@ import { waitForElementById, waitForElementByText, } from '../helpers'; -import { BridgeBackgroundAction } from '../../../shared/types/bridge'; jest.mock('../../../ui/store/background-connection', () => ({ ...jest.requireActual('../../../ui/store/background-connection'), diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index a31801cfb458..717110b8e144 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -1,13 +1,15 @@ import { EthAccountType, EthScope } from '@metamask/keyring-api'; -import { DEFAULT_BRIDGE_STATE } from '@metamask/bridge-controller'; +import { + DEFAULT_BRIDGE_STATE, + BRIDGE_PREFERRED_GAS_ESTIMATE, + formatChainIdToCaip, +} from '@metamask/bridge-controller'; import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../shared/constants/network'; import { KeyringType } from '../../shared/constants/keyring'; import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods'; import { mockNetworkState } from '../stub/networks'; import { DEFAULT_BRIDGE_STATUS_STATE } from '../../app/scripts/controllers/bridge-status/constants'; -import { BRIDGE_PREFERRED_GAS_ESTIMATE } from '../../shared/constants/bridge'; import { mockTokenData } from '../data/bridge/mock-token-data'; -import { formatChainIdToCaip } from '../../shared/modules/bridge-utils/caip-formatters'; export const createGetSmartTransactionFeesApiResponse = () => { return { diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts index ee3cc1c6b7f6..a8026e8827be 100644 --- a/ui/ducks/bridge/actions.ts +++ b/ui/ducks/bridge/actions.ts @@ -3,7 +3,7 @@ import { BridgeBackgroundAction, BridgeUserAction, type GenericQuoteRequest, -} from '../../../shared/types/bridge'; +} from '@metamask/bridge-controller'; import { forceUpdateMetamaskState } from '../../store/actions'; import { submitRequestToBackground } from '../../store/background-connection'; import type { MetaMaskReduxDispatch } from '../../store/store'; diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts index c9005fbaa703..d65d1e1b844d 100644 --- a/ui/ducks/bridge/bridge.test.ts +++ b/ui/ducks/bridge/bridge.test.ts @@ -1,16 +1,16 @@ import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { zeroAddress } from 'ethereumjs-util'; -import { createBridgeMockStore } from '../../../test/jest/mock-store'; -import { CHAIN_IDS } from '../../../shared/constants/network'; -import { setBackgroundConnection } from '../../store/background-connection'; import { BridgeBackgroundAction, BridgeUserAction, -} from '../../../shared/types/bridge'; + BRIDGE_DEFAULT_SLIPPAGE, + formatChainIdToCaip, +} from '@metamask/bridge-controller'; +import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { CHAIN_IDS } from '../../../shared/constants/network'; +import { setBackgroundConnection } from '../../store/background-connection'; import * as util from '../../helpers/utils/util'; -import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../shared/constants/bridge'; -import { formatChainIdToCaip } from '../../../shared/modules/bridge-utils/caip-formatters'; import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; import bridgeReducer from './bridge'; import { diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 7816c9978db5..c1415bee47b4 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -7,9 +7,9 @@ import { type QuoteMetadata, type QuoteResponse, SortOrder, -} from '../../../shared/types/bridge'; -import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../shared/constants/bridge'; -import { formatChainIdToCaip } from '../../../shared/modules/bridge-utils/caip-formatters'; + BRIDGE_DEFAULT_SLIPPAGE, + formatChainIdToCaip, +} from '@metamask/bridge-controller'; import { getTokenExchangeRate } from './utils'; export type BridgeState = { diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index acb83330e6c5..72ced7de9f13 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -1,17 +1,17 @@ import { BigNumber } from 'bignumber.js'; import { zeroAddress } from 'ethereumjs-util'; +import { + type QuoteMetadata, + type QuoteResponse, + SortOrder, + formatChainIdToCaip, +} from '@metamask/bridge-controller'; import { createBridgeMockStore } from '../../../test/jest/mock-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'; import mockErc20Erc20Quotes from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json'; -import { - type QuoteMetadata, - type QuoteResponse, - SortOrder, -} from '../../../shared/types/bridge'; -import { formatChainIdToCaip } from '../../../shared/modules/bridge-utils/caip-formatters'; import { getAllBridgeableNetworks, getBridgeQuotes, diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 8ed4bbd19b17..7fae13b0d11c 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -12,7 +12,22 @@ import type { GasFeeEstimates } from '@metamask/gas-fee-controller'; import { BigNumber } from 'bignumber.js'; import { calcTokenAmount } from '@metamask/notification-services-controller/push-services'; import { CaipChainId, Hex } from '@metamask/utils'; -import { isSolanaChainId } from '@metamask/bridge-controller'; +import { + isSolanaChainId, + type L1GasFees, + type BridgeToken, + type QuoteMetadata, + type QuoteResponse, + SortOrder, + BridgeFeatureFlagsKey, + RequestStatus, + type BridgeControllerState, + type SolanaFees, + isNativeAddress, + formatChainIdToCaip, + BRIDGE_PREFERRED_GAS_ESTIMATE, + BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, +} from '@metamask/bridge-controller'; import { MultichainNetworks, ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) @@ -26,28 +41,11 @@ import { getUSDConversionRateByChainId, selectConversionRateByChainId, } from '../../selectors/selectors'; -import { - ALLOWED_BRIDGE_CHAIN_IDS, - BRIDGE_PREFERRED_GAS_ESTIMATE, - BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, -} from '../../../shared/constants/bridge'; -import type { - BridgeControllerState, - SolanaFees, -} from '../../../shared/types/bridge'; +import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps'; import { getNetworkConfigurationsByChainId } from '../../../shared/modules/selectors/networks'; import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask'; -import { - type L1GasFees, - type BridgeToken, - type QuoteMetadata, - type QuoteResponse, - SortOrder, - BridgeFeatureFlagsKey, - RequestStatus, -} from '../../../shared/types/bridge'; import { calcAdjustedReturn, calcCost, @@ -58,10 +56,6 @@ import { calcEstimatedAndMaxTotalGasFee, calcSolanaTotalNetworkFee, } from '../../pages/bridge/utils/quote'; -import { - isNativeAddress, - formatChainIdToCaip, -} from '../../../shared/modules/bridge-utils/caip-formatters'; import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils'; import { CHAIN_ID_TOKEN_IMAGE_MAP, diff --git a/ui/ducks/bridge/utils.ts b/ui/ducks/bridge/utils.ts index dd4bd9595cd3..8c28d5ed7797 100644 --- a/ui/ducks/bridge/utils.ts +++ b/ui/ducks/bridge/utils.ts @@ -6,15 +6,18 @@ import { NetworkConfiguration, } from '@metamask/network-controller'; import { toChecksumAddress } from 'ethereumjs-util'; -import { isSolanaChainId } from '@metamask/bridge-controller'; +import { + isSolanaChainId, + ChainId, + type TxData, + formatChainIdToHex, + BridgeClientId, +} from '@metamask/bridge-controller'; import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils'; import { Numeric } from '../../../shared/modules/Numeric'; -import { ChainId, type TxData } from '../../../shared/types/bridge'; import { getTransaction1559GasFeeEstimates } from '../../pages/swaps/swaps.util'; import { fetchTokenExchangeRates as fetchTokenExchangeRatesUtil } from '../../helpers/utils/util'; -import { formatChainIdToHex } from '../../../shared/modules/bridge-utils/caip-formatters'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; -import { BRIDGE_CLIENT_ID } from '../../../shared/constants/bridge'; type GasFeeEstimate = { suggestedMaxPriorityFeePerGas: string; @@ -93,7 +96,7 @@ const fetchTokenExchangeRates = async ( url, fetchOptions: { method: 'GET', - headers: { 'X-Client-Id': BRIDGE_CLIENT_ID }, + headers: { 'X-Client-Id': BridgeClientId.EXTENSION }, }, cacheOptions: { cacheRefreshTime: 0 }, functionName: 'fetchSolanaTokenExchangeRates', diff --git a/ui/hooks/bridge/events/useRequestMetadataProperties.ts b/ui/hooks/bridge/events/useRequestMetadataProperties.ts index d0effe026803..7def75a37fbd 100644 --- a/ui/hooks/bridge/events/useRequestMetadataProperties.ts +++ b/ui/hooks/bridge/events/useRequestMetadataProperties.ts @@ -1,5 +1,6 @@ /* eslint-disable camelcase */ import { useSelector } from 'react-redux'; +import { BRIDGE_DEFAULT_SLIPPAGE } from '@metamask/bridge-controller'; import { getIsBridgeTx, getQuoteRequest, @@ -7,7 +8,6 @@ import { import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { getCurrentKeyring } from '../../../selectors'; import { getIsSmartTransaction } from '../../../../shared/modules/selectors'; -import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../../shared/constants/bridge'; import { ActionType } from './types'; import { useConvertedUsdAmounts } from './useConvertedUsdAmounts'; diff --git a/ui/hooks/bridge/events/useRequestProperties.ts b/ui/hooks/bridge/events/useRequestProperties.ts index a5edff55f019..72bb97e99aff 100644 --- a/ui/hooks/bridge/events/useRequestProperties.ts +++ b/ui/hooks/bridge/events/useRequestProperties.ts @@ -1,11 +1,11 @@ /* eslint-disable camelcase */ import { useSelector } from 'react-redux'; +import { formatChainIdToCaip } from '@metamask/bridge-controller'; import { getQuoteRequest, getFromToken, getToToken, } from '../../../ducks/bridge/selectors'; -import { formatChainIdToCaip } from '../../../../shared/modules/bridge-utils/caip-formatters'; export const useRequestProperties = () => { const { srcChainId, destChainId, srcTokenAddress, destTokenAddress } = diff --git a/ui/hooks/bridge/useBridgeChainInfo.ts b/ui/hooks/bridge/useBridgeChainInfo.ts index 51279ec308d1..25cc7a168d8f 100644 --- a/ui/hooks/bridge/useBridgeChainInfo.ts +++ b/ui/hooks/bridge/useBridgeChainInfo.ts @@ -5,6 +5,7 @@ import { } from '@metamask/transaction-controller'; import type { NetworkConfiguration } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; +import { formatChainIdToHexOrCaip } from '@metamask/bridge-controller'; import type { BridgeHistoryItem } from '../../../shared/types/bridge-status'; import { CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, @@ -12,7 +13,6 @@ import { } from '../../../shared/constants/network'; import { CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../shared/constants/common'; import { getMultichainNetworkConfigurationsByChainId } from '../../selectors'; -import { formatChainIdToHexOrCaip } from '../../../shared/modules/bridge-utils/caip-formatters'; const getSourceAndDestChainIds = ({ bridgeHistoryItem, diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index 770a2bf1c0b7..12b8ecce2283 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { toChecksumAddress } from 'ethereumjs-util'; import { isStrictHexString } from '@metamask/utils'; +import { formatChainIdToCaip } from '@metamask/bridge-controller'; import { setBridgeFeatureFlags } from '../../ducks/bridge/actions'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -32,8 +33,6 @@ import { import { getPortfolioUrl } from '../../helpers/utils/portfolio'; import { SwapsTokenObject } from '../../../shared/constants/swaps'; import { getProviderConfig } from '../../../shared/modules/selectors/networks'; -// eslint-disable-next-line import/no-restricted-paths -import { formatChainIdToCaip } from '../../../shared/modules/bridge-utils/caip-formatters'; import { useCrossChainSwapsEventTracker } from './useCrossChainSwapsEventTracker'; ///: END:ONLY_INCLUDE_IF diff --git a/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts b/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts index 1e38d8cbd7b3..545e6c7d4103 100644 --- a/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts +++ b/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts @@ -1,11 +1,11 @@ import { useCallback, useContext } from 'react'; +import { SortOrder } from '@metamask/bridge-controller'; import { MetaMetricsContext } from '../../contexts/metametrics'; import { MetaMetricsEventCategory, MetaMetricsEventName, MetaMetricsSwapsEventSource, } from '../../../shared/constants/metametrics'; -import { SortOrder } from '../../../shared/types/bridge'; import { RequestParams, RequestMetadata, diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index 085dc6b0dcb6..45d1393d79bb 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -1,8 +1,10 @@ import { type Hex, type CaipChainId, isCaipChainId } from '@metamask/utils'; import { useMemo } from 'react'; -import { isSolanaChainId } from '@metamask/bridge-controller'; +import { + isSolanaChainId, + calcLatestSrcBalance, +} from '@metamask/bridge-controller'; import { getSelectedInternalAccount } from '../../selectors'; -import { calcLatestSrcBalance } from '../../../shared/modules/bridge-utils/balance'; import { useAsyncResult } from '../useAsyncResult'; import { Numeric } from '../../../shared/modules/Numeric'; import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; diff --git a/ui/hooks/bridge/useTokensWithFiltering.test.ts b/ui/hooks/bridge/useTokensWithFiltering.test.ts index d9d137cd1fce..2963e0940acf 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.test.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.test.ts @@ -12,7 +12,7 @@ const mockFetchBridgeTokens = jest.fn().mockResolvedValue({ [NATIVE_TOKEN.address]: NATIVE_TOKEN, ...STATIC_MAINNET_TOKEN_LIST, }); -jest.mock('../../../shared/modules/bridge-utils/bridge.util', () => ({ +jest.mock('@metamask/bridge-controller', () => ({ fetchBridgeTokens: (c: string) => mockFetchBridgeTokens(c), })); diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 802ef1757a36..49c4aec01cd2 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -3,7 +3,14 @@ import { useSelector } from 'react-redux'; import { ChainId } from '@metamask/controller-utils'; import { type CaipChainId, isStrictHexString, type Hex } from '@metamask/utils'; import { zeroAddress } from 'ethereumjs-util'; -import { isSolanaChainId } from '@metamask/bridge-controller'; +import { + isSolanaChainId, + formatChainIdToCaip, + formatChainIdToHex, + type BridgeToken, + isNativeAddress, + fetchBridgeTokens, +} from '@metamask/bridge-controller'; import { getAllDetectedTokensForSelectedAddress, selectERC20TokensByChain, @@ -15,18 +22,12 @@ import { NativeAsset, } from '../../components/multichain/asset-picker-amount/asset-picker-modal/types'; import { AssetType } from '../../../shared/constants/transaction'; -import { - formatChainIdToCaip, - formatChainIdToHex, - isNativeAddress, -} from '../../../shared/modules/bridge-utils/caip-formatters'; import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../shared/constants/network'; import { Token } from '../../components/app/assets/types'; import { useMultichainBalances } from '../useMultichainBalances'; import { useAsyncResult } from '../useAsyncResult'; import { fetchTopAssetsList } from '../../pages/swaps/swaps.util'; import { - fetchBridgeTokens, fetchNonEvmTokens, getAssetImageUrl, isTokenV3Asset, @@ -36,7 +37,6 @@ import { type BridgeAppState, getTopAssetsFromFeatureFlags, } from '../../ducks/bridge/selectors'; -import { type BridgeToken } from '../../../shared/types/bridge'; type FilterPredicate = ( symbol: string, diff --git a/ui/pages/bridge/hooks/useAddToken.ts b/ui/pages/bridge/hooks/useAddToken.ts index f1a148ce7732..34e14a36ee41 100644 --- a/ui/pages/bridge/hooks/useAddToken.ts +++ b/ui/pages/bridge/hooks/useAddToken.ts @@ -1,6 +1,6 @@ import { useDispatch, useSelector } from 'react-redux'; import type { NetworkConfiguration } from '@metamask/network-controller'; -import type { QuoteResponse } from '../../../../shared/types/bridge'; +import type { QuoteResponse } from '@metamask/bridge-controller'; import { FEATURED_RPCS } from '../../../../shared/constants/network'; import { addToken, addNetwork } from '../../../store/actions'; import { diff --git a/ui/pages/bridge/hooks/useHandleApprovalTx.ts b/ui/pages/bridge/hooks/useHandleApprovalTx.ts index d4ffb0a75855..433df36af9d4 100644 --- a/ui/pages/bridge/hooks/useHandleApprovalTx.ts +++ b/ui/pages/bridge/hooks/useHandleApprovalTx.ts @@ -5,11 +5,9 @@ import { type TxData, type QuoteResponse, FeeType, -} from '../../../../shared/types/bridge'; -import { isEthUsdt, getEthUsdtResetData, -} from '../../../../shared/modules/bridge-utils/bridge.util'; +} from '@metamask/bridge-controller'; import { ETH_USDT_ADDRESS } from '../../../../shared/constants/bridge'; import { getBridgeERC20Allowance } from '../../../ducks/bridge/actions'; import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; diff --git a/ui/pages/bridge/hooks/useHandleBridgeTx.ts b/ui/pages/bridge/hooks/useHandleBridgeTx.ts index 4d15caa710db..95adebcb6ca7 100644 --- a/ui/pages/bridge/hooks/useHandleBridgeTx.ts +++ b/ui/pages/bridge/hooks/useHandleBridgeTx.ts @@ -1,8 +1,8 @@ import { BigNumber } from 'bignumber.js'; import { TransactionType } from '@metamask/transaction-controller'; import { useDispatch } from 'react-redux'; +import { FeeType, type QuoteResponse } from '@metamask/bridge-controller'; import { Numeric } from '../../../../shared/modules/Numeric'; -import { FeeType, type QuoteResponse } from '../../../../shared/types/bridge'; import useHandleTx from './useHandleTx'; export default function useHandleBridgeTx() { diff --git a/ui/pages/bridge/hooks/useHandleTx.ts b/ui/pages/bridge/hooks/useHandleTx.ts index 622da95aaf46..ff9a7d4c715c 100644 --- a/ui/pages/bridge/hooks/useHandleTx.ts +++ b/ui/pages/bridge/hooks/useHandleTx.ts @@ -9,6 +9,7 @@ import { KeyringRpcMethod } from '@metamask/keyring-api'; import { useEffect } from 'react'; import { Hex } from '@metamask/utils'; import { useHistory } from 'react-router-dom'; +import type { ChainId } from '@metamask/bridge-controller'; import { forceUpdateMetamaskState, addTransaction, @@ -21,7 +22,6 @@ import { } from '../../../ducks/bridge/utils'; import { getGasFeeEstimates } from '../../../ducks/metamask/metamask'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; -import type { ChainId } from '../../../../shared/types/bridge'; import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils'; import { getIsSmartTransaction } from '../../../../shared/modules/selectors'; import { diff --git a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts index 1e45ba7a2d66..59934c0a99b1 100644 --- a/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts +++ b/ui/pages/bridge/hooks/useSubmitBridgeTransaction.ts @@ -4,10 +4,7 @@ import { useHistory } from 'react-router-dom'; import { TransactionMeta } from '@metamask/transaction-controller'; import { createProjectLogger, Hex } from '@metamask/utils'; import { isSolanaChainId } from '@metamask/bridge-controller'; -import type { - QuoteMetadata, - QuoteResponse, -} from '../../../../shared/types/bridge'; +import type { QuoteMetadata, QuoteResponse } from '@metamask/bridge-controller'; import { AWAITING_SIGNATURES_ROUTE, CROSS_CHAIN_SWAP_ROUTE, diff --git a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx index 3d148b852ddf..ac2a42d76151 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.test.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.test.tsx @@ -1,11 +1,13 @@ import React from 'react'; +import { + RequestStatus, + formatChainIdToCaip, +} from '@metamask/bridge-controller'; import { renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json'; -import { RequestStatus } from '../../../../shared/types/bridge'; -import { formatChainIdToCaip } from '../../../../shared/modules/bridge-utils/caip-formatters'; import { BridgeCTAButton } from './bridge-cta-button'; describe('BridgeCTAButton', () => { diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx index a31094ee299e..8594d411e587 100644 --- a/ui/pages/bridge/prepare/bridge-input-group.tsx +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { BigNumber } from 'bignumber.js'; import { isNativeAddress } from '@metamask/bridge-controller'; +import type { BridgeToken } from '@metamask/bridge-controller'; import { Text, TextField, @@ -30,7 +31,6 @@ import { getValidationErrors, } from '../../../ducks/bridge/selectors'; import { shortenString } from '../../../helpers/utils/util'; -import type { BridgeToken } from '../../../../shared/types/bridge'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; import { MINUTE } from '../../../../shared/constants/time'; import { getIntlLocale } from '../../../ducks/locale/locale'; diff --git a/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx index 267b0d5df7e3..739dcef0fc09 100644 --- a/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx +++ b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { BRIDGE_DEFAULT_SLIPPAGE } from '@metamask/bridge-controller'; import { Button, ButtonPrimary, @@ -35,7 +36,6 @@ import { getSlippage } from '../../../ducks/bridge/selectors'; import { setSlippage } from '../../../ducks/bridge/actions'; import { useCrossChainSwapsEventTracker } from '../../../hooks/bridge/useCrossChainSwapsEventTracker'; import { MetaMetricsEventName } from '../../../../shared/constants/metametrics'; -import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../../shared/constants/bridge'; import { Column, Row, Tooltip } from '../layout'; const HARDCODED_SLIPPAGE_OPTIONS = [BRIDGE_DEFAULT_SLIPPAGE, 3]; diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx index 4d0de6feb9b9..de0790553426 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx @@ -10,7 +10,7 @@ import { PREPARE_SWAP_ROUTE, } from '../../../helpers/constants/routes'; import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json'; -import { RequestStatus } from '../../../../shared/types/bridge'; +import { RequestStatus } from '@metamask/bridge-controller'; const storybook = { title: 'Pages/Bridge/CrossChainSwapPage', diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 04385a5d0109..2472b0bc2ba2 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -15,6 +15,8 @@ import { toChecksumAddress, zeroAddress } from 'ethereumjs-util'; import { isSolanaChainId, isValidQuoteRequest, + BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, + type GenericQuoteRequest, } from '@metamask/bridge-controller'; import { setFromToken, @@ -72,7 +74,6 @@ import { setActiveNetworkWithError, setSelectedAccount, } from '../../../store/actions'; -import type { GenericQuoteRequest } from '../../../../shared/types/bridge'; import { calcTokenValue } from '../../../../shared/lib/swaps-utils'; import { formatTokenAmount, @@ -100,10 +101,7 @@ import { } from '../../../selectors'; import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { SECOND } from '../../../../shared/constants/time'; -import { - BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, - SOLANA_USDC_ASSET, -} from '../../../../shared/constants/bridge'; +import { SOLANA_USDC_ASSET } from '../../../../shared/constants/bridge'; import { getIntlLocale } from '../../../ducks/locale/locale'; import { useIsMultichainSwap } from '../hooks/useIsMultichainSwap'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; diff --git a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx index af3bfaf3e12a..4c25f5a9809e 100644 --- a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx +++ b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx @@ -1,13 +1,15 @@ import React from 'react'; +import { + RequestStatus, + formatChainIdToCaip, +} from '@metamask/bridge-controller'; import { renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json'; import mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json'; -import { RequestStatus } from '../../../../shared/types/bridge'; import { mockNetworkState } from '../../../../test/stub/networks'; -import { formatChainIdToCaip } from '../../../../shared/modules/bridge-utils/caip-formatters'; import { BridgeQuoteCard } from './bridge-quote-card'; describe('BridgeQuoteCard', () => { diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx index 18989768bc4b..a49f2c152e34 100644 --- a/ui/pages/bridge/quotes/bridge-quote-card.tsx +++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; +import { BRIDGE_MM_FEE_RATE } from '@metamask/bridge-controller'; import { Text, PopoverPosition, @@ -41,10 +42,7 @@ import { TextVariant, } from '../../../helpers/constants/design-system'; import { Row, Column, Tooltip } from '../layout'; -import { - BRIDGE_MM_FEE_RATE, - NETWORK_TO_SHORT_NETWORK_NAME_MAP, -} from '../../../../shared/constants/bridge'; +import { NETWORK_TO_SHORT_NETWORK_NAME_MAP } from '../../../../shared/constants/bridge'; import { TERMS_OF_USE_LINK } from '../../../../shared/constants/terms'; import { getIntlLocale } from '../../../ducks/locale/locale'; import { getImageForChainId } from '../../../selectors/multichain'; diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx index fe999108bec8..63a95febe03e 100644 --- a/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx +++ b/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx @@ -4,7 +4,7 @@ import configureStore from '../../../store/store'; import { BridgeQuotesModal } from './bridge-quotes-modal'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json'; -import { SortOrder } from '../../../../shared/types/bridge'; +import { SortOrder } from '@metamask/bridge-controller'; const storybook = { title: 'Pages/Bridge/BridgeQuotesModal', diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx index 2d6f3dd992ea..1ab398dc84d5 100644 --- a/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx +++ b/ui/pages/bridge/quotes/bridge-quotes-modal.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { RequestStatus } from '../../../../shared/types/bridge'; +import { RequestStatus } from '@metamask/bridge-controller'; import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx index 748679bafe74..b5fe4a652dc2 100644 --- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx +++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx @@ -2,6 +2,11 @@ import React from 'react'; import { IconName } from '@metamask/snaps-sdk/jsx'; import { useDispatch, useSelector } from 'react-redux'; import { startCase } from 'lodash'; +import { + type QuoteMetadata, + type QuoteResponse, + SortOrder, +} from '@metamask/bridge-controller'; import { ButtonLink, IconSize, @@ -25,11 +30,6 @@ import { } from '../utils/quote'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { setSelectedQuote, setSortOrder } from '../../../ducks/bridge/actions'; -import { - type QuoteMetadata, - type QuoteResponse, - SortOrder, -} from '../../../../shared/types/bridge'; import { getBridgeQuotes, getBridgeSortOrder, diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx index e48b2183133a..6c12e984ab74 100644 --- a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx @@ -1,6 +1,10 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; -import { isSolanaChainId } from '@metamask/bridge-controller'; +import { + isSolanaChainId, + BRIDGE_MM_FEE_RATE, +} from '@metamask/bridge-controller'; +import type { ChainId } from '@metamask/bridge-controller'; import { Text, PopoverPosition, @@ -36,10 +40,7 @@ import { TextVariant, } from '../../../helpers/constants/design-system'; import { Row, Column, Tooltip } from '../layout'; -import { - BRIDGE_MM_FEE_RATE, - NETWORK_TO_SHORT_NETWORK_NAME_MAP, -} from '../../../../shared/constants/bridge'; +import { NETWORK_TO_SHORT_NETWORK_NAME_MAP } from '../../../../shared/constants/bridge'; import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; import { MULTICHAIN_TOKEN_IMAGE_MAP, @@ -47,7 +48,6 @@ import { } from '../../../../shared/constants/multichain/networks'; import { decimalToHex } from '../../../../shared/modules/conversion.utils'; import { getIntlLocale } from '../../../ducks/locale/locale'; -import type { ChainId } from '../../../../shared/types/bridge'; import { BridgeQuotesModal } from './bridge-quotes-modal'; export const MultichainBridgeQuoteCard = () => { diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts index dd845f791644..3060ba0094c1 100644 --- a/ui/pages/bridge/utils/quote.ts +++ b/ui/pages/bridge/utils/quote.ts @@ -1,12 +1,13 @@ import { BigNumber } from 'bignumber.js'; +import { + type QuoteResponse, + type Quote, + type L1GasFees, + type TokenAmountValues, + type SolanaFees, + isNativeAddress, +} from '@metamask/bridge-controller'; import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; -import type { - QuoteResponse, - Quote, - L1GasFees, - TokenAmountValues, - SolanaFees, -} from '../../../../shared/types/bridge'; import { hexToDecimal, sumDecimals, @@ -16,7 +17,6 @@ import { Numeric } from '../../../../shared/modules/Numeric'; import { EtherDenomination } from '../../../../shared/constants/common'; import { DEFAULT_PRECISION } from '../../../hooks/useCurrencyDisplay'; import { formatAmount } from '../../confirmations/components/simulation-details/formatAmount'; -import { isNativeAddress } from '../../../../shared/modules/bridge-utils/caip-formatters'; export const isQuoteExpired = ( isQuoteGoingToRefresh: boolean, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 094b8d6c82af..c60e54abc3b2 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -24,6 +24,7 @@ import { getPermittedEthChainIds, } from '@metamask/multichain'; import { KeyringTypes } from '@metamask/keyring-controller'; +import { BridgeFeatureFlagsKey } from '@metamask/bridge-controller'; import { getCurrentChainId, getProviderConfig, @@ -114,7 +115,6 @@ import { BackgroundColor } from '../helpers/constants/design-system'; import { NOTIFICATION_SOLANA_ON_METAMASK } from '../../shared/notifications'; import { ENVIRONMENT_TYPE_POPUP } from '../../shared/constants/app'; import { MULTICHAIN_NETWORK_TO_ASSET_TYPES } from '../../shared/constants/multichain/assets'; -import { BridgeFeatureFlagsKey } from '../../shared/types/bridge'; import { hasTransactionData } from '../../shared/modules/transaction.utils'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { createDeepEqualSelector } from '../../shared/modules/selectors/util'; From 6eb3f7bde51773e3aca64d0a1959ab2730e3ec3b Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 14:42:28 -0700 Subject: [PATCH 05/71] refactor: remove bridgeState from controller state --- app/scripts/constants/sentry-state.ts | 40 +++++++++---------- test/e2e/default-fixture.js | 32 +++++++-------- test/e2e/fixture-builder.js | 10 ++--- test/e2e/tests/metrics/errors.spec.js | 34 ++++++++-------- ...rs-after-init-opt-in-background-state.json | 34 ++++++++-------- .../errors-after-init-opt-in-ui-state.json | 32 +++++++-------- ...s-before-init-opt-in-background-state.json | 16 ++++---- .../errors-before-init-opt-in-ui-state.json | 16 ++++---- .../data/onboarding-completion-route.json | 8 ++-- test/jest/mock-store.js | 4 +- .../app/wallet-overview/eth-overview.test.js | 8 ++-- ui/ducks/bridge/selectors.ts | 33 +++++++-------- ui/hooks/bridge/useBridging.test.ts | 18 ++++----- ui/pages/home/home.container.js | 4 +- ui/selectors/selectors.js | 2 +- 15 files changed, 132 insertions(+), 159 deletions(-) diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index a20934b57ed6..2c0b160cf8d6 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -112,29 +112,27 @@ export const SENTRY_BACKGROUND_STATE = { assetsRates: false, }, BridgeController: { - bridgeState: { - bridgeFeatureFlags: { - extensionConfig: { - support: false, - chains: {}, - }, + bridgeFeatureFlags: { + extensionConfig: { + support: false, + chains: {}, }, - quoteRequest: { - walletAddress: false, - srcTokenAddress: true, - slippage: true, - srcChainId: true, - destChainId: true, - destTokenAddress: true, - srcTokenAmount: true, - }, - quotes: [], - quotesInitialLoadTime: true, - quotesLastFetched: true, - quotesLoadingStatus: true, - quoteFetchError: true, - quotesRefreshCount: true, }, + quoteRequest: { + walletAddress: false, + srcTokenAddress: true, + slippage: true, + srcChainId: true, + destChainId: true, + destTokenAddress: true, + srcTokenAmount: true, + }, + quotes: [], + quotesInitialLoadTime: true, + quotesLastFetched: true, + quotesLoadingStatus: true, + quoteFetchError: true, + quotesRefreshCount: true, }, BridgeStatusController: { bridgeStatusState: { diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index ef8f9103ac68..ed0f098d73a2 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -120,23 +120,21 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { snapsInstallPrivacyWarningShown: true, }, BridgeController: { - bridgeState: { - bridgeFeatureFlags: { - extensionConfig: { - support: false, - chains: { - 'eip155:1': { - isActiveSrc: true, - isActiveDest: true, - }, - 'eip155:10': { - isActiveSrc: true, - isActiveDest: true, - }, - 'eip155:59144': { - isActiveSrc: true, - isActiveDest: true, - }, + bridgeFeatureFlags: { + extensionConfig: { + support: false, + chains: { + 'eip155:1': { + isActiveSrc: true, + isActiveDest: true, + }, + 'eip155:10': { + isActiveSrc: true, + isActiveDest: true, + }, + 'eip155:59144': { + isActiveSrc: true, + isActiveDest: true, }, }, }, diff --git a/test/e2e/fixture-builder.js b/test/e2e/fixture-builder.js index 1de8452296f6..f06ba7bc39b6 100644 --- a/test/e2e/fixture-builder.js +++ b/test/e2e/fixture-builder.js @@ -462,12 +462,10 @@ class FixtureBuilder { withBridgeControllerDefaultState() { this.fixture.data.BridgeController = { - bridgeState: { - bridgeFeatureFlags: { - extensionConfig: { - support: false, - chains: {}, - }, + bridgeFeatureFlags: { + extensionConfig: { + support: false, + chains: {}, }, }, }; diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js index 2b17f25bbb6c..8c826a62acea 100644 --- a/test/e2e/tests/metrics/errors.spec.js +++ b/test/e2e/tests/metrics/errors.spec.js @@ -60,8 +60,8 @@ const removedBackgroundFields = [ 'AppStateController.currentPopupId', 'AppStateController.timeoutMinutes', 'AppStateController.lastInteractedConfirmationInfo', - 'BridgeController.bridgeState.quoteRequest.walletAddress', - 'BridgeController.bridgeState.quoteRequest.slippage', + 'BridgeController.quoteRequest.walletAddress', + 'BridgeController.quoteRequest.slippage', 'PPOMController.chainStatus.0x539.lastVisited', 'PPOMController.versionInfo', // This property is timing-dependent @@ -843,23 +843,21 @@ describe('Sentry errors', function () { it('should not have extra properties in UI state mask', async function () { const expectedMissingState = { - bridgeState: { - // This can get wiped out during initialization due to a bug in - // the "resetState" method - quoteRequest: { - destChainId: true, - destTokenAddress: true, - srcChainId: true, - srcTokenAmount: true, - walletAddress: false, - slippage: true, - }, - quotesLastFetched: true, - quotesLoadingStatus: true, - quotesRefreshCount: true, - quoteFetchError: true, - quotesInitialLoadTime: true, + // This can get wiped out during initialization due to a bug in + // the "resetState" method + quoteRequest: { + destChainId: true, + destTokenAddress: true, + srcChainId: true, + srcTokenAmount: true, + walletAddress: false, + slippage: true, }, + quotesLastFetched: true, + quotesLoadingStatus: true, + quotesRefreshCount: true, + quoteFetchError: true, + quotesInitialLoadTime: true, currentPopupId: false, // Initialized as undefined // Part of transaction controller store, but missing from the initial // state diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index e430661afe81..dbb064af275c 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -62,25 +62,23 @@ }, "AuthenticationController": { "isSignedIn": "boolean" }, "BridgeController": { - "bridgeState": { - "bridgeFeatureFlags": { - "extensionConfig": { - "refreshRate": "number", - "maxRefreshCount": "number", - "support": "boolean", - "chains": { - "eip155:1": "object", - "eip155:42161": "object", - "eip155:59144": "object" - } + "bridgeFeatureFlags": { + "extensionConfig": { + "refreshRate": "number", + "maxRefreshCount": "number", + "support": "boolean", + "chains": { + "eip155:1": "object", + "eip155:42161": "object", + "eip155:59144": "object" } - }, - "quoteRequest": { - "srcTokenAddress": "0x0000000000000000000000000000000000000000" - }, - "quotes": {}, - "quotesRefreshCount": 0 - } + } + }, + "quoteRequest": { + "srcTokenAddress": "0x0000000000000000000000000000000000000000" + }, + "quotes": {}, + "quotesRefreshCount": 0 }, "BridgeStatusController": { "bridgeStatusState": { "txHistory": "object" } }, "CronjobController": { "jobs": "object", "events": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index da184d74a0fb..487619db6df7 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -340,25 +340,23 @@ "swapsStxMaxFeeMultiplier": 2, "swapsFeatureFlags": {} }, - "bridgeState": { - "bridgeFeatureFlags": { - "extensionConfig": { - "refreshRate": "number", - "maxRefreshCount": "number", - "support": "boolean", - "chains": { - "eip155:1": "object", - "eip155:42161": "object", - "eip155:59144": "object" - } + "bridgeFeatureFlags": { + "extensionConfig": { + "refreshRate": "number", + "maxRefreshCount": "number", + "support": "boolean", + "chains": { + "eip155:1": "object", + "eip155:42161": "object", + "eip155:59144": "object" } - }, - "quoteRequest": { - "srcTokenAddress": "0x0000000000000000000000000000000000000000" - }, - "quotes": {}, - "quotesRefreshCount": 0 + } + }, + "quoteRequest": { + "srcTokenAddress": "0x0000000000000000000000000000000000000000" }, + "quotes": {}, + "quotesRefreshCount": 0, "bridgeStatusState": { "txHistory": "object" }, "ensEntries": "object", "ensResolutionsByAddress": "object", diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index 81f085a353b2..b3d34b1c7454 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -139,15 +139,13 @@ } }, "BridgeController": { - "bridgeState": { - "bridgeFeatureFlags": { - "extensionConfig": { - "support": "boolean", - "chains": { - "eip155:1": "object", - "eip155:10": "object", - "eip155:59144": "object" - } + "bridgeFeatureFlags": { + "extensionConfig": { + "support": "boolean", + "chains": { + "eip155:1": "object", + "eip155:10": "object", + "eip155:59144": "object" } } } diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 66ec9385f054..9cf068c46162 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -46,15 +46,13 @@ "snapsInstallPrivacyWarningShown": true }, "BridgeController": { - "bridgeState": { - "bridgeFeatureFlags": { - "extensionConfig": { - "support": "boolean", - "chains": { - "eip155:1": "object", - "eip155:10": "object", - "eip155:59144": "object" - } + "bridgeFeatureFlags": { + "extensionConfig": { + "support": "boolean", + "chains": { + "eip155:1": "object", + "eip155:10": "object", + "eip155:59144": "object" } } } diff --git a/test/integration/data/onboarding-completion-route.json b/test/integration/data/onboarding-completion-route.json index 4895e935e2ef..722c2920b54c 100644 --- a/test/integration/data/onboarding-completion-route.json +++ b/test/integration/data/onboarding-completion-route.json @@ -57,11 +57,9 @@ "33": { "id": 33, "date": "2024-04-09", "isShown": false } }, "approvalFlows": [], - "bridgeState": { - "bridgeFeatureFlags": { - "extensionConfig": { - "support": false - } + "bridgeFeatureFlags": { + "extensionConfig": { + "support": false } }, "browserEnvironment": { "os": "mac", "browser": "firefox" }, diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 717110b8e144..9c62ccc68569 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -782,7 +782,7 @@ export const createBridgeMockStore = ( }, ...mockTokenData, ...metamaskStateOverrides, - bridgeState: { + ...{ ...DEFAULT_BRIDGE_STATE, bridgeFeatureFlags: { ...featureFlagOverrides, @@ -805,8 +805,8 @@ export const createBridgeMockStore = ( }, }, }, - ...bridgeStateOverrides, }, + ...bridgeStateOverrides, bridgeStatusState: { ...DEFAULT_BRIDGE_STATUS_STATE, ...bridgeStatusStateOverrides, diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js index 7ef9df20c394..49b8ef623daa 100644 --- a/ui/components/app/wallet-overview/eth-overview.test.js +++ b/ui/components/app/wallet-overview/eth-overview.test.js @@ -259,11 +259,9 @@ describe('EthOverview', () => { ...mockStore.metamask, ...mockNetworkState({ chainId: '0xa86a' }), useExternalServices: true, - bridgeState: { - bridgeFeatureFlags: { - extensionConfig: { - support: false, - }, + bridgeFeatureFlags: { + extensionConfig: { + support: false, }, }, }, diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 7fae13b0d11c..7ed5c2f9a816 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -133,7 +133,7 @@ export const getAllBridgeableNetworks = createDeepEqualSelector( export const getFromChains = createDeepEqualSelector( getAllBridgeableNetworks, - (state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags, + (state: BridgeAppState) => state.metamask.bridgeFeatureFlags, (state: BridgeAppState) => hasSolanaAccounts(state), (allBridgeableNetworks, bridgeFeatureFlags, hasSolanaAccount) => { // First filter out Solana from source chains if no Solana account exists @@ -165,7 +165,7 @@ export const getFromChain = createDeepEqualSelector( export const getToChains = createDeepEqualSelector( getAllBridgeableNetworks, - (state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags, + (state: BridgeAppState) => state.metamask.bridgeFeatureFlags, (allBridgeableNetworks, bridgeFeatureFlags) => uniqBy([...allBridgeableNetworks, ...FEATURED_RPCS], 'chainId').filter( ({ chainId }) => @@ -182,7 +182,7 @@ export const getTopAssetsFromFeatureFlags = ( if (!chainId) { return undefined; } - const bridgeFeatureFlags = state.metamask.bridgeState?.bridgeFeatureFlags; + const { bridgeFeatureFlags } = state.metamask; return bridgeFeatureFlags?.[BridgeFeatureFlagsKey.EXTENSION_CONFIG].chains[ formatChainIdToCaip(chainId) ]?.topAssets; @@ -235,14 +235,13 @@ export const getFromAmount = (state: BridgeAppState): string | null => export const getSlippage = (state: BridgeAppState) => state.bridge.slippage; export const getQuoteRequest = (state: BridgeAppState) => { - const { quoteRequest } = state.metamask.bridgeState; + const { quoteRequest } = state.metamask; return quoteRequest; }; export const getBridgeQuotesConfig = (state: BridgeAppState) => - state.metamask.bridgeState?.bridgeFeatureFlags[ - BridgeFeatureFlagsKey.EXTENSION_CONFIG - ] ?? {}; + state.metamask.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG] ?? + {}; export const getQuoteRefreshRate = createSelector( getBridgeQuotesConfig, @@ -396,7 +395,7 @@ export const getToTokenConversionRate = createDeepEqualSelector( ); const _getQuotesWithMetadata = createSelector( - (state: BridgeAppState) => state.metamask.bridgeState.quotes, + (state: BridgeAppState) => state.metamask.quotes, getToTokenConversionRate, getFromTokenConversionRate, getConversionRate, @@ -519,7 +518,7 @@ const _getQuoteIdentifier = ({ quote }: QuoteResponse & L1GasFees) => `${quote.bridgeId}-${quote.bridges[0]}-${quote.steps.length}`; const _getSelectedQuote = createSelector( - (state: BridgeAppState) => state.metamask.bridgeState.quotesRefreshCount, + (state: BridgeAppState) => state.metamask.quotesRefreshCount, (state: BridgeAppState) => state.bridge.selectedQuote, _getSortedQuotesWithMetadata, (quotesRefreshCount, selectedQuote, sortedQuotesWithMetadata) => @@ -537,12 +536,11 @@ export const getBridgeQuotes = createSelector( [ _getSortedQuotesWithMetadata, _getSelectedQuote, - (state) => state.metamask.bridgeState.quotesLastFetched, - (state) => - state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING, - (state: BridgeAppState) => state.metamask.bridgeState.quotesRefreshCount, - (state: BridgeAppState) => state.metamask.bridgeState.quotesInitialLoadTime, - (state: BridgeAppState) => state.metamask.bridgeState.quoteFetchError, + (state) => state.metamask.quotesLastFetched, + (state) => state.metamask.quotesLoadingStatus === RequestStatus.LOADING, + (state: BridgeAppState) => state.metamask.quotesRefreshCount, + (state: BridgeAppState) => state.metamask.quotesInitialLoadTime, + (state: BridgeAppState) => state.metamask.quoteFetchError, getBridgeQuotesConfig, getQuoteRequest, ], @@ -583,8 +581,7 @@ export const getIsBridgeTx = createDeepEqualSelector( const _getValidatedSrcAmount = createSelector( getFromToken, - (state: BridgeAppState) => - state.metamask.bridgeState.quoteRequest.srcTokenAmount, + (state: BridgeAppState) => state.metamask.quoteRequest.srcTokenAmount, (fromToken, srcTokenAmount) => srcTokenAmount && fromToken?.decimals ? calcTokenAmount(srcTokenAmount, Number(fromToken.decimals)).toString() @@ -686,7 +683,7 @@ export const getWasTxDeclined = (state: BridgeAppState): boolean => { * Checks if Solana is enabled as either a fromChain or toChain for bridging */ export const isBridgeSolanaEnabled = createDeepEqualSelector( - (state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags, + (state: BridgeAppState) => state.metamask.bridgeFeatureFlags, (bridgeFeatureFlags) => { const solanaChainId = MultichainNetworks.SOLANA; const solanaChainIdCaip = formatChainIdToCaip(solanaChainId); diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts index 291401270a3e..978fc1e1957c 100644 --- a/ui/hooks/bridge/useBridging.test.ts +++ b/ui/hooks/bridge/useBridging.test.ts @@ -89,11 +89,9 @@ describe('useBridging', () => { useExternalServices: true, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), metaMetricsId: MOCK_METAMETRICS_ID, - bridgeState: { - bridgeFeatureFlags: { - extensionConfig: { - support: false, - }, + bridgeFeatureFlags: { + extensionConfig: { + support: false, }, }, internalAccounts: { @@ -162,12 +160,10 @@ describe('useBridging', () => { useExternalServices: true, ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }), metaMetricsId: MOCK_METAMETRICS_ID, - bridgeState: { - isBridgeEnabled: true, - bridgeFeatureFlags: { - extensionConfig: { - support: true, - }, + isBridgeEnabled: true, + bridgeFeatureFlags: { + extensionConfig: { + support: true, }, }, internalAccounts: { diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 433bf0d19bd2..e5cb3c6d3d79 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -79,7 +79,7 @@ const mapStateToProps = (state) => { connectedStatusPopoverHasBeenShown, defaultHomeActiveTabName, swapsState, - bridgeState, + quotes, dataCollectionForMarketing, participateInMetaMetrics, firstTimeFlowType, @@ -146,7 +146,7 @@ const mapStateToProps = (state) => { haveSwapsQuotes: Boolean(Object.values(swapsState.quotes || {}).length), swapsFetchParams: swapsState.fetchParams, showAwaitingSwapScreen: swapsState.routeState === 'awaiting', - haveBridgeQuotes: Boolean(Object.values(bridgeState?.quotes || {}).length), + haveBridgeQuotes: Boolean(Object.values(quotes || {}).length), isMainnet: getIsMainnet(state), originOfCurrentTab, shouldShowWeb3ShimUsageNotification, diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index c60e54abc3b2..94c31a7e3d1a 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1738,7 +1738,7 @@ export function getIsBridgeChain(state, overrideChainId) { } function getBridgeFeatureFlags(state) { - return state.metamask.bridgeState?.bridgeFeatureFlags; + return state.metamask.bridgeFeatureFlags; } export const getIsBridgeEnabled = createSelector( From 5e47b242d2661e69a1b055fa64e8fb9b65a94814 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 15:02:18 -0700 Subject: [PATCH 06/71] fix: type errors --- ui/hooks/bridge/useLatestBalance.ts | 24 ++++++++++++----------- ui/hooks/bridge/useTokensWithFiltering.ts | 18 ++++++++++++++++- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index 45d1393d79bb..0a2157384fb7 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -41,7 +41,7 @@ const useLatestBalance = ( const nonEvmBalances = nonEvmBalancesByAccountId?.[id]; - const value = useAsyncResult(async () => { + const value = useAsyncResult(async () => { if ( token?.address && // TODO check whether chainId is EVM when MultichainNetworkController is integrated @@ -49,12 +49,14 @@ const useLatestBalance = ( chainId && currentChainId === chainId ) { - return await calcLatestSrcBalance( - global.ethereumProvider, - selectedAddress, - token.address, - chainId, - ); + return ( + await calcLatestSrcBalance( + global.ethereumProvider, + selectedAddress, + token.address, + chainId, + ) + )?.toString(); } // No need to fetch the balance for non-EVM tokens, use the balance provided by the @@ -63,7 +65,9 @@ const useLatestBalance = ( return Numeric.from( nonEvmBalances?.[token.address]?.amount ?? token?.string, 10, - ).shiftedBy(-1 * token.decimals); + ) + .shiftedBy(-1 * token.decimals) + .toString(); } return undefined; @@ -84,9 +88,7 @@ const useLatestBalance = ( return useMemo( () => - value?.value - ? calcTokenAmount(value.value.toString(), token?.decimals) - : undefined, + value?.value ? calcTokenAmount(value.value, token?.decimals) : undefined, [value.value, token?.decimals], ); }; diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 49c4aec01cd2..f1449ecf8b93 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -10,6 +10,7 @@ import { type BridgeToken, isNativeAddress, fetchBridgeTokens, + BridgeClientId, } from '@metamask/bridge-controller'; import { getAllDetectedTokensForSelectedAddress, @@ -37,6 +38,8 @@ import { type BridgeAppState, getTopAssetsFromFeatureFlags, } from '../../ducks/bridge/selectors'; +import fetchWithCache from '../../../shared/lib/fetch-with-cache'; +import { BRIDGE_API_BASE_URL } from '../../../shared/constants/bridge'; type FilterPredicate = ( symbol: string, @@ -85,7 +88,20 @@ export const useTokensWithFiltering = ( return cachedTokens[hexChainId]?.data; } // Otherwise fetch new token data - return await fetchBridgeTokens(hexChainId); + return await fetchBridgeTokens( + hexChainId, + BridgeClientId.EXTENSION, + async (url, options) => { + const { headers, ...requestOptions } = options ?? {}; + return await fetchWithCache({ + url: url as string, + ...requestOptions, + fetchOptions: { method: 'GET', headers }, + functionName: 'fetchBridgeTokens', + }); + }, + BRIDGE_API_BASE_URL, + ); } if (chainId && isSolanaChainId(chainId)) { return await fetchNonEvmTokens(formatChainIdToCaip(chainId)); From 54f8d436084811c7df22eaf3f6523c03f1d5dffb Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 18:59:04 -0700 Subject: [PATCH 07/71] refactor: use SWAPS_CHAINID_DEFAULT_TOKEN_MAP from core --- ui/ducks/bridge/selectors.ts | 2 +- ui/hooks/bridge/useIsTxSubmittable.ts | 2 +- ui/hooks/bridge/useQuoteFetchEvents.ts | 2 +- ui/pages/bridge/prepare/bridge-cta-button.tsx | 2 +- ui/pages/bridge/prepare/prepare-bridge-page.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 7ed5c2f9a816..c3d57c18a462 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -27,6 +27,7 @@ import { formatChainIdToCaip, BRIDGE_PREFERRED_GAS_ESTIMATE, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, } from '@metamask/bridge-controller'; import { MultichainNetworks, @@ -43,7 +44,6 @@ import { } from '../../selectors/selectors'; import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge'; import { createDeepEqualSelector } from '../../../shared/modules/selectors/util'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps'; import { getNetworkConfigurationsByChainId } from '../../../shared/modules/selectors/networks'; import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask'; import { diff --git a/ui/hooks/bridge/useIsTxSubmittable.ts b/ui/hooks/bridge/useIsTxSubmittable.ts index 32f3d98ed92e..166763dcf128 100644 --- a/ui/hooks/bridge/useIsTxSubmittable.ts +++ b/ui/hooks/bridge/useIsTxSubmittable.ts @@ -1,5 +1,5 @@ import { useSelector } from 'react-redux'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; import { getBridgeQuotes, getFromAmount, diff --git a/ui/hooks/bridge/useQuoteFetchEvents.ts b/ui/hooks/bridge/useQuoteFetchEvents.ts index d890d5b30e60..82a9be56a46f 100644 --- a/ui/hooks/bridge/useQuoteFetchEvents.ts +++ b/ui/hooks/bridge/useQuoteFetchEvents.ts @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; import { MetaMetricsEventName } from '../../../shared/constants/metametrics'; import { getBridgeQuotes, @@ -10,7 +11,6 @@ import { getQuoteRequest, getValidationErrors, } from '../../ducks/bridge/selectors'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps'; import { useCrossChainSwapsEventTracker } from './useCrossChainSwapsEventTracker'; import useLatestBalance from './useLatestBalance'; import { useRequestMetadataProperties } from './events/useRequestMetadataProperties'; diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index ee91053b3b94..1bb834898c91 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; import { ButtonLink, ButtonPrimary, @@ -33,7 +34,6 @@ import { useRequestProperties } from '../../../hooks/bridge/events/useRequestPro import { useRequestMetadataProperties } from '../../../hooks/bridge/events/useRequestMetadataProperties'; import { useTradeProperties } from '../../../hooks/bridge/events/useTradeProperties'; import { MetaMetricsEventName } from '../../../../shared/constants/metametrics'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../../shared/constants/swaps'; import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { Row } from '../layout'; import { isQuoteExpired as isQuoteExpiredUtil } from '../utils/quote'; diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 2472b0bc2ba2..36b28d7b6b87 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -17,6 +17,7 @@ import { isValidQuoteRequest, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, type GenericQuoteRequest, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, } from '@metamask/bridge-controller'; import { setFromToken, @@ -67,7 +68,6 @@ import { TextVariant, } from '../../../helpers/constants/design-system'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../../shared/constants/swaps'; import { useTokensWithFiltering } from '../../../hooks/bridge/useTokensWithFiltering'; import { setActiveNetwork, From 4aced8e3ccac047f5d60fc936677b1277ac637b4 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 19:00:44 -0700 Subject: [PATCH 08/71] fix: use bridge token endpoint for solana --- .../modules/bridge-utils/bridge.util.test.ts | 364 ------------------ shared/modules/bridge-utils/bridge.util.ts | 48 --- shared/modules/bridge-utils/validators.ts | 51 --- ui/ducks/bridge/bridge.ts | 11 +- .../bridge/useTokensWithFiltering.test.ts | 2 +- ui/hooks/bridge/useTokensWithFiltering.ts | 150 +++----- 6 files changed, 71 insertions(+), 555 deletions(-) delete mode 100644 shared/modules/bridge-utils/bridge.util.test.ts delete mode 100644 shared/modules/bridge-utils/bridge.util.ts delete mode 100644 shared/modules/bridge-utils/validators.ts diff --git a/shared/modules/bridge-utils/bridge.util.test.ts b/shared/modules/bridge-utils/bridge.util.test.ts deleted file mode 100644 index 60ca6255baa1..000000000000 --- a/shared/modules/bridge-utils/bridge.util.test.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { zeroAddress } from 'ethereumjs-util'; -import fetchWithCache from '../../lib/fetch-with-cache'; -import mockBridgeQuotesErc20Erc20 from '../../../test/data/bridge/mock-quotes-erc20-erc20.json'; -import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json'; -import { ChainId } from '../../types/bridge'; -import { - fetchBridgeFeatureFlags, - fetchBridgeQuotes, - fetchBridgeTokens, -} from './bridge.util'; - -jest.mock('../../../shared/lib/fetch-with-cache'); - -describe('Bridge utils', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('fetchBridgeFeatureFlags', () => { - it('should fetch bridge feature flags successfully', async () => { - const mockResponse = { - 'extension-config': { - refreshRate: 3, - maxRefreshCount: 1, - support: true, - chains: { - '1': { - isActiveSrc: true, - isActiveDest: true, - }, - '10': { - isActiveSrc: true, - isActiveDest: false, - }, - '59144': { - isActiveSrc: true, - isActiveDest: true, - }, - '120': { - isActiveSrc: true, - isActiveDest: false, - }, - '137': { - isActiveSrc: false, - isActiveDest: true, - }, - '11111': { - isActiveSrc: false, - isActiveDest: true, - }, - [ChainId.SOLANA]: { - isActiveSrc: false, - isActiveDest: true, - }, - }, - }, - }; - - (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); - - const result = await fetchBridgeFeatureFlags(); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - }, - cacheOptions: { cacheRefreshTime: 600000 }, - functionName: 'fetchBridgeFeatureFlags', - }); - - expect(result).toStrictEqual({ - extensionConfig: { - maxRefreshCount: 1, - refreshRate: 3, - support: true, - chains: { - 'eip155:1': { - isActiveSrc: true, - isActiveDest: true, - }, - 'eip155:10': { - isActiveSrc: true, - isActiveDest: false, - }, - 'eip155:59144': { - isActiveSrc: true, - isActiveDest: true, - }, - 'eip155:120': { - isActiveSrc: true, - isActiveDest: false, - }, - 'eip155:11111': { - isActiveSrc: false, - isActiveDest: true, - }, - 'eip155:137': { - isActiveSrc: false, - isActiveDest: true, - }, - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - isActiveSrc: false, - isActiveDest: true, - }, - }, - }, - }); - }); - - it('should use fallback bridge feature flags if response is unexpected', async () => { - const mockResponse = { - 'extension-config': { - refreshRate: 3, - maxRefreshCount: 1, - support: 25, - chains: { - a: { - isActiveSrc: 1, - isActiveDest: 'test', - }, - '2': { - isActiveSrc: 'test', - isActiveDest: 2, - }, - }, - }, - }; - - (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); - - const result = await fetchBridgeFeatureFlags(); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - }, - cacheOptions: { cacheRefreshTime: 600000 }, - functionName: 'fetchBridgeFeatureFlags', - }); - - expect(result).toStrictEqual({ - extensionConfig: { - maxRefreshCount: 5, - refreshRate: 30000, - support: false, - chains: {}, - }, - }); - }); - - it('should handle fetch error', async () => { - const mockError = new Error('Failed to fetch'); - - (fetchWithCache as jest.Mock).mockRejectedValue(mockError); - - await expect(fetchBridgeFeatureFlags()).rejects.toThrow(mockError); - }); - }); - - describe('fetchBridgeTokens', () => { - it('should fetch bridge tokens successfully', async () => { - const mockResponse = [ - { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - symbol: 'ABC', - decimals: 16, - }, - { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', - decimals: 16, - }, - { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', - decimals: 16, - symbol: 'DEF', - aggregators: ['lifi'], - }, - { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f987', - symbol: 'DEF', - }, - { - address: 'NONevmTOken1324fgcdrskljffsiodujfkl,jfd', - symbol: 'JKL', - decimals: 16, - }, - ]; - - (fetchWithCache as jest.Mock).mockResolvedValue(mockResponse); - - const result = await fetchBridgeTokens('0xa'); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - }, - cacheOptions: { cacheRefreshTime: 600000 }, - functionName: 'fetchBridgeTokens', - }); - - expect(result).toStrictEqual({ - '0x0000000000000000000000000000000000000000': { - address: '0x0000000000000000000000000000000000000000', - decimals: 18, - iconUrl: './images/eth_logo.svg', - name: 'Ether', - symbol: 'ETH', - }, - '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986': { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', - decimals: 16, - symbol: 'DEF', - aggregators: ['lifi'], - }, - 'NONevmTOken1324fgcdrskljffsiodujfkl,jfd': { - address: 'NONevmTOken1324fgcdrskljffsiodujfkl,jfd', - decimals: 16, - symbol: 'JKL', - }, - '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { - address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - decimals: 16, - symbol: 'ABC', - }, - }); - }); - - it('should handle fetch error', async () => { - const mockError = new Error('Failed to fetch'); - - (fetchWithCache as jest.Mock).mockRejectedValue(mockError); - - await expect(fetchBridgeTokens('0xa')).rejects.toThrow(mockError); - }); - }); - - describe('fetchBridgeQuotes', () => { - it('should fetch bridge quotes successfully, no approvals', async () => { - (fetchWithCache as jest.Mock).mockResolvedValue( - mockBridgeQuotesNativeErc20, - ); - const { signal } = new AbortController(); - - const result = await fetchBridgeQuotes( - { - walletAddress: '0x123', - srcChainId: 1, - destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), - srcTokenAmount: '20000', - slippage: 0.5, - }, - signal, - ); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&destWalletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - expect(result).toStrictEqual(mockBridgeQuotesNativeErc20); - }); - - it('should fetch bridge quotes successfully, with approvals', async () => { - (fetchWithCache as jest.Mock).mockResolvedValue( - mockBridgeQuotesErc20Erc20, - ); - const { signal } = new AbortController(); - - const result = await fetchBridgeQuotes( - { - walletAddress: '0x123', - srcChainId: 1, - destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), - srcTokenAmount: '20000', - slippage: 0.5, - }, - signal, - ); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&destWalletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); - }); - - it('should filter out malformed bridge quotes', async () => { - (fetchWithCache as jest.Mock).mockResolvedValue([ - ...mockBridgeQuotesErc20Erc20, - ...mockBridgeQuotesErc20Erc20.map( - ({ quote, ...restOfQuote }) => restOfQuote, - ), - { - ...mockBridgeQuotesErc20Erc20[0], - quote: { - srcAsset: { - ...mockBridgeQuotesErc20Erc20[0].quote.srcAsset, - decimals: undefined, - }, - }, - }, - { - ...mockBridgeQuotesErc20Erc20[1], - quote: { - srcAsset: { - ...mockBridgeQuotesErc20Erc20[1].quote.destAsset, - address: undefined, - }, - }, - }, - ]); - const { signal } = new AbortController(); - - const result = await fetchBridgeQuotes( - { - walletAddress: '0x123', - srcChainId: 1, - destChainId: 10, - srcTokenAddress: zeroAddress(), - destTokenAddress: zeroAddress(), - srcTokenAmount: '20000', - slippage: 0.5, - }, - signal, - ); - - expect(fetchWithCache).toHaveBeenCalledWith({ - url: 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&destWalletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', - fetchOptions: { - method: 'GET', - headers: { 'X-Client-Id': 'extension' }, - signal, - }, - cacheOptions: { cacheRefreshTime: 0 }, - functionName: 'fetchBridgeQuotes', - }); - - expect(result).toStrictEqual(mockBridgeQuotesErc20Erc20); - }); - }); -}); diff --git a/shared/modules/bridge-utils/bridge.util.ts b/shared/modules/bridge-utils/bridge.util.ts deleted file mode 100644 index c9f941da5a10..000000000000 --- a/shared/modules/bridge-utils/bridge.util.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CaipChainId } from '@metamask/utils'; -import { BridgeClientId } from '@metamask/bridge-controller'; -import { STATIC_METAMASK_BASE_URL } from '../../constants/bridge'; -import fetchWithCache from '../../lib/fetch-with-cache'; -import { TOKEN_API_BASE_URL } from '../../constants/swaps'; - -import { validateResponse, ASSET_VALIDATORS } from './validators'; - -const CLIENT_ID_HEADER = { 'X-Client-Id': BridgeClientId.EXTENSION }; - -type TokenV3Asset = { - assetId: string; - symbol: string; - name: string; - decimals: number; -}; - -// Returns a list of non-EVM assets -export async function fetchNonEvmTokens( - chainId: CaipChainId, -): Promise> { - const url = `${TOKEN_API_BASE_URL}/v3/chains/${chainId}/assets?first=15000`; - const { data: tokens } = await fetchWithCache({ - url, - fetchOptions: { method: 'GET', headers: CLIENT_ID_HEADER }, - cacheOptions: { cacheRefreshTime: 60000 }, - functionName: 'fetchNonEvmTokens', - }); - - const transformedTokens: Record = {}; - tokens.forEach((token: unknown) => { - if (validateResponse(ASSET_VALIDATORS, token, url, false)) { - transformedTokens[token.assetId] = token; - } - }); - return transformedTokens; -} - -export const isTokenV3Asset = (asset: object): asset is TokenV3Asset => { - return 'assetId' in asset && typeof asset.assetId === 'string'; -}; - -// Returns the image url for a caip-formatted asset -export const getAssetImageUrl = (assetId: string) => - `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${assetId?.replaceAll( - ':', - '/', - )}.png`; diff --git a/shared/modules/bridge-utils/validators.ts b/shared/modules/bridge-utils/validators.ts deleted file mode 100644 index cdcdf7f3c437..000000000000 --- a/shared/modules/bridge-utils/validators.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { validateData } from '../../lib/swaps-utils'; - -type Validator = { - property: keyof ExpectedResponse | string; - type: string; - validator?: (value: unknown) => boolean; -}; - -export const validateResponse = ( - validators: Validator[], - data: unknown, - urlUsed: string, - logError = true, -): data is ExpectedResponse => { - return validateData(validators, data, urlUsed, logError); -}; - -export const isValidNumber = (v: unknown): v is number => typeof v === 'number'; -const isValidObject = (v: unknown): v is object => - typeof v === 'object' && v !== null; -const isValidString = (v: unknown): v is string => - typeof v === 'string' && v.length > 0; - -export const TOKEN_AGGREGATOR_VALIDATORS = [ - { - property: 'aggregators', - type: 'object', - validator: (v: unknown): v is number[] => - isValidObject(v) && Object.values(v).every(isValidString), - }, -]; - -export const ASSET_VALIDATORS = [ - { property: 'decimals', type: 'number' }, - { property: 'assetId', type: 'string', validator: isValidString }, - { - property: 'symbol', - type: 'string', - validator: (v: unknown) => isValidString(v) && v.length <= 12, - }, -]; - -export const TOKEN_VALIDATORS = [ - { property: 'decimals', type: 'number' }, - { property: 'address', type: 'string', validator: isValidString }, - { - property: 'symbol', - type: 'string', - validator: (v: unknown) => isValidString(v) && v.length <= 12, - }, -]; diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index c1415bee47b4..908d4d1604af 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -1,6 +1,5 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { type Hex, type CaipChainId } from '@metamask/utils'; -import { zeroAddress } from 'ethereumjs-util'; import { type BridgeToken, ChainId, @@ -9,6 +8,8 @@ import { SortOrder, BRIDGE_DEFAULT_SLIPPAGE, formatChainIdToCaip, + formatChainIdToHexOrCaip, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, } from '@metamask/bridge-controller'; import { getTokenExchangeRate } from './utils'; @@ -95,7 +96,13 @@ const bridgeSlice = createSlice({ balance: payload.balance ?? '0', string: payload.string ?? '0', chainId: payload.chainId, - address: payload.address || zeroAddress(), + address: + payload.address || + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + formatChainIdToHexOrCaip( + payload.chainId, + ) as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]?.address, }; } else { state.toToken = payload; diff --git a/ui/hooks/bridge/useTokensWithFiltering.test.ts b/ui/hooks/bridge/useTokensWithFiltering.test.ts index 2963e0940acf..dc6e7074ef68 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.test.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.test.ts @@ -1,7 +1,7 @@ +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; import { renderHookWithProvider } from '../../../test/lib/render-helpers'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { STATIC_MAINNET_TOKEN_LIST } from '../../../shared/constants/tokens'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { MINUTE } from '../../../shared/constants/time'; import { useTokensWithFiltering } from './useTokensWithFiltering'; diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index f1449ecf8b93..5577aecaeaa8 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -1,8 +1,7 @@ import { useCallback } from 'react'; import { useSelector } from 'react-redux'; import { ChainId } from '@metamask/controller-utils'; -import { type CaipChainId, isStrictHexString, type Hex } from '@metamask/utils'; -import { zeroAddress } from 'ethereumjs-util'; +import { type CaipChainId, type Hex } from '@metamask/utils'; import { isSolanaChainId, formatChainIdToCaip, @@ -11,35 +10,34 @@ import { isNativeAddress, fetchBridgeTokens, BridgeClientId, + type BridgeAsset, + formatChainIdToHexOrCaip, } from '@metamask/bridge-controller'; import { getAllDetectedTokensForSelectedAddress, selectERC20TokensByChain, } from '../../selectors'; -import { SwapsTokenObject } from '../../../shared/constants/swaps'; -import { - AssetWithDisplayData, - ERC20Asset, - NativeAsset, -} from '../../components/multichain/asset-picker-amount/asset-picker-modal/types'; import { AssetType } from '../../../shared/constants/transaction'; import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../shared/constants/network'; import { Token } from '../../components/app/assets/types'; import { useMultichainBalances } from '../useMultichainBalances'; import { useAsyncResult } from '../useAsyncResult'; import { fetchTopAssetsList } from '../../pages/swaps/swaps.util'; -import { - fetchNonEvmTokens, - getAssetImageUrl, - isTokenV3Asset, -} from '../../../shared/modules/bridge-utils/bridge.util'; import { MINUTE } from '../../../shared/constants/time'; import { type BridgeAppState, getTopAssetsFromFeatureFlags, } from '../../ducks/bridge/selectors'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; -import { BRIDGE_API_BASE_URL } from '../../../shared/constants/bridge'; +import { + BRIDGE_API_BASE_URL, + STATIC_METAMASK_BASE_URL, +} from '../../../shared/constants/bridge'; +import type { + AssetWithDisplayData, + ERC20Asset, + NativeAsset, +} from '../../components/multichain/asset-picker-amount/asset-picker-modal/types'; type FilterPredicate = ( symbol: string, @@ -47,6 +45,13 @@ type FilterPredicate = ( tokenChainId?: string, ) => boolean; +// Returns the image url for a caip-formatted asset +const getAssetImageUrl = (assetId: string) => + `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${assetId?.replaceAll( + ':', + '/', + )}.png`; + /** * Returns a token list generator that filters and sorts tokens in this order * - matches search query @@ -78,18 +83,20 @@ export const useTokensWithFiltering = ( const cachedTokens = useSelector(selectERC20TokensByChain); const { value: tokenList, pending: isTokenListLoading } = useAsyncResult< - Record + Record >(async () => { - if (chainId && !isSolanaChainId(chainId)) { - const hexChainId = formatChainIdToHex(chainId); - const timestamp = cachedTokens[hexChainId]?.timestamp; - // Use cached token data if updated in the last 10 minutes - if (timestamp && Date.now() - timestamp <= 10 * MINUTE) { - return cachedTokens[hexChainId]?.data; + if (chainId) { + if (!isSolanaChainId(chainId)) { + const hexChainId = formatChainIdToHex(chainId); + const timestamp = cachedTokens[hexChainId]?.timestamp; + // Use cached token data if updated in the last 10 minutes + if (timestamp && Date.now() - timestamp <= 10 * MINUTE) { + return cachedTokens[hexChainId]?.data; + } } // Otherwise fetch new token data return await fetchBridgeTokens( - hexChainId, + chainId, BridgeClientId.EXTENSION, async (url, options) => { const { headers, ...requestOptions } = options ?? {}; @@ -103,9 +110,7 @@ export const useTokensWithFiltering = ( BRIDGE_API_BASE_URL, ); } - if (chainId && isSolanaChainId(chainId)) { - return await fetchNonEvmTokens(formatChainIdToCaip(chainId)); - } + return {}; }, [chainId, cachedTokens]); @@ -127,24 +132,30 @@ export const useTokensWithFiltering = ( // This transforms the token object from the bridge-api into the format expected by the AssetPicker const buildTokenData = ( - token?: SwapsTokenObject, - ): AssetWithDisplayData | undefined => { - if (!chainId || !token || !isStrictHexString(chainId)) { + token?: BridgeAsset, + ): + | AssetWithDisplayData + | AssetWithDisplayData + | undefined => { + if (!chainId || !token) { return undefined; } - const hexChainId = formatChainIdToHex(chainId); // Only tokens on the active chain are processed here here - const sharedFields = { ...token, chainId: hexChainId }; + const sharedFields = { + ...token, + chainId: formatChainIdToHexOrCaip(chainId), + assetId: token.assetId, + }; if (isNativeAddress(token.address)) { return { ...sharedFields, type: AssetType.native, - address: token.address === zeroAddress() ? null : token.address, + address: '', // Return empty string to match useMultichainBalances output image: CHAIN_ID_TOKEN_IMAGE_MAP[ - chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP - ], + sharedFields.chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP + ] ?? token.iconUrl, // Only unimported native assets are processed here so hardcode balance to 0 balance: '0', string: '0', @@ -158,7 +169,7 @@ export const useTokensWithFiltering = ( // Only tokens with 0 balance are processed here so hardcode empty string balance: '', string: undefined, - address: token.address, + address: isSolanaChainId(chainId) ? token.assetId : token.address, }; }; @@ -200,14 +211,13 @@ export const useTokensWithFiltering = ( token.chainId, ) ) { - // If there's no address, set it to the native address in swaps/bridge if (isNativeAddress(token.address)) { yield { symbol: token.symbol, chainId: token.chainId, tokenFiatAmount: token.tokenFiatAmount, decimals: token.decimals, - address: token.address, + address: '', // token.address, type: AssetType.native, balance: token.balance ?? '0', string: token.string ?? undefined, @@ -227,66 +237,22 @@ export const useTokensWithFiltering = ( balance: token.balance ?? '', string: token.string ?? undefined, image: - tokenList?.[token.address.toLowerCase()]?.iconUrl ?? + (token.image || + tokenList?.[token.address.toLowerCase()]?.iconUrl) ?? getAssetImageUrl(token.address), }; } } } - // Yield tokens for solana from TokenApi V3 then return - if (isSolanaChainId(chainId)) { - // Yield topTokens from selected chain - for (const { address: tokenAddress } of topTokens) { - const assetId = `${chainId}/token:${tokenAddress}`; - const matchedToken = tokenList?.[assetId]; - if ( - matchedToken && - isTokenV3Asset(matchedToken) && - shouldAddToken(matchedToken.symbol, matchedToken.assetId, chainId) - ) { - yield { - ...matchedToken, - type: AssetType.token, - image: getAssetImageUrl(assetId), - balance: '', - string: undefined, - address: assetId, - chainId, - }; - } - } - - // Yield Solana top tokens - for (const token_ of Object.values(tokenList)) { - if ( - token_ && - !token_.symbol.includes('$') && - isTokenV3Asset(token_) && - shouldAddToken(token_.symbol, token_.assetId, chainId) - ) { - yield { - ...token_, - type: AssetType.token, - image: getAssetImageUrl(token_.assetId), - balance: '', - string: undefined, - address: token_.assetId, - chainId, - }; - } - } - return; - } - - // Yield topTokens from selected EVM chain + // Yield topTokens from selected chain for (const token_ of topTokens) { const matchedToken = tokenList?.[token_.address]; + const token = buildTokenData(matchedToken); if ( - matchedToken && - shouldAddToken(matchedToken.symbol, matchedToken.address, chainId) + token && + shouldAddToken(token.symbol, token.address ?? undefined, chainId) ) { - const token = buildTokenData(matchedToken); if (token) { yield token; } @@ -295,11 +261,17 @@ export const useTokensWithFiltering = ( // Yield other tokens from selected chain for (const token_ of Object.values(tokenList)) { + const token = buildTokenData(token_); if ( - token_ && - shouldAddToken(token_.symbol, token_.address, chainId) + token && + !token.symbol.includes('$') && + shouldAddToken( + token.symbol, + // useMultichainBalances returns the assetId for solana tokens + token.address ?? undefined, + chainId, + ) ) { - const token = buildTokenData(token_); if (token) { yield token; } From 734c9d85b08cb9ce09955a29220e28aeb659e77f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 19:01:16 -0700 Subject: [PATCH 09/71] fix: token alerts payload --- ui/hooks/bridge/useTokenAlerts.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/hooks/bridge/useTokenAlerts.ts b/ui/hooks/bridge/useTokenAlerts.ts index 6b44e4a4829d..22384d1bb726 100644 --- a/ui/hooks/bridge/useTokenAlerts.ts +++ b/ui/hooks/bridge/useTokenAlerts.ts @@ -1,4 +1,5 @@ import { useSelector } from 'react-redux'; +import { formatAddressToString } from '@metamask/bridge-controller'; import { getFromToken, getFromChain, @@ -26,7 +27,10 @@ export const useTokenAlerts = () => { toChain?.chainId as AllowedBridgeChainIds, ); if (chainName) { - return await fetchTokenAlert(chainName, toToken.address); + return await fetchTokenAlert( + chainName, + formatAddressToString(toToken.address), + ); } } return null; From 6fcc731ff8d3cc1583f5edd579e45963c3f5a4af Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 10:53:15 -0700 Subject: [PATCH 10/71] chore: use token types from bridge-controller --- ui/hooks/bridge/useBridging.ts | 6 ++++-- ui/hooks/swap/useSwapDefaultToToken.ts | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts index 12b8ecce2283..0a778ef0f5b7 100644 --- a/ui/hooks/bridge/useBridging.ts +++ b/ui/hooks/bridge/useBridging.ts @@ -3,7 +3,10 @@ import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { toChecksumAddress } from 'ethereumjs-util'; import { isStrictHexString } from '@metamask/utils'; -import { formatChainIdToCaip } from '@metamask/bridge-controller'; +import { + formatChainIdToCaip, + type SwapsTokenObject, +} from '@metamask/bridge-controller'; import { setBridgeFeatureFlags } from '../../ducks/bridge/actions'; import { ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -31,7 +34,6 @@ import { } from '../../helpers/constants/routes'; ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) import { getPortfolioUrl } from '../../helpers/utils/portfolio'; -import { SwapsTokenObject } from '../../../shared/constants/swaps'; import { getProviderConfig } from '../../../shared/modules/selectors/networks'; import { useCrossChainSwapsEventTracker } from './useCrossChainSwapsEventTracker'; ///: END:ONLY_INCLUDE_IF diff --git a/ui/hooks/swap/useSwapDefaultToToken.ts b/ui/hooks/swap/useSwapDefaultToToken.ts index 0c301445c448..6aae658ada40 100644 --- a/ui/hooks/swap/useSwapDefaultToToken.ts +++ b/ui/hooks/swap/useSwapDefaultToToken.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { isEqual } from 'lodash'; -import { getCurrentChainId } from '../../../shared/modules/selectors/networks'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP, - SWAPS_CHAINID_COMMON_TOKEN_PAIR, - SwapsTokenObject, -} from '../../../shared/constants/swaps'; + type SwapsTokenObject, +} from '@metamask/bridge-controller'; +import { getCurrentChainId } from '../../../shared/modules/selectors/networks'; +import { SWAPS_CHAINID_COMMON_TOKEN_PAIR } from '../../../shared/constants/swaps'; import { getFromToken } from '../../ducks/swaps/swaps'; type UseSwapDefaultToTokenReturnType = { From 2e47491b85d80921a856d1804834a4c995fd1080 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 11:49:56 -0700 Subject: [PATCH 11/71] chore: clean up comments --- ui/hooks/bridge/useTokensWithFiltering.ts | 9 ++------- ui/pages/bridge/prepare/prepare-bridge-page.tsx | 5 ----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 5577aecaeaa8..321596607488 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -217,7 +217,7 @@ export const useTokensWithFiltering = ( chainId: token.chainId, tokenFiatAmount: token.tokenFiatAmount, decimals: token.decimals, - address: '', // token.address, + address: '', type: AssetType.native, balance: token.balance ?? '0', string: token.string ?? undefined, @@ -265,12 +265,7 @@ export const useTokensWithFiltering = ( if ( token && !token.symbol.includes('$') && - shouldAddToken( - token.symbol, - // useMultichainBalances returns the assetId for solana tokens - token.address ?? undefined, - chainId, - ) + shouldAddToken(token.symbol, token.address ?? undefined, chainId) ) { if (token) { yield token; diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 36b28d7b6b87..2d21a262675d 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -319,10 +319,6 @@ const PrepareBridgePage = () => { : undefined, srcChainId: fromChain?.chainId, destChainId: toChain?.chainId, - // This override allows quotes to be returned when the rpcUrl is a tenderly fork - // Otherwise quotes get filtered out by the bridge-api when the wallet's real - // balance is less than the tenderly balance - // insufficientBal: Boolean(providerConfig?.rpcUrl?.includes('tenderly')), slippage, walletAddress: selectedAccount?.address ?? '', destWalletAddress: selectedDestinationAccount?.address, @@ -334,7 +330,6 @@ const PrepareBridgePage = () => { fromAmount, fromChain?.chainId, toChain?.chainId, - // providerConfig?.rpcUrl, slippage, selectedAccount?.address, selectedDestinationAccount?.address, From ac7af48514fbe364c2ef69aace761150d10bb803 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:50:15 -0700 Subject: [PATCH 12/71] refactor: use getNativeAssetForChainId --- ui/ducks/bridge/bridge.ts | 9 ++------- ui/ducks/bridge/selectors.ts | 6 ++---- ui/hooks/bridge/useIsTxSubmittable.ts | 8 ++------ ui/hooks/bridge/useQuoteFetchEvents.ts | 8 ++------ ui/hooks/bridge/useTokensWithFiltering.test.ts | 4 ++-- ui/pages/bridge/prepare/bridge-cta-button.tsx | 8 ++------ ui/pages/bridge/prepare/prepare-bridge-page.tsx | 6 ++---- 7 files changed, 14 insertions(+), 35 deletions(-) diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 908d4d1604af..32e022046262 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -8,8 +8,7 @@ import { SortOrder, BRIDGE_DEFAULT_SLIPPAGE, formatChainIdToCaip, - formatChainIdToHexOrCaip, - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + getNativeAssetForChainId, } from '@metamask/bridge-controller'; import { getTokenExchangeRate } from './utils'; @@ -98,11 +97,7 @@ const bridgeSlice = createSlice({ chainId: payload.chainId, address: payload.address || - SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - formatChainIdToHexOrCaip( - payload.chainId, - ) as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ]?.address, + getNativeAssetForChainId(payload.chainId)?.address, }; } else { state.toToken = payload; diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index c3d57c18a462..3788553fcb9f 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -27,7 +27,7 @@ import { formatChainIdToCaip, BRIDGE_PREFERRED_GAS_ESTIMATE, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + getNativeAssetForChainId, } from '@metamask/bridge-controller'; import { MultichainNetworks, @@ -211,9 +211,7 @@ export const getFromToken = createSelector( return fromToken; } return { - ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ], + ...getNativeAssetForChainId(fromChain.chainId), chainId: formatChainIdToCaip(fromChain.chainId), image: CHAIN_ID_TOKEN_IMAGE_MAP[ diff --git a/ui/hooks/bridge/useIsTxSubmittable.ts b/ui/hooks/bridge/useIsTxSubmittable.ts index 166763dcf128..3fe3229315e6 100644 --- a/ui/hooks/bridge/useIsTxSubmittable.ts +++ b/ui/hooks/bridge/useIsTxSubmittable.ts @@ -1,5 +1,5 @@ import { useSelector } from 'react-redux'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; +import { getNativeAssetForChainId } from '@metamask/bridge-controller'; import { getBridgeQuotes, getFromAmount, @@ -30,11 +30,7 @@ export const useIsTxSubmittable = () => { const balanceAmount = useLatestBalance(fromToken, fromChainId); const nativeAssetBalance = useLatestBalance( - fromChainId - ? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - fromChainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ] - : null, + fromChainId ? getNativeAssetForChainId(fromChainId) : null, fromChainId, ); diff --git a/ui/hooks/bridge/useQuoteFetchEvents.ts b/ui/hooks/bridge/useQuoteFetchEvents.ts index 82a9be56a46f..c6e29e101125 100644 --- a/ui/hooks/bridge/useQuoteFetchEvents.ts +++ b/ui/hooks/bridge/useQuoteFetchEvents.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import { useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; +import { getNativeAssetForChainId } from '@metamask/bridge-controller'; import { MetaMetricsEventName } from '../../../shared/constants/metametrics'; import { getBridgeQuotes, @@ -46,11 +46,7 @@ export const useQuoteFetchEvents = () => { const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); const nativeAssetBalance = useLatestBalance( - fromChain?.chainId - ? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ] - : null, + fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, fromChain?.chainId, ); diff --git a/ui/hooks/bridge/useTokensWithFiltering.test.ts b/ui/hooks/bridge/useTokensWithFiltering.test.ts index dc6e7074ef68..371438a67073 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.test.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.test.ts @@ -1,4 +1,4 @@ -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; +import { getNativeAssetForChainId } from '@metamask/bridge-controller'; import { renderHookWithProvider } from '../../../test/lib/render-helpers'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { STATIC_MAINNET_TOKEN_LIST } from '../../../shared/constants/tokens'; @@ -6,7 +6,7 @@ import { CHAIN_IDS } from '../../../shared/constants/network'; import { MINUTE } from '../../../shared/constants/time'; import { useTokensWithFiltering } from './useTokensWithFiltering'; -const NATIVE_TOKEN = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[CHAIN_IDS.MAINNET]; +const NATIVE_TOKEN = getNativeAssetForChainId(CHAIN_IDS.MAINNET); const mockFetchBridgeTokens = jest.fn().mockResolvedValue({ [NATIVE_TOKEN.address]: NATIVE_TOKEN, diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index 1bb834898c91..f750f7fc3e19 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '@metamask/bridge-controller'; +import { getNativeAssetForChainId } from '@metamask/bridge-controller'; import { ButtonLink, ButtonPrimary, @@ -77,11 +77,7 @@ export const BridgeCTAButton = ({ const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); const nativeAssetBalance = useLatestBalance( - fromChain?.chainId - ? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ] - : null, + fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, fromChain?.chainId, ); diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 2d21a262675d..0b162bc83382 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -17,7 +17,7 @@ import { isValidQuoteRequest, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, type GenericQuoteRequest, - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + getNativeAssetForChainId, } from '@metamask/bridge-controller'; import { setFromToken, @@ -190,9 +190,7 @@ const PrepareBridgePage = () => { const { openBuyCryptoInPdapp } = useRamps(); const nativeAssetBalance = useLatestBalance( - SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - fromChain?.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ], + fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, fromChain?.chainId, ); From 22c403970bfa2408f4e57a222a7a45b2bead81d1 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:50:56 -0700 Subject: [PATCH 13/71] fix: undo using bridge-controller constants in useSwapDefaultToToken --- ui/hooks/swap/useSwapDefaultToToken.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/hooks/swap/useSwapDefaultToToken.ts b/ui/hooks/swap/useSwapDefaultToToken.ts index 6aae658ada40..6589fb3d58cd 100644 --- a/ui/hooks/swap/useSwapDefaultToToken.ts +++ b/ui/hooks/swap/useSwapDefaultToToken.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { isEqual } from 'lodash'; +import { getCurrentChainId } from '../../../shared/modules/selectors/networks'; import { - SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SWAPS_CHAINID_COMMON_TOKEN_PAIR, type SwapsTokenObject, -} from '@metamask/bridge-controller'; -import { getCurrentChainId } from '../../../shared/modules/selectors/networks'; -import { SWAPS_CHAINID_COMMON_TOKEN_PAIR } from '../../../shared/constants/swaps'; + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, +} from '../../../shared/constants/swaps'; import { getFromToken } from '../../ducks/swaps/swaps'; type UseSwapDefaultToTokenReturnType = { From b660414856f435a01a6ea311619ea1b72fa23b1e Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:51:31 -0700 Subject: [PATCH 14/71] fix: invalid type imports --- shared/types/bridge-status.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shared/types/bridge-status.ts b/shared/types/bridge-status.ts index 9a33a2082a26..e215139c0bb4 100644 --- a/shared/types/bridge-status.ts +++ b/shared/types/bridge-status.ts @@ -2,12 +2,17 @@ import { TransactionControllerState, TransactionMeta, } from '@metamask/transaction-controller'; +import type { + ChainId, + Quote, + QuoteMetadata, + QuoteResponse, +} from '@metamask/bridge-controller'; import { NetworkState, ProviderConfigState, } from '../modules/selectors/networks'; import { SmartTransactionsMetaMaskState } from '../modules/selectors'; -import type { ChainId, Quote, QuoteMetadata, QuoteResponse } from './bridge'; // All fields need to be types not interfaces, same with their children fields // o/w you get a type error From b0e8b3778fde267c80ef116e4f6a76010c33e402 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:54:37 -0700 Subject: [PATCH 15/71] refactor: caip formatter updates --- ui/hooks/bridge/useBridgeChainInfo.ts | 62 ++++++++++--------- ui/hooks/bridge/useTokenAlerts.ts | 4 +- ui/hooks/bridge/useTokensWithFiltering.ts | 5 +- .../prepare/prepare-bridge-page.test.tsx | 6 +- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/ui/hooks/bridge/useBridgeChainInfo.ts b/ui/hooks/bridge/useBridgeChainInfo.ts index 25cc7a168d8f..13b90497493a 100644 --- a/ui/hooks/bridge/useBridgeChainInfo.ts +++ b/ui/hooks/bridge/useBridgeChainInfo.ts @@ -3,9 +3,11 @@ import { type TransactionMeta, TransactionType, } from '@metamask/transaction-controller'; -import type { NetworkConfiguration } from '@metamask/network-controller'; -import type { Hex } from '@metamask/utils'; -import { formatChainIdToHexOrCaip } from '@metamask/bridge-controller'; +import { + formatChainIdToCaip, + formatChainIdToHex, + isSolanaChainId, +} from '@metamask/bridge-controller'; import type { BridgeHistoryItem } from '../../../shared/types/bridge-status'; import { CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, @@ -19,10 +21,10 @@ const getSourceAndDestChainIds = ({ }: UseBridgeChainInfoProps) => { return { srcChainId: bridgeHistoryItem - ? formatChainIdToHexOrCaip(bridgeHistoryItem.quote.srcChainId) + ? bridgeHistoryItem.quote.srcChainId : undefined, destChainId: bridgeHistoryItem - ? formatChainIdToHexOrCaip(bridgeHistoryItem.quote.destChainId) + ? bridgeHistoryItem.quote.destChainId : undefined, }; }; @@ -58,43 +60,47 @@ export default function useBridgeChainInfo({ } // Source chain info - const srcNetwork = networkConfigurationsByChainId[ - srcChainId as keyof typeof networkConfigurationsByChainId - ] - ? networkConfigurationsByChainId[ - srcChainId as keyof typeof networkConfigurationsByChainId - ] - : undefined; + const srcChainIdInCaip = formatChainIdToCaip(srcChainId); + const srcNetwork = networkConfigurationsByChainId[srcChainIdInCaip]; + const normalizedSrcChainId = isSolanaChainId(srcChainId) + ? srcChainIdInCaip + : formatChainIdToHex(srcChainId); const fallbackSrcNetwork = { - chainId: srcChainId, - name: NETWORK_TO_NAME_MAP[srcChainId as keyof typeof NETWORK_TO_NAME_MAP], + chainId: normalizedSrcChainId, + name: NETWORK_TO_NAME_MAP[ + normalizedSrcChainId as keyof typeof NETWORK_TO_NAME_MAP + ], nativeCurrency: CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ - srcChainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + normalizedSrcChainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP ], defaultBlockExplorerUrlIndex: 0, - blockExplorerUrls: [CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[srcChainId]], + blockExplorerUrls: [ + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[normalizedSrcChainId], + ], defaultRpcEndpointIndex: 0, rpcEndpoints: [], }; // Dest chain info - const destNetwork = networkConfigurationsByChainId[ - destChainId as keyof typeof networkConfigurationsByChainId - ] - ? networkConfigurationsByChainId[ - destChainId as keyof typeof networkConfigurationsByChainId - ] - : undefined; - const fallbackDestNetwork: NetworkConfiguration = { - chainId: destChainId as Hex, - name: NETWORK_TO_NAME_MAP[destChainId as keyof typeof NETWORK_TO_NAME_MAP], + const destChainIdInCaip = formatChainIdToCaip(destChainId); + const destNetwork = networkConfigurationsByChainId[destChainIdInCaip]; + const normalizedDestChainId = isSolanaChainId(destChainId) + ? destChainIdInCaip + : formatChainIdToHex(destChainId); + const fallbackDestNetwork = { + chainId: normalizedDestChainId, + name: NETWORK_TO_NAME_MAP[ + normalizedDestChainId as keyof typeof NETWORK_TO_NAME_MAP + ], nativeCurrency: CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ - destChainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + normalizedDestChainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP ], defaultBlockExplorerUrlIndex: 0, - blockExplorerUrls: [CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[destChainId]], + blockExplorerUrls: [ + CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[normalizedDestChainId], + ], defaultRpcEndpointIndex: 0, rpcEndpoints: [], }; diff --git a/ui/hooks/bridge/useTokenAlerts.ts b/ui/hooks/bridge/useTokenAlerts.ts index 22384d1bb726..1a27708c0c71 100644 --- a/ui/hooks/bridge/useTokenAlerts.ts +++ b/ui/hooks/bridge/useTokenAlerts.ts @@ -1,5 +1,5 @@ import { useSelector } from 'react-redux'; -import { formatAddressToString } from '@metamask/bridge-controller'; +import { formatAddressToCaipReference } from '@metamask/bridge-controller'; import { getFromToken, getFromChain, @@ -29,7 +29,7 @@ export const useTokenAlerts = () => { if (chainName) { return await fetchTokenAlert( chainName, - formatAddressToString(toToken.address), + formatAddressToCaipReference(toToken.address), ); } } diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 321596607488..1a6b9c2d999e 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -11,7 +11,6 @@ import { fetchBridgeTokens, BridgeClientId, type BridgeAsset, - formatChainIdToHexOrCaip, } from '@metamask/bridge-controller'; import { getAllDetectedTokensForSelectedAddress, @@ -143,7 +142,9 @@ export const useTokensWithFiltering = ( // Only tokens on the active chain are processed here here const sharedFields = { ...token, - chainId: formatChainIdToHexOrCaip(chainId), + chainId: isSolanaChainId(chainId) + ? formatChainIdToCaip(chainId) + : formatChainIdToHex(chainId), assetId: token.assetId, }; diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx index ababc2e0de0a..235dc7f61064 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx @@ -3,12 +3,12 @@ import { act } from '@testing-library/react'; import * as reactRouterUtils from 'react-router-dom-v5-compat'; import { zeroAddress } from 'ethereumjs-util'; import userEvent from '@testing-library/user-event'; +import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { fireEvent, renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import { createBridgeMockStore } from '../../../../test/jest/mock-store'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { createTestProviderTools } from '../../../../test/stub/provider'; -import { formatChainIdToCaip } from '../../../../shared/modules/bridge-utils/caip-formatters'; import PrepareBridgePage from './prepare-bridge-page'; describe('PrepareBridgePage', () => { @@ -135,7 +135,7 @@ describe('PrepareBridgePage', () => { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', decimals: 6, }, - toChainId: formatChainIdToCaip(CHAIN_IDS.LINEA_MAINNET), + toChainId: toEvmCaipChainId(CHAIN_IDS.LINEA_MAINNET), }, bridgeStateOverrides: { quoteRequest: { @@ -202,7 +202,7 @@ describe('PrepareBridgePage', () => { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', decimals: 6, }, - toChainId: formatChainIdToCaip(CHAIN_IDS.LINEA_MAINNET), + toChainId: toEvmCaipChainId(CHAIN_IDS.LINEA_MAINNET), }, }); From 7e85f3b7bc14cb9cee4a848ae01d98e2da175980 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:55:08 -0700 Subject: [PATCH 16/71] chore: package updates --- yarn.lock | 47 +---------------------------------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/yarn.lock b/yarn.lock index cebad3c80f9d..b8ecf9c498b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5057,17 +5057,13 @@ __metadata: "@ethersproject/providers": "npm:^5.7.0" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.6.0" - "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/multichain-network-controller": "npm:^0.1.0" "@metamask/polling-controller": "npm:^12.0.3" - "@metamask/snaps-controllers": "npm:^10.0.1" - "@metamask/snaps-utils": "npm:^9.0.1" "@metamask/utils": "npm:^11.2.0" peerDependencies: "@metamask/accounts-controller": ^26.0.0 "@metamask/network-controller": ^22.0.0 - "@metamask/transaction-controller": ^49.0.0 + "@metamask/transaction-controller": ^50.0.0 languageName: node linkType: soft @@ -6305,47 +6301,6 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-controllers@npm:^10.0.1": - version: 10.0.1 - resolution: "@metamask/snaps-controllers@npm:10.0.1" - dependencies: - "@metamask/approval-controller": "npm:^7.1.3" - "@metamask/base-controller": "npm:^8.0.0" - "@metamask/json-rpc-engine": "npm:^10.0.2" - "@metamask/json-rpc-middleware-stream": "npm:^8.0.7" - "@metamask/key-tree": "npm:^10.0.2" - "@metamask/object-multiplex": "npm:^2.1.0" - "@metamask/permission-controller": "npm:^11.0.6" - "@metamask/phishing-controller": "npm:^12.4.0" - "@metamask/post-message-stream": "npm:^9.0.0" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/snaps-registry": "npm:^3.2.3" - "@metamask/snaps-rpc-methods": "npm:^11.13.0" - "@metamask/snaps-sdk": "npm:^6.19.0" - "@metamask/snaps-utils": "npm:^9.0.1" - "@metamask/utils": "npm:^11.2.0" - "@xstate/fsm": "npm:^2.0.0" - async-mutex: "npm:^0.5.0" - browserify-zlib: "npm:^0.2.0" - concat-stream: "npm:^2.0.0" - fast-deep-equal: "npm:^3.1.3" - get-npm-tarball-url: "npm:^2.0.3" - immer: "npm:^9.0.6" - luxon: "npm:^3.5.0" - nanoid: "npm:^3.1.31" - readable-stream: "npm:^3.6.2" - readable-web-to-node-stream: "npm:^3.0.2" - semver: "npm:^7.5.4" - tar-stream: "npm:^3.1.7" - peerDependencies: - "@metamask/snaps-execution-environments": ^7.0.0 - peerDependenciesMeta: - "@metamask/snaps-execution-environments": - optional: true - checksum: 10/aa1e1b3da0edfba50e7c0ae78fa02479b6f345ae6e5e6ebf3fdaf072a52fede176d954ea99c25a468856b91331003920610b214fcf21f8e23328310b516e068b - languageName: node - linkType: hard - "@metamask/snaps-controllers@npm:^11.0.0": version: 11.0.0 resolution: "@metamask/snaps-controllers@npm:11.0.0" From c47fe68ff6ea99bef5dd8a86fa514c286c8af9cd Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 15:11:35 -0700 Subject: [PATCH 17/71] chore: use preview bridge-controller build --- package.json | 2 +- yarn.lock | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4ab9507fbd00..fe1010933149 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "readable-stream-3@^3.6.2": "npm:readable-stream@^3.6.2", "semver@7.3.7": "^7.5.4", "semver@7.3.8": "^7.5.4", - "@metamask/bridge-controller": "portal:./../core/packages/bridge-controller", + "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@8.0.0-preview-03c4c9c3", "lavamoat-core@npm:^16.2.2": "patch:lavamoat-core@npm%3A16.2.2#~/.yarn/patches/lavamoat-core-npm-16.2.2-e361ff1f8a.patch", "@metamask/snaps-sdk": "^6.19.0", "@swc/types@0.1.5": "^0.1.6", diff --git a/yarn.lock b/yarn.lock index b8ecf9c498b4..87cbd7dad0e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5046,9 +5046,9 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@portal:./../core/packages/bridge-controller::locator=metamask-crx%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@metamask/bridge-controller@portal:./../core/packages/bridge-controller::locator=metamask-crx%40workspace%3A." +"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@8.0.0-preview-03c4c9c3": + version: 8.0.0-preview-03c4c9c3 + resolution: "@metamask-previews/bridge-controller@npm:8.0.0-preview-03c4c9c3" dependencies: "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" @@ -5064,8 +5064,9 @@ __metadata: "@metamask/accounts-controller": ^26.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/transaction-controller": ^50.0.0 + checksum: 10/59f2bb6baad0e0fc11081a70eed0cd3d6bceafc3c9cea7c344655602aee0c3dd40910fdaee4c3b227cf419165def93ae5426fa7a2816082a34d3adb1b398f4e5 languageName: node - linkType: soft + linkType: hard "@metamask/browser-passworder@npm:^4.3.0": version: 4.3.0 From 1e00869dc70ca881e99463ffb2a59ecd8058e465 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 15:26:54 -0700 Subject: [PATCH 18/71] fix: e2e constants for bridge feature flags --- test/e2e/tests/bridge/constants.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/e2e/tests/bridge/constants.ts b/test/e2e/tests/bridge/constants.ts index 13ef91a47953..9cce43575390 100644 --- a/test/e2e/tests/bridge/constants.ts +++ b/test/e2e/tests/bridge/constants.ts @@ -11,6 +11,14 @@ export const DEFAULT_FEATURE_FLAGS_RESPONSE: FeatureFlagResponse = { '59144': { isActiveSrc: true, isActiveDest: true }, }, }, + 'mobile-config': { + refreshRate: 30, + maxRefreshCount: 5, + support: false, + chains: { + '1': { isActiveSrc: true, isActiveDest: true }, + }, + }, }; export const LOCATOR = { From 362157052d0c8de635bb7ccc2cbd506a5df24bc2 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 15:50:54 -0700 Subject: [PATCH 19/71] fix: circular dependency --- shared/lib/bridge-status/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/lib/bridge-status/utils.ts b/shared/lib/bridge-status/utils.ts index df0a7b6091d2..849d20f971c6 100644 --- a/shared/lib/bridge-status/utils.ts +++ b/shared/lib/bridge-status/utils.ts @@ -1,4 +1,4 @@ -import { QuoteMetadata, QuoteResponse } from '../../types/bridge'; +import type { QuoteMetadata, QuoteResponse } from '@metamask/bridge-controller'; import { QuoteMetadataSerialized, StatusRequest, From 0f3a67d5456dfdf20eed83602bb48ce33826c942 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 15:47:05 -0700 Subject: [PATCH 20/71] fix: getDefaultBridgeControllerState --- package.json | 2 +- test/jest/mock-store.js | 4 ++-- yarn.lock | 32 ++++++++++++++++++++++++-------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index fe1010933149..de31a121b0d9 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "readable-stream-3@^3.6.2": "npm:readable-stream@^3.6.2", "semver@7.3.7": "^7.5.4", "semver@7.3.8": "^7.5.4", - "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@8.0.0-preview-03c4c9c3", + "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@9.0.0-preview-8c783cb1", "lavamoat-core@npm:^16.2.2": "patch:lavamoat-core@npm%3A16.2.2#~/.yarn/patches/lavamoat-core-npm-16.2.2-e361ff1f8a.patch", "@metamask/snaps-sdk": "^6.19.0", "@swc/types@0.1.5": "^0.1.6", diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js index 9c62ccc68569..1cbdf04ea141 100644 --- a/test/jest/mock-store.js +++ b/test/jest/mock-store.js @@ -1,6 +1,6 @@ import { EthAccountType, EthScope } from '@metamask/keyring-api'; import { - DEFAULT_BRIDGE_STATE, + getDefaultBridgeControllerState, BRIDGE_PREFERRED_GAS_ESTIMATE, formatChainIdToCaip, } from '@metamask/bridge-controller'; @@ -783,7 +783,7 @@ export const createBridgeMockStore = ( ...mockTokenData, ...metamaskStateOverrides, ...{ - ...DEFAULT_BRIDGE_STATE, + ...getDefaultBridgeControllerState(), bridgeFeatureFlags: { ...featureFlagOverrides, extensionConfig: { diff --git a/yarn.lock b/yarn.lock index 87cbd7dad0e4..ddeabc029966 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5046,9 +5046,9 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@8.0.0-preview-03c4c9c3": - version: 8.0.0-preview-03c4c9c3 - resolution: "@metamask-previews/bridge-controller@npm:8.0.0-preview-03c4c9c3" +"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@9.0.0-preview-8c783cb1": + version: 9.0.0-preview-8c783cb1 + resolution: "@metamask-previews/bridge-controller@npm:9.0.0-preview-8c783cb1" dependencies: "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" @@ -5058,13 +5058,13 @@ __metadata: "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.6.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/polling-controller": "npm:^12.0.3" + "@metamask/polling-controller": "npm:^13.0.0" "@metamask/utils": "npm:^11.2.0" peerDependencies: - "@metamask/accounts-controller": ^26.0.0 - "@metamask/network-controller": ^22.0.0 - "@metamask/transaction-controller": ^50.0.0 - checksum: 10/59f2bb6baad0e0fc11081a70eed0cd3d6bceafc3c9cea7c344655602aee0c3dd40910fdaee4c3b227cf419165def93ae5426fa7a2816082a34d3adb1b398f4e5 + "@metamask/accounts-controller": ^27.0.0 + "@metamask/network-controller": ^23.0.0 + "@metamask/transaction-controller": ^51.0.0 + checksum: 10/76f89331075da63eb1472ed8d054835bf934f0ed87e26e300db2d4490cb1e5db3bd4418701a03bf2dee17a6c18decdddc284ad231405a78b8ea1bf9d002d38c2 languageName: node linkType: hard @@ -6058,6 +6058,22 @@ __metadata: languageName: node linkType: hard +"@metamask/polling-controller@npm:^13.0.0": + version: 13.0.0 + resolution: "@metamask/polling-controller@npm:13.0.0" + dependencies: + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/controller-utils": "npm:^11.6.0" + "@metamask/utils": "npm:^11.2.0" + "@types/uuid": "npm:^8.3.0" + fast-json-stable-stringify: "npm:^2.1.0" + uuid: "npm:^8.3.2" + peerDependencies: + "@metamask/network-controller": ^23.0.0 + checksum: 10/9d3750fe55f7d5829fd5cd6f1750558f66bb8449341d368a34e110b8bc2ba3f43e6805358d0d90fa9c8a8255ea38c4d8063ef0a4bd7702ed75b0251e4002d9ab + languageName: node + linkType: hard + "@metamask/post-message-stream@npm:^8.0.0": version: 8.1.1 resolution: "@metamask/post-message-stream@npm:8.1.1" From 62a446971e9977a6f6b42f1c6e2005e74f26e762 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 11:22:22 -0700 Subject: [PATCH 21/71] fix: lavamoat policy --- lavamoat/browserify/beta/policy.json | 33 +++++++++++++++++++++++++++ lavamoat/browserify/flask/policy.json | 33 +++++++++++++++++++++++++++ lavamoat/browserify/main/policy.json | 33 +++++++++++++++++++++++++++ lavamoat/browserify/mmi/policy.json | 33 +++++++++++++++++++++++++++ lavamoat/build-system/policy.json | 8 +++++++ 5 files changed, 140 insertions(+) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 402afbd3f613..b7b01bbaffec 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1275,6 +1275,27 @@ "immer": true } }, + "@metamask/bridge-controller": { + "globals": { + "AbortController": true, + "URLSearchParams": true, + "console.log": true + }, + "packages": { + "ethers>@ethersproject/address": true, + "ethers>@ethersproject/constants": true, + "@ethersproject/contracts": true, + "@ethersproject/providers": true, + "@metamask/controller-utils": true, + "@metamask/keyring-api": true, + "@metamask/metamask-eth-abis": true, + "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/polling-controller": true, + "@metamask/snaps-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true + } + }, "@metamask/browser-passworder": { "globals": { "CryptoKey": true, @@ -1998,6 +2019,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 402afbd3f613..b7b01bbaffec 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1275,6 +1275,27 @@ "immer": true } }, + "@metamask/bridge-controller": { + "globals": { + "AbortController": true, + "URLSearchParams": true, + "console.log": true + }, + "packages": { + "ethers>@ethersproject/address": true, + "ethers>@ethersproject/constants": true, + "@ethersproject/contracts": true, + "@ethersproject/providers": true, + "@metamask/controller-utils": true, + "@metamask/keyring-api": true, + "@metamask/metamask-eth-abis": true, + "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/polling-controller": true, + "@metamask/snaps-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true + } + }, "@metamask/browser-passworder": { "globals": { "CryptoKey": true, @@ -1998,6 +2019,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 402afbd3f613..b7b01bbaffec 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1275,6 +1275,27 @@ "immer": true } }, + "@metamask/bridge-controller": { + "globals": { + "AbortController": true, + "URLSearchParams": true, + "console.log": true + }, + "packages": { + "ethers>@ethersproject/address": true, + "ethers>@ethersproject/constants": true, + "@ethersproject/contracts": true, + "@ethersproject/providers": true, + "@metamask/controller-utils": true, + "@metamask/keyring-api": true, + "@metamask/metamask-eth-abis": true, + "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/polling-controller": true, + "@metamask/snaps-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true + } + }, "@metamask/browser-passworder": { "globals": { "CryptoKey": true, @@ -1998,6 +2019,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index dc5b5c366b5e..94d72cf6dd83 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1421,6 +1421,27 @@ "immer": true } }, + "@metamask/bridge-controller": { + "globals": { + "AbortController": true, + "URLSearchParams": true, + "console.log": true + }, + "packages": { + "ethers>@ethersproject/address": true, + "ethers>@ethersproject/constants": true, + "@ethersproject/contracts": true, + "@ethersproject/providers": true, + "@metamask/controller-utils": true, + "@metamask/keyring-api": true, + "@metamask/metamask-eth-abis": true, + "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/polling-controller": true, + "@metamask/snaps-utils": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true + } + }, "@metamask/browser-passworder": { "globals": { "CryptoKey": true, @@ -2144,6 +2165,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index c53f03b68c29..29e47a478cfa 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1810,6 +1810,7 @@ "chokidar>anymatch": true, "chokidar>braces": true, "chokidar>fsevents": true, + "tsx>fsevents": true, "eslint>glob-parent": true, "chokidar>is-binary-path": true, "del>is-glob": true, @@ -3380,6 +3381,13 @@ "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, + "tsx>fsevents": { + "globals": { + "console.assert": true, + "process.platform": true + }, + "native": true + }, "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge": { "builtin": { "util.format": true From 68dd678a59e2624549fea7b05fccc847ba412fb3 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 12:09:13 -0700 Subject: [PATCH 22/71] fix: unit tests --- ui/ducks/bridge/selectors.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 72ced7de9f13..20e22b8e76da 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -399,6 +399,7 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual({ address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', chainId: 'eip155:1', decimals: 18, iconUrl: './images/eth_logo.svg', @@ -418,6 +419,7 @@ describe('Bridge selectors', () => { expect(result).toStrictEqual({ address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:1/slip44:60', chainId: 'eip155:1', decimals: 18, iconUrl: './images/eth_logo.svg', From 2d571632dc794987512418b83ed1a1ce8e43a2f8 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 13:32:36 -0700 Subject: [PATCH 23/71] fix: missing import --- test/integration/onboarding/import-wallet.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/onboarding/import-wallet.test.tsx b/test/integration/onboarding/import-wallet.test.tsx index fc04ce643159..0caf8bf2e641 100644 --- a/test/integration/onboarding/import-wallet.test.tsx +++ b/test/integration/onboarding/import-wallet.test.tsx @@ -1,4 +1,5 @@ import { waitFor } from '@testing-library/react'; +import { BridgeBackgroundAction } from '@metamask/bridge-controller'; import nock from 'nock'; import mockMetaMaskState from '../data/onboarding-completion-route.json'; import { integrationTestRender } from '../../lib/render-helpers'; @@ -13,7 +14,6 @@ import { waitForElementById, waitForElementByText, } from '../helpers'; -import { BridgeBackgroundAction } from '../../../shared/types/bridge'; import { FirstTimeFlowType } from '../../../shared/constants/onboarding'; jest.mock('../../../ui/store/background-connection', () => ({ From 8ba857c6f402278a385cae224db5a124ac5f4da9 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 13:54:31 -0700 Subject: [PATCH 24/71] fix: bridge-status mocks --- app/scripts/controllers/bridge-status/mocks.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/scripts/controllers/bridge-status/mocks.ts b/app/scripts/controllers/bridge-status/mocks.ts index 80c3cf6cdff9..ecfe0f69591c 100644 --- a/app/scripts/controllers/bridge-status/mocks.ts +++ b/app/scripts/controllers/bridge-status/mocks.ts @@ -1,4 +1,5 @@ import { TransactionMeta } from '@metamask/transaction-controller'; +import { getNativeAssetForChainId } from '@metamask/bridge-controller'; import { BridgeId, StatusResponse, @@ -123,6 +124,7 @@ export const getMockQuote = ({ srcTokenAmount: '991250000000000', srcAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: getNativeAssetForChainId(srcChainId).assetId, chainId: srcChainId, symbol: 'ETH', decimals: 18, @@ -137,6 +139,7 @@ export const getMockQuote = ({ destTokenAmount: '990654755978612', destAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: getNativeAssetForChainId(destChainId).assetId, chainId: destChainId, symbol: 'ETH', decimals: 18, @@ -152,6 +155,7 @@ export const getMockQuote = ({ amount: '8750000000000', asset: { address: '0x0000000000000000000000000000000000000000', + assetId: getNativeAssetForChainId(srcChainId).assetId, chainId: srcChainId, symbol: 'ETH', decimals: 18, @@ -178,6 +182,7 @@ export const getMockQuote = ({ }, srcAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: getNativeAssetForChainId(srcChainId).assetId, chainId: srcChainId, symbol: 'ETH', decimals: 18, @@ -190,6 +195,7 @@ export const getMockQuote = ({ }, destAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: getNativeAssetForChainId(destChainId).assetId, chainId: destChainId, symbol: 'ETH', decimals: 18, From 30da77395789eda400f9cd19d2aeb96f511fad22 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 14:15:59 -0700 Subject: [PATCH 25/71] fix: bridge selectors unit tests --- ui/ducks/bridge/selectors.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 20e22b8e76da..536101cd93fb 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -582,7 +582,7 @@ describe('Bridge selectors', () => { quotesRefreshCount: 5, quotesInitialLoadTimeMs: 11000, isQuoteGoingToRefresh: false, - quoteFetchError: undefined, + quoteFetchError: null, }); }); @@ -706,7 +706,7 @@ describe('Bridge selectors', () => { quotesRefreshCount: 2, isQuoteGoingToRefresh: true, quotesInitialLoadTimeMs: 11000, - quoteFetchError: undefined, + quoteFetchError: null, }); }); @@ -831,7 +831,7 @@ describe('Bridge selectors', () => { isLoading: false, quotesRefreshCount: 1, isQuoteGoingToRefresh: false, - quoteFetchError: undefined, + quoteFetchError: null, }); }); }); @@ -846,12 +846,12 @@ describe('Bridge selectors', () => { activeQuote: undefined, isLoading: false, isQuoteGoingToRefresh: false, - quotesLastFetchedMs: undefined, + quotesLastFetchedMs: null, quotesRefreshCount: 0, recommendedQuote: undefined, - quotesInitialLoadTimeMs: undefined, + quotesInitialLoadTimeMs: null, sortedQuotes: [], - quoteFetchError: undefined, + quoteFetchError: null, }); }); From bff378de0db2cf11846cae0732aa788ac29356f4 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 14:43:18 -0700 Subject: [PATCH 26/71] fix: getFromToken selector --- ui/ducks/bridge/selectors.test.ts | 2 -- ui/ducks/bridge/selectors.ts | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 536101cd93fb..507651c9b435 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -402,7 +402,6 @@ describe('Bridge selectors', () => { assetId: 'eip155:1/slip44:60', chainId: 'eip155:1', decimals: 18, - iconUrl: './images/eth_logo.svg', image: './images/eth_logo.svg', name: 'Ether', symbol: 'ETH', @@ -422,7 +421,6 @@ describe('Bridge selectors', () => { assetId: 'eip155:1/slip44:60', chainId: 'eip155:1', decimals: 18, - iconUrl: './images/eth_logo.svg', image: './images/eth_logo.svg', name: 'Ether', symbol: 'ETH', diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 3788553fcb9f..47180d85629b 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -62,9 +62,9 @@ import { FEATURED_RPCS, } from '../../../shared/constants/network'; import { + getImageForChainId, getMultichainCoinRates, getMultichainProviderConfig, - getImageForChainId, } from '../../selectors/multichain'; import { getAssetsRates } from '../../selectors/assets'; import { @@ -210,8 +210,11 @@ export const getFromToken = createSelector( if (fromToken?.address) { return fromToken; } + const { iconUrl, ...nativeAsset } = getNativeAssetForChainId( + fromChain.chainId, + ); return { - ...getNativeAssetForChainId(fromChain.chainId), + ...nativeAsset, chainId: formatChainIdToCaip(fromChain.chainId), image: CHAIN_ID_TOKEN_IMAGE_MAP[ From fd34aba835a29d303408cf7d38970b7e18018c14 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 17:15:44 -0700 Subject: [PATCH 27/71] chore: bump bridge-controller --- package.json | 2 +- yarn.lock | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9b6929f6297b..93ac7b5343b9 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "readable-stream-3@^3.6.2": "npm:readable-stream@^3.6.2", "semver@7.3.7": "^7.5.4", "semver@7.3.8": "^7.5.4", - "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@9.0.0-preview-8c783cb1", + "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@9.0.0-preview-330335c1", "lavamoat-core@npm:^16.2.2": "patch:lavamoat-core@npm%3A16.2.2#~/.yarn/patches/lavamoat-core-npm-16.2.2-e361ff1f8a.patch", "@metamask/snaps-sdk": "^6.19.0", "@swc/types@0.1.5": "^0.1.6", diff --git a/yarn.lock b/yarn.lock index ddeabc029966..3f8f72336e8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5046,9 +5046,9 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@9.0.0-preview-8c783cb1": - version: 9.0.0-preview-8c783cb1 - resolution: "@metamask-previews/bridge-controller@npm:9.0.0-preview-8c783cb1" +"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@9.0.0-preview-330335c1": + version: 9.0.0-preview-330335c1 + resolution: "@metamask-previews/bridge-controller@npm:9.0.0-preview-330335c1" dependencies: "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" @@ -5057,14 +5057,18 @@ __metadata: "@ethersproject/providers": "npm:^5.7.0" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.6.0" + "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-network-controller": "npm:^0.3.0" "@metamask/polling-controller": "npm:^13.0.0" + "@metamask/snaps-utils": "npm:^8.10.0" "@metamask/utils": "npm:^11.2.0" peerDependencies: "@metamask/accounts-controller": ^27.0.0 "@metamask/network-controller": ^23.0.0 + "@metamask/snaps-controllers": ^9.19.0 "@metamask/transaction-controller": ^51.0.0 - checksum: 10/76f89331075da63eb1472ed8d054835bf934f0ed87e26e300db2d4490cb1e5db3bd4418701a03bf2dee17a6c18decdddc284ad231405a78b8ea1bf9d002d38c2 + checksum: 10/c9f6ef45f246b805903507824c6b32cb932ad582f863ce61f9f43ac0f01ef89b249a8a0a4985a5d4942aa0ff894c9c63cd4d713d91670136d8520d0eb395f3cc languageName: node linkType: hard @@ -5789,6 +5793,21 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain-network-controller@npm:^0.3.0": + version: 0.3.0 + resolution: "@metamask/multichain-network-controller@npm:0.3.0" + dependencies: + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/keyring-api": "npm:^17.2.0" + "@metamask/utils": "npm:^11.2.0" + "@solana/addresses": "npm:^2.0.0" + peerDependencies: + "@metamask/accounts-controller": ^27.0.0 + "@metamask/network-controller": ^23.0.0 + checksum: 10/eed3a230271bfe610027b6739d383151a573fe10db66f407ec8c673cceaab3c2b50a5fcc3fb2cc0dcfa4fbb9748d63f615799bd27bb5b4303260321ec9d07757 + languageName: node + linkType: hard + "@metamask/multichain-transactions-controller@npm:^0.7.2": version: 0.7.2 resolution: "@metamask/multichain-transactions-controller@npm:0.7.2" From 8ee4016d3b1d7234789b4b8d3622e6013d2496e1 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 17:17:54 -0700 Subject: [PATCH 28/71] fix: unit tests --- .../bridge-status-controller.test.ts.snap | 10 ++++++++++ .../bridge-status/bridge-status-controller.test.ts | 11 ++++++----- ui/ducks/bridge/bridge.ts | 2 +- ui/ducks/bridge/selectors.ts | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap b/app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap index 1a0f1c09a820..1c0ae680b79a 100644 --- a/app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap +++ b/app/scripts/controllers/bridge-status/__snapshots__/bridge-status-controller.test.ts.snap @@ -20,6 +20,7 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] ], "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -36,6 +37,7 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] "amount": "8750000000000", "asset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -50,6 +52,7 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -67,6 +70,7 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] "destAmount": "990654755978612", "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -85,6 +89,7 @@ exports[`BridgeStatusController constructor rehydrates the tx history state 1`] "srcAmount": "991250000000000", "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -133,6 +138,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx ], "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -149,6 +155,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx "amount": "8750000000000", "asset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -163,6 +170,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -180,6 +188,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx "destAmount": "990654755978612", "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -198,6 +207,7 @@ exports[`BridgeStatusController startPollingForBridgeTxStatus sets the inital tx "srcAmount": "991250000000000", "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, diff --git a/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts index 2b12f9d0794f..35cbe33a8cf6 100644 --- a/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts +++ b/app/scripts/controllers/bridge-status/bridge-status-controller.test.ts @@ -1,3 +1,4 @@ +import { ChainId } from '@metamask/bridge-controller'; import { flushPromises } from '../../../../test/lib/timer-helpers'; import { Numeric } from '../../../../shared/modules/Numeric'; import BridgeStatusController from './bridge-status-controller'; @@ -358,7 +359,7 @@ describe('BridgeStatusController', () => { srcTxHash: '0xsrcTxHash2', txMetaId: 'bridgeTxMetaId2', srcChainId: 10, - destChainId: 123, + destChainId: ChainId.SOLANA, }), ); jest.advanceTimersByTime(10_000); @@ -381,7 +382,7 @@ describe('BridgeStatusController', () => { expect( bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId2 .quote.destChainId, - ).toEqual(123); + ).toEqual(1151111081099710); bridgeStatusController.wipeBridgeStatus({ address: '0xaccount1', @@ -455,7 +456,7 @@ describe('BridgeStatusController', () => { srcTxHash: '0xsrcTxHash2', txMetaId: 'bridgeTxMetaId2', srcChainId: 10, - destChainId: 123, + destChainId: 137, }), ); jest.advanceTimersByTime(10_000); @@ -478,7 +479,7 @@ describe('BridgeStatusController', () => { expect( bridgeStatusController.state.bridgeStatusState.txHistory.bridgeTxMetaId2 .quote.destChainId, - ).toEqual(123); + ).toEqual(137); bridgeStatusController.wipeBridgeStatus({ address: '0xaccount1', @@ -491,7 +492,7 @@ describe('BridgeStatusController', () => { ); expect(txHistoryItems).toHaveLength(1); expect(txHistoryItems[0].quote.srcChainId).toEqual(10); - expect(txHistoryItems[0].quote.destChainId).toEqual(123); + expect(txHistoryItems[0].quote.destChainId).toEqual(137); }); }); }); diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts index 32e022046262..606f92066fa1 100644 --- a/ui/ducks/bridge/bridge.ts +++ b/ui/ducks/bridge/bridge.ts @@ -97,7 +97,7 @@ const bridgeSlice = createSlice({ chainId: payload.chainId, address: payload.address || - getNativeAssetForChainId(payload.chainId)?.address, + getNativeAssetForChainId(payload.chainId).address, }; } else { state.toToken = payload; diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts index 47180d85629b..237b6a46f3b5 100644 --- a/ui/ducks/bridge/selectors.ts +++ b/ui/ducks/bridge/selectors.ts @@ -62,9 +62,9 @@ import { FEATURED_RPCS, } from '../../../shared/constants/network'; import { - getImageForChainId, getMultichainCoinRates, getMultichainProviderConfig, + getImageForChainId, } from '../../selectors/multichain'; import { getAssetsRates } from '../../selectors/assets'; import { From b86bba9112e532b390ee360ad2c86a9c39d2a55e Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 18:41:47 -0700 Subject: [PATCH 29/71] fix: lavamoat --- lavamoat/browserify/beta/policy.json | 62 ++++++++++++++++++++++++++- lavamoat/browserify/flask/policy.json | 62 ++++++++++++++++++++++++++- lavamoat/browserify/main/policy.json | 62 ++++++++++++++++++++++++++- lavamoat/browserify/mmi/policy.json | 62 ++++++++++++++++++++++++++- 4 files changed, 240 insertions(+), 8 deletions(-) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index b7b01bbaffec..04fbb64af353 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -1289,9 +1289,9 @@ "@metamask/controller-utils": true, "@metamask/keyring-api": true, "@metamask/metamask-eth-abis": true, - "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller": true, "@metamask/bridge-controller>@metamask/polling-controller": true, - "@metamask/snaps-utils": true, + "@metamask/bridge-controller>@metamask/snaps-utils": true, "@metamask/utils>@metamask/superstruct": true, "@metamask/utils": true } @@ -1875,6 +1875,15 @@ "@metamask/multichain-network-controller>@solana/addresses": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/utils": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": true + } + }, "@metamask/multichain-transactions-controller": { "globals": { "console.error": true @@ -2370,6 +2379,41 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/bridge-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { "globals": { "File": true, @@ -2977,6 +3021,20 @@ "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index b7b01bbaffec..04fbb64af353 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -1289,9 +1289,9 @@ "@metamask/controller-utils": true, "@metamask/keyring-api": true, "@metamask/metamask-eth-abis": true, - "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller": true, "@metamask/bridge-controller>@metamask/polling-controller": true, - "@metamask/snaps-utils": true, + "@metamask/bridge-controller>@metamask/snaps-utils": true, "@metamask/utils>@metamask/superstruct": true, "@metamask/utils": true } @@ -1875,6 +1875,15 @@ "@metamask/multichain-network-controller>@solana/addresses": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/utils": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": true + } + }, "@metamask/multichain-transactions-controller": { "globals": { "console.error": true @@ -2370,6 +2379,41 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/bridge-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { "globals": { "File": true, @@ -2977,6 +3021,20 @@ "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index b7b01bbaffec..04fbb64af353 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -1289,9 +1289,9 @@ "@metamask/controller-utils": true, "@metamask/keyring-api": true, "@metamask/metamask-eth-abis": true, - "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller": true, "@metamask/bridge-controller>@metamask/polling-controller": true, - "@metamask/snaps-utils": true, + "@metamask/bridge-controller>@metamask/snaps-utils": true, "@metamask/utils>@metamask/superstruct": true, "@metamask/utils": true } @@ -1875,6 +1875,15 @@ "@metamask/multichain-network-controller>@solana/addresses": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/utils": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": true + } + }, "@metamask/multichain-transactions-controller": { "globals": { "console.error": true @@ -2370,6 +2379,41 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/bridge-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { "globals": { "File": true, @@ -2977,6 +3021,20 @@ "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 94d72cf6dd83..1763816cf6a9 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -1435,9 +1435,9 @@ "@metamask/controller-utils": true, "@metamask/keyring-api": true, "@metamask/metamask-eth-abis": true, - "@metamask/multichain-network-controller": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller": true, "@metamask/bridge-controller>@metamask/polling-controller": true, - "@metamask/snaps-utils": true, + "@metamask/bridge-controller>@metamask/snaps-utils": true, "@metamask/utils>@metamask/superstruct": true, "@metamask/utils": true } @@ -2021,6 +2021,15 @@ "@metamask/multichain-network-controller>@solana/addresses": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller": { + "packages": { + "@metamask/base-controller": true, + "@metamask/keyring-api": true, + "@metamask/network-controller": true, + "@metamask/utils": true, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": true + } + }, "@metamask/multichain-transactions-controller": { "globals": { "console.error": true @@ -2516,6 +2525,41 @@ "@metamask/snaps-utils>validate-npm-package-name": true } }, + "@metamask/bridge-controller>@metamask/snaps-utils": { + "globals": { + "File": true, + "FileReader": true, + "TextDecoder": true, + "TextEncoder": true, + "URL": true, + "console.error": true, + "console.log": true, + "console.warn": true, + "crypto": true, + "document.body.appendChild": true, + "document.createElement": true, + "fetch": true + }, + "packages": { + "@metamask/snaps-sdk>@metamask/key-tree": true, + "@metamask/permission-controller": true, + "@metamask/rpc-errors": true, + "@metamask/snaps-utils>@metamask/slip44": true, + "@metamask/snaps-sdk": true, + "@metamask/utils>@metamask/superstruct": true, + "@metamask/utils": true, + "@noble/hashes": true, + "@metamask/utils>@scure/base": true, + "chalk": true, + "@metamask/snaps-utils>cron-parser": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "@metamask/snaps-utils>fast-xml-parser": true, + "@metamask/snaps-utils>marked": true, + "@metamask/snaps-utils>rfdc": true, + "semver": true, + "@metamask/snaps-utils>validate-npm-package-name": true + } + }, "@metamask/multichain-transactions-controller>@metamask/snaps-utils": { "globals": { "File": true, @@ -3123,6 +3167,20 @@ "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true } }, + "@metamask/bridge-controller>@metamask/multichain-network-controller>@solana/addresses": { + "globals": { + "Intl.Collator": true, + "TextEncoder": true, + "crypto.subtle.digest": true, + "crypto.subtle.exportKey": true + }, + "packages": { + "@metamask/multichain-network-controller>@solana/addresses>@solana/assertions": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-core": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/codecs-strings": true, + "@metamask/multichain-network-controller>@solana/addresses>@solana/errors": true + } + }, "@solana/addresses>@solana/assertions": { "globals": { "crypto": true, From d5cb20eb4d5e00706d4ef15e4da7114123b1f215 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 18:54:46 -0700 Subject: [PATCH 30/71] fix: MultichainBridgeQuoteCard storybook --- .../bridge/quotes/multichain-bridge-quote-card.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx index 79616e27271a..2e0d16a051d1 100644 --- a/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx @@ -44,7 +44,7 @@ DefaultStory.decorators = [ export const WithDestinationAddress = () => { return ( - + ); }; @@ -70,7 +70,7 @@ WithDestinationAddress.decorators = [ export const WithLowEstimatedReturn = () => { return ( - + ); }; From e02517f3486ae78e2a142346bf5c7f37dff78f23 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 21 Mar 2025 13:49:00 -0700 Subject: [PATCH 31/71] fix: useLatestBalance --- ui/hooks/bridge/useLatestBalance.test.ts | 98 +++++++++++++++--------- ui/hooks/bridge/useLatestBalance.ts | 42 ++++------ 2 files changed, 78 insertions(+), 62 deletions(-) diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts index 7b0b6b7befc1..12e10523bb82 100644 --- a/ui/hooks/bridge/useLatestBalance.test.ts +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -1,29 +1,20 @@ -import { BigNumber } from 'bignumber.js'; import { zeroAddress } from 'ethereumjs-util'; +import * as bridgeController from '@metamask/bridge-controller'; +import { MultichainNetwork } from '@metamask/multichain-transactions-controller'; import { renderHookWithProvider } from '../../../test/lib/render-helpers'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; -import { createTestProviderTools } from '../../../test/stub/provider'; -import * as tokenutil from '../../../shared/lib/token-util'; import useLatestBalance from './useLatestBalance'; -const mockGetBalance = jest.fn(); -jest.mock('@ethersproject/providers', () => { +const mockCalcLatestSrcBalance = jest.fn(); +jest.mock('@metamask/bridge-controller', () => { return { - Web3Provider: jest.fn().mockImplementation(() => { - return { - getBalance: mockGetBalance, - }; - }), + ...jest.requireActual('@metamask/bridge-controller'), + calcLatestSrcBalance: (...args: unknown[]) => + mockCalcLatestSrcBalance(...args), }; }); -const mockFetchTokenBalance = jest.spyOn(tokenutil, 'fetchTokenBalance'); -jest.mock('../../../shared/lib/token-util', () => ({ - ...jest.requireActual('../../../shared/lib/token-util'), - fetchTokenBalance: jest.fn(), -})); - const renderUseLatestBalance = ( token: { address: string; decimals?: number | string }, chainId: string, @@ -38,17 +29,10 @@ const renderUseLatestBalance = ( describe('useLatestBalance', () => { beforeEach(() => { jest.clearAllMocks(); - const { provider } = createTestProviderTools({ - networkId: 'Ethereum', - chainId: CHAIN_IDS.MAINNET, - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - global.ethereumProvider = provider as any; }); it('returns balanceAmount for native asset in current chain', async () => { - mockGetBalance.mockResolvedValue(new BigNumber('1000000000000000000')); + mockCalcLatestSrcBalance.mockResolvedValueOnce('1000000000000000000'); const { result, waitForNextUpdate } = renderUseLatestBalance( { address: zeroAddress(), decimals: 18 }, @@ -57,33 +41,73 @@ describe('useLatestBalance', () => { ); await waitForNextUpdate(); - expect(result.current).toStrictEqual(new BigNumber('1')); + expect(result.current?.toString()).toBe('1'); - expect(mockGetBalance).toHaveBeenCalledTimes(1); - expect(mockGetBalance).toHaveBeenCalledWith( - '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + expect(mockCalcLatestSrcBalance).toHaveBeenCalledTimes(1); + expect(mockCalcLatestSrcBalance).toHaveBeenCalledWith( + 'https://localhost/rpc/0x1', + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + '0x0000000000000000000000000000000000000000', ); - expect(mockFetchTokenBalance).toHaveBeenCalledTimes(0); }); it('returns balanceAmount for ERC20 asset in current chain', async () => { - mockFetchTokenBalance.mockResolvedValueOnce(new BigNumber('15390000')); + mockCalcLatestSrcBalance.mockResolvedValueOnce('15390000'); const { result, waitForNextUpdate } = renderUseLatestBalance( - { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: '6' }, + { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: 6 }, CHAIN_IDS.MAINNET, createBridgeMockStore(), ); await waitForNextUpdate(); - expect(result.current).toStrictEqual(new BigNumber('15.39')); + expect(result.current?.toString()).toStrictEqual('15.39'); - expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1); - expect(mockFetchTokenBalance).toHaveBeenCalledWith( - '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + expect(mockCalcLatestSrcBalance).toHaveBeenCalledTimes(1); + expect(mockCalcLatestSrcBalance).toHaveBeenCalledWith( + 'https://localhost/rpc/0x1', '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - global.ethereumProvider, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', ); - expect(mockGetBalance).toHaveBeenCalledTimes(0); + }); + + it('returns balance amount for Solana token', async () => { + const mockStoreState = createBridgeMockStore({ + metamaskStateOverrides: { + internalAccounts: { + selectedAccount: 'test-account-id', + accounts: { + 'test-account-id': { + id: 'test-account-id', + type: 'solana', + address: 'account-address', + }, + }, + }, + balances: { + 'test-account-id': { + [bridgeController.getNativeAssetForChainId( + bridgeController.ChainId.SOLANA, + ).assetId]: { + amount: '2', + }, + }, + }, + }, + }); + + const { result, waitForNextUpdate } = renderUseLatestBalance( + { + address: bridgeController.getNativeAssetForChainId( + bridgeController.ChainId.SOLANA, + ).assetId, + decimals: 9, + }, + MultichainNetwork.Solana, + mockStoreState, + ); + + await waitForNextUpdate(); + expect(result.current?.toString()).toBe('2'); }); }); diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index 0a2157384fb7..11e9217cccf6 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -1,9 +1,11 @@ -import { type Hex, type CaipChainId, isCaipChainId } from '@metamask/utils'; +import { type Hex, type CaipChainId } from '@metamask/utils'; import { useMemo } from 'react'; import { isSolanaChainId, calcLatestSrcBalance, + formatChainIdToCaip, } from '@metamask/bridge-controller'; +import { useSelector } from 'react-redux'; import { getSelectedInternalAccount } from '../../selectors'; import { useAsyncResult } from '../useAsyncResult'; import { Numeric } from '../../../shared/modules/Numeric'; @@ -13,6 +15,7 @@ import { getMultichainBalances, getMultichainCurrentChainId, } from '../../selectors/multichain'; +import { getProviderConfig } from '../../../shared/modules/selectors/networks'; /** * Custom hook to fetch and format the latest balance of a given token or native asset. @@ -38,30 +41,18 @@ const useLatestBalance = ( const nonEvmBalancesByAccountId = useMultichainSelector( getMultichainBalances, ); + const { rpcUrl } = useSelector(getProviderConfig); const nonEvmBalances = nonEvmBalancesByAccountId?.[id]; const value = useAsyncResult(async () => { - if ( - token?.address && - // TODO check whether chainId is EVM when MultichainNetworkController is integrated - !isCaipChainId(chainId) && - chainId && - currentChainId === chainId - ) { - return ( - await calcLatestSrcBalance( - global.ethereumProvider, - selectedAddress, - token.address, - chainId, - ) - )?.toString(); + if (!chainId || !token) { + return undefined; } // No need to fetch the balance for non-EVM tokens, use the balance provided by the // multichain balances controller - if (chainId && isSolanaChainId(chainId) && token?.decimals) { + if (isSolanaChainId(chainId) && token.decimals) { return Numeric.from( nonEvmBalances?.[token.address]?.amount ?? token?.string, 10, @@ -70,15 +61,16 @@ const useLatestBalance = ( .toString(); } + if ( + token.address && + formatChainIdToCaip(currentChainId) === formatChainIdToCaip(chainId) && + rpcUrl + ) { + return await calcLatestSrcBalance(rpcUrl, selectedAddress, token.address); + } + return undefined; - }, [ - chainId, - currentChainId, - token, - selectedAddress, - global.ethereumProvider, - nonEvmBalances, - ]); + }, [chainId, currentChainId, token, selectedAddress, rpcUrl, nonEvmBalances]); if (token && !token.decimals) { throw new Error( From 43af28bab111503b462c4b3105698ed16fb6ad99 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 21 Mar 2025 14:03:13 -0700 Subject: [PATCH 32/71] chore: undo calcLatestSrcBalance change --- ui/hooks/bridge/useLatestBalance.test.ts | 35 ++++++++++++++++++++++-- ui/hooks/bridge/useLatestBalance.ts | 10 ++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts index 12e10523bb82..8373c786c3ac 100644 --- a/ui/hooks/bridge/useLatestBalance.test.ts +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -1,9 +1,11 @@ import { zeroAddress } from 'ethereumjs-util'; import * as bridgeController from '@metamask/bridge-controller'; import { MultichainNetwork } from '@metamask/multichain-transactions-controller'; +import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { renderHookWithProvider } from '../../../test/lib/render-helpers'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; +import { createTestProviderTools } from '../../../test/stub/provider'; import useLatestBalance from './useLatestBalance'; const mockCalcLatestSrcBalance = jest.fn(); @@ -29,6 +31,12 @@ const renderUseLatestBalance = ( describe('useLatestBalance', () => { beforeEach(() => { jest.clearAllMocks(); + const { provider } = createTestProviderTools({ + networkId: 'Ethereum', + chainId: CHAIN_IDS.MAINNET, + }); + + global.ethereumProvider = provider; }); it('returns balanceAmount for native asset in current chain', async () => { @@ -45,9 +53,10 @@ describe('useLatestBalance', () => { expect(mockCalcLatestSrcBalance).toHaveBeenCalledTimes(1); expect(mockCalcLatestSrcBalance).toHaveBeenCalledWith( - 'https://localhost/rpc/0x1', + global.ethereumProvider, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', '0x0000000000000000000000000000000000000000', + CHAIN_IDS.MAINNET, ); }); @@ -65,9 +74,31 @@ describe('useLatestBalance', () => { expect(mockCalcLatestSrcBalance).toHaveBeenCalledTimes(1); expect(mockCalcLatestSrcBalance).toHaveBeenCalledWith( - 'https://localhost/rpc/0x1', + global.ethereumProvider, + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + CHAIN_IDS.MAINNET, + ); + }); + + it('returns balanceAmount for ERC20 asset in current caip-formatted EVM chain', async () => { + mockCalcLatestSrcBalance.mockResolvedValueOnce('15390000'); + + const { result, waitForNextUpdate } = renderUseLatestBalance( + { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: 6 }, + toEvmCaipChainId(CHAIN_IDS.MAINNET), + createBridgeMockStore(), + ); + + await waitForNextUpdate(); + expect(result.current?.toString()).toStrictEqual('15.39'); + + expect(mockCalcLatestSrcBalance).toHaveBeenCalledTimes(1); + expect(mockCalcLatestSrcBalance).toHaveBeenCalledWith( + global.ethereumProvider, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + CHAIN_IDS.MAINNET, ); }); diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index 11e9217cccf6..98f5f35d5c9f 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -4,6 +4,7 @@ import { isSolanaChainId, calcLatestSrcBalance, formatChainIdToCaip, + formatChainIdToHex, } from '@metamask/bridge-controller'; import { useSelector } from 'react-redux'; import { getSelectedInternalAccount } from '../../selectors'; @@ -66,7 +67,14 @@ const useLatestBalance = ( formatChainIdToCaip(currentChainId) === formatChainIdToCaip(chainId) && rpcUrl ) { - return await calcLatestSrcBalance(rpcUrl, selectedAddress, token.address); + return ( + await calcLatestSrcBalance( + global.ethereumProvider, + selectedAddress, + token.address, + formatChainIdToHex(chainId), + ) + )?.toString(); } return undefined; From b775b67b6d7c2f40a2236452d137ed3157d6617a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 21 Mar 2025 14:31:55 -0700 Subject: [PATCH 33/71] chore: bump bridge-controller --- package.json | 2 +- yarn.lock | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 93ac7b5343b9..a93bafdb6a97 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "readable-stream-3@^3.6.2": "npm:readable-stream@^3.6.2", "semver@7.3.7": "^7.5.4", "semver@7.3.8": "^7.5.4", - "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@9.0.0-preview-330335c1", + "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@10.0.0-preview-e7013a54", "lavamoat-core@npm:^16.2.2": "patch:lavamoat-core@npm%3A16.2.2#~/.yarn/patches/lavamoat-core-npm-16.2.2-e361ff1f8a.patch", "@metamask/snaps-sdk": "^6.19.0", "@swc/types@0.1.5": "^0.1.6", diff --git a/yarn.lock b/yarn.lock index 3f8f72336e8a..4361c753b4fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5046,9 +5046,9 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@9.0.0-preview-330335c1": - version: 9.0.0-preview-330335c1 - resolution: "@metamask-previews/bridge-controller@npm:9.0.0-preview-330335c1" +"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@10.0.0-preview-e7013a54": + version: 10.0.0-preview-e7013a54 + resolution: "@metamask-previews/bridge-controller@npm:10.0.0-preview-e7013a54" dependencies: "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" @@ -5067,8 +5067,8 @@ __metadata: "@metamask/accounts-controller": ^27.0.0 "@metamask/network-controller": ^23.0.0 "@metamask/snaps-controllers": ^9.19.0 - "@metamask/transaction-controller": ^51.0.0 - checksum: 10/c9f6ef45f246b805903507824c6b32cb932ad582f863ce61f9f43ac0f01ef89b249a8a0a4985a5d4942aa0ff894c9c63cd4d713d91670136d8520d0eb395f3cc + "@metamask/transaction-controller": ^52.0.0 + checksum: 10/3c8d79c4eeb0dbdadcce6897884b1a837cb590965fed5366286fa43a80d98d896f99e3700849db806d4c8fe4742988dc729bdd4335bbfa9b5b330b3d0172ecda languageName: node linkType: hard @@ -5620,7 +5620,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^17.0.0, @metamask/keyring-api@npm:^17.2.0, @metamask/keyring-api@npm:^17.2.1": +"@metamask/keyring-api@npm:^17.2.0, @metamask/keyring-api@npm:^17.2.1": version: 17.2.1 resolution: "@metamask/keyring-api@npm:17.2.1" dependencies: @@ -5779,17 +5779,17 @@ __metadata: linkType: hard "@metamask/multichain-network-controller@npm:^0.1.0": - version: 0.1.1 - resolution: "@metamask/multichain-network-controller@npm:0.1.1" + version: 0.1.2 + resolution: "@metamask/multichain-network-controller@npm:0.1.2" dependencies: "@metamask/base-controller": "npm:^8.0.0" - "@metamask/keyring-api": "npm:^17.0.0" - "@metamask/utils": "npm:^11.1.0" + "@metamask/keyring-api": "npm:^17.2.0" + "@metamask/utils": "npm:^11.2.0" "@solana/addresses": "npm:^2.0.0" peerDependencies: "@metamask/accounts-controller": ^24.0.0 "@metamask/network-controller": ^22.0.0 - checksum: 10/0753831d802d84d154dbdfacac8ad8f7277d25957a3ec369a5fa588bee219656c4b6b0992699191eafa965ef2b43d1d4c827c0c7e417b82d94bf244040ca8451 + checksum: 10/4f5423a6de319db540a13e5dc5d637b5e55cf7af901cbe040353912c4aaba640a9454abcecb93fa53ca397f5c40d097835b001f40963f8624c02961dbf29931a languageName: node linkType: hard From 5349aef7f0bc18dff9872b174cb10769ca6cdce2 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 21 Mar 2025 18:25:13 -0700 Subject: [PATCH 34/71] fix: bridge-cta-button warnings --- ui/pages/bridge/prepare/bridge-cta-button.tsx | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index f750f7fc3e19..89b12d8cfdc8 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -34,7 +34,6 @@ import { useRequestProperties } from '../../../hooks/bridge/events/useRequestPro import { useRequestMetadataProperties } from '../../../hooks/bridge/events/useRequestMetadataProperties'; import { useTradeProperties } from '../../../hooks/bridge/events/useTradeProperties'; import { MetaMetricsEventName } from '../../../../shared/constants/metametrics'; -import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { Row } from '../layout'; import { isQuoteExpired as isQuoteExpiredUtil } from '../utils/quote'; @@ -76,10 +75,12 @@ export const BridgeCTAButton = ({ const wasTxDeclined = useSelector(getWasTxDeclined); const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); - const nativeAssetBalance = useLatestBalance( - fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, - fromChain?.chainId, + const nativeAsset = useMemo( + () => + fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, + [fromChain?.chainId], ); + const nativeAssetBalance = useLatestBalance(nativeAsset, fromChain?.chainId); const isTxSubmittable = useIsTxSubmittable(); const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker(); @@ -87,8 +88,6 @@ export const BridgeCTAButton = ({ const requestMetadataProperties = useRequestMetadataProperties(); const tradeProperties = useTradeProperties(); - const ticker = useSelector(getNativeCurrency); - const isInsufficientBalance = isInsufficientBalance_(balanceAmount); const isInsufficientGasBalance = @@ -98,65 +97,64 @@ export const BridgeCTAButton = ({ const label = useMemo(() => { if (wasTxDeclined) { - return t('youDeclinedTheTransaction'); + return 'youDeclinedTheTransaction'; } if (isQuoteExpired) { - return t('bridgeQuoteExpired'); + return 'bridgeQuoteExpired'; } if (isLoading && !isTxSubmittable && !activeQuote) { - return ''; + return undefined; } if (isInsufficientGasBalance || isNoQuotesAvailable) { - return ''; + return undefined; } if (isInsufficientBalance || isInsufficientGasForQuote) { - return t('alertReasonInsufficientBalance'); + return 'alertReasonInsufficientBalance'; } if (!fromAmount) { if (!toToken) { return needsDestinationAddress - ? t('bridgeSelectTokenAmountAndAccount') - : t('bridgeSelectTokenAndAmount'); + ? 'bridgeSelectTokenAmountAndAccount' + : 'bridgeSelectTokenAndAmount'; } return needsDestinationAddress - ? t('bridgeEnterAmountAndSelectAccount') - : t('bridgeEnterAmount'); + ? 'bridgeEnterAmountAndSelectAccount' + : 'bridgeEnterAmount'; } if (needsDestinationAddress) { - return t('bridgeSelectDestinationAccount'); + return 'bridgeSelectDestinationAccount'; } if (isTxSubmittable) { - return t('submit'); + return 'submit'; } - return t('swapSelectToken'); + return 'swapSelectToken'; }, [ isLoading, fromAmount, toToken, - ticker, isTxSubmittable, - balanceAmount, isInsufficientBalance, - isQuoteExpired, isInsufficientGasBalance, isInsufficientGasForQuote, wasTxDeclined, isQuoteExpired, needsDestinationAddress, + activeQuote, + isNoQuotesAvailable, ]); // Label for the secondary button that re-starts quote fetching const secondaryButtonLabel = useMemo(() => { if (wasTxDeclined || isQuoteExpired) { - return t('bridgeFetchNewQuotes'); + return 'bridgeFetchNewQuotes'; } return undefined; }, [wasTxDeclined, isQuoteExpired]); @@ -200,7 +198,7 @@ export const BridgeCTAButton = ({ needsDestinationAddress } > - {label} + {label ? t(label) : ''} ) : ( - {label} + {label ? t(label) : ''} {secondaryButtonLabel && ( - {secondaryButtonLabel} + {t(secondaryButtonLabel)} )} From 06475246ca37d47d7c936532062b72cd5c30bacd Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 21 Mar 2025 18:29:01 -0700 Subject: [PATCH 35/71] chore: memoize native assets --- ui/hooks/bridge/useIsTxSubmittable.ts | 8 +++++--- ui/hooks/bridge/useQuoteFetchEvents.ts | 8 +++++--- ui/pages/bridge/prepare/prepare-bridge-page.tsx | 8 +++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/ui/hooks/bridge/useIsTxSubmittable.ts b/ui/hooks/bridge/useIsTxSubmittable.ts index 3fe3229315e6..909779e62992 100644 --- a/ui/hooks/bridge/useIsTxSubmittable.ts +++ b/ui/hooks/bridge/useIsTxSubmittable.ts @@ -1,5 +1,6 @@ import { useSelector } from 'react-redux'; import { getNativeAssetForChainId } from '@metamask/bridge-controller'; +import { useMemo } from 'react'; import { getBridgeQuotes, getFromAmount, @@ -29,10 +30,11 @@ export const useIsTxSubmittable = () => { } = useSelector(getValidationErrors); const balanceAmount = useLatestBalance(fromToken, fromChainId); - const nativeAssetBalance = useLatestBalance( - fromChainId ? getNativeAssetForChainId(fromChainId) : null, - fromChainId, + const nativeAsset = useMemo( + () => getNativeAssetForChainId(fromChainId), + [fromChainId], ); + const nativeAssetBalance = useLatestBalance(nativeAsset, fromChainId); return Boolean( fromToken && diff --git a/ui/hooks/bridge/useQuoteFetchEvents.ts b/ui/hooks/bridge/useQuoteFetchEvents.ts index c6e29e101125..20996462c9d6 100644 --- a/ui/hooks/bridge/useQuoteFetchEvents.ts +++ b/ui/hooks/bridge/useQuoteFetchEvents.ts @@ -45,10 +45,12 @@ export const useQuoteFetchEvents = () => { const fromChain = useSelector(getFromChain); const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); - const nativeAssetBalance = useLatestBalance( - fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, - fromChain?.chainId, + const nativeAsset = useMemo( + () => + fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, + [fromChain?.chainId], ); + const nativeAssetBalance = useLatestBalance(nativeAsset, fromChain?.chainId); const warnings = useMemo(() => { const { diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 0b162bc83382..867f9b43fba7 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -189,10 +189,12 @@ const PrepareBridgePage = () => { const { quotesRefreshCount } = useSelector(getBridgeQuotes); const { openBuyCryptoInPdapp } = useRamps(); - const nativeAssetBalance = useLatestBalance( - fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, - fromChain?.chainId, + const nativeAsset = useMemo( + () => + fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, + [fromChain?.chainId], ); + const nativeAssetBalance = useLatestBalance(nativeAsset, fromChain?.chainId); const { tokenAlert } = useTokenAlerts(); const srcTokenBalance = useLatestBalance(fromToken, fromChain?.chainId); From 9e51d60c6c8fe67c4113bbe7e71ff6698374ee3c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 10:07:10 -0700 Subject: [PATCH 36/71] fix: warnings in useTokensWithFiltering --- ui/hooks/bridge/useTokensWithFiltering.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 1a6b9c2d999e..ce384218faaf 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -12,13 +12,9 @@ import { BridgeClientId, type BridgeAsset, } from '@metamask/bridge-controller'; -import { - getAllDetectedTokensForSelectedAddress, - selectERC20TokensByChain, -} from '../../selectors'; +import { selectERC20TokensByChain } from '../../selectors'; import { AssetType } from '../../../shared/constants/transaction'; import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../shared/constants/network'; -import { Token } from '../../components/app/assets/types'; import { useMultichainBalances } from '../useMultichainBalances'; import { useAsyncResult } from '../useAsyncResult'; import { fetchTopAssetsList } from '../../pages/swaps/swaps.util'; @@ -69,9 +65,6 @@ export const useTokensWithFiltering = ( chainId?: ChainId | Hex | CaipChainId, tokenToExclude?: null | Pick, ) => { - const allDetectedTokens: Record = useSelector( - getAllDetectedTokensForSelectedAddress, - ); const topAssetsFromFeatureFlags = useSelector((state: BridgeAppState) => getTopAssetsFromFeatureFlags(state, chainId), ); @@ -130,7 +123,7 @@ export const useTokensWithFiltering = ( }, [chainId, topAssetsFromFeatureFlags]); // This transforms the token object from the bridge-api into the format expected by the AssetPicker - const buildTokenData = ( + const buildTokenDataFn = ( token?: BridgeAsset, ): | AssetWithDisplayData @@ -166,7 +159,7 @@ export const useTokensWithFiltering = ( return { ...sharedFields, type: AssetType.token, - image: token.iconUrl ?? tokenList?.[token.address]?.iconUrl ?? '', + image: token.iconUrl ?? '', // Only tokens with 0 balance are processed here so hardcode empty string balance: '', string: undefined, @@ -174,6 +167,8 @@ export const useTokensWithFiltering = ( }; }; + const buildTokenData = useCallback(buildTokenDataFn, [chainId]); + // shouldAddToken is a filter condition passed in from the AssetPicker that determines whether a token should be included const filteredTokenListGenerator = useCallback( (filterCondition: FilterPredicate) => @@ -275,11 +270,11 @@ export const useTokensWithFiltering = ( } })(), [ + buildTokenData, multichainTokensWithBalance, topTokens, chainId, tokenList, - allDetectedTokens, tokenToExclude, ], ); From 7e27ccab1696db5e0757e0a2bbfcc888287675f1 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 10:36:31 -0700 Subject: [PATCH 37/71] refactor: rm chainId from useLatestBalance --- ui/hooks/bridge/useIsTxSubmittable.ts | 4 +-- ui/hooks/bridge/useLatestBalance.test.ts | 34 ++++++++++--------- ui/hooks/bridge/useLatestBalance.ts | 14 ++++---- ui/hooks/bridge/useQuoteFetchEvents.ts | 4 +-- ui/pages/bridge/prepare/bridge-cta-button.tsx | 4 +-- .../bridge/prepare/bridge-input-group.tsx | 2 +- .../bridge/prepare/prepare-bridge-page.tsx | 4 +-- .../quotes/multichain-bridge-quote-card.tsx | 12 +++---- 8 files changed, 41 insertions(+), 37 deletions(-) diff --git a/ui/hooks/bridge/useIsTxSubmittable.ts b/ui/hooks/bridge/useIsTxSubmittable.ts index 909779e62992..1bc8188b2545 100644 --- a/ui/hooks/bridge/useIsTxSubmittable.ts +++ b/ui/hooks/bridge/useIsTxSubmittable.ts @@ -29,12 +29,12 @@ export const useIsTxSubmittable = () => { isInsufficientGasForQuote, } = useSelector(getValidationErrors); - const balanceAmount = useLatestBalance(fromToken, fromChainId); + const balanceAmount = useLatestBalance(fromToken); const nativeAsset = useMemo( () => getNativeAssetForChainId(fromChainId), [fromChainId], ); - const nativeAssetBalance = useLatestBalance(nativeAsset, fromChainId); + const nativeAssetBalance = useLatestBalance(nativeAsset); return Boolean( fromToken && diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts index 8373c786c3ac..e8624bea480b 100644 --- a/ui/hooks/bridge/useLatestBalance.test.ts +++ b/ui/hooks/bridge/useLatestBalance.test.ts @@ -1,11 +1,11 @@ import { zeroAddress } from 'ethereumjs-util'; import * as bridgeController from '@metamask/bridge-controller'; -import { MultichainNetwork } from '@metamask/multichain-transactions-controller'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { renderHookWithProvider } from '../../../test/lib/render-helpers'; import { CHAIN_IDS } from '../../../shared/constants/network'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { createTestProviderTools } from '../../../test/stub/provider'; +import { MultichainNetworks } from '../../../shared/constants/multichain/networks'; import useLatestBalance from './useLatestBalance'; const mockCalcLatestSrcBalance = jest.fn(); @@ -18,15 +18,12 @@ jest.mock('@metamask/bridge-controller', () => { }); const renderUseLatestBalance = ( - token: { address: string; decimals?: number | string }, - chainId: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + token: { address: string; decimals?: number | string; chainId: any }, mockStoreState: object, ) => - renderHookWithProvider( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - () => useLatestBalance(token as any, chainId as any), - mockStoreState, - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + renderHookWithProvider(() => useLatestBalance(token as any), mockStoreState); describe('useLatestBalance', () => { beforeEach(() => { @@ -43,8 +40,7 @@ describe('useLatestBalance', () => { mockCalcLatestSrcBalance.mockResolvedValueOnce('1000000000000000000'); const { result, waitForNextUpdate } = renderUseLatestBalance( - { address: zeroAddress(), decimals: 18 }, - CHAIN_IDS.MAINNET, + { address: zeroAddress(), decimals: 18, chainId: CHAIN_IDS.MAINNET }, createBridgeMockStore(), ); @@ -64,8 +60,11 @@ describe('useLatestBalance', () => { mockCalcLatestSrcBalance.mockResolvedValueOnce('15390000'); const { result, waitForNextUpdate } = renderUseLatestBalance( - { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: 6 }, - CHAIN_IDS.MAINNET, + { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + decimals: 6, + chainId: CHAIN_IDS.MAINNET, + }, createBridgeMockStore(), ); @@ -85,8 +84,11 @@ describe('useLatestBalance', () => { mockCalcLatestSrcBalance.mockResolvedValueOnce('15390000'); const { result, waitForNextUpdate } = renderUseLatestBalance( - { address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: 6 }, - toEvmCaipChainId(CHAIN_IDS.MAINNET), + { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + decimals: 6, + chainId: toEvmCaipChainId(CHAIN_IDS.MAINNET), + }, createBridgeMockStore(), ); @@ -130,11 +132,11 @@ describe('useLatestBalance', () => { const { result, waitForNextUpdate } = renderUseLatestBalance( { address: bridgeController.getNativeAssetForChainId( - bridgeController.ChainId.SOLANA, + MultichainNetworks.SOLANA, ).assetId, decimals: 9, + chainId: MultichainNetworks.SOLANA, }, - MultichainNetwork.Solana, mockStoreState, ); diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts index 98f5f35d5c9f..c5511ae5acb9 100644 --- a/ui/hooks/bridge/useLatestBalance.ts +++ b/ui/hooks/bridge/useLatestBalance.ts @@ -5,6 +5,7 @@ import { calcLatestSrcBalance, formatChainIdToCaip, formatChainIdToHex, + ChainId, } from '@metamask/bridge-controller'; import { useSelector } from 'react-redux'; import { getSelectedInternalAccount } from '../../selectors'; @@ -22,7 +23,6 @@ import { getProviderConfig } from '../../../shared/modules/selectors/networks'; * Custom hook to fetch and format the latest balance of a given token or native asset. * * @param token - The token object for which the balance is to be fetched. Can be null. - * @param chainId - The chain ID to be used for fetching the balance. Optional. * @returns An object containing the balanceAmount as a string. */ const useLatestBalance = ( @@ -31,8 +31,8 @@ const useLatestBalance = ( decimals: number; symbol: string; string?: string; + chainId?: Hex | CaipChainId | ChainId; } | null, - chainId?: Hex | CaipChainId, ) => { const { address: selectedAddress, id } = useMultichainSelector( getSelectedInternalAccount, @@ -47,15 +47,17 @@ const useLatestBalance = ( const nonEvmBalances = nonEvmBalancesByAccountId?.[id]; const value = useAsyncResult(async () => { - if (!chainId || !token) { + if (!token?.chainId || !token) { return undefined; } + const { chainId } = token; + // No need to fetch the balance for non-EVM tokens, use the balance provided by the // multichain balances controller if (isSolanaChainId(chainId) && token.decimals) { return Numeric.from( - nonEvmBalances?.[token.address]?.amount ?? token?.string, + nonEvmBalances?.[token.address]?.amount ?? token.string, 10, ) .shiftedBy(-1 * token.decimals) @@ -78,7 +80,7 @@ const useLatestBalance = ( } return undefined; - }, [chainId, currentChainId, token, selectedAddress, rpcUrl, nonEvmBalances]); + }, [currentChainId, token, selectedAddress, rpcUrl, nonEvmBalances]); if (token && !token.decimals) { throw new Error( @@ -89,7 +91,7 @@ const useLatestBalance = ( return useMemo( () => value?.value ? calcTokenAmount(value.value, token?.decimals) : undefined, - [value.value, token?.decimals], + [value?.value, token?.decimals], ); }; diff --git a/ui/hooks/bridge/useQuoteFetchEvents.ts b/ui/hooks/bridge/useQuoteFetchEvents.ts index 20996462c9d6..bf2bff2fa40e 100644 --- a/ui/hooks/bridge/useQuoteFetchEvents.ts +++ b/ui/hooks/bridge/useQuoteFetchEvents.ts @@ -44,13 +44,13 @@ export const useQuoteFetchEvents = () => { const fromToken = useSelector(getFromToken); const fromChain = useSelector(getFromChain); - const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); + const balanceAmount = useLatestBalance(fromToken); const nativeAsset = useMemo( () => fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, [fromChain?.chainId], ); - const nativeAssetBalance = useLatestBalance(nativeAsset, fromChain?.chainId); + const nativeAssetBalance = useLatestBalance(nativeAsset); const warnings = useMemo(() => { const { diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx index 89b12d8cfdc8..cfcb3416a189 100644 --- a/ui/pages/bridge/prepare/bridge-cta-button.tsx +++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx @@ -74,13 +74,13 @@ export const BridgeCTAButton = ({ const wasTxDeclined = useSelector(getWasTxDeclined); - const balanceAmount = useLatestBalance(fromToken, fromChain?.chainId); + const balanceAmount = useLatestBalance(fromToken); const nativeAsset = useMemo( () => fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, [fromChain?.chainId], ); - const nativeAssetBalance = useLatestBalance(nativeAsset, fromChain?.chainId); + const nativeAssetBalance = useLatestBalance(nativeAsset); const isTxSubmittable = useIsTxSubmittable(); const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker(); diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx index 8594d411e587..244943ae7437 100644 --- a/ui/pages/bridge/prepare/bridge-input-group.tsx +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -87,7 +87,7 @@ export const BridgeInputGroup = ({ const locale = useSelector(getIntlLocale); const selectedChainId = networkProps?.network?.chainId; - const balanceAmount = useLatestBalance(token, selectedChainId); + const balanceAmount = useLatestBalance(token); const [, handleCopy] = useCopyToClipboard(MINUTE) as [ boolean, diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 867f9b43fba7..552a45c23f4c 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -194,10 +194,10 @@ const PrepareBridgePage = () => { fromChain?.chainId ? getNativeAssetForChainId(fromChain.chainId) : null, [fromChain?.chainId], ); - const nativeAssetBalance = useLatestBalance(nativeAsset, fromChain?.chainId); + const nativeAssetBalance = useLatestBalance(nativeAsset); const { tokenAlert } = useTokenAlerts(); - const srcTokenBalance = useLatestBalance(fromToken, fromChain?.chainId); + const srcTokenBalance = useLatestBalance(fromToken); const { selectedDestinationAccount, setSelectedDestinationAccount } = useDestinationAccount(isSwap); diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx index 6c12e984ab74..dab71e44d4f1 100644 --- a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { isSolanaChainId, BRIDGE_MM_FEE_RATE, + formatChainIdToHex, } from '@metamask/bridge-controller'; import type { ChainId } from '@metamask/bridge-controller'; import { @@ -46,7 +47,6 @@ import { MULTICHAIN_TOKEN_IMAGE_MAP, MultichainNetworks, } from '../../../../shared/constants/multichain/networks'; -import { decimalToHex } from '../../../../shared/modules/conversion.utils'; import { getIntlLocale } from '../../../ducks/locale/locale'; import { BridgeQuotesModal } from './bridge-quotes-modal'; @@ -72,20 +72,20 @@ export const MultichainBridgeQuoteCard = () => { return MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.SOLANA]; } return CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[ - `0x${decimalToHex( + formatChainIdToHex( chainId, - )}` as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP + ) as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP ]; }; const getNetworkName = (chainId: ChainId) => { if (isSolanaChainId(chainId)) { - return 'Solana'; + return NETWORK_TO_SHORT_NETWORK_NAME_MAP[MultichainNetworks.SOLANA]; } return NETWORK_TO_SHORT_NETWORK_NAME_MAP[ - `0x${decimalToHex( + formatChainIdToHex( chainId, - )}` as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP + ) as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP ]; }; From 1c23accbcf99e937d86e7a5b8f1a4c577eeb6f03 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 10:42:50 -0700 Subject: [PATCH 38/71] chore: use @metamask/bridge-controller 11.0.0 --- package.json | 3 +-- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 16988829219a..84e461ac5a0f 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,6 @@ "readable-stream-3@^3.6.2": "npm:readable-stream@^3.6.2", "semver@7.3.7": "^7.5.4", "semver@7.3.8": "^7.5.4", - "@metamask/bridge-controller": "npm:@metamask-previews/bridge-controller@10.0.0-preview-e7013a54", "lavamoat-core@npm:^16.2.2": "patch:lavamoat-core@npm%3A16.2.2#~/.yarn/patches/lavamoat-core-npm-16.2.2-e361ff1f8a.patch", "@metamask/snaps-sdk": "^6.19.0", "@swc/types@0.1.5": "^0.1.6", @@ -266,7 +265,7 @@ "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A53.1.1#~/.yarn/patches/@metamask-assets-controllers-npm-53.1.1-644979f986.patch", "@metamask/base-controller": "^8.0.0", "@metamask/bitcoin-wallet-snap": "^0.9.0", - "@metamask/bridge-controller": "*", + "@metamask/bridge-controller": "^11.0.0", "@metamask/browser-passworder": "^4.3.0", "@metamask/contract-metadata": "^2.5.0", "@metamask/controller-utils": "^11.4.0", diff --git a/yarn.lock b/yarn.lock index 2cb6179d8bf1..1ce7377736a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5046,9 +5046,9 @@ __metadata: languageName: node linkType: hard -"@metamask/bridge-controller@npm:@metamask-previews/bridge-controller@10.0.0-preview-e7013a54": - version: 10.0.0-preview-e7013a54 - resolution: "@metamask-previews/bridge-controller@npm:10.0.0-preview-e7013a54" +"@metamask/bridge-controller@npm:^11.0.0": + version: 11.0.0 + resolution: "@metamask/bridge-controller@npm:11.0.0" dependencies: "@ethersproject/address": "npm:^5.7.0" "@ethersproject/bignumber": "npm:^5.7.0" @@ -5068,7 +5068,7 @@ __metadata: "@metamask/network-controller": ^23.0.0 "@metamask/snaps-controllers": ^9.19.0 "@metamask/transaction-controller": ^52.0.0 - checksum: 10/3c8d79c4eeb0dbdadcce6897884b1a837cb590965fed5366286fa43a80d98d896f99e3700849db806d4c8fe4742988dc729bdd4335bbfa9b5b330b3d0172ecda + checksum: 10/e8312b568d893046e7d0b0db02a174e53e6908640d7ac0df7f7c21cdc042e59fab74af6364ea9e58cdfb73b326e43cbd4d837aa3f21deee0aea95e18015e4746 languageName: node linkType: hard @@ -27304,7 +27304,7 @@ __metadata: "@metamask/auto-changelog": "npm:^2.1.0" "@metamask/base-controller": "npm:^8.0.0" "@metamask/bitcoin-wallet-snap": "npm:^0.9.0" - "@metamask/bridge-controller": "npm:*" + "@metamask/bridge-controller": "npm:^11.0.0" "@metamask/browser-passworder": "npm:^4.3.0" "@metamask/build-utils": "npm:^3.0.0" "@metamask/contract-metadata": "npm:^2.5.0" From 4c50bae18602b648393186dc802513151cb13740 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 11:06:58 -0700 Subject: [PATCH 39/71] fix: useTokensWithFiltering tests --- .../useTokensWithFiltering.test.ts.snap | 32 +++++++++++++++---- .../bridge/useTokensWithFiltering.test.ts | 14 ++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap b/ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap index 9340ed61b7c5..924e849631a4 100644 --- a/ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap +++ b/ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap @@ -60,6 +60,7 @@ exports[`useTokensWithFiltering should fetch bridge tokens if cached tokens have { "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", "aggregators": [], + "assetId": "eip155:1/erc20:0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", "balance": "", "chainId": "0x1", "decimals": 18, @@ -75,6 +76,7 @@ exports[`useTokensWithFiltering should fetch bridge tokens if cached tokens have { "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x1", "decimals": 18, @@ -89,6 +91,7 @@ exports[`useTokensWithFiltering should fetch bridge tokens if cached tokens have { "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x1", "decimals": 6, @@ -101,11 +104,12 @@ exports[`useTokensWithFiltering should fetch bridge tokens if cached tokens have "type": "TOKEN", }, { - "address": null, + "address": "", + "assetId": "eip155:1/slip44:60", "balance": "0", "chainId": "0x1", "decimals": 18, - "iconUrl": "./images/eth_logo.svg", + "iconUrl": "", "image": "./images/eth_logo.svg", "name": "Ether", "string": "0", @@ -115,6 +119,7 @@ exports[`useTokensWithFiltering should fetch bridge tokens if cached tokens have { "address": "0x12652c6d93fdb6f4f37d48a8687783c782bb0d10", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x1", "decimals": 18, @@ -189,6 +194,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x1", "decimals": 18, @@ -204,6 +210,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x1", "decimals": 18, @@ -218,6 +225,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x1", "decimals": 6, @@ -230,11 +238,12 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active "type": "TOKEN", }, { - "address": null, + "address": "", + "assetId": "eip155:1/slip44:60", "balance": "0", "chainId": "0x1", "decimals": 18, - "iconUrl": "./images/eth_logo.svg", + "iconUrl": "", "image": "./images/eth_logo.svg", "name": "Ether", "string": "0", @@ -244,6 +253,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x12652c6d93fdb6f4f37d48a8687783c782bb0d10", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x1", "decimals": 18, @@ -263,6 +273,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", "aggregators": [], + "assetId": "eip155:1/erc20:0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", "balance": "", "chainId": "0x89", "decimals": 18, @@ -278,6 +289,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 18, @@ -292,6 +304,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 6, @@ -304,11 +317,12 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active "type": "TOKEN", }, { - "address": null, + "address": "", + "assetId": "eip155:1/slip44:60", "balance": "0", "chainId": "0x89", "decimals": 18, - "iconUrl": "./images/eth_logo.svg", + "iconUrl": "", "image": "./images/pol-token.svg", "name": "Ether", "string": "0", @@ -318,6 +332,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x12652c6d93fdb6f4f37d48a8687783c782bb0d10", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 18, @@ -332,6 +347,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0xb50721bcf8d664c30412cfbc6cf7a15145234ad1", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 18, @@ -346,6 +362,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x4d0528598f916fd1d8dc80e5f54a8feedcfd4b18", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 18, @@ -360,6 +377,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x57b946008913b82e4df85f501cbaed910e58d26c", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 18, @@ -374,6 +392,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x5eed99d066a8caf10f3e4327c1b3d8b673485eed", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 18, @@ -388,6 +407,7 @@ exports[`useTokensWithFiltering should return all tokens when chainId !== active { "address": "0x487d62468282bd04ddf976631c23128a425555ee", "aggregators": [], + "assetId": undefined, "balance": "", "chainId": "0x89", "decimals": 5, diff --git a/ui/hooks/bridge/useTokensWithFiltering.test.ts b/ui/hooks/bridge/useTokensWithFiltering.test.ts index 371438a67073..4488ea0eaa90 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.test.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.test.ts @@ -1,4 +1,7 @@ -import { getNativeAssetForChainId } from '@metamask/bridge-controller'; +import { + BridgeToken, + getNativeAssetForChainId, +} from '@metamask/bridge-controller'; import { renderHookWithProvider } from '../../../test/lib/render-helpers'; import { createBridgeMockStore } from '../../../test/jest/mock-store'; import { STATIC_MAINNET_TOKEN_LIST } from '../../../shared/constants/tokens'; @@ -11,8 +14,15 @@ const NATIVE_TOKEN = getNativeAssetForChainId(CHAIN_IDS.MAINNET); const mockFetchBridgeTokens = jest.fn().mockResolvedValue({ [NATIVE_TOKEN.address]: NATIVE_TOKEN, ...STATIC_MAINNET_TOKEN_LIST, + '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2': { + ...(STATIC_MAINNET_TOKEN_LIST as unknown as Record)[ + '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' + ], + assetId: 'eip155:1/erc20:0x6b3595068778dd592e39a122f4f5a5cf09c90fe2', + }, }); jest.mock('@metamask/bridge-controller', () => ({ + ...jest.requireActual('@metamask/bridge-controller'), fetchBridgeTokens: (c: string) => mockFetchBridgeTokens(c), })); @@ -30,7 +40,7 @@ describe('useTokensWithFiltering', () => { jest.clearAllMocks(); }); - it('should return all tokens when chainId !== activeChainId and chainId has been imported, sorted by balance', async () => { + it.only('should return all tokens when chainId !== activeChainId and chainId has been imported, sorted by balance', async () => { const mockStore = createBridgeMockStore({ metamaskStateOverrides: { completedOnboarding: true, From a7f22a4e4309a05be85e54b4111d48d9ef451736 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 11:53:46 -0700 Subject: [PATCH 40/71] chore: update lavamoat --- lavamoat/browserify/beta/policy.json | 12 ++++++++++++ lavamoat/browserify/flask/policy.json | 12 ++++++++++++ lavamoat/browserify/main/policy.json | 12 ++++++++++++ lavamoat/browserify/mmi/policy.json | 12 ++++++++++++ 4 files changed, 48 insertions(+) diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index 347b5f9c9569..7d2f3a382554 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -2040,6 +2040,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index 347b5f9c9569..7d2f3a382554 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -2040,6 +2040,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index 347b5f9c9569..7d2f3a382554 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -2040,6 +2040,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, diff --git a/lavamoat/browserify/mmi/policy.json b/lavamoat/browserify/mmi/policy.json index 1caf41dba6b1..651e0b0ec4a5 100644 --- a/lavamoat/browserify/mmi/policy.json +++ b/lavamoat/browserify/mmi/policy.json @@ -2186,6 +2186,18 @@ "uuid": true } }, + "@metamask/bridge-controller>@metamask/polling-controller": { + "globals": { + "clearTimeout": true, + "console.error": true, + "setTimeout": true + }, + "packages": { + "@metamask/base-controller": true, + "@metamask/snaps-utils>fast-json-stable-stringify": true, + "uuid": true + } + }, "@metamask/post-message-stream": { "globals": { "MessageEvent.prototype": true, From ae416fbc798795434c9bf90cb9b1cfa75e8f3afd Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 13:59:36 -0700 Subject: [PATCH 41/71] fix: sentry tests --- app/scripts/constants/sentry-state.ts | 1 + test/e2e/default-fixture.js | 17 +++++++++++++++++ ...rors-after-init-opt-in-background-state.json | 13 +++++++------ .../errors-after-init-opt-in-ui-state.json | 13 +++++++------ ...ors-before-init-opt-in-background-state.json | 1 + .../errors-before-init-opt-in-ui-state.json | 1 + 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts index 2c0b160cf8d6..13f49e0aecd7 100644 --- a/app/scripts/constants/sentry-state.ts +++ b/app/scripts/constants/sentry-state.ts @@ -117,6 +117,7 @@ export const SENTRY_BACKGROUND_STATE = { support: false, chains: {}, }, + mobileConfig: false, }, quoteRequest: { walletAddress: false, diff --git a/test/e2e/default-fixture.js b/test/e2e/default-fixture.js index ed0f098d73a2..0085a243e262 100644 --- a/test/e2e/default-fixture.js +++ b/test/e2e/default-fixture.js @@ -121,6 +121,23 @@ function defaultFixture(inputChainId = CHAIN_IDS.LOCALHOST) { }, BridgeController: { bridgeFeatureFlags: { + mobileConfig: { + support: false, + chains: { + 'eip155:1': { + isActiveSrc: true, + isActiveDest: true, + }, + 'eip155:10': { + isActiveSrc: true, + isActiveDest: true, + }, + 'eip155:59144': { + isActiveSrc: true, + isActiveDest: true, + }, + }, + }, extensionConfig: { support: false, chains: { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index dbb064af275c..9f7a025fb623 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -63,22 +63,23 @@ "AuthenticationController": { "isSignedIn": "boolean" }, "BridgeController": { "bridgeFeatureFlags": { + "mobileConfig": "object", "extensionConfig": { "refreshRate": "number", "maxRefreshCount": "number", "support": "boolean", - "chains": { - "eip155:1": "object", - "eip155:42161": "object", - "eip155:59144": "object" - } + "chains": {} } }, "quoteRequest": { "srcTokenAddress": "0x0000000000000000000000000000000000000000" }, "quotes": {}, - "quotesRefreshCount": 0 + "quotesRefreshCount": 0, + "quoteFetchError": null, + "quotesInitialLoadTime": null, + "quotesLastFetched": null, + "quotesLoadingStatus": null }, "BridgeStatusController": { "bridgeStatusState": { "txHistory": "object" } }, "CronjobController": { "jobs": "object", "events": "object" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index 487619db6df7..c80aa06752e9 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -345,13 +345,14 @@ "refreshRate": "number", "maxRefreshCount": "number", "support": "boolean", - "chains": { - "eip155:1": "object", - "eip155:42161": "object", - "eip155:59144": "object" - } - } + "chains": {} + }, + "mobileConfig": "object" }, + "quoteFetchError": null, + "quotesInitialLoadTime": null, + "quotesLastFetched": null, + "quotesLoadingStatus": null, "quoteRequest": { "srcTokenAddress": "0x0000000000000000000000000000000000000000" }, diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json index b3d34b1c7454..4ecb4dd3441f 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-background-state.json @@ -140,6 +140,7 @@ }, "BridgeController": { "bridgeFeatureFlags": { + "mobileConfig": "object", "extensionConfig": { "support": "boolean", "chains": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 9cf068c46162..32c92e5186b5 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -47,6 +47,7 @@ }, "BridgeController": { "bridgeFeatureFlags": { + "mobileConfig": "object", "extensionConfig": { "support": "boolean", "chains": { From 8cd2a895c9b03a410cf460eec9df8b7ef002be43 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 14:05:39 -0700 Subject: [PATCH 42/71] fix: lavamoat --- lavamoat/build-system/policy.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lavamoat/build-system/policy.json b/lavamoat/build-system/policy.json index 29e47a478cfa..c53f03b68c29 100644 --- a/lavamoat/build-system/policy.json +++ b/lavamoat/build-system/policy.json @@ -1810,7 +1810,6 @@ "chokidar>anymatch": true, "chokidar>braces": true, "chokidar>fsevents": true, - "tsx>fsevents": true, "eslint>glob-parent": true, "chokidar>is-binary-path": true, "del>is-glob": true, @@ -3381,13 +3380,6 @@ "gulp-watch>chokidar>fsevents>node-pre-gyp": true } }, - "tsx>fsevents": { - "globals": { - "console.assert": true, - "process.platform": true - }, - "native": true - }, "@lavamoat/allow-scripts>@npmcli/run-script>node-gyp>npmlog>gauge": { "builtin": { "util.format": true From 71629c70def6a22bdef7b86aa1c8e50f7b821b8c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 14:47:16 -0700 Subject: [PATCH 43/71] fix: e2e tests --- test/e2e/mock-e2e-allowlist.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/mock-e2e-allowlist.js b/test/e2e/mock-e2e-allowlist.js index 19f3f3392840..f3ee8b8fd35c 100644 --- a/test/e2e/mock-e2e-allowlist.js +++ b/test/e2e/mock-e2e-allowlist.js @@ -29,6 +29,7 @@ const ALLOWLISTED_URLS = [ 'https://authentication.api.cx.metamask.io/siwe/verify', 'https://bafkreifvhjdf6ve4jfv6qytqtux5nd4nwnelioeiqx5x2ez5yrgrzk7ypi.ipfs.dweb.link/', 'https://bafybeidxfmwycgzcp4v2togflpqh2gnibuexjy4m4qqwxp7nh3jx5zlh4y.ipfs.dweb.link/1.json', + 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', 'https://bridge.api.cx.metamask.io/getTokens?chainId=1', 'https://cdn.contentful.com/spaces/jdkgyfmyd9sw/environments/master/entries?content_type=productAnnouncement&order=-sys.createdAt&fields.clients=portfolio', 'https://cdn.segment.com/analytics-next/bundles/ajs-destination.bundle.ed53a26b6edc80c65d73.js', From 5e6856e93973e854e6909c0b3437147d808abcab Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 15:20:31 -0700 Subject: [PATCH 44/71] chore: mock bridge calls --- test/e2e/mock-e2e-allowlist.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/mock-e2e-allowlist.js b/test/e2e/mock-e2e-allowlist.js index f3ee8b8fd35c..19f3f3392840 100644 --- a/test/e2e/mock-e2e-allowlist.js +++ b/test/e2e/mock-e2e-allowlist.js @@ -29,7 +29,6 @@ const ALLOWLISTED_URLS = [ 'https://authentication.api.cx.metamask.io/siwe/verify', 'https://bafkreifvhjdf6ve4jfv6qytqtux5nd4nwnelioeiqx5x2ez5yrgrzk7ypi.ipfs.dweb.link/', 'https://bafybeidxfmwycgzcp4v2togflpqh2gnibuexjy4m4qqwxp7nh3jx5zlh4y.ipfs.dweb.link/1.json', - 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', 'https://bridge.api.cx.metamask.io/getTokens?chainId=1', 'https://cdn.contentful.com/spaces/jdkgyfmyd9sw/environments/master/entries?content_type=productAnnouncement&order=-sys.createdAt&fields.clients=portfolio', 'https://cdn.segment.com/analytics-next/bundles/ajs-destination.bundle.ed53a26b6edc80c65d73.js', From 004122b22d925f0864000d46c9e3878ee6afaab7 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 17:16:13 -0700 Subject: [PATCH 45/71] fix: add bridge featureFlags to allowed e2e urls --- test/e2e/mock-e2e-allowlist.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/mock-e2e-allowlist.js b/test/e2e/mock-e2e-allowlist.js index 19f3f3392840..f3ee8b8fd35c 100644 --- a/test/e2e/mock-e2e-allowlist.js +++ b/test/e2e/mock-e2e-allowlist.js @@ -29,6 +29,7 @@ const ALLOWLISTED_URLS = [ 'https://authentication.api.cx.metamask.io/siwe/verify', 'https://bafkreifvhjdf6ve4jfv6qytqtux5nd4nwnelioeiqx5x2ez5yrgrzk7ypi.ipfs.dweb.link/', 'https://bafybeidxfmwycgzcp4v2togflpqh2gnibuexjy4m4qqwxp7nh3jx5zlh4y.ipfs.dweb.link/1.json', + 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', 'https://bridge.api.cx.metamask.io/getTokens?chainId=1', 'https://cdn.contentful.com/spaces/jdkgyfmyd9sw/environments/master/entries?content_type=productAnnouncement&order=-sys.createdAt&fields.clients=portfolio', 'https://cdn.segment.com/analytics-next/bundles/ajs-destination.bundle.ed53a26b6edc80c65d73.js', From 736f937cb0e0c6eb89b0ff75faa088f8465faa7d Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 17:50:53 -0700 Subject: [PATCH 46/71] fix: sentry tests --- .../errors-after-init-opt-in-background-state.json | 13 ++++++++++++- .../errors-after-init-opt-in-ui-state.json | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json index 9f7a025fb623..266a087c7820 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json @@ -68,7 +68,18 @@ "refreshRate": "number", "maxRefreshCount": "number", "support": "boolean", - "chains": {} + "chains": { + "eip155:1": "object", + "eip155:10": "object", + "eip155:137": "object", + "eip155:324": "object", + "eip155:42161": "object", + "eip155:43114": "object", + "eip155:56": "object", + "eip155:59144": "object", + "eip155:8453": "object", + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": "object" + } } }, "quoteRequest": { diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json index c80aa06752e9..79bc970a71c0 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-ui-state.json @@ -345,7 +345,18 @@ "refreshRate": "number", "maxRefreshCount": "number", "support": "boolean", - "chains": {} + "chains": { + "eip155:1": "object", + "eip155:10": "object", + "eip155:137": "object", + "eip155:324": "object", + "eip155:42161": "object", + "eip155:43114": "object", + "eip155:56": "object", + "eip155:59144": "object", + "eip155:8453": "object", + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": "object" + } }, "mobileConfig": "object" }, From 7bb0763ea7530cdbc0ac70f989b54cc8e24a3007 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 21 Mar 2025 14:20:58 -0700 Subject: [PATCH 47/71] feat: add asset-utils module --- shared/lib/asset-utils.test.ts | 242 ++++++++++++++++++++++ shared/lib/asset-utils.ts | 132 ++++++++++++ ui/hooks/bridge/useTokensWithFiltering.ts | 25 ++- 3 files changed, 386 insertions(+), 13 deletions(-) create mode 100644 shared/lib/asset-utils.test.ts create mode 100644 shared/lib/asset-utils.ts diff --git a/shared/lib/asset-utils.test.ts b/shared/lib/asset-utils.test.ts new file mode 100644 index 000000000000..ce8e1fff60a4 --- /dev/null +++ b/shared/lib/asset-utils.test.ts @@ -0,0 +1,242 @@ +import { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; +import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; +import { MultichainNetwork } from '@metamask/multichain-transactions-controller'; +import { toHex } from '@metamask/controller-utils'; +import { MINUTE } from '../constants/time'; +import { MultichainNetworks } from '../constants/multichain/networks'; +import fetchWithCache from './fetch-with-cache'; +import { getAssetImageUrl, fetchAssetMetadata } from './asset-utils'; + +jest.mock('./fetch-with-cache'); +jest.mock('@metamask/multichain-network-controller'); +jest.mock('@metamask/controller-utils'); + +describe('asset-utils', () => { + const STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io'; + const TOKEN_API_V3_BASE_URL = 'https://tokens.api.cx.metamask.io/v3'; + + describe('getAssetImageUrl', () => { + it('should return correct image URL for a CAIP asset ID', () => { + const assetId = 'eip155:1/erc20:0x123' as CaipAssetType; + const expectedUrl = `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/eip155/1/erc20/0x123.png`; + + expect(getAssetImageUrl(assetId, 'eip155:1')).toBe(expectedUrl); + }); + + it('should return correct image URL for non-hex CAIP asset ID', () => { + const assetId = + `${MultichainNetworks.SOLANA}/token:aBCD` as CaipAssetType; + const expectedUrl = `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/aBCD.png`; + + expect(getAssetImageUrl(assetId, 'eip155:1')).toBe(expectedUrl); + }); + + it('should handle asset IDs with multiple colons', () => { + const assetId = 'test:chain:1/token:0x123' as CaipAssetType; + + expect(getAssetImageUrl(assetId, 'eip155:1')).toBe(undefined); + }); + }); + + describe('fetchAssetMetadata', () => { + const mockAddress = '0x123' as Hex; + const mockChainId = 'eip155:1' as CaipChainId; + const mockHexChainId = '0x1' as Hex; + const mockAssetId = 'eip155:1/erc20:0x123' as CaipAssetType; + + beforeEach(() => { + jest.clearAllMocks(); + (toEvmCaipChainId as jest.Mock).mockReturnValue(mockChainId); + (toHex as jest.Mock).mockImplementation((val) => val as Hex); + }); + + it('should fetch EVM token metadata successfully', async () => { + const mockMetadata = { + assetId: mockAssetId, + symbol: 'TEST', + name: 'Test Token', + decimals: 18, + }; + + (fetchWithCache as jest.Mock).mockResolvedValueOnce([mockMetadata]); + + const result = await fetchAssetMetadata(mockAddress, mockHexChainId); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: `${TOKEN_API_V3_BASE_URL}/assets?assetIds=${mockAssetId}`, + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { + cacheRefreshTime: MINUTE, + }, + functionName: 'fetchAssetMetadata', + }); + + expect(result).toStrictEqual({ + symbol: 'TEST', + decimals: 18, + image: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0x123.png', + assetId: mockAssetId, + address: mockAddress, + chainId: mockHexChainId, + }); + }); + + it('should fetch Solana token metadata successfully', async () => { + const solanaChainId = MultichainNetwork.Solana; + const solanaAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + const solanaAssetId = `${solanaChainId}/token:${solanaAddress}`; + + const mockMetadata = { + assetId: solanaAssetId, + symbol: 'SOL', + name: 'Solana Token', + decimals: 9, + }; + + (fetchWithCache as jest.Mock).mockResolvedValueOnce([mockMetadata]); + + const result = await fetchAssetMetadata(solanaAddress, solanaChainId); + + expect(result).toStrictEqual({ + symbol: 'SOL', + decimals: 9, + image: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png', + assetId: solanaAssetId, + address: solanaAssetId, + chainId: solanaChainId, + }); + }); + + it('should handle CAIP chain IDs', async () => { + const mockMetadata = { + assetId: mockAssetId, + symbol: 'TEST', + name: 'Test Token', + decimals: 18, + }; + + (fetchWithCache as jest.Mock).mockResolvedValueOnce([mockMetadata]); + + const result = await fetchAssetMetadata(mockAddress, mockChainId); + + expect(toEvmCaipChainId).not.toHaveBeenCalled(); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: `${TOKEN_API_V3_BASE_URL}/assets?assetIds=${mockAssetId}`, + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { + cacheRefreshTime: MINUTE, + }, + functionName: 'fetchAssetMetadata', + }); + + expect(result).toStrictEqual({ + symbol: 'TEST', + decimals: 18, + image: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0x123.png', + assetId: mockAssetId, + address: mockAddress, + chainId: mockHexChainId, + }); + }); + + it('should handle hex chain IDs', async () => { + const mockMetadata = { + assetId: mockAssetId, + symbol: 'TEST', + name: 'Test Token', + decimals: 18, + }; + + (fetchWithCache as jest.Mock).mockResolvedValueOnce([mockMetadata]); + + const result = await fetchAssetMetadata(mockAddress, mockHexChainId); + + expect(toEvmCaipChainId).toHaveBeenCalledWith(mockHexChainId); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: `${TOKEN_API_V3_BASE_URL}/assets?assetIds=${mockAssetId}`, + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { + cacheRefreshTime: MINUTE, + }, + functionName: 'fetchAssetMetadata', + }); + + expect(result).toStrictEqual({ + symbol: 'TEST', + decimals: 18, + image: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0x123.png', + assetId: mockAssetId, + address: mockAddress, + chainId: mockHexChainId, + }); + }); + + it('should return undefined when API call fails', async () => { + (fetchWithCache as jest.Mock).mockRejectedValueOnce( + new Error('API Error'), + ); + + const result = await fetchAssetMetadata(mockAddress, mockHexChainId); + + expect(result).toBeUndefined(); + }); + + it('should return undefined when metadata processing fails', async () => { + (fetchWithCache as jest.Mock).mockResolvedValueOnce([null]); + + const result = await fetchAssetMetadata(mockAddress, mockHexChainId); + + expect(result).toBeUndefined(); + }); + + it('should return undefined when EVM address is not valid', async () => { + const mockMetadata = { + assetId: 'hjk', + symbol: 'TEST', + name: 'Test Token', + decimals: 18, + }; + + (fetchWithCache as jest.Mock).mockResolvedValueOnce([mockMetadata]); + + const result = await fetchAssetMetadata(mockAddress, mockHexChainId); + + expect(fetchWithCache).toHaveBeenCalledWith({ + url: `${TOKEN_API_V3_BASE_URL}/assets?assetIds=${mockAssetId}`, + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { + cacheRefreshTime: MINUTE, + }, + functionName: 'fetchAssetMetadata', + }); + + expect(result).toStrictEqual({ + symbol: 'TEST', + decimals: 18, + image: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0x123.png', + assetId: mockAssetId, + address: mockAddress, + chainId: mockHexChainId, + }); + }); + }); +}); diff --git a/shared/lib/asset-utils.ts b/shared/lib/asset-utils.ts new file mode 100644 index 000000000000..68a72d130d42 --- /dev/null +++ b/shared/lib/asset-utils.ts @@ -0,0 +1,132 @@ +import { + CaipAssetType, + parseCaipChainId, + CaipAssetTypeStruct, + CaipChainId, + Hex, + isCaipAssetType, + isCaipChainId, + isStrictHexString, + parseCaipAssetType, +} from '@metamask/utils'; + +import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; +import { MultichainNetwork } from '@metamask/multichain-transactions-controller'; +import { toHex } from '@metamask/controller-utils'; +import { MINUTE } from '../constants/time'; +import { decimalToPrefixedHex } from '../modules/conversion.utils'; +import fetchWithCache from './fetch-with-cache'; + +const TOKEN_API_V3_BASE_URL = 'https://tokens.api.cx.metamask.io/v3'; +const STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io'; + +const buildAssetId = ( + address: Hex | CaipAssetType | string, + chainId: CaipChainId, +): CaipAssetType | undefined => { + if (isCaipAssetType(address)) { + return address; + } else if (chainId === MultichainNetwork.Solana) { + return CaipAssetTypeStruct.create(`${chainId}/token:${address}`); + } + // EVM assets + if (!isStrictHexString(address)) { + return undefined; + } + return CaipAssetTypeStruct.create(`${chainId}/erc20:${address}`); +}; + +/** + * Returns the image url for a caip-formatted asset + * + * @param assetId - The hex address or caip-formatted asset id + * @param chainId - The chainId in caip or hex format + * @returns The image url for the asset + */ +export const getAssetImageUrl = ( + assetId: CaipAssetType | string, + chainId: CaipChainId | Hex, +) => { + const chainIdInCaip = isCaipChainId(chainId) + ? chainId + : toEvmCaipChainId(chainId); + + const assetIdInCaip = buildAssetId(assetId, chainIdInCaip); + if (!assetIdInCaip) { + return undefined; + } + + return `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${assetIdInCaip.replaceAll( + ':', + '/', + )}.png`; +}; + +type AssetMetadata = { + assetId: string; + symbol: string; + name: string; + decimals: number; +}; + +/** + * Fetches the metadata for a token + * + * @param address - The address of the token + * @param chainId - The chainId of the token + * @returns The metadata for the token + */ +export const fetchAssetMetadata = async ( + address: string | CaipAssetType | Hex, + chainId: Hex | CaipChainId, +) => { + const chainIdInCaip = isCaipChainId(chainId) + ? chainId + : toEvmCaipChainId(chainId); + + const assetId = buildAssetId(address, chainIdInCaip); + + if (!assetId) { + return undefined; + } + + try { + const [assetMetadata]: AssetMetadata[] = await fetchWithCache({ + url: `${TOKEN_API_V3_BASE_URL}/assets?assetIds=${assetId}`, + fetchOptions: { + method: 'GET', + headers: { 'X-Client-Id': 'extension' }, + }, + cacheOptions: { + cacheRefreshTime: MINUTE, + }, + functionName: 'fetchAssetMetadata', + }); + + const commonFields = { + symbol: assetMetadata.symbol, + decimals: assetMetadata.decimals, + image: getAssetImageUrl(assetId, chainIdInCaip), + assetId, + }; + + if (chainId === MultichainNetwork.Solana && assetId) { + const { assetReference } = parseCaipAssetType(assetId); + return { + ...commonFields, + address: assetReference, + assetId, + chainId, + }; + } + + const { reference } = parseCaipChainId(chainIdInCaip); + return { + ...commonFields, + address: toHex(address), + chainId: decimalToPrefixedHex(reference), + }; + } catch (error) { + return undefined; + } +}; diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index d212673f13e2..290e92f3a879 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -24,15 +24,13 @@ import { getTopAssetsFromFeatureFlags, } from '../../ducks/bridge/selectors'; import fetchWithCache from '../../../shared/lib/fetch-with-cache'; -import { - BRIDGE_API_BASE_URL, - STATIC_METAMASK_BASE_URL, -} from '../../../shared/constants/bridge'; +import { BRIDGE_API_BASE_URL } from '../../../shared/constants/bridge'; import type { AssetWithDisplayData, ERC20Asset, NativeAsset, } from '../../components/multichain/asset-picker-amount/asset-picker-modal/types'; +import { getAssetImageUrl } from '../../../shared/lib/asset-utils'; type FilterPredicate = ( symbol: string, @@ -40,13 +38,6 @@ type FilterPredicate = ( tokenChainId?: string, ) => boolean; -// Returns the image url for a caip-formatted asset -const getAssetImageUrl = (assetId: string) => - `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${assetId?.replaceAll( - ':', - '/', - )}.png`; - /** * Returns a token list generator that filters and sorts tokens in this order * - matches search query @@ -220,7 +211,11 @@ export const useTokensWithFiltering = ( image: CHAIN_ID_TOKEN_IMAGE_MAP[ token.chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP - ] ?? getAssetImageUrl(token.address), + ] ?? + getAssetImageUrl( + token.address, + formatChainIdToCaip(token.chainId), + ), }; } else { yield { @@ -235,7 +230,11 @@ export const useTokensWithFiltering = ( image: (token.image || tokenList?.[token.address.toLowerCase()]?.iconUrl) ?? - getAssetImageUrl(token.address), + getAssetImageUrl( + token.address, + formatChainIdToCaip(token.chainId), + ) ?? + '', }; } } From 51c48b7432be22e51eaec3f5ce57fb72a2183097 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 11:54:57 -0700 Subject: [PATCH 48/71] chore: add useAssetMetadata hoook --- ui/hooks/useAssetMetadata.test.ts | 127 ++++++++++++++++++++++++++++++ ui/hooks/useAssetMetadata.ts | 70 ++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 ui/hooks/useAssetMetadata.test.ts create mode 100644 ui/hooks/useAssetMetadata.ts diff --git a/ui/hooks/useAssetMetadata.test.ts b/ui/hooks/useAssetMetadata.test.ts new file mode 100644 index 000000000000..114b67994c50 --- /dev/null +++ b/ui/hooks/useAssetMetadata.test.ts @@ -0,0 +1,127 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useSelector } from 'react-redux'; +import { AssetType } from '../../shared/constants/transaction'; +import { useAssetMetadata } from './useAssetMetadata'; + +// Mock dependencies +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +const mockFetchAssetMetadata = jest.fn(); +const mockGetAssetImageUrl = jest.fn(); + +jest.mock('../../shared/lib/asset-utils', () => ({ + fetchAssetMetadata: (...args: unknown[]) => mockFetchAssetMetadata(...args), + getAssetImageUrl: (...args: unknown[]) => mockGetAssetImageUrl(...args), +})); + +const mockChainId = '0x1'; +const mockSearchQuery = '0x123'; +const mockAssetId = 'eip155:1/erc20:0x123'; + +describe('useAssetMetadata', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useSelector as jest.Mock).mockReturnValue(true); // allowExternalServices = true + mockGetAssetImageUrl.mockReturnValue('mock-image-url'); + }); + + it('should return undefined when external services are disabled', async () => { + (useSelector as jest.Mock).mockReturnValue(false); // allowExternalServices = false + + const { result, waitForNextUpdate } = renderHook(() => + useAssetMetadata(mockSearchQuery, true, mockChainId), + ); + + await waitForNextUpdate(); + expect(result.current).toBeUndefined(); + expect(mockFetchAssetMetadata).not.toHaveBeenCalled(); + }); + + it('should return undefined when chainId is not provided', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAssetMetadata(mockSearchQuery, true, undefined), + ); + + await waitForNextUpdate(); + expect(result.current).toBeUndefined(); + expect(mockFetchAssetMetadata).not.toHaveBeenCalled(); + }); + + it('should return undefined when search query is empty', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAssetMetadata('', true, mockChainId), + ); + + await waitForNextUpdate(); + expect(result.current).toBeUndefined(); + expect(mockFetchAssetMetadata).not.toHaveBeenCalled(); + }); + + it('should fetch and return asset metadata when conditions are met', async () => { + const mockMetadata = { + address: '0x123', + symbol: 'TEST', + decimals: 18, + assetId: mockAssetId, + chainId: mockChainId, + }; + + mockFetchAssetMetadata.mockResolvedValueOnce(mockMetadata); + + const { result, waitForNextUpdate } = renderHook(() => + useAssetMetadata(mockSearchQuery, true, mockChainId), + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + ...mockMetadata, + chainId: mockChainId, + isNative: false, + type: AssetType.token, + image: 'mock-image-url', + balance: '', + string: '', + }); + + expect(mockFetchAssetMetadata).toHaveBeenCalledWith( + mockSearchQuery.trim(), + mockChainId, + ); + expect(mockGetAssetImageUrl).toHaveBeenCalledWith(mockAssetId, mockChainId); + }); + + it('should return undefined when fetchAssetMetadata returns undefined', async () => { + mockFetchAssetMetadata.mockResolvedValueOnce(undefined); + + const { result, waitForNextUpdate } = renderHook(() => + useAssetMetadata(mockSearchQuery, true, mockChainId), + ); + + await waitForNextUpdate(); + expect(result.current).toBeUndefined(); + + expect(mockFetchAssetMetadata).toHaveBeenCalledWith( + mockSearchQuery.trim(), + mockChainId, + ); + }); + + it('should handle errors gracefully', async () => { + mockFetchAssetMetadata.mockRejectedValueOnce(new Error('API Error')); + + const { result, waitForNextUpdate } = renderHook(() => + useAssetMetadata(mockSearchQuery, true, mockChainId), + ); + + await waitForNextUpdate(); + expect(result.current).toBeUndefined(); + + expect(mockFetchAssetMetadata).toHaveBeenCalledWith( + mockSearchQuery.trim(), + mockChainId, + ); + }); +}); diff --git a/ui/hooks/useAssetMetadata.ts b/ui/hooks/useAssetMetadata.ts new file mode 100644 index 000000000000..287580958de5 --- /dev/null +++ b/ui/hooks/useAssetMetadata.ts @@ -0,0 +1,70 @@ +import { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; +import { useSelector } from 'react-redux'; +import { getUseExternalServices } from '../selectors'; +import { + fetchAssetMetadata, + getAssetImageUrl, +} from '../../shared/lib/asset-utils'; +import { AssetType } from '../../shared/constants/transaction'; +import { useAsyncResult } from './useAsync'; + +/** + * Fetches token metadata for a single token if searchQuery is defined but filteredTokenList is empty + * + * @param searchQuery - The search query to fetch metadata for + * @param shouldFetchMetadata - Whether to fetch metadata + * @param chainId - The chain id to fetch metadata for + * @returns The asset metadata + */ +export const useAssetMetadata = ( + searchQuery: string, + shouldFetchMetadata: boolean, + chainId?: Hex | CaipChainId, +) => { + const allowExternalServices = useSelector(getUseExternalServices); + + const { value: assetMetadata } = useAsyncResult< + | { + address: Hex | CaipAssetType | string; + symbol: string; + decimals: number; + image: string; + chainId: Hex | CaipChainId; + isNative: boolean; + type: AssetType.token; + balance: string; + string: string; + } + | undefined + >(async () => { + if (!chainId || !searchQuery) { + return undefined; + } + + const trimmedSearchQuery = searchQuery.trim(); + if ( + allowExternalServices && + chainId && + shouldFetchMetadata && + trimmedSearchQuery.length > 0 + ) { + const metadata = await fetchAssetMetadata(trimmedSearchQuery, chainId); + + if (metadata) { + return { + ...metadata, + chainId, + isNative: false, + type: AssetType.token, + image: getAssetImageUrl(metadata.assetId, chainId) ?? '', + balance: '', + string: '', + } as const; + } + return undefined; + } + return undefined; + }, [shouldFetchMetadata, searchQuery]); + + return assetMetadata; +}; From da851d459eea286bff648caea3467943e2c6a8e9 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 10:37:33 -0700 Subject: [PATCH 49/71] test: add unit tests for asset-picker-modal --- .../asset-picker-modal/AssetList.tsx | 3 +- .../asset-picker-modal.test.tsx.snap | 545 ++++++++++++++++++ .../asset-picker-modal.test.tsx | 323 ++++++++++- 3 files changed, 850 insertions(+), 21 deletions(-) create mode 100644 ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx index 6515bbfe71a6..4f78bb055c91 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/AssetList.tsx @@ -5,6 +5,7 @@ import { NetworkConfiguration, } from '@metamask/network-controller'; import type { CaipChainId } from '@metamask/utils'; +import { useSelector } from 'react-redux'; import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; import { AssetType } from '../../../../../shared/constants/transaction'; import { Box } from '../../../component-library'; @@ -80,7 +81,7 @@ export default function AssetList({ const balanceValue = useMultichainSelector( getMultichainSelectedAccountCachedBalance, ); - const currentCurrency = useMultichainSelector(getMultichainCurrentCurrency); + const currentCurrency = useSelector(getMultichainCurrentCurrency); const [primaryCurrencyValue] = useCurrencyDisplay(balanceValue, { currency: currentCurrency, diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap new file mode 100644 index 000000000000..46dbbcdf32fa --- /dev/null +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap @@ -0,0 +1,545 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssetPickerModal token filtering should filter tokens by address 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should filter tokens by address 2`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should filter tokens by chain when multichain network is selected 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": true, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "name": "Solana", + }, + "tokenList": [ + { + "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "balance": "0x2faf080", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1000", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "decimals": 18, + "image": {}, + "string": undefined, + "symbol": "ETH", + "type": "NATIVE", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should filter tokens by symbol 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should handle case-insensitive search 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should handle case-insensitive search 2`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should only show tokens with balances in send mode 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": false, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f985", + "balance": "0x989680", + "chainId": "0xa", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1000", + "chainId": "0xa", + "decimals": 18, + "image": {}, + "string": undefined, + "symbol": "ETH", + "type": "NATIVE", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should only show tokens with balances in send mode 2`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": false, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f985", + "balance": "0x989680", + "chainId": "0xa", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1000", + "chainId": "0xa", + "decimals": 18, + "image": {}, + "string": undefined, + "symbol": "ETH", + "type": "NATIVE", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should render all tokens from multiple chains 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "", + "balance": "1.5", + "chainId": "0x1", + "decimals": 18, + "image": "./images/eth_logo.svg", + "isNative": true, + "symbol": "ETH", + "type": "NATIVE", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f985", + "balance": "0x989680", + "chainId": "0xa", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "balance": "0x2faf080", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1000", + "chainId": "0xa", + "decimals": 18, + "image": {}, + "string": undefined, + "symbol": "ETH", + "type": "NATIVE", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should render all tokens from single chain 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": true, + "isTitleNetworkName": false, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0x1", + "name": "Ethereum Mainnet", + }, + "tokenList": [ + { + "address": "", + "balance": "1.5", + "chainId": "0x1", + "decimals": 18, + "image": "./images/eth_logo.svg", + "isNative": true, + "symbol": "ETH", + "type": "NATIVE", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should respect MAX_UNOWNED_TOKENS_RENDERED limit 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "", + "balance": "1.5", + "chainId": "0x1", + "decimals": 18, + "image": "./images/eth_logo.svg", + "isNative": true, + "symbol": "ETH", + "type": "NATIVE", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f985", + "balance": "0x989680", + "chainId": "0xa", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "balance": "0x2faf080", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1000", + "chainId": "0xa", + "decimals": 18, + "image": {}, + "string": undefined, + "symbol": "ETH", + "type": "NATIVE", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should show all tokens when search query is cleared 1`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + ], + }, +] +`; + +exports[`AssetPickerModal token filtering should show all tokens when search query is cleared 2`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "", + "balance": "1.5", + "chainId": "0x1", + "decimals": 18, + "image": "./images/eth_logo.svg", + "isNative": true, + "symbol": "ETH", + "type": "NATIVE", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f985", + "balance": "0x989680", + "chainId": "0xa", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "balance": "0x2faf080", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1000", + "chainId": "0xa", + "decimals": 18, + "image": {}, + "string": undefined, + "symbol": "ETH", + "type": "NATIVE", + }, + ], + }, +] +`; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index 7eb2ad5518df..cd7ccf1d08d3 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -4,7 +4,10 @@ import configureStore from 'redux-mock-store'; import { useSelector } from 'react-redux'; import thunk from 'redux-thunk'; import sinon from 'sinon'; -import { RpcEndpointType } from '@metamask/network-controller'; +import { + NetworkConfiguration, + RpcEndpointType, +} from '@metamask/network-controller'; import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useNftsCollections } from '../../../../hooks/useNftsCollections'; import { useTokenTracker } from '../../../../hooks/useTokenTracker'; @@ -25,25 +28,39 @@ import { getTokens, } from '../../../../ducks/metamask/metamask'; import { getTopAssets } from '../../../../ducks/swaps/swaps'; -import { getRenderableTokenData } from '../../../../hooks/useTokensToSearch'; import * as actions from '../../../../store/actions'; import { getSwapsBlockedTokens } from '../../../../ducks/send'; import { getMultichainNetworkConfigurationsByChainId, getMultichainCurrentChainId, getMultichainCurrentCurrency, + getMultichainIsEvm, + getMultichainNativeCurrency, + getMultichainCurrentNetwork, + getMultichainSelectedAccountCachedBalance, } from '../../../../selectors/multichain'; +import { MultichainNetworks } from '../../../../../shared/constants/multichain/networks'; +import { useMultichainBalances } from '../../../../hooks/useMultichainBalances'; import { AssetPickerModal } from './asset-picker-modal'; -import AssetList from './AssetList'; import { ERC20Asset } from './types'; -jest.mock('./AssetList', () => jest.fn(() =>
AssetList
)); +const mockAssetList = jest.fn(); +jest.mock('./AssetList', () => (props: unknown) => { + mockAssetList(props); + return <>AssetList; +}); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), })); +const mockUseMultichainSelector = jest.fn(); +jest.mock('../../../../hooks/useMultichainSelector', () => ({ + useMultichainSelector: (selector: unknown) => + mockUseMultichainSelector(selector), +})); + jest.mock('../../../../hooks/useI18nContext', () => ({ useI18nContext: jest.fn(), })); @@ -56,8 +73,9 @@ jest.mock('../../../../hooks/useTokenTracker', () => ({ useTokenTracker: jest.fn(), })); +const mockGetRenderableTokenData = jest.fn(); jest.mock('../../../../hooks/useTokensToSearch', () => ({ - getRenderableTokenData: jest.fn(), + getRenderableTokenData: (data: unknown) => mockGetRenderableTokenData(data), })); const mockUseMultichainBalances = jest.fn(); @@ -81,6 +99,7 @@ describe('AssetPickerModal', () => { const onAssetChangeMock = jest.fn(); const onCloseMock = jest.fn(); + mockAssetList.mockReturnValue(() =>
AssetList
); const defaultProps = { header: 'sendSelectReceiveAsset', @@ -168,7 +187,7 @@ describe('AssetPickerModal', () => { useTokenTrackerMock.mockReturnValue({ tokensWithBalances: [], }); - (getRenderableTokenData as jest.Mock).mockReturnValue({}); + mockGetRenderableTokenData.mockReturnValue({}); mockUseMultichainBalances.mockReturnValue({ assetsWithBalance: [] }); }); @@ -224,9 +243,7 @@ describe('AssetPickerModal', () => { }, ); - expect( - (AssetList as jest.Mock).mock.calls.slice(-1)[0][0].tokenList.length, - ).toBe(2); + expect(mockAssetList.mock.calls.slice(-1)[0][0].tokenList.length).toBe(2); fireEvent.change( screen.getByPlaceholderText('searchTokensByNameOrAddress'), @@ -235,7 +252,7 @@ describe('AssetPickerModal', () => { }, ); - expect((AssetList as jest.Mock).mock.calls[1][0]).not.toEqual( + expect(mockAssetList.mock.calls[1][0]).not.toEqual( expect.objectContaining({ asset: { balance: '0x0', @@ -297,9 +314,7 @@ describe('AssetPickerModal', () => { }, ); - expect( - (AssetList as jest.Mock).mock.calls.slice(-1)[0][0].tokenList.length, - ).toBe(2); + expect(mockAssetList.mock.calls.slice(-1)[0][0].tokenList.length).toBe(2); fireEvent.change( screen.getByPlaceholderText('searchTokensByNameOrAddress'), @@ -308,7 +323,7 @@ describe('AssetPickerModal', () => { }, ); - expect((AssetList as jest.Mock).mock.calls[1][0]).not.toEqual( + expect(mockAssetList.mock.calls[1][0]).not.toEqual( expect.objectContaining({ asset: { balance: '0x0', @@ -319,14 +334,10 @@ describe('AssetPickerModal', () => { }), ); - expect( - (AssetList as jest.Mock).mock.calls.slice(-1)[0][0].tokenList.length, - ).toBe(1); + expect(mockAssetList.mock.calls.slice(-1)[0][0].tokenList.length).toBe(1); expect( - (AssetList as jest.Mock).mock.calls[2][0].isTokenDisabled({ - address: '0xtoken1', - }), + mockAssetList.mock.calls[2][0].isTokenDisabled({ address: '0xtoken1' }), ).toBe(true); }); @@ -393,3 +404,275 @@ describe('AssetPickerModal', () => { expect(getAllByRole('img')).toHaveLength(1); }); }); + +describe('AssetPickerModal token filtering', () => { + const onAssetChangeMock = jest.fn(); + const useI18nContextMock = useI18nContext as jest.Mock; + + const defaultProps = { + header: 'Select Token', + isOpen: true, + onClose: jest.fn(), + onAssetChange: onAssetChangeMock, + autoFocus: true, + network: { + chainId: '0xa', + name: 'Optimism', + } as unknown as NetworkConfiguration, + selectedChainIds: ['0xa', '0x1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + isMultiselectEnabled: true, + networks: [ + { + chainId: '0x1', + name: 'Ethereum Mainnet', + }, + { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana', + }, + { + chainId: '0xa', + name: 'Optimism', + }, + ] as unknown as NetworkConfiguration[], + }; + + beforeEach(() => { + jest.clearAllMocks(); + + useI18nContextMock.mockReturnValue((key: string) => key); + mockGetRenderableTokenData.mockImplementation((data) => data); + mockUseMultichainBalances.mockReturnValue({ + assetsWithBalance: [ + { + address: '', + balance: '1.5', + chainId: '0x1', + decimals: 18, + image: './images/eth_logo.svg', + isNative: true, + symbol: 'ETH', + type: 'NATIVE', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + balance: '100', + chainId: '0x1', + decimals: 18, + isNative: false, + symbol: 'UNI', + type: 'TOKEN', + }, + { + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', + balance: '10', + chainId: '0xa', + decimals: 6, + isNative: false, + symbol: 'USDC', + type: 'TOKEN', + }, + { + address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + balance: '50', + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + decimals: 6, + isNative: false, + symbol: 'USDC', + type: 'TOKEN', + }, + ], + }); + + const useSelectorMock = useSelector as jest.Mock; + useSelectorMock.mockImplementation((selector) => { + switch (selector) { + case getMultichainCurrentChainId: + return '0xa'; + case getMultichainIsEvm: + return true; + case getSwapsBlockedTokens: + return []; + case getMultichainCurrentCurrency: + return 'USD'; + default: + return {}; + } + }); + + mockUseMultichainSelector.mockImplementation((selector) => { + if (selector === getMultichainCurrentNetwork) { + return 'ETH'; + } + switch (selector) { + case getMultichainCurrentNetwork: + return { + chainId: '0xa', + name: 'Optimism', + }; + case getMultichainNativeCurrency: + return 'ETH'; + case getMultichainSelectedAccountCachedBalance: + return '1000'; + default: + return {}; + } + }); + }); + + it('should render all tokens from multiple chains', () => { + renderWithProvider(); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should render all tokens from single chain', () => { + renderWithProvider( + , + ); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should filter tokens by symbol', async () => { + renderWithProvider(); + + const searchInput = screen.getByPlaceholderText( + 'searchTokensByNameOrAddress', + ); + fireEvent.change(searchInput, { target: { value: 'UNI' } }); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should filter tokens by address', async () => { + renderWithProvider(); + + const searchInput = screen.getByPlaceholderText( + 'searchTokensByNameOrAddress', + ); + fireEvent.change(searchInput, { + target: { value: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' }, + }); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + + // Test case-insensitive search + fireEvent.change(searchInput, { + target: { value: '0x1f9840a85d5af5bf1d1762f925bdaddc4201F984' }, + }); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should filter tokens by chain when multichain network is selected', async () => { + renderWithProvider( + , + ); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should show all tokens when search query is cleared', async () => { + renderWithProvider(); + + const searchInput = screen.getByPlaceholderText( + 'searchTokensByNameOrAddress', + ); + fireEvent.change(searchInput, { target: { value: 'UNI' } }); + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + + fireEvent.change(searchInput, { target: { value: '' } }); + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should only show tokens with balances in send mode', async () => { + renderWithProvider( + , + ); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + + // Add a token without balance to the list + mockUseMultichainBalances.mockImplementationOnce(() => ({ + assetsWithBalance: [ + ...useMultichainBalances().assetsWithBalance, + { + address: '0xnewtoken', + balance: '0', + chainId: '0x1', + decimals: 18, + isNative: false, + symbol: 'ZERO', + type: AssetType.token, + }, + ], + })); + + renderWithProvider( + , + ); + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should handle case-insensitive search', async () => { + renderWithProvider(); + + const searchInput = screen.getByPlaceholderText( + 'searchTokensByNameOrAddress', + ); + + fireEvent.change(searchInput, { target: { value: 'uni' } }); + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + + fireEvent.change(searchInput, { target: { value: 'UNI' } }); + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should respect MAX_UNOWNED_TOKENS_RENDERED limit', async () => { + // Create an array of 31 tokens (MAX_UNOWNED_TOKENS_RENDERED + 1) + const manyTokens = Array.from({ length: 31 }, (_, i) => ({ + address: `0xtoken${i}`, + balance: '0', + chainId: '0x1', + decimals: 18, + isNative: false, + symbol: `TOKEN${i}`, + type: AssetType.token, + })); + + mockUseMultichainBalances.mockImplementationOnce(() => ({ + assetsWithBalance: manyTokens, + })); + + renderWithProvider(); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); +}); From 3586769307823898a3582f9fc6ff9e5ff8c002f9 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 18:30:01 -0700 Subject: [PATCH 50/71] feat: use asset metadata in asset-picker --- .../asset-picker-modal/asset-picker-modal.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index f40cc4292eec..6e56e68be73b 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -65,6 +65,7 @@ import { import { MultichainNetworks } from '../../../../../shared/constants/multichain/networks'; import { getAssetsMetadata } from '../../../../selectors/assets'; import { Numeric } from '../../../../../shared/modules/Numeric'; +import { useAssetMetadata } from '../../../../hooks/useAssetMetadata'; import type { ERC20Asset, NativeAsset, @@ -413,9 +414,11 @@ export function AssetPickerModal({ // If filteredTokensGenerator is passed in, use it to generate the filtered tokens // Otherwise use the default tokenGenerator - for (const token of (customTokenListGenerator ?? tokenListGenerator)( + const tokenGenerator = (customTokenListGenerator ?? tokenListGenerator)( shouldAddToken, - )) { + ); + + for (const token of tokenGenerator) { if (action === 'send' && token.balance === undefined) { continue; } @@ -465,6 +468,17 @@ export function AssetPickerModal({ currentCurrency, ]); + // This fetches the metadata for the asset if it is not already in the filteredTokenList + const unlistedAssetMetadata = useAssetMetadata( + searchQuery, + filteredTokenList.length === 0, + selectedNetwork?.chainId, + ); + + const displayedTokens = useMemo(() => { + return unlistedAssetMetadata ? [unlistedAssetMetadata] : filteredTokenList; + }, [unlistedAssetMetadata, filteredTokenList]); + const getNetworkPickerLabel = () => { if (!isMultiselectEnabled) { return ( @@ -569,7 +583,7 @@ export function AssetPickerModal({ network={network} handleAssetChange={handleAssetChange} asset={asset?.type === AssetType.NFT ? undefined : asset} - tokenList={filteredTokenList} + tokenList={displayedTokens} isTokenDisabled={getIsDisabled} isTokenListLoading={isTokenListLoading} assetItemProps={{ From 6c6453a264aaa5f429910f252cd79a2eaf9933b3 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 24 Mar 2025 18:31:46 -0700 Subject: [PATCH 51/71] fix: use solana assetId --- ui/hooks/bridge/useTokensWithFiltering.ts | 5 ++--- ui/hooks/useMultichainBalances.ts | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index 290e92f3a879..a8096dfb4105 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -150,11 +150,10 @@ export const useTokensWithFiltering = ( return { ...sharedFields, type: AssetType.token, - image: token.iconUrl ?? '', + image: token.iconUrl ?? token.icon ?? '', // Only tokens with 0 balance are processed here so hardcode empty string balance: '', string: undefined, - address: isSolanaChainId(chainId) ? token.assetId : token.address, }; }; @@ -198,7 +197,7 @@ export const useTokensWithFiltering = ( token.chainId, ) ) { - if (isNativeAddress(token.address)) { + if (isNativeAddress(token.address) || token.isNative) { yield { symbol: token.symbol, chainId: token.chainId, diff --git a/ui/hooks/useMultichainBalances.ts b/ui/hooks/useMultichainBalances.ts index 59c643fefb78..49dd78f368a3 100644 --- a/ui/hooks/useMultichainBalances.ts +++ b/ui/hooks/useMultichainBalances.ts @@ -1,6 +1,10 @@ import { useMemo } from 'react'; import { useSelector } from 'react-redux'; -import type { CaipChainId, Hex } from '@metamask/utils'; +import { + parseCaipAssetType, + type CaipChainId, + type Hex, +} from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import type { TokenWithBalance } from '../components/app/assets/types'; import { @@ -23,7 +27,8 @@ const useNonEvmAssetsWithBalances = (): ( | Omit & { chainId: `${string}:${string}`; decimals: number; - address: `${string}:${string}`; + address: string; + assetId: `${string}:${string}`; string: string; balance: string; tokenFiatAmount: number; @@ -58,17 +63,18 @@ const useNonEvmAssetsWithBalances = (): ( return assetIds .filter((caipAssetId) => assetMetadataById[caipAssetId]) .map((caipAssetId) => { - const [caipChainId, address] = caipAssetId.split('/'); - const [type] = address.split(':'); + const { chainId, assetReference, assetNamespace } = + parseCaipAssetType(caipAssetId); return { - chainId: caipChainId as `${string}:${string}`, + chainId, symbol: assetMetadataById[caipAssetId]?.symbol ?? '', - address: caipAssetId, + assetId: caipAssetId, + address: assetReference, string: balancesByAssetId[caipAssetId]?.amount ?? '0', balance: balancesByAssetId[caipAssetId]?.amount ?? '0', decimals: assetMetadataById[caipAssetId]?.units[0]?.decimals, image: assetMetadataById[caipAssetId]?.iconUrl ?? '', - type: type === 'token' ? AssetType.token : AssetType.native, + type: assetNamespace === 'token' ? AssetType.token : AssetType.native, tokenFiatAmount: new BigNumber( balancesByAssetId[caipAssetId]?.amount ?? '1', ) From aa4e2116b66f795c4eb77a6ec1a329f70df02d25 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 11:48:24 -0700 Subject: [PATCH 52/71] chore: add unit test for useAssetMetadata call --- .../asset-picker-modal.test.tsx.snap | 36 +++++++++++++++++++ .../asset-picker-modal.test.tsx | 27 ++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap index 46dbbcdf32fa..9085d67505ee 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap @@ -1,5 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`AssetPickerModal token filtering should fetch metadata for unlisted tokens 1`] = ` +[ + "0x1f9840a85d5af5bf1d1762f925bdaddc4201f123", + true, + "0xa", +] +`; + +exports[`AssetPickerModal token filtering should fetch metadata for unlisted tokens 2`] = ` +[ + { + "asset": undefined, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f123", + "chainId": "0x1", + "decimals": 18, + "image": "https://example.com/image.png", + "symbol": "UNI", + }, + ], + }, +] +`; + exports[`AssetPickerModal token filtering should filter tokens by address 1`] = ` [ { diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index cd7ccf1d08d3..e6959d150f30 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -50,6 +50,11 @@ jest.mock('./AssetList', () => (props: unknown) => { return <>AssetList; }); +const mockUseAssetMetadata = jest.fn(); +jest.mock('../../../../hooks/useAssetMetadata', () => ({ + useAssetMetadata: (...args: unknown[]) => mockUseAssetMetadata(...args), +})); + jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), @@ -675,4 +680,26 @@ describe('AssetPickerModal token filtering', () => { expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); }); + + it('should fetch metadata for unlisted tokens', async () => { + mockUseAssetMetadata.mockReturnValue({ + address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f123', + chainId: '0x1', + decimals: 18, + image: 'https://example.com/image.png', + symbol: 'UNI', + }); + + renderWithProvider(); + + const searchInput = screen.getByPlaceholderText( + 'searchTokensByNameOrAddress', + ); + fireEvent.change(searchInput, { + target: { value: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f123' }, + }); + + expect(mockUseAssetMetadata.mock.calls.at(-1)).toMatchSnapshot(); + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); }); From 504ea1b8a8e66e620f8999c6af87feb6eb678914 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 13:08:43 -0700 Subject: [PATCH 53/71] fix: duplicate native assets --- .../asset-picker-modal/asset-picker-modal.tsx | 18 +----------------- ui/hooks/bridge/useTokensWithFiltering.ts | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index 6e56e68be73b..390288daee65 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -63,7 +63,6 @@ import { getMultichainIsEvm, } from '../../../../selectors/multichain'; import { MultichainNetworks } from '../../../../../shared/constants/multichain/networks'; -import { getAssetsMetadata } from '../../../../selectors/assets'; import { Numeric } from '../../../../../shared/modules/Numeric'; import { useAssetMetadata } from '../../../../hooks/useAssetMetadata'; import type { @@ -203,7 +202,6 @@ export function AssetPickerModal({ useMultichainBalances(); const evmTokenMetadataByAddress = useSelector(getTokenList) as TokenListMap; - const nonEvmTokenMetadataByAddress = useSelector(getAssetsMetadata); const allowExternalServices = useSelector(getUseExternalServices); // Swaps top tokens @@ -315,21 +313,8 @@ export function AssetPickerModal({ } // Return early when SOLANA is selected since blocked and top tokens are not available + // All available solana tokens are in the multichainTokensWithBalance results if (selectedNetwork?.chainId === MultichainNetworks.SOLANA) { - for (const [address, token] of Object.entries( - nonEvmTokenMetadataByAddress, - )) { - const [caipChainId] = address.split('/'); - - if (shouldAddToken(token.symbol, address, caipChainId)) { - yield { - ...token, - address, - chainId: caipChainId, - decimals: token.units[0].decimals, - }; - } - } return; } @@ -370,7 +355,6 @@ export function AssetPickerModal({ selectedNetwork?.chainId, multichainTokensWithBalance, allDetectedTokens, - nonEvmTokenMetadataByAddress, topTokens, evmTokenMetadataByAddress, getIsDisabled, diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts index a8096dfb4105..1d0d4d12e085 100644 --- a/ui/hooks/bridge/useTokensWithFiltering.ts +++ b/ui/hooks/bridge/useTokensWithFiltering.ts @@ -11,6 +11,7 @@ import { fetchBridgeTokens, BridgeClientId, type BridgeAsset, + getNativeAssetForChainId, } from '@metamask/bridge-controller'; import { selectERC20TokensByChain } from '../../selectors'; import { AssetType } from '../../../shared/constants/transaction'; @@ -31,6 +32,7 @@ import type { NativeAsset, } from '../../components/multichain/asset-picker-amount/asset-picker-modal/types'; import { getAssetImageUrl } from '../../../shared/lib/asset-utils'; +import { MULTICHAIN_TOKEN_IMAGE_MAP } from '../../../shared/constants/multichain/networks'; type FilterPredicate = ( symbol: string, @@ -140,7 +142,8 @@ export const useTokensWithFiltering = ( image: CHAIN_ID_TOKEN_IMAGE_MAP[ sharedFields.chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP - ] ?? token.iconUrl, + ] ?? + (token.iconUrl || token.icon || ''), // Only unimported native assets are processed here so hardcode balance to 0 balance: '0', string: '0', @@ -211,10 +214,15 @@ export const useTokensWithFiltering = ( CHAIN_ID_TOKEN_IMAGE_MAP[ token.chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP ] ?? - getAssetImageUrl( - token.address, - formatChainIdToCaip(token.chainId), - ), + MULTICHAIN_TOKEN_IMAGE_MAP[ + token.chainId as keyof typeof MULTICHAIN_TOKEN_IMAGE_MAP + ] ?? + (getNativeAssetForChainId(token.chainId)?.icon || + getNativeAssetForChainId(token.chainId)?.iconUrl || + getAssetImageUrl( + token.address, + formatChainIdToCaip(token.chainId), + )), }; } else { yield { From 2a00f22d6d74cb1c4d8b80b129d69d79f52477af Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 14:09:23 -0700 Subject: [PATCH 54/71] chore: debounce search input --- .../asset-picker-modal/asset-picker-modal.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index 390288daee65..094362f6daa4 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { useSelector } from 'react-redux'; import type { Token, @@ -7,6 +7,7 @@ import type { } from '@metamask/assets-controllers'; import { isCaipChainId, isStrictHexString, type Hex } from '@metamask/utils'; import { zeroAddress } from 'ethereumjs-util'; +import { debounce } from 'lodash'; import { Modal, ModalContent, @@ -144,6 +145,11 @@ export function AssetPickerModal({ const t = useI18nContext(); const [searchQuery, setSearchQuery] = useState(''); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery); + const debouncedSetSearchQuery = debounce(setDebouncedSearchQuery, 200); + useEffect(() => { + debouncedSetSearchQuery(searchQuery); + }, [searchQuery, debouncedSetSearchQuery]); const swapsBlockedTokens = useSelector(getSwapsBlockedTokens); const memoizedSwapsBlockedTokens = useMemo(() => { @@ -379,7 +385,7 @@ export function AssetPickerModal({ address?: string | null, tokenChainId?: string, ) => { - const trimmedSearchQuery = searchQuery.trim().toLowerCase(); + const trimmedSearchQuery = debouncedSearchQuery.trim().toLowerCase(); const isMatchedBySearchQuery = Boolean( !trimmedSearchQuery || symbol?.toLowerCase().includes(trimmedSearchQuery) || @@ -439,7 +445,7 @@ export function AssetPickerModal({ return filteredTokens; }, [ currentChainId, - searchQuery, + debouncedSearchQuery, isMultiselectEnabled, selectedChainIds, selectedNetwork?.chainId, @@ -454,7 +460,7 @@ export function AssetPickerModal({ // This fetches the metadata for the asset if it is not already in the filteredTokenList const unlistedAssetMetadata = useAssetMetadata( - searchQuery, + debouncedSearchQuery, filteredTokenList.length === 0, selectedNetwork?.chainId, ); From 3c7a116c9c9706b16e597fcfb8a9fc4a04062471 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 14:30:46 -0700 Subject: [PATCH 55/71] fix: reset searchQuery on network change --- .../asset-picker-modal/asset-picker-modal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index 094362f6daa4..3ef9019ec280 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -168,6 +168,10 @@ export function AssetPickerModal({ const isSelectedNetworkActive = selectedNetwork.chainId === currentChainId; const isEvm = useMultichainSelector(getMultichainIsEvm); + useEffect(() => { + setSearchQuery(''); + }, [selectedNetwork?.chainId]); + const nativeCurrencyImage = useMultichainSelector(getMultichainCurrencyImage); const nativeCurrency = useMultichainSelector(getMultichainNativeCurrency); const balanceValue = useMultichainSelector( From 008f37eaa2164024627d69214f5232f4832a8049 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 14:29:19 -0700 Subject: [PATCH 56/71] fix: unit tests --- shared/lib/asset-utils.test.ts | 2 +- .../asset-picker-modal/asset-picker-modal.test.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/shared/lib/asset-utils.test.ts b/shared/lib/asset-utils.test.ts index ce8e1fff60a4..220537e9a637 100644 --- a/shared/lib/asset-utils.test.ts +++ b/shared/lib/asset-utils.test.ts @@ -107,7 +107,7 @@ describe('asset-utils', () => { image: 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v.png', assetId: solanaAssetId, - address: solanaAssetId, + address: solanaAddress, chainId: solanaChainId, }); }); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index e6959d150f30..87112f317719 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -94,6 +94,11 @@ jest.mock('../../../../hooks/useNfts', () => ({ }), })); +jest.mock('lodash', () => ({ + ...jest.requireActual('lodash'), + debounce: jest.fn().mockImplementation((fn) => fn), +})); + describe('AssetPickerModal', () => { const useSelectorMock = useSelector as jest.Mock; const useI18nContextMock = useI18nContext as jest.Mock; From 7fde7f296745ee65fc1089d2ce1cb7c16465360f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 16:25:26 -0700 Subject: [PATCH 57/71] refactor: move useAssetMetadata --- .../asset-picker-modal/asset-picker-modal.test.tsx | 2 +- .../asset-picker-modal/asset-picker-modal.tsx | 2 +- .../asset-picker-modal}/hooks/useAssetMetadata.test.ts | 4 ++-- .../asset-picker-modal}/hooks/useAssetMetadata.ts | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) rename ui/{ => components/multichain/asset-picker-amount/asset-picker-modal}/hooks/useAssetMetadata.test.ts (96%) rename ui/{ => components/multichain/asset-picker-amount/asset-picker-modal}/hooks/useAssetMetadata.ts (87%) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index 87112f317719..10b7cb83da75 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -51,7 +51,7 @@ jest.mock('./AssetList', () => (props: unknown) => { }); const mockUseAssetMetadata = jest.fn(); -jest.mock('../../../../hooks/useAssetMetadata', () => ({ +jest.mock('./hooks/useAssetMetadata', () => ({ useAssetMetadata: (...args: unknown[]) => mockUseAssetMetadata(...args), })); diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index 3ef9019ec280..e02c133df5a6 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -65,7 +65,7 @@ import { } from '../../../../selectors/multichain'; import { MultichainNetworks } from '../../../../../shared/constants/multichain/networks'; import { Numeric } from '../../../../../shared/modules/Numeric'; -import { useAssetMetadata } from '../../../../hooks/useAssetMetadata'; +import { useAssetMetadata } from './hooks/useAssetMetadata'; import type { ERC20Asset, NativeAsset, diff --git a/ui/hooks/useAssetMetadata.test.ts b/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.test.ts similarity index 96% rename from ui/hooks/useAssetMetadata.test.ts rename to ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.test.ts index 114b67994c50..213793125082 100644 --- a/ui/hooks/useAssetMetadata.test.ts +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { useSelector } from 'react-redux'; -import { AssetType } from '../../shared/constants/transaction'; +import { AssetType } from '../../../../../../shared/constants/transaction'; import { useAssetMetadata } from './useAssetMetadata'; // Mock dependencies @@ -11,7 +11,7 @@ jest.mock('react-redux', () => ({ const mockFetchAssetMetadata = jest.fn(); const mockGetAssetImageUrl = jest.fn(); -jest.mock('../../shared/lib/asset-utils', () => ({ +jest.mock('../../../../../../shared/lib/asset-utils', () => ({ fetchAssetMetadata: (...args: unknown[]) => mockFetchAssetMetadata(...args), getAssetImageUrl: (...args: unknown[]) => mockGetAssetImageUrl(...args), })); diff --git a/ui/hooks/useAssetMetadata.ts b/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.ts similarity index 87% rename from ui/hooks/useAssetMetadata.ts rename to ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.ts index 287580958de5..16c707b5c950 100644 --- a/ui/hooks/useAssetMetadata.ts +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.ts @@ -1,12 +1,12 @@ import { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; import { useSelector } from 'react-redux'; -import { getUseExternalServices } from '../selectors'; +import { getUseExternalServices } from '../../../../../selectors'; import { fetchAssetMetadata, getAssetImageUrl, -} from '../../shared/lib/asset-utils'; -import { AssetType } from '../../shared/constants/transaction'; -import { useAsyncResult } from './useAsync'; +} from '../../../../../../shared/lib/asset-utils'; +import { AssetType } from '../../../../../../shared/constants/transaction'; +import { useAsyncResult } from '../../../../../hooks/useAsync'; /** * Fetches token metadata for a single token if searchQuery is defined but filteredTokenList is empty From 4d6549287c7e1d59c406126172f56dc360ab556f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 16:32:12 -0700 Subject: [PATCH 58/71] chore: abort asset metadata fetch when searchQuery changes --- shared/lib/asset-utils.ts | 3 +++ .../asset-picker-modal.test.tsx.snap | 3 +++ .../asset-picker-modal/asset-picker-modal.tsx | 20 ++++++++++++++++--- .../hooks/useAssetMetadata.ts | 12 ++++++++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/shared/lib/asset-utils.ts b/shared/lib/asset-utils.ts index 68a72d130d42..8b7394ef347b 100644 --- a/shared/lib/asset-utils.ts +++ b/shared/lib/asset-utils.ts @@ -74,11 +74,13 @@ type AssetMetadata = { * * @param address - The address of the token * @param chainId - The chainId of the token + * @param abortSignal - The abort signal for the fetch request * @returns The metadata for the token */ export const fetchAssetMetadata = async ( address: string | CaipAssetType | Hex, chainId: Hex | CaipChainId, + abortSignal?: AbortSignal, ) => { const chainIdInCaip = isCaipChainId(chainId) ? chainId @@ -96,6 +98,7 @@ export const fetchAssetMetadata = async ( fetchOptions: { method: 'GET', headers: { 'X-Client-Id': 'extension' }, + signal: abortSignal, }, cacheOptions: { cacheRefreshTime: MINUTE, diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap index 9085d67505ee..d4a3bbce7a12 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap @@ -4,6 +4,9 @@ exports[`AssetPickerModal token filtering should fetch metadata for unlisted tok [ "0x1f9840a85d5af5bf1d1762f925bdaddc4201f123", true, + { + "current": null, + }, "0xa", ] `; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index e02c133df5a6..b682e3c0b0a2 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -1,4 +1,10 @@ -import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import React, { + useState, + useCallback, + useMemo, + useEffect, + useRef, +} from 'react'; import { useSelector } from 'react-redux'; import type { Token, @@ -150,6 +156,7 @@ export function AssetPickerModal({ useEffect(() => { debouncedSetSearchQuery(searchQuery); }, [searchQuery, debouncedSetSearchQuery]); + const abortControllerRef = useRef(null); const swapsBlockedTokens = useSelector(getSwapsBlockedTokens); const memoizedSwapsBlockedTokens = useMemo(() => { @@ -464,8 +471,9 @@ export function AssetPickerModal({ // This fetches the metadata for the asset if it is not already in the filteredTokenList const unlistedAssetMetadata = useAssetMetadata( - debouncedSearchQuery, + searchQuery, filteredTokenList.length === 0, + abortControllerRef, selectedNetwork?.chainId, ); @@ -570,7 +578,13 @@ export function AssetPickerModal({ setSearchQuery(value)} + onChange={(value) => { + if (abortControllerRef.current) { + // Cancel previous asset metadata fetch + abortControllerRef.current.abort(); + } + setSearchQuery(value); + }} autoFocus={autoFocus} /> , chainId?: Hex | CaipChainId, ) => { const allowExternalServices = useSelector(getUseExternalServices); @@ -44,11 +46,15 @@ export const useAssetMetadata = ( const trimmedSearchQuery = searchQuery.trim(); if ( allowExternalServices && - chainId && shouldFetchMetadata && - trimmedSearchQuery.length > 0 + trimmedSearchQuery.length > 30 ) { - const metadata = await fetchAssetMetadata(trimmedSearchQuery, chainId); + abortControllerRef.current = new AbortController(); + const metadata = await fetchAssetMetadata( + trimmedSearchQuery, + chainId, + abortControllerRef.current.signal, + ); if (metadata) { return { From 3528a762f01dbf7a43d22e72c9516f963f4551c9 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 16:57:28 -0700 Subject: [PATCH 59/71] chore: show selected asset at the top of list --- .../asset-picker-modal.test.tsx.snap | 83 +++++++++++++++++++ .../asset-picker-modal.test.tsx | 17 ++++ .../asset-picker-modal/asset-picker-modal.tsx | 19 +++++ 3 files changed, 119 insertions(+) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap index d4a3bbce7a12..f4f60eb691df 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap @@ -582,3 +582,86 @@ exports[`AssetPickerModal token filtering should show all tokens when search que }, ] `; + +exports[`AssetPickerModal token filtering should show selected token first 1`] = ` +[ + { + "asset": { + "address": "NEWTOKEN", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "image": "image.png", + "symbol": "USDT", + "type": "TOKEN", + }, + "assetItemProps": { + "isTitleHidden": false, + "isTitleNetworkName": true, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "0xa", + "name": "Optimism", + }, + "tokenList": [ + { + "address": "NEWTOKEN", + "balance": "0", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "image": "image.png", + "string": "0", + "symbol": "USDT", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1.5", + "chainId": "0x1", + "decimals": 18, + "image": "./images/eth_logo.svg", + "isNative": true, + "symbol": "ETH", + "type": "NATIVE", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "balance": "0x56bc75e2d63100000", + "chainId": "0x1", + "decimals": 18, + "isNative": false, + "symbol": "UNI", + "type": "TOKEN", + }, + { + "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f985", + "balance": "0x989680", + "chainId": "0xa", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "balance": "0x2faf080", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + { + "address": "", + "balance": "1000", + "chainId": "0xa", + "decimals": 18, + "image": {}, + "string": undefined, + "symbol": "ETH", + "type": "NATIVE", + }, + ], + }, +] +`; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index 10b7cb83da75..cdb01bf08e2d 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -584,6 +584,23 @@ describe('AssetPickerModal token filtering', () => { expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); }); + it('should show selected token first', async () => { + renderWithProvider( + , + ); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + it('should filter tokens by chain when multichain network is selected', async () => { renderWithProvider( ); + + filteredTokensAddresses.add(getTokenKey(asset.address, asset.chainId)); + } + } + // If filteredTokensGenerator is passed in, use it to generate the filtered tokens // Otherwise use the default tokenGenerator const tokenGenerator = (customTokenListGenerator ?? tokenListGenerator)( @@ -467,6 +485,7 @@ export function AssetPickerModal({ tokenConversionRates, conversionRate, currentCurrency, + asset, ]); // This fetches the metadata for the asset if it is not already in the filteredTokenList From 8bb76e4588e58cccd5f41de1bf95946c6c6e908a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 17:06:32 -0700 Subject: [PATCH 60/71] chore: toAssetId util --- shared/lib/asset-utils.test.ts | 59 ++++++++++++++++++++++++++++++++-- shared/lib/asset-utils.ts | 6 ++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/shared/lib/asset-utils.test.ts b/shared/lib/asset-utils.test.ts index 220537e9a637..24f7fcaf3b6c 100644 --- a/shared/lib/asset-utils.test.ts +++ b/shared/lib/asset-utils.test.ts @@ -1,11 +1,16 @@ -import { CaipAssetType, CaipChainId, Hex } from '@metamask/utils'; +import { + CaipAssetType, + CaipAssetTypeStruct, + CaipChainId, + Hex, +} from '@metamask/utils'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { MultichainNetwork } from '@metamask/multichain-transactions-controller'; import { toHex } from '@metamask/controller-utils'; import { MINUTE } from '../constants/time'; import { MultichainNetworks } from '../constants/multichain/networks'; import fetchWithCache from './fetch-with-cache'; -import { getAssetImageUrl, fetchAssetMetadata } from './asset-utils'; +import { getAssetImageUrl, fetchAssetMetadata, toAssetId } from './asset-utils'; jest.mock('./fetch-with-cache'); jest.mock('@metamask/multichain-network-controller'); @@ -15,6 +20,56 @@ describe('asset-utils', () => { const STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io'; const TOKEN_API_V3_BASE_URL = 'https://tokens.api.cx.metamask.io/v3'; + describe('toAssetId', () => { + it('should return the same asset ID if input is already a CAIP asset type', () => { + const caipAssetId = CaipAssetTypeStruct.create('eip155:1/erc20:0x123'); + const chainId = 'eip155:1' as CaipChainId; + + const result = toAssetId(caipAssetId, chainId); + expect(result).toBe(caipAssetId); + }); + + it('should create Solana token asset ID correctly', () => { + const address = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + const chainId = MultichainNetwork.Solana as CaipChainId; + + const result = toAssetId(address, chainId); + expect(result).toBe(`${MultichainNetwork.Solana}/token:${address}`); + }); + + it('should create EVM token asset ID correctly', () => { + const address = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'; + const chainId = 'eip155:1' as CaipChainId; + + const result = toAssetId(address, chainId); + expect(result).toBe(`eip155:1/erc20:${address}`); + }); + + it('should return undefined for non-hex address on EVM chains', () => { + const address = 'not-a-hex-address'; + const chainId = 'eip155:1' as CaipChainId; + + const result = toAssetId(address, chainId); + expect(result).toBeUndefined(); + }); + + it('should handle different EVM chain IDs', () => { + const address = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984'; + const chainId = 'eip155:137' as CaipChainId; + + const result = toAssetId(address, chainId); + expect(result).toBe(`eip155:137/erc20:${address}`); + }); + + it('should handle checksummed addresses', () => { + const address = '0x1F9840a85d5aF5bf1D1762F925BDADdC4201F984'; + const chainId = 'eip155:1' as CaipChainId; + + const result = toAssetId(address, chainId); + expect(result).toBe(`eip155:1/erc20:${address}`); + }); + }); + describe('getAssetImageUrl', () => { it('should return correct image URL for a CAIP asset ID', () => { const assetId = 'eip155:1/erc20:0x123' as CaipAssetType; diff --git a/shared/lib/asset-utils.ts b/shared/lib/asset-utils.ts index 8b7394ef347b..1623e497f617 100644 --- a/shared/lib/asset-utils.ts +++ b/shared/lib/asset-utils.ts @@ -20,7 +20,7 @@ import fetchWithCache from './fetch-with-cache'; const TOKEN_API_V3_BASE_URL = 'https://tokens.api.cx.metamask.io/v3'; const STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io'; -const buildAssetId = ( +export const toAssetId = ( address: Hex | CaipAssetType | string, chainId: CaipChainId, ): CaipAssetType | undefined => { @@ -51,7 +51,7 @@ export const getAssetImageUrl = ( ? chainId : toEvmCaipChainId(chainId); - const assetIdInCaip = buildAssetId(assetId, chainIdInCaip); + const assetIdInCaip = toAssetId(assetId, chainIdInCaip); if (!assetIdInCaip) { return undefined; } @@ -86,7 +86,7 @@ export const fetchAssetMetadata = async ( ? chainId : toEvmCaipChainId(chainId); - const assetId = buildAssetId(address, chainIdInCaip); + const assetId = toAssetId(address, chainIdInCaip); if (!assetId) { return undefined; From f241f10ed45ea5f5669e885cf3a28bbd8130d81c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 25 Mar 2025 17:25:59 -0700 Subject: [PATCH 61/71] fix: useAssetMetadata tests --- .../hooks/useAssetMetadata.test.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.test.ts b/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.test.ts index 213793125082..034bfddd6a1f 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.test.ts +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/hooks/useAssetMetadata.test.ts @@ -17,9 +17,9 @@ jest.mock('../../../../../../shared/lib/asset-utils', () => ({ })); const mockChainId = '0x1'; -const mockSearchQuery = '0x123'; +const mockSearchQuery = '0x123asdfasdfasdfasdfasdfasadssdas'; const mockAssetId = 'eip155:1/erc20:0x123'; - +const mockAbortController = { current: new AbortController() }; describe('useAssetMetadata', () => { beforeEach(() => { jest.clearAllMocks(); @@ -31,7 +31,7 @@ describe('useAssetMetadata', () => { (useSelector as jest.Mock).mockReturnValue(false); // allowExternalServices = false const { result, waitForNextUpdate } = renderHook(() => - useAssetMetadata(mockSearchQuery, true, mockChainId), + useAssetMetadata(mockSearchQuery, true, mockAbortController, mockChainId), ); await waitForNextUpdate(); @@ -41,7 +41,7 @@ describe('useAssetMetadata', () => { it('should return undefined when chainId is not provided', async () => { const { result, waitForNextUpdate } = renderHook(() => - useAssetMetadata(mockSearchQuery, true, undefined), + useAssetMetadata(mockSearchQuery, true, mockAbortController), ); await waitForNextUpdate(); @@ -51,7 +51,7 @@ describe('useAssetMetadata', () => { it('should return undefined when search query is empty', async () => { const { result, waitForNextUpdate } = renderHook(() => - useAssetMetadata('', true, mockChainId), + useAssetMetadata('', true, mockAbortController, mockChainId), ); await waitForNextUpdate(); @@ -61,7 +61,7 @@ describe('useAssetMetadata', () => { it('should fetch and return asset metadata when conditions are met', async () => { const mockMetadata = { - address: '0x123', + address: '0x123asdfasdfasdfasdfasdfasadssdas', symbol: 'TEST', decimals: 18, assetId: mockAssetId, @@ -71,7 +71,7 @@ describe('useAssetMetadata', () => { mockFetchAssetMetadata.mockResolvedValueOnce(mockMetadata); const { result, waitForNextUpdate } = renderHook(() => - useAssetMetadata(mockSearchQuery, true, mockChainId), + useAssetMetadata(mockSearchQuery, true, mockAbortController, mockChainId), ); await waitForNextUpdate(); @@ -89,6 +89,7 @@ describe('useAssetMetadata', () => { expect(mockFetchAssetMetadata).toHaveBeenCalledWith( mockSearchQuery.trim(), mockChainId, + mockAbortController.current.signal, ); expect(mockGetAssetImageUrl).toHaveBeenCalledWith(mockAssetId, mockChainId); }); @@ -97,7 +98,7 @@ describe('useAssetMetadata', () => { mockFetchAssetMetadata.mockResolvedValueOnce(undefined); const { result, waitForNextUpdate } = renderHook(() => - useAssetMetadata(mockSearchQuery, true, mockChainId), + useAssetMetadata(mockSearchQuery, true, mockAbortController, mockChainId), ); await waitForNextUpdate(); @@ -106,6 +107,7 @@ describe('useAssetMetadata', () => { expect(mockFetchAssetMetadata).toHaveBeenCalledWith( mockSearchQuery.trim(), mockChainId, + mockAbortController.current.signal, ); }); @@ -113,7 +115,7 @@ describe('useAssetMetadata', () => { mockFetchAssetMetadata.mockRejectedValueOnce(new Error('API Error')); const { result, waitForNextUpdate } = renderHook(() => - useAssetMetadata(mockSearchQuery, true, mockChainId), + useAssetMetadata(mockSearchQuery, true, mockAbortController, mockChainId), ); await waitForNextUpdate(); @@ -122,6 +124,7 @@ describe('useAssetMetadata', () => { expect(mockFetchAssetMetadata).toHaveBeenCalledWith( mockSearchQuery.trim(), mockChainId, + mockAbortController.current.signal, ); }); }); From 4327e1973c3d5dbe1f96aa1d6fa8939d786ada7c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 26 Mar 2025 10:19:49 -0700 Subject: [PATCH 62/71] fix: propagate abort signal to fetch function --- app/scripts/metamask-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 014f4ffa4875..31e8592c2851 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1677,10 +1677,10 @@ export default class MetamaskController extends EventEmitter { clientId: BridgeClientId.EXTENSION, // TODO: Remove once TransactionController exports this action type getLayer1GasFee: (...args) => this.txController.getLayer1GasFee(...args), - fetchFn: async (url, { headers, ...requestOptions }) => + fetchFn: async (url, { headers, signal, ...requestOptions }) => await fetchWithCache({ url, - fetchOptions: { method: 'GET', headers }, + fetchOptions: { method: 'GET', headers, signal }, ...requestOptions, }), config: { From b63cd398a4bc5d51b897b43a71dd215a749691ff Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 26 Mar 2025 10:31:01 -0700 Subject: [PATCH 63/71] chore: add useMemo dep --- ui/pages/bridge/prepare/prepare-bridge-page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index 00ec7ab64d23..04e801213f5f 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -339,6 +339,7 @@ const PrepareBridgePage = () => { slippage, selectedAccount?.address, selectedDestinationAccount?.address, + providerConfig?.rpcUrl, ], ); From b7309b8dd96be539a84b2f83c3fda922f7c0e2f4 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 26 Mar 2025 16:11:25 -0700 Subject: [PATCH 64/71] fix: show selected asset first --- .../asset-picker-modal/asset-picker-modal.tsx | 64 ++++++++----------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index cddaf945ecdc..e90154e59b34 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -413,24 +413,6 @@ export function AssetPickerModal({ ); }; - // If an asset is selected, display it first - if ( - asset && - 'symbol' in asset && - 'address' in asset && - 'chainId' in asset - ) { - if (shouldAddToken(asset.symbol, asset.address, asset.chainId)) { - filteredTokens.push({ - ...asset, - balance: 'balance' in asset ? asset.balance : '0', - string: 'string' in asset ? asset.string : '0', - } as unknown as AssetWithDisplayData); - - filteredTokensAddresses.add(getTokenKey(asset.address, asset.chainId)); - } - } - // If filteredTokensGenerator is passed in, use it to generate the filtered tokens // Otherwise use the default tokenGenerator const tokenGenerator = (customTokenListGenerator ?? tokenListGenerator)( @@ -443,27 +425,33 @@ export function AssetPickerModal({ } filteredTokensAddresses.add(getTokenKey(token.address, token.chainId)); - if (!customTokenListGenerator && isStrictHexString(token.address)) { - filteredTokens.push( - getRenderableTokenData( - token.address - ? ({ - ...token, - ...evmTokenMetadataByAddress[token.address.toLowerCase()], - type: AssetType.token, - } as AssetWithDisplayData) - : token, - tokenConversionRates, - conversionRate, - currentCurrency, - token.chainId, - evmTokenMetadataByAddress, - ), - ); + + const tokenWithBalanceData = + !customTokenListGenerator && isStrictHexString(token.address) + ? getRenderableTokenData( + token.address + ? ({ + ...token, + ...evmTokenMetadataByAddress[token.address.toLowerCase()], + type: AssetType.token, + } as AssetWithDisplayData) + : token, + tokenConversionRates, + conversionRate, + currentCurrency, + token.chainId, + evmTokenMetadataByAddress, + ) + : (token as unknown as AssetWithDisplayData); + + // Add selected asset to the top of the list if it is the selected asset + if ( + asset?.address === tokenWithBalanceData.address && + selectedNetwork?.chainId === tokenWithBalanceData.chainId + ) { + filteredTokens.unshift(tokenWithBalanceData); } else { - filteredTokens.push( - token as unknown as AssetWithDisplayData, - ); + filteredTokens.push(tokenWithBalanceData); } if (filteredTokens.length > MAX_UNOWNED_TOKENS_RENDERED) { From 0f271a8c5721b590d2438d3e4bdaceb0985ebe56 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 27 Mar 2025 09:56:32 -0700 Subject: [PATCH 65/71] fix: asset-picker-modal unit tests --- .../asset-picker-modal.test.tsx.snap | 56 +++++++++++++++---- .../asset-picker-modal.test.tsx | 49 +++++++++++++++- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap index f4f60eb691df..750411d99c50 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/__snapshots__/asset-picker-modal.test.tsx.snap @@ -583,12 +583,12 @@ exports[`AssetPickerModal token filtering should show all tokens when search que ] `; -exports[`AssetPickerModal token filtering should show selected token first 1`] = ` +exports[`AssetPickerModal token filtering should show selected token first when selected network is active 1`] = ` [ { "asset": { "address": "NEWTOKEN", - "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "chainId": "0xa", "image": "image.png", "symbol": "USDT", "type": "TOKEN", @@ -605,15 +605,6 @@ exports[`AssetPickerModal token filtering should show selected token first 1`] = "name": "Optimism", }, "tokenList": [ - { - "address": "NEWTOKEN", - "balance": "0", - "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", - "image": "image.png", - "string": "0", - "symbol": "USDT", - "type": "TOKEN", - }, { "address": "", "balance": "1.5", @@ -665,3 +656,46 @@ exports[`AssetPickerModal token filtering should show selected token first 1`] = }, ] `; + +exports[`AssetPickerModal token filtering should show selected token first when selected network is not active 1`] = ` +[ + { + "asset": { + "address": "NEWTOKEN", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "image": "image.png", + "symbol": "USDT", + "type": "TOKEN", + }, + "assetItemProps": { + "isTitleHidden": true, + "isTitleNetworkName": false, + }, + "handleAssetChange": [MockFunction], + "isTokenDisabled": [Function], + "isTokenListLoading": false, + "network": { + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "name": "Solana", + }, + "tokenList": [ + { + "address": "NEWTOKEN", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "image": "image.png", + "symbol": "USDT", + "type": "TOKEN", + }, + { + "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "balance": "50", + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "decimals": 6, + "isNative": false, + "symbol": "USDC", + "type": "TOKEN", + }, + ], + }, +] +`; diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx index cdb01bf08e2d..5f975fa94f56 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.test.tsx @@ -584,13 +584,58 @@ describe('AssetPickerModal token filtering', () => { expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); }); - it('should show selected token first', async () => { + it('should show selected token first when selected network is not active', () => { renderWithProvider( + [ + { + address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + balance: '50', + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + decimals: 6, + isNative: false, + symbol: 'USDC', + type: 'TOKEN', + }, + { + address: 'NEWTOKEN', + chainId: MultichainNetworks.SOLANA, + symbol: 'USDT', + image: 'image.png', + type: AssetType.token, + }, + ] as unknown as (keyof typeof AssetPickerModal)['customTokenListGenerator'] + } + />, + ); + + expect(mockAssetList.mock.calls.at(-1)).toMatchSnapshot(); + }); + + it('should show selected token first when selected network is active', () => { + renderWithProvider( + Date: Thu, 27 Mar 2025 10:47:32 -0700 Subject: [PATCH 66/71] fix: swap-send e2e test --- test/e2e/tests/swap-send/swap-send-test-utils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/e2e/tests/swap-send/swap-send-test-utils.ts b/test/e2e/tests/swap-send/swap-send-test-utils.ts index 09efdf26951a..4c10a444afba 100644 --- a/test/e2e/tests/swap-send/swap-send-test-utils.ts +++ b/test/e2e/tests/swap-send/swap-send-test-utils.ts @@ -56,6 +56,11 @@ export class SwapSendPage { ); await f.press(i); } + // Search input is debounced, so we need to wait for the token list to update + await this.driver.elementCountBecomesN( + '[data-testid="multichain-token-list-item"]', + 1, + ); // Verify that only matching tokens are listed assert.equal( ( From c7611e08338ec137565bac556415fbab71642a09 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 27 Mar 2025 11:35:06 -0700 Subject: [PATCH 67/71] chore: add constants for network names --- shared/constants/bridge.ts | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index 5943c1fa0863..22e927a7f199 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -1,8 +1,14 @@ +import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { MultichainNetworks } from './multichain/networks'; import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './network'; -// TODO read from feature flags -export const ALLOWED_BRIDGE_CHAIN_IDS = [ +const ALLOWED_MULTICHAIN_BRIDGE_CHAIN_IDS = [ + ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) + MultichainNetworks.SOLANA, + ///: END:ONLY_INCLUDE_IF +]; + +const ALLOWED_EVM_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.MAINNET, CHAIN_IDS.BSC, CHAIN_IDS.POLYGON, @@ -12,12 +18,25 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.ARBITRUM, CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.BASE, +]; + +const ALLOWED_BRIDGE_CHAIN_IDS = [ + ...ALLOWED_MULTICHAIN_BRIDGE_CHAIN_IDS, + ...ALLOWED_EVM_BRIDGE_CHAIN_IDS, + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.BASE, ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) MultichainNetworks.SOLANA, ///: END:ONLY_INCLUDE_IF ]; -export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; +const ALLOWED_BRIDGE_CHAIN_IDS_IN_CAIP = ALLOWED_EVM_BRIDGE_CHAIN_IDS.map( + toEvmCaipChainId, +).concat(ALLOWED_MULTICHAIN_BRIDGE_CHAIN_IDS); + +export type AllowedBridgeChainIds = + | (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number] + | (typeof ALLOWED_BRIDGE_CHAIN_IDS_IN_CAIP)[number]; export const BRIDGE_DEV_API_BASE_URL = 'https://bridge.dev-api.cx.metamask.io'; export const BRIDGE_PROD_API_BASE_URL = 'https://bridge.api.cx.metamask.io'; @@ -49,6 +68,17 @@ export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< [CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], [CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era', [CHAIN_IDS.BASE]: 'Base', + [toEvmCaipChainId(CHAIN_IDS.BASE)]: 'Base', + [toEvmCaipChainId(CHAIN_IDS.LINEA_MAINNET)]: 'Linea', + [toEvmCaipChainId(CHAIN_IDS.POLYGON)]: NETWORK_TO_NAME_MAP[CHAIN_IDS.POLYGON], + [toEvmCaipChainId(CHAIN_IDS.AVALANCHE)]: 'Avalanche', + [toEvmCaipChainId(CHAIN_IDS.BSC)]: NETWORK_TO_NAME_MAP[CHAIN_IDS.BSC], + [toEvmCaipChainId(CHAIN_IDS.ARBITRUM)]: + NETWORK_TO_NAME_MAP[CHAIN_IDS.ARBITRUM], + [toEvmCaipChainId(CHAIN_IDS.OPTIMISM)]: + NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], + [toEvmCaipChainId(CHAIN_IDS.ZKSYNC_ERA)]: 'ZkSync Era', + [toEvmCaipChainId(CHAIN_IDS.BASE)]: 'Base', ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) [MultichainNetworks.SOLANA]: 'Solana', [MultichainNetworks.SOLANA_TESTNET]: 'Solana Testnet', From c472f8d42188f49cd30921471cc68ea9bdc98243 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 27 Mar 2025 11:52:15 -0700 Subject: [PATCH 68/71] fix: export const --- shared/constants/bridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index 22e927a7f199..eb0a0f091642 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -20,7 +20,7 @@ const ALLOWED_EVM_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.BASE, ]; -const ALLOWED_BRIDGE_CHAIN_IDS = [ +export const ALLOWED_BRIDGE_CHAIN_IDS = [ ...ALLOWED_MULTICHAIN_BRIDGE_CHAIN_IDS, ...ALLOWED_EVM_BRIDGE_CHAIN_IDS, CHAIN_IDS.LINEA_MAINNET, From 5136ff341db9e813ffd743c9e7d9f213128e597a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 27 Mar 2025 12:43:06 -0700 Subject: [PATCH 69/71] fix: asset-picker test --- test/e2e/tests/multichain/asset-picker-send.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/tests/multichain/asset-picker-send.spec.ts b/test/e2e/tests/multichain/asset-picker-send.spec.ts index 66f651588e44..e274b6192cc0 100644 --- a/test/e2e/tests/multichain/asset-picker-send.spec.ts +++ b/test/e2e/tests/multichain/asset-picker-send.spec.ts @@ -85,6 +85,10 @@ describe('AssetPickerSendFlow', function () { ); await searchInputField.sendKeys('CHZ'); + await driver.elementCountBecomesN( + '[data-testid="multichain-token-list-button"]', + 1, + ); // check that CHZ is disabled const [tkn] = await driver.findElements( '[data-testid="multichain-token-list-button"]', From 53ddea67bcd04bc511e2ec74ffc992f79c8adcef Mon Sep 17 00:00:00 2001 From: Micaela Estabillo <100321200+micaelae@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:45:51 -0700 Subject: [PATCH 70/71] chore: add network names for caip chainids (#31372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Some components use hex and some use caip so adding network name mappings for both chainId formats [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/31372?quickstart=1) ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- shared/constants/bridge.ts | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index b4c96030cc4e..7bdc87a758e2 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -1,3 +1,4 @@ +import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { BRIDGE_DEV_API_BASE_URL, BRIDGE_PROD_API_BASE_URL, @@ -5,8 +6,13 @@ import { import { MultichainNetworks } from './multichain/networks'; import { CHAIN_IDS, NETWORK_TO_NAME_MAP } from './network'; -// TODO read from feature flags -export const ALLOWED_BRIDGE_CHAIN_IDS = [ +const ALLOWED_MULTICHAIN_BRIDGE_CHAIN_IDS = [ + ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) + MultichainNetworks.SOLANA, + ///: END:ONLY_INCLUDE_IF +]; + +const ALLOWED_EVM_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.MAINNET, CHAIN_IDS.BSC, CHAIN_IDS.POLYGON, @@ -16,12 +22,25 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.ARBITRUM, CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.BASE, +]; + +export const ALLOWED_BRIDGE_CHAIN_IDS = [ + ...ALLOWED_MULTICHAIN_BRIDGE_CHAIN_IDS, + ...ALLOWED_EVM_BRIDGE_CHAIN_IDS, + CHAIN_IDS.LINEA_MAINNET, + CHAIN_IDS.BASE, ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) MultichainNetworks.SOLANA, ///: END:ONLY_INCLUDE_IF ]; -export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; +const ALLOWED_BRIDGE_CHAIN_IDS_IN_CAIP = ALLOWED_EVM_BRIDGE_CHAIN_IDS.map( + toEvmCaipChainId, +).concat(ALLOWED_MULTICHAIN_BRIDGE_CHAIN_IDS); + +export type AllowedBridgeChainIds = + | (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number] + | (typeof ALLOWED_BRIDGE_CHAIN_IDS_IN_CAIP)[number]; export const BRIDGE_API_BASE_URL = process.env.BRIDGE_USE_DEV_APIS ? BRIDGE_DEV_API_BASE_URL @@ -41,6 +60,17 @@ export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< [CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], [CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era', [CHAIN_IDS.BASE]: 'Base', + [toEvmCaipChainId(CHAIN_IDS.BASE)]: 'Base', + [toEvmCaipChainId(CHAIN_IDS.LINEA_MAINNET)]: 'Linea', + [toEvmCaipChainId(CHAIN_IDS.POLYGON)]: NETWORK_TO_NAME_MAP[CHAIN_IDS.POLYGON], + [toEvmCaipChainId(CHAIN_IDS.AVALANCHE)]: 'Avalanche', + [toEvmCaipChainId(CHAIN_IDS.BSC)]: NETWORK_TO_NAME_MAP[CHAIN_IDS.BSC], + [toEvmCaipChainId(CHAIN_IDS.ARBITRUM)]: + NETWORK_TO_NAME_MAP[CHAIN_IDS.ARBITRUM], + [toEvmCaipChainId(CHAIN_IDS.OPTIMISM)]: + NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], + [toEvmCaipChainId(CHAIN_IDS.ZKSYNC_ERA)]: 'ZkSync Era', + [toEvmCaipChainId(CHAIN_IDS.BASE)]: 'Base', ///: BEGIN:ONLY_INCLUDE_IF(solana-swaps) [MultichainNetworks.SOLANA]: 'Solana', [MultichainNetworks.SOLANA_TESTNET]: 'Solana Testnet', From 8b5166487ed5ab40c6ea8e5132e4307f4e077614 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 27 Mar 2025 13:57:36 -0700 Subject: [PATCH 71/71] chore: simplify asset metadata abort call --- .../asset-picker-modal/asset-picker-modal.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index e90154e59b34..0275af4585f2 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -586,10 +586,8 @@ export function AssetPickerModal({ { - if (abortControllerRef.current) { - // Cancel previous asset metadata fetch - abortControllerRef.current.abort(); - } + // Cancel previous asset metadata fetch + abortControllerRef.current?.abort(); setSearchQuery(value); }} autoFocus={autoFocus}