From d546e88e462881192822aedffc4168b4a70f11cf Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 14 Mar 2025 14:03:06 -0700 Subject: [PATCH 01/47] feat: add multichain support to bridge-controller --- package.json | 1 + packages/bridge-controller/package.json | 5 + .../src/bridge-controller.ts | 249 +++++++++++------- .../bridge-controller/src/constants/bridge.ts | 52 ++-- .../bridge-controller/src/constants/tokens.ts | 12 + packages/bridge-controller/src/index.ts | 4 +- packages/bridge-controller/src/types.ts | 80 ++++-- .../bridge-controller/src/utils/bridge.ts | 7 +- .../src/utils/caip-formatters.ts | 108 ++++++++ packages/bridge-controller/src/utils/fetch.ts | 55 ++-- packages/bridge-controller/src/utils/quote.ts | 20 +- .../bridge-controller/src/utils/validators.ts | 7 +- .../bridge-controller/tsconfig.build.json | 3 +- packages/bridge-controller/tsconfig.json | 3 +- yarn.lock | 165 +++++++++++- 15 files changed, 600 insertions(+), 171 deletions(-) create mode 100644 packages/bridge-controller/src/utils/caip-formatters.ts diff --git a/package.json b/package.json index 99adddd6cf8..15f5a8bd014 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "pre-push": "yarn lint" }, "resolutions": { + "@metamask/snaps-sdk": "^6.19.0", "elliptic@6.5.4": "^6.5.7", "fast-xml-parser@^4.3.4": "^4.4.1", "ws@7.4.6": "^7.5.10" diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 17f6f175a93..1c8ea8595d1 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -54,8 +54,12 @@ "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^8.0.0", "@metamask/controller-utils": "^11.6.0", + "@metamask/keyring-api": "^17.2.0", "@metamask/metamask-eth-abis": "^3.1.1", + "@metamask/multichain-network-controller": "^0.1.0", "@metamask/polling-controller": "^12.0.3", + "@metamask/snaps-controllers": "^10.0.1", + "@metamask/snaps-utils": "^9.0.1", "@metamask/utils": "^11.2.0" }, "devDependencies": { @@ -65,6 +69,7 @@ "@metamask/network-controller": "^22.2.1", "@metamask/superstruct": "^3.1.0", "@metamask/transaction-controller": "^50.0.0", + "@ts-bridge/cli": "^0.6.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 3b78be46355..f64f4940235 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -3,15 +3,18 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; import type { StateMetadata } from '@metamask/base-controller'; import type { ChainId } from '@metamask/controller-utils'; +import { SolScope } from '@metamask/keyring-api'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { NetworkClientId } from '@metamask/network-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; +import { type SnapId } from '@metamask/snaps-sdk'; +import { HandlerType } from '@metamask/snaps-utils'; import type { TransactionParams } from '@metamask/transaction-controller'; import { numberToHex } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import type { BridgeClientId } from './constants/bridge'; import { + type BridgeClientId, BRIDGE_CONTROLLER_NAME, BRIDGE_PROD_API_BASE_URL, DEFAULT_BRIDGE_CONTROLLER_STATE, @@ -19,9 +22,9 @@ import { REFRESH_INTERVAL_MS, } from './constants/bridge'; import { CHAIN_IDS } from './constants/chains'; +import type { GenericQuoteRequest, SolanaFees } from './types'; import { type L1GasFees, - type QuoteRequest, type QuoteResponse, type TxData, type BridgeControllerState, @@ -32,39 +35,16 @@ import { } from './types'; import { hasSufficientBalance } from './utils/balance'; import { getDefaultBridgeControllerState, sumHexes } from './utils/bridge'; +import { + formatAddressToString, + formatChainIdToCaip, + formatChainIdToHex, +} from './utils/caip-formatters'; import { fetchBridgeFeatureFlags, fetchBridgeQuotes } from './utils/fetch'; import { isValidQuoteRequest } from './utils/quote'; const metadata: StateMetadata = { - bridgeFeatureFlags: { - persist: false, - anonymous: false, - }, - quoteRequest: { - persist: false, - anonymous: false, - }, - quotes: { - persist: false, - anonymous: false, - }, - quotesInitialLoadTime: { - persist: false, - anonymous: false, - }, - quotesLastFetched: { - persist: false, - anonymous: false, - }, - quotesLoadingStatus: { - persist: false, - anonymous: false, - }, - quoteFetchError: { - persist: false, - anonymous: false, - }, - quotesRefreshCount: { + bridgeState: { persist: false, anonymous: false, }, @@ -75,7 +55,7 @@ const RESET_STATE_ABORT_MESSAGE = 'Reset controller state'; /** The input to start polling for the {@link BridgeController} */ type BridgePollingInput = { networkClientId: NetworkClientId; - updatedQuoteRequest: QuoteRequest; + updatedQuoteRequest: GenericQuoteRequest; }; export class BridgeController extends StaticIntervalPollingController()< @@ -125,7 +105,7 @@ export class BridgeController extends StaticIntervalPollingController, + paramsToUpdate: Partial, ) => { this.stopAllPolling(); this.#abortController?.abort('Quote request updated'); @@ -172,7 +152,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update(({ bridgeState: state }) => { state.quoteRequest = updatedQuoteRequest; state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes; state.quotesLastFetched = @@ -188,36 +168,50 @@ export class BridgeController extends StaticIntervalPollingController { - const walletAddress = this.#getSelectedAccount().address; - const srcChainIdInHex = numberToHex(quoteRequest.srcChainId); + readonly #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, - quoteRequest.srcTokenAddress, + srcTokenAddressWithoutPrefix, quoteRequest.srcTokenAmount, srcChainIdInHex, )) @@ -228,7 +222,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update(({ bridgeState: state }) => { // Cannot do direct assignment to state, i.e. state = {... }, need to manually assign each field state.quoteRequest = DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest; state.quotesInitialLoadTime = @@ -254,12 +248,27 @@ export class BridgeController extends StaticIntervalPollingController { + this.update(({ bridgeState: state }) => { state.bridgeFeatureFlags = bridgeFeatureFlags; }); - this.setIntervalLength( - bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].refreshRate, - ); + this.#setIntervalLength(); + }; + + /** + * Sets the interval length based on the source chain + */ + readonly #setIntervalLength = () => { + const { bridgeState: state } = this.state; + const { srcChainId } = state.quoteRequest; + const refreshRateOverride = srcChainId + ? state.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].chains[ + formatChainIdToCaip(srcChainId) + ]?.refreshRate + : undefined; + const defaultRefreshRate = + state.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG] + .refreshRate; + this.setIntervalLength(refreshRateOverride ?? defaultRefreshRate); }; readonly #fetchBridgeQuotes = async ({ @@ -267,13 +276,11 @@ export class BridgeController extends StaticIntervalPollingController { const { bridgeFeatureFlags, quotesInitialLoadTime, quotesRefreshCount } = - this.state; + this.state.bridgeState; this.#abortController?.abort('New quote request'); this.#abortController = new AbortController(); - if (updatedQuoteRequest.srcChainId === updatedQuoteRequest.destChainId) { - return; - } - this.update((state) => { + + this.update(({ bridgeState: state }) => { state.quotesLoadingStatus = RequestStatus.LOADING; state.quoteRequest = updatedQuoteRequest; state.quoteFetchError = DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError; @@ -293,9 +300,10 @@ export class BridgeController extends StaticIntervalPollingController { - state.quotes = quotesWithL1GasFees; + this.update(({ bridgeState: state }) => { + state.quotes = quotesWithL1GasFees ?? quotesWithSolanaFees ?? quotes; state.quotesLoadingStatus = RequestStatus.FETCHED; }); } catch (error) { @@ -305,10 +313,11 @@ export class BridgeController extends StaticIntervalPollingController { + this.update(({ bridgeState: state }) => { state.quoteFetchError = error instanceof Error ? error.message : 'Unknown error'; state.quotesLoadingStatus = RequestStatus.ERROR; + state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes; }); console.log('Failed to fetch bridge quotes', error); } finally { @@ -327,7 +336,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update(({ bridgeState: state }) => { state.quotesInitialLoadTime = updatedQuotesRefreshCount === 1 && this.#quotesFirstFetched ? quotesLastFetched - this.#quotesFirstFetched @@ -340,36 +349,88 @@ export class BridgeController extends StaticIntervalPollingController => { + ): 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 = numberToHex(quote.srcChainId) as ChainId; - if ( - [CHAIN_IDS.OPTIMISM.toString(), CHAIN_IDS.BASE.toString()].includes( - 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, - }); + + 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; + }), + ); + }; + + readonly #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) !== SolScope.Mainnet; + }) + ) { + 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, - l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees), + solanaFeesInLamports: fees, }; } return quoteResponse; @@ -377,8 +438,10 @@ export class BridgeController extends StaticIntervalPollingController = { [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, }; + +export const MULTICHAIN_ID_TO_NATIVE_ASSET_MAP: Record = + Object.entries( + getDefaultMultichainNetworkControllerState() + .multichainNetworkConfigurationsByChainId, + ).reduce( + (acc, [chainId, config]) => ({ ...acc, [chainId]: config.nativeCurrency }), + {}, + ); + +export const MULTICHAIN_NETWORK_CONFIGURATIONS: Record< + CaipChainId, + MultichainNetworkConfiguration +> = + getDefaultMultichainNetworkControllerState() + .multichainNetworkConfigurationsByChainId; diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index be67ca8ccd8..72ed306fe01 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,3 +1,6 @@ +import { SolScope } from '@metamask/keyring-api'; + +import { MULTICHAIN_ID_TO_NATIVE_ASSET_MAP } from './bridge'; import { CHAIN_IDS } from './chains'; export type SwapsTokenObject = { @@ -126,6 +129,14 @@ export const BASE_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { ...ETH_SWAPS_TOKEN_OBJECT, } as const; +const SOLANA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { + symbol: 'SOL', + name: 'Solana', + address: MULTICHAIN_ID_TO_NATIVE_ASSET_MAP[SolScope.Mainnet], + decimals: 9, + iconUrl: '', +}; + const SWAPS_TESTNET_CHAIN_ID = '0x539'; export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { @@ -141,4 +152,5 @@ export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, [CHAIN_IDS.LINEA_MAINNET]: LINEA_SWAPS_TOKEN_OBJECT, [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, + [SolScope.Mainnet]: SOLANA_SWAPS_TOKEN_OBJECT, } as const; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index eac0f9fe62d..9a740790e23 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -8,7 +8,7 @@ export type { GasMultiplierByChainId, FeatureFlagResponse, BridgeAsset, - QuoteRequest, + GenericQuoteRequest, Protocol, Step, RefuelData, @@ -60,3 +60,5 @@ export type { SwapsTokenObject } from './constants/tokens'; export { SWAPS_API_V2_BASE_URL } from './constants/swaps'; export { getEthUsdtResetData, isEthUsdt } from './utils/bridge'; + +export { isValidQuoteRequest } from './utils/quote'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 44b3ee939e5..273fe76992c 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -1,4 +1,4 @@ -import type { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; +import type { AccountsControllerGetSelectedMultichainAccountAction } from '@metamask/accounts-controller'; import type { ControllerStateChangeEvent, RestrictedMessenger, @@ -8,15 +8,29 @@ import type { NetworkControllerGetStateAction, NetworkControllerGetNetworkClientByIdAction, } from '@metamask/network-controller'; -import type { Hex } from '@metamask/utils'; +import type { HandleSnapRequest } from '@metamask/snaps-controllers'; +import type { + CaipAccountId, + CaipAssetId, + CaipChainId, + Hex, +} from '@metamask/utils'; import type { BigNumber } from 'bignumber.js'; import type { BridgeController } from './bridge-controller'; import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge'; +// The extension's fetch function accepts additional options +type FetchWithCacheOptions = { + cacheOptions?: { + cacheRefreshTime: number; + }; + functionName?: string; +}; + export type FetchFunction = ( input: RequestInfo | URL, - init?: RequestInit, + init?: RequestInit & FetchWithCacheOptions, // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => Promise; @@ -41,11 +55,18 @@ export enum AssetType { 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 @@ -72,12 +93,11 @@ export type BridgeToken = { symbol: string; image: string; decimals: number; - chainId: Hex; + chainId: number | Hex | ChainId | CaipChainId; balance: string; // raw balance string: string | undefined; // normalized balance as a stringified number tokenFiatAmount?: number | null; -} | null; -// Types copied from Metabridge API +}; export enum BridgeFlag { EXTENSION_CONFIG = 'extension-config', @@ -105,20 +125,28 @@ export type BridgeAsset = { name: string; decimals: number; icon?: string; + assetId: string; }; -export type QuoteRequest = { - walletAddress: string; - destWalletAddress?: string; - srcChainId: ChainId; - destChainId: ChainId; - srcTokenAddress: string; - destTokenAddress: string; +// This is the interface for the quote request sent to the bridge-api +// and should only be used by the fetchBridgeQuotes function +// Components and redux store should use the GenericQuoteRequest type +export type QuoteRequest< + ChainIdType = ChainId | number, + TokenAddressType = string, + WalletAddressType = string, +> = { + walletAddress: WalletAddressType; + destWalletAddress?: WalletAddressType; + srcChainId: ChainIdType; + destChainId: ChainIdType; + srcTokenAddress: TokenAddressType; + destTokenAddress: TokenAddressType; /** * This is the amount sent, in atomic amount */ srcTokenAmount: string; - slippage: number; + slippage?: number; aggIds?: string[]; bridgeIds?: string[]; insufficientBal?: boolean; @@ -126,6 +154,15 @@ export type QuoteRequest = { refuel?: boolean; }; +// 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. +// Payloads with this type are transformed into QuoteRequest by fetchBridgeQuotes right before fetching quotes +export type GenericQuoteRequest = QuoteRequest< + Hex | CaipChainId | string | number, // chainIds + Hex | CaipAssetId | string, // assetIds/addresses + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments + Hex | CaipAccountId | string // accountIds/addresses +>; + export type Protocol = { name: string; displayName?: string; @@ -185,6 +222,7 @@ export enum ChainId { ARBITRUM = 42161, AVALANCHE = 43114, LINEA = 59144, + SOLANA = 1151111081099710, } export enum FeeType { @@ -212,7 +250,7 @@ type FeatureFlagsPlatformConfig = { refreshRate: number; maxRefreshCount: number; support: boolean; - chains: Record; + chains: Record; }; export type BridgeFeatureFlags = { @@ -233,10 +271,11 @@ export enum BridgeBackgroundAction { RESET_STATE = 'resetState', GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', } -export type BridgeControllerState = { + +type BridgeState = { bridgeFeatureFlags: BridgeFeatureFlags; - quoteRequest: Partial; - quotes: (QuoteResponse & L1GasFees)[]; + quoteRequest: Partial; + quotes: (QuoteResponse & L1GasFees & SolanaFees)[]; quotesInitialLoadTime: number | null; quotesLastFetched: number | null; quotesLoadingStatus: RequestStatus | null; @@ -244,6 +283,8 @@ export type BridgeControllerState = { quotesRefreshCount: number; }; +export type BridgeControllerState = { bridgeState: BridgeState }; + export type BridgeControllerAction< FunctionName extends keyof BridgeController, > = { @@ -264,7 +305,8 @@ export type BridgeControllerEvents = ControllerStateChangeEvent< >; export type AllowedActions = - | AccountsControllerGetSelectedAccountAction + | AccountsControllerGetSelectedMultichainAccountAction + | HandleSnapRequest | NetworkControllerFindNetworkClientIdByChainIdAction | NetworkControllerGetStateAction | NetworkControllerGetNetworkClientByIdAction; diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 124bd396e3d..80644a8a002 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -11,9 +11,10 @@ import { CHAIN_IDS } from '../constants/chains'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { BridgeControllerState } from '../types'; -export const getDefaultBridgeControllerState = (): BridgeControllerState => { - return DEFAULT_BRIDGE_CONTROLLER_STATE; -}; +export const getDefaultBridgeControllerState = + (): BridgeControllerState['bridgeState'] => { + return DEFAULT_BRIDGE_CONTROLLER_STATE; + }; /** * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts new file mode 100644 index 00000000000..9f8c7148a24 --- /dev/null +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -0,0 +1,108 @@ +import { getAddress } from '@ethersproject/address'; +import { AddressZero } from '@ethersproject/constants'; +import { convertHexToDecimal } from '@metamask/controller-utils'; +import { SolScope } from '@metamask/keyring-api'; +import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; +import { + type Hex, + type CaipChainId, + isCaipChainId, + isStrictHexString, + parseCaipChainId, + isCaipReference, + numberToHex, +} from '@metamask/utils'; + +import { ChainId } from '../types'; + +// Returns true if the address looka like a native asset +export const isNativeAddress = (address?: string | null) => + address === AddressZero || + address === '' || + !address || + [`${SolScope.Mainnet}/slip44:501`].some( + (assetId) => assetId.includes(address) && !isStrictHexString(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 === ChainId.SOLANA.toString()) { + return SolScope.Mainnet; + } + return toEvmCaipChainId(numberToHex(Number(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 convertHexToDecimal(chainId); + } + if (chainId === SolScope.Mainnet) { + 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 numberToHex(Number(chainId)); + } + if (isCaipChainId(chainId)) { + const { reference } = parseCaipChainId(chainId); + if (isCaipReference(reference) && !isNaN(Number(reference))) { + return numberToHex(Number(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 getAddress(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 AddressZero; + } + 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 SolScope.Mainnet; + } + return formatChainIdToHex(chainId); +}; diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 33bc3e7533e..f0d0c64c086 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -1,10 +1,15 @@ import type { Hex } from '@metamask/utils'; -import { hexToNumber, numberToHex } from '@metamask/utils'; +import { Duration, hexToNumber } from '@metamask/utils'; import { isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, } from './bridge'; +import { + formatAddressToString, + formatChainIdToCaip, + formatChainIdToDec, +} from './caip-formatters'; import { validateFeatureFlagsResponse, validateQuoteResponse, @@ -14,16 +19,16 @@ import { DEFAULT_FEATURE_FLAG_CONFIG } from '../constants/bridge'; import type { SwapsTokenObject } from '../constants/tokens'; import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { - QuoteRequest, QuoteResponse, BridgeFeatureFlags, FetchFunction, ChainConfiguration, + GenericQuoteRequest, + QuoteRequest, } from '../types'; import { BridgeFlag, BridgeFeatureFlagsKey } from '../types'; -// TODO put this back in once we have a fetchWithCache equivalent -// const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; +const CACHE_REFRESH_TEN_MINUTES = 10 * Duration.Minute; export const getClientIdHeader = (clientId: string) => ({ 'X-Client-Id': clientId, @@ -45,6 +50,8 @@ export async function fetchBridgeFeatureFlags( const url = `${bridgeApiBaseUrl}/getAllFeatureFlags`; const rawFeatureFlags: unknown = await fetchFn(url, { headers: getClientIdHeader(clientId), + cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, + functionName: 'fetchBridgeFeatureFlags', }); if (validateFeatureFlagsResponse(rawFeatureFlags)) { @@ -52,7 +59,7 @@ export async function fetchBridgeFeatureFlags( Object.entries(chains).reduce( (acc, [chainId, value]) => ({ ...acc, - [numberToHex(Number(chainId))]: value, + [formatChainIdToCaip(chainId)]: value, }), {}, ); @@ -100,6 +107,8 @@ export async function fetchBridgeTokens( // note that the Assets controller won't be able to provide tokens. In extension we fetch+cache the token list from bridge-api to handle this const tokens = await fetchFn(url, { headers: getClientIdHeader(clientId), + cacheOptions: { cacheRefreshTime: CACHE_REFRESH_TEN_MINUTES }, + functionName: 'fetchBridgeTokens', }); const nativeToken = @@ -126,8 +135,9 @@ export async function fetchBridgeTokens( return transformedTokens; } -// Returns a list of bridge tx quotes /** + * Converts the generic quote request to the type that the bridge-api expects + * and fetches quotes from the bridge-api * * @param request - The quote request * @param signal - The abort signal @@ -137,27 +147,40 @@ export async function fetchBridgeTokens( * @returns A list of bridge tx quotes */ export async function fetchBridgeQuotes( - request: QuoteRequest, + request: GenericQuoteRequest, signal: AbortSignal, clientId: string, fetchFn: FetchFunction, bridgeApiBaseUrl: string, ): Promise { - const queryParams = new URLSearchParams({ - walletAddress: request.walletAddress, - srcChainId: request.srcChainId.toString(), - destChainId: request.destChainId.toString(), - srcTokenAddress: request.srcTokenAddress, - destTokenAddress: request.destTokenAddress, + const normalizedRequest: QuoteRequest = { + walletAddress: formatAddressToString(request.walletAddress), + destWalletAddress: formatAddressToString( + request.destWalletAddress ?? request.walletAddress, + ), + srcChainId: formatChainIdToDec(request.srcChainId), + destChainId: formatChainIdToDec(request.destChainId), + srcTokenAddress: formatAddressToString(request.srcTokenAddress), + destTokenAddress: formatAddressToString(request.destTokenAddress), srcTokenAmount: request.srcTokenAmount, - slippage: request.slippage.toString(), - insufficientBal: request.insufficientBal ? 'true' : 'false', - resetApproval: request.resetApproval ? 'true' : 'false', + insufficientBal: Boolean(request.insufficientBal), + resetApproval: Boolean(request.resetApproval), + }; + if (request.slippage !== undefined) { + normalizedRequest.slippage = request.slippage; + } + + const queryParams = new URLSearchParams(); + Object.entries(normalizedRequest).forEach(([key, value]) => { + queryParams.append(key, value.toString()); }); const url = `${bridgeApiBaseUrl}/getQuote?${queryParams}`; + const quotes: unknown[] = await fetchFn(url, { headers: getClientIdHeader(clientId), signal, + cacheOptions: { cacheRefreshTime: 0 }, + functionName: 'fetchBridgeQuotes', }); const filteredQuotes = quotes.filter((quoteResponse: unknown) => { diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts index 8ea616fd345..459be8b5e5e 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -1,14 +1,24 @@ -import type { QuoteRequest } from '../types'; +import type { GenericQuoteRequest } from '../types'; export const isValidQuoteRequest = ( - partialRequest: Partial, + partialRequest: Partial, requireAmount = true, -): partialRequest is QuoteRequest => { - const stringFields = ['srcTokenAddress', 'destTokenAddress']; +): partialRequest is GenericQuoteRequest => { + const stringFields = [ + 'srcTokenAddress', + 'destTokenAddress', + 'srcChainId', + 'destChainId', + 'walletAddress', + ]; if (requireAmount) { stringFields.push('srcTokenAmount'); } - const numberFields = ['srcChainId', 'destChainId', 'slippage']; + const numberFields = []; + // if slippage is defined, require it to be a number + if (partialRequest.slippage !== undefined) { + numberFields.push('slippage'); + } return ( stringFields.every( diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index f2e5171c656..73891a41070 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -11,8 +11,8 @@ import { optional, enums, define, - intersection, size, + union, } from '@metamask/superstruct'; import { isStrictHexString } from '@metamask/utils'; @@ -37,7 +37,7 @@ const TruthyDigitStringSchema = define( const SwapsTokenObjectSchema = type({ decimals: number(), - address: intersection([string(), HexAddressSchema]), + address: string(), symbol: size(string(), 1, 12), }); @@ -77,6 +77,7 @@ export const validateQuoteResponse = (data: unknown): data is QuoteResponse => { const BridgeAssetSchema = type({ chainId: ChainIdSchema, address: string(), + assetId: string(), symbol: string(), name: string(), decimals: number(), @@ -134,7 +135,7 @@ export const validateQuoteResponse = (data: unknown): data is QuoteResponse => { const QuoteResponseSchema = type({ quote: QuoteSchema, approval: optional(TxDataSchema), - trade: TxDataSchema, + trade: union([TxDataSchema, string()]), estimatedProcessingTimeInSeconds: number(), }); diff --git a/packages/bridge-controller/tsconfig.build.json b/packages/bridge-controller/tsconfig.build.json index b62ec3ff054..58aa4cff2e5 100644 --- a/packages/bridge-controller/tsconfig.build.json +++ b/packages/bridge-controller/tsconfig.build.json @@ -11,7 +11,8 @@ { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, { "path": "../polling-controller/tsconfig.build.json" }, - { "path": "../transaction-controller/tsconfig.build.json" } + { "path": "../transaction-controller/tsconfig.build.json" }, + { "path": "../multichain-network-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/bridge-controller/tsconfig.json b/packages/bridge-controller/tsconfig.json index 3f93de1f5e6..ed8da7195aa 100644 --- a/packages/bridge-controller/tsconfig.json +++ b/packages/bridge-controller/tsconfig.json @@ -10,7 +10,8 @@ { "path": "../controller-utils" }, { "path": "../network-controller" }, { "path": "../polling-controller" }, - { "path": "../transaction-controller" } + { "path": "../transaction-controller" }, + { "path": "../multichain-network-controller" } ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index 4aa8e3bb4ad..1abe5d59c2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2684,12 +2684,17 @@ __metadata: "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.6.0" "@metamask/eth-json-rpc-provider": "npm:^4.1.8" + "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-network-controller": "npm:^0.1.0" "@metamask/network-controller": "npm:^22.2.1" "@metamask/polling-controller": "npm:^12.0.3" + "@metamask/snaps-controllers": "npm:^10.0.1" + "@metamask/snaps-utils": "npm:^9.0.1" "@metamask/superstruct": "npm:^3.1.0" "@metamask/transaction-controller": "npm:^50.0.0" "@metamask/utils": "npm:^11.2.0" + "@ts-bridge/cli": "npm:^0.6.3" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -3413,7 +3418,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-middleware-stream@npm:^8.0.6, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": +"@metamask/json-rpc-middleware-stream@npm:^8.0.6, @metamask/json-rpc-middleware-stream@npm:^8.0.7, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": version: 0.0.0-use.local resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" dependencies: @@ -3648,6 +3653,21 @@ __metadata: languageName: unknown linkType: soft +"@metamask/multichain-network-controller@npm:^0.1.0": + 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.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/4f5423a6de319db540a13e5dc5d637b5e55cf7af901cbe040353912c4aaba640a9454abcecb93fa53ca397f5c40d097835b001f40963f8624c02961dbf29931a + languageName: node + linkType: hard + "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" @@ -3924,7 +3944,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/phishing-controller@npm:^12.3.1, @metamask/phishing-controller@workspace:packages/phishing-controller": +"@metamask/phishing-controller@npm:^12.3.1, @metamask/phishing-controller@npm:^12.4.0, @metamask/phishing-controller@workspace:packages/phishing-controller": version: 0.0.0-use.local resolution: "@metamask/phishing-controller@workspace:packages/phishing-controller" dependencies: @@ -4047,7 +4067,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/providers@npm:^18.1.1, @metamask/providers@npm:^18.3.1": +"@metamask/providers@npm:^18.1.1": version: 18.3.1 resolution: "@metamask/providers@npm:18.3.1" dependencies: @@ -4068,6 +4088,27 @@ __metadata: languageName: node linkType: hard +"@metamask/providers@npm:^20.0.0": + version: 20.0.0 + resolution: "@metamask/providers@npm:20.0.0" + dependencies: + "@metamask/json-rpc-engine": "npm:^10.0.2" + "@metamask/json-rpc-middleware-stream": "npm:^8.0.6" + "@metamask/object-multiplex": "npm:^2.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/safe-event-emitter": "npm:^3.1.1" + "@metamask/utils": "npm:^11.0.1" + detect-browser: "npm:^5.2.0" + extension-port-stream: "npm:^4.1.0" + fast-deep-equal: "npm:^3.1.3" + is-stream: "npm:^2.0.0" + readable-stream: "npm:^3.6.2" + peerDependencies: + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 10/b958d03a9380d86e605db239109a3debcc1ffde90371abe5beb82a5bed46c7718303a2bb92ec269eae16eff145b9ebbfcb3445a2b6bad4f297a590ee725a5bad + languageName: node + linkType: hard + "@metamask/queued-request-controller@workspace:packages/queued-request-controller": version: 0.0.0-use.local resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" @@ -4254,6 +4295,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:^9.19.0": version: 9.19.1 resolution: "@metamask/snaps-controllers@npm:9.19.1" @@ -4324,16 +4406,33 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^6.17.0, @metamask/snaps-sdk@npm:^6.17.1": - version: 6.17.1 - resolution: "@metamask/snaps-sdk@npm:6.17.1" +"@metamask/snaps-rpc-methods@npm:^11.13.0": + version: 11.13.1 + resolution: "@metamask/snaps-rpc-methods@npm:11.13.1" dependencies: "@metamask/key-tree": "npm:^10.0.2" - "@metamask/providers": "npm:^18.3.1" + "@metamask/permission-controller": "npm:^11.0.6" "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/snaps-sdk": "npm:^6.19.0" + "@metamask/snaps-utils": "npm:^9.0.1" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" - checksum: 10/05c5170c6250115535bc6d06a417157bb55005dd6fe86e768d70fabfba610ec8114cf45a8a5aad1219b1cfb0bcf5e080974735a0ac9a8c8bd0ac102f5c3cf42f + "@metamask/utils": "npm:^11.2.0" + "@noble/hashes": "npm:^1.3.1" + luxon: "npm:^3.5.0" + checksum: 10/72298b0cb5cf79ffaacb48368590513ec5befa10f9ffb84865ce1eab5dc2ff28f5b0e765a7fb55885e8e0583091af404e879fad8510e9c8cb49ee455cf7b9444 + languageName: node + linkType: hard + +"@metamask/snaps-sdk@npm:^6.19.0": + version: 6.19.0 + resolution: "@metamask/snaps-sdk@npm:6.19.0" + dependencies: + "@metamask/key-tree": "npm:^10.0.2" + "@metamask/providers": "npm:^20.0.0" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.2.0" + checksum: 10/585f56b11e82e835ce104b9c719326c0f009cb87a76dd7c49eb324c7175a3afea0a08d5e8a0bd66bbcd9b25c90a8af6bf15342b830898a736d7b94fa951bd8ff languageName: node linkType: hard @@ -4368,6 +4467,37 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-utils@npm:^9.0.1": + version: 9.0.1 + resolution: "@metamask/snaps-utils@npm:9.0.1" + dependencies: + "@babel/core": "npm:^7.23.2" + "@babel/types": "npm:^7.23.0" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/key-tree": "npm:^10.0.2" + "@metamask/permission-controller": "npm:^11.0.6" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/slip44": "npm:^4.1.0" + "@metamask/snaps-registry": "npm:^3.2.3" + "@metamask/snaps-sdk": "npm:^6.19.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.2.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.1" + chalk: "npm:^4.1.2" + cron-parser: "npm:^4.5.0" + fast-deep-equal: "npm:^3.1.3" + fast-json-stable-stringify: "npm:^2.1.0" + fast-xml-parser: "npm:^4.4.1" + marked: "npm:^12.0.1" + rfdc: "npm:^1.3.0" + semver: "npm:^7.5.4" + ses: "npm:^1.1.0" + validate-npm-package-name: "npm:^5.0.0" + checksum: 10/3031ebda558b3fd78636806a34dbe03b0ff6f0cbbf16fec9b9cb64eba4eb00bd035cea3a07fbc55fe17b75b4f02a6b376918922011df599b94cfc1dea5a9aced + languageName: node + linkType: hard + "@metamask/stake-sdk@npm:^1.0.0": version: 1.0.0 resolution: "@metamask/stake-sdk@npm:1.0.0" @@ -5107,6 +5237,23 @@ __metadata: languageName: node linkType: hard +"@ts-bridge/cli@npm:^0.6.3": + version: 0.6.3 + resolution: "@ts-bridge/cli@npm:0.6.3" + dependencies: + "@ts-bridge/resolver": "npm:^0.2.0" + chalk: "npm:^5.3.0" + cjs-module-lexer: "npm:^1.3.1" + yargs: "npm:^17.7.2" + peerDependencies: + typescript: ">=4.8.0" + bin: + ts-bridge: ./dist/index.js + tsbridge: ./dist/index.js + checksum: 10/01cba56ff0f097ca0ef15b79cff80a6be07b7f237e7153f63f7a9acf911583d0a410385fa1711d7b14e3a5e95fed63310b6a743700e7ecc0dd3a2d97a0df75b3 + languageName: node + linkType: hard + "@ts-bridge/resolver@npm:^0.2.0": version: 0.2.0 resolution: "@ts-bridge/resolver@npm:0.2.0" From 2a7842856b6f2938b2649a0acca957e047d0a33f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 14 Mar 2025 15:43:45 -0700 Subject: [PATCH 02/47] chore: updates after self-review --- .../src/bridge-controller.ts | 103 +++++++++--------- packages/bridge-controller/src/index.ts | 7 +- packages/bridge-controller/src/types.ts | 30 +++-- .../bridge-controller/src/utils/bridge.ts | 47 ++++++-- .../src/utils/caip-formatters.ts | 60 ++++++---- packages/bridge-controller/src/utils/fetch.ts | 2 +- 6 files changed, 154 insertions(+), 95 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index f64f4940235..906f5764cea 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -3,7 +3,6 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; import type { StateMetadata } from '@metamask/base-controller'; import type { ChainId } from '@metamask/controller-utils'; -import { SolScope } from '@metamask/keyring-api'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { NetworkClientId } from '@metamask/network-controller'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; @@ -34,7 +33,7 @@ import { RequestStatus, } from './types'; import { hasSufficientBalance } from './utils/balance'; -import { getDefaultBridgeControllerState, sumHexes } from './utils/bridge'; +import { isSolanaChainId, sumHexes } from './utils/bridge'; import { formatAddressToString, formatChainIdToCaip, @@ -105,7 +104,7 @@ export class BridgeController extends StaticIntervalPollingController => { - // 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; - } + // Indicates whether some of the quotes are not for optimism or base + const hasInvalidQuotes = quotes.some(({ quote }) => { + const chainId = formatChainIdToCaip(quote.srcChainId); + return ![CHAIN_IDS.OPTIMISM, CHAIN_IDS.BASE] + .map(formatChainIdToCaip) + .includes(chainId); + }); - return await Promise.all( - quotes.map(async (quoteResponse) => { - const { quote, trade, approval } = quoteResponse; - const chainId = numberToHex(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), - }; + // Only append L1 gas fees if all quotes are for either optimism or base + if (!hasInvalidQuotes) { + return await Promise.all( + quotes.map(async (quoteResponse) => { + const { quote, trade, approval } = quoteResponse; + const chainId = numberToHex(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; - }), - ); + return undefined; }; readonly #appendSolanaFees = async ( quotes: QuoteResponse[], ): Promise<(QuoteResponse & SolanaFees)[] | undefined> => { - // Return undefined if some of the quotes are not for solana + // Return early if some of the quotes are not for solana if ( - quotes.some(({ quote }) => { - return formatChainIdToCaip(quote.srcChainId) !== SolScope.Mainnet; - }) + quotes.some(({ quote: { srcChainId } }) => !isSolanaChainId(srcChainId)) ) { return undefined; } @@ -444,10 +440,15 @@ export class BridgeController extends StaticIntervalPollingController { - return DEFAULT_BRIDGE_CONTROLLER_STATE; - }; +import { ChainId } from '../types'; /** * 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; @@ -45,6 +41,7 @@ export const sumHexes = (...hexStrings: string[]): Hex => { const sum = hexStrings.reduce((acc, hex) => acc + BigInt(hex), BigInt(0)); return `0x${sum.toString(16)}`; }; + /** * Checks whether the provided address is strictly equal to the address for * the default swaps token of the provided chain. @@ -53,7 +50,6 @@ export const sumHexes = (...hexStrings: string[]): Hex => { * @param chainId - The hex encoded chain ID of the default swaps token to check * @returns Whether the address is the provided chain's default token address */ - export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { if (!address || !chainId) { return false; @@ -66,6 +62,7 @@ export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { ]?.address ); }; + /** * Checks whether the provided symbol is strictly equal to the symbol for * the default swaps token of the provided chain. @@ -74,7 +71,6 @@ export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { * @param chainId - The hex encoded chain ID of the default swaps token to check * @returns Whether the symbol is the provided chain's default token symbol */ - export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { if (!symbol || !chainId) { return false; @@ -87,3 +83,32 @@ export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { ]?.symbol ); }; + +/** + * Checks whether the address is a native asset in any supported xchain swapsnetwork + * + * @param address - The address to check + * @returns Whether the address is a native asset + */ +export const isNativeAddress = (address?: string | null) => + address === AddressZero || // bridge and swap apis set the native asset address to zero + address === '' || // assets controllers set the native asset address to empty string + !address || + [`${SolScope.Mainnet}/slip44:501`].some( + (assetId) => assetId.includes(address) && !isStrictHexString(address), + ); // multichain native assets are represented in caip format + +/** + * Checks whether the chainId matches Solana in CaipChainId or number format + * + * @param chainId - The chainId to check + * @returns Whether the chainId is Solana + */ +export const isSolanaChainId = ( + chainId: Hex | number | CaipChainId | string, +) => { + if (isCaipChainId(chainId)) { + return chainId === SolScope.Mainnet.toString(); + } + return chainId.toString() === ChainId.SOLANA.toString(); +}; diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index 9f8c7148a24..d8d133158b4 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -13,34 +13,36 @@ import { numberToHex, } from '@metamask/utils'; +import { isNativeAddress, isSolanaChainId } from './bridge'; import { ChainId } from '../types'; -// Returns true if the address looka like a native asset -export const isNativeAddress = (address?: string | null) => - address === AddressZero || - address === '' || - !address || - [`${SolScope.Mainnet}/slip44:501`].some( - (assetId) => assetId.includes(address) && !isStrictHexString(address), - ); - -// Converts a chainId to a CaipChainId +/** + * Converts a chainId to a CaipChainId + * + * @param chainId - The chainId to convert + * @returns The CaipChainId + */ export const formatChainIdToCaip = ( chainId: Hex | number | CaipChainId | string, ): CaipChainId => { if (isCaipChainId(chainId)) { return chainId; - } else if (isStrictHexString(chainId)) { + } + if (isStrictHexString(chainId)) { return toEvmCaipChainId(chainId); } - const chainIdString = chainId.toString(); - if (chainIdString === ChainId.SOLANA.toString()) { + if (isSolanaChainId(chainId)) { return SolScope.Mainnet; } - return toEvmCaipChainId(numberToHex(Number(chainIdString))); + return toEvmCaipChainId(numberToHex(Number(chainId))); }; -// Converts a chainId to a decimal number that can be used for bridge-api requests +/** + * Converts a chainId to a decimal number that can be used for bridge-api requests + * + * @param chainId - The chainId to convert + * @returns The decimal number + */ export const formatChainIdToDec = ( chainId: number | Hex | CaipChainId | string, ) => { @@ -59,8 +61,13 @@ export const formatChainIdToDec = ( 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 +/** + * Converts a chainId to a hex string used to read controller data within the app + * Hex chainIds are also used for fetching exchange rates + * + * @param chainId - The chainId to convert + * @returns The hex string + */ export const formatChainIdToHex = ( chainId: Hex | CaipChainId | string | number, ) => { @@ -81,7 +88,12 @@ export const formatChainIdToHex = ( 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 +/** + * Converts an asset or account address to a string that can be used for bridge-api requests + * + * @param address - The address to convert + * @returns The converted address + */ export const formatAddressToString = (address: string) => { if (isStrictHexString(address)) { return getAddress(address); @@ -100,8 +112,16 @@ export const formatAddressToString = (address: string) => { return addressWithoutPrefix; }; -export const formatChainIdToHexOrCaip = (chainId: number) => { - if (chainId === ChainId.SOLANA) { +/** + * Converts a chainId to a hex string or CaipChainId + * + * @param chainId - The chainId to convert + * @returns The hex string or CaipChainId + */ +export const formatChainIdToHexOrCaip = ( + chainId: number | Hex | CaipChainId, +) => { + if (isSolanaChainId(chainId)) { return SolScope.Mainnet; } return formatChainIdToHex(chainId); diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index f0d0c64c086..c252b44aaaa 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -153,6 +153,7 @@ export async function fetchBridgeQuotes( fetchFn: FetchFunction, bridgeApiBaseUrl: string, ): Promise { + // Transform the generic quote request into QuoteRequest const normalizedRequest: QuoteRequest = { walletAddress: formatAddressToString(request.walletAddress), destWalletAddress: formatAddressToString( @@ -175,7 +176,6 @@ export async function fetchBridgeQuotes( queryParams.append(key, value.toString()); }); const url = `${bridgeApiBaseUrl}/getQuote?${queryParams}`; - const quotes: unknown[] = await fetchFn(url, { headers: getClientIdHeader(clientId), signal, From e39a23745d780a84f44f2037ae709fb5efda2164 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 14 Mar 2025 16:33:27 -0700 Subject: [PATCH 03/47] chore: update comments and rm hardcoded values --- .../bridge-controller/src/constants/bridge.ts | 20 +------------------ .../bridge-controller/src/constants/tokens.ts | 7 ++++--- packages/bridge-controller/src/types.ts | 6 +++--- .../bridge-controller/src/utils/bridge.ts | 11 ++++++---- packages/bridge-controller/src/utils/fetch.ts | 7 +++---- 5 files changed, 18 insertions(+), 33 deletions(-) diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index 9873a366466..ddf471b0b16 100644 --- a/packages/bridge-controller/src/constants/bridge.ts +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -1,7 +1,5 @@ import { AddressZero } from '@ethersproject/constants'; -import type { MultichainNetworkConfiguration } from '@metamask/multichain-network-controller'; -import { getDefaultMultichainNetworkControllerState } from '@metamask/multichain-network-controller'; -import type { CaipChainId, Hex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; import { CHAIN_IDS } from './chains'; import type { BridgeControllerState } from '../types'; @@ -71,19 +69,3 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState['bridgeState export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, }; - -export const MULTICHAIN_ID_TO_NATIVE_ASSET_MAP: Record = - Object.entries( - getDefaultMultichainNetworkControllerState() - .multichainNetworkConfigurationsByChainId, - ).reduce( - (acc, [chainId, config]) => ({ ...acc, [chainId]: config.nativeCurrency }), - {}, - ); - -export const MULTICHAIN_NETWORK_CONFIGURATIONS: Record< - CaipChainId, - MultichainNetworkConfiguration -> = - getDefaultMultichainNetworkControllerState() - .multichainNetworkConfigurationsByChainId; diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index 72ed306fe01..bad65f066e2 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,6 +1,5 @@ import { SolScope } from '@metamask/keyring-api'; -import { MULTICHAIN_ID_TO_NATIVE_ASSET_MAP } from './bridge'; import { CHAIN_IDS } from './chains'; export type SwapsTokenObject = { @@ -27,6 +26,7 @@ export type SwapsTokenObject = { }; const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; +export const DEFAULT_SOLANA_TOKEN_ADDRESS = `${SolScope.Mainnet}/slip44:501`; export const CURRENCY_SYMBOLS = { ARBITRUM: 'ETH', @@ -51,6 +51,7 @@ export const CURRENCY_SYMBOLS = { GLIMMER: 'GLMR', MOONRIVER: 'MOVR', ONE: 'ONE', + SOL: 'SOL', } as const; export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { @@ -130,9 +131,9 @@ export const BASE_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { } as const; const SOLANA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { - symbol: 'SOL', + symbol: CURRENCY_SYMBOLS.SOL, name: 'Solana', - address: MULTICHAIN_ID_TO_NATIVE_ASSET_MAP[SolScope.Mainnet], + address: DEFAULT_SOLANA_TOKEN_ADDRESS, decimals: 9, iconUrl: '', }; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 8500451efd6..a1e927f9c57 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -21,7 +21,7 @@ import type { BridgeController } from './bridge-controller'; import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge'; /** - * The extension's fetch function accepts additional options + * Additional options accepted by the extension's fetchWithCache function */ type FetchWithCacheOptions = { cacheOptions?: { @@ -134,8 +134,8 @@ export type BridgeAsset = { /** * This is the interface for the quote request sent to the bridge-api - * and should only be used by the fetchBridgeQuotes function - * Components and redux store should use the GenericQuoteRequest type + * and should only be used by the fetchBridgeQuotes utility function + * Components and redux stores should use the {@link GenericQuoteRequest} type */ export type QuoteRequest< ChainIdType = ChainId | number, diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index aa68a5e2f61..0c1c3a2a82f 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -10,7 +10,10 @@ import { METABRIDGE_ETHEREUM_ADDRESS, } from '../constants/bridge'; import { CHAIN_IDS } from '../constants/chains'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; +import { + DEFAULT_SOLANA_TOKEN_ADDRESS, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, +} from '../constants/tokens'; import { ChainId } from '../types'; /** @@ -85,16 +88,16 @@ export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { }; /** - * Checks whether the address is a native asset in any supported xchain swapsnetwork + * Checks whether the address is a native asset in any supported xchain swaps network * * @param address - The address to check * @returns Whether the address is a native asset */ export const isNativeAddress = (address?: string | null) => address === AddressZero || // bridge and swap apis set the native asset address to zero - address === '' || // assets controllers set the native asset address to empty string + address === '' || // assets controllers set the native asset address to an empty string !address || - [`${SolScope.Mainnet}/slip44:501`].some( + [DEFAULT_SOLANA_TOKEN_ADDRESS].some( (assetId) => assetId.includes(address) && !isStrictHexString(address), ); // multichain native assets are represented in caip format diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index c252b44aaaa..49dd720b72c 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -137,7 +137,7 @@ export async function fetchBridgeTokens( /** * Converts the generic quote request to the type that the bridge-api expects - * and fetches quotes from the bridge-api + * then fetches quotes from the bridge-api * * @param request - The quote request * @param signal - The abort signal @@ -153,12 +153,11 @@ export async function fetchBridgeQuotes( fetchFn: FetchFunction, bridgeApiBaseUrl: string, ): Promise { + const destWalletAddress = request.destWalletAddress ?? request.walletAddress; // Transform the generic quote request into QuoteRequest const normalizedRequest: QuoteRequest = { walletAddress: formatAddressToString(request.walletAddress), - destWalletAddress: formatAddressToString( - request.destWalletAddress ?? request.walletAddress, - ), + destWalletAddress: formatAddressToString(destWalletAddress), srcChainId: formatChainIdToDec(request.srcChainId), destChainId: formatChainIdToDec(request.destChainId), srcTokenAddress: formatAddressToString(request.srcTokenAddress), From 959b593c8b291afbac0a229e535e315d2a57be25 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Fri, 14 Mar 2025 16:58:41 -0700 Subject: [PATCH 04/47] chore: move tenderly insufficientBal param to controller --- packages/bridge-controller/src/bridge-controller.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 906f5764cea..956333d549f 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -167,14 +167,20 @@ export class BridgeController extends StaticIntervalPollingController Date: Mon, 17 Mar 2025 11:39:07 -0700 Subject: [PATCH 05/47] chore: revert BridgeControllerState change --- .../src/bridge-controller.ts | 56 +++++++++++++++---- .../bridge-controller/src/constants/bridge.ts | 31 +++++----- packages/bridge-controller/src/types.ts | 4 +- .../bridge-controller/src/utils/bridge.ts | 6 ++ 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 956333d549f..2c7317eff7b 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -33,7 +33,11 @@ import { RequestStatus, } from './types'; import { hasSufficientBalance } from './utils/balance'; -import { isSolanaChainId, sumHexes } from './utils/bridge'; +import { + getDefaultBridgeControllerState, + isSolanaChainId, + sumHexes, +} from './utils/bridge'; import { formatAddressToString, formatChainIdToCaip, @@ -43,7 +47,35 @@ import { fetchBridgeFeatureFlags, fetchBridgeQuotes } from './utils/fetch'; import { isValidQuoteRequest } from './utils/quote'; const metadata: StateMetadata = { - bridgeState: { + bridgeFeatureFlags: { + persist: false, + anonymous: false, + }, + quoteRequest: { + persist: false, + anonymous: false, + }, + quotes: { + persist: false, + anonymous: false, + }, + quotesInitialLoadTime: { + persist: false, + anonymous: false, + }, + quotesLastFetched: { + persist: false, + anonymous: false, + }, + quotesLoadingStatus: { + persist: false, + anonymous: false, + }, + quoteFetchError: { + persist: false, + anonymous: false, + }, + quotesRefreshCount: { persist: false, anonymous: false, }, @@ -104,7 +136,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update((state) => { state.quoteRequest = updatedQuoteRequest; state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes; state.quotesLastFetched = @@ -227,7 +259,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update((state) => { // Cannot do direct assignment to state, i.e. state = {... }, need to manually assign each field state.quoteRequest = DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest; state.quotesInitialLoadTime = @@ -253,7 +285,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update((state) => { state.bridgeFeatureFlags = bridgeFeatureFlags; }); this.#setIntervalLength(); @@ -263,7 +295,7 @@ export class BridgeController extends StaticIntervalPollingController { - const { bridgeState: state } = this.state; + const { state } = this; const { srcChainId } = state.quoteRequest; const refreshRateOverride = srcChainId ? state.bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].chains[ @@ -281,11 +313,11 @@ export class BridgeController extends StaticIntervalPollingController { const { bridgeFeatureFlags, quotesInitialLoadTime, quotesRefreshCount } = - this.state.bridgeState; + this.state; this.#abortController?.abort('New quote request'); this.#abortController = new AbortController(); - this.update(({ bridgeState: state }) => { + this.update((state) => { state.quotesLoadingStatus = RequestStatus.LOADING; state.quoteRequest = updatedQuoteRequest; state.quoteFetchError = DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError; @@ -307,7 +339,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update((state) => { state.quotes = quotesWithL1GasFees ?? quotesWithSolanaFees ?? quotes; state.quotesLoadingStatus = RequestStatus.FETCHED; }); @@ -318,7 +350,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update((state) => { state.quoteFetchError = error instanceof Error ? error.message : 'Unknown error'; state.quotesLoadingStatus = RequestStatus.ERROR; @@ -341,7 +373,7 @@ export class BridgeController extends StaticIntervalPollingController { + this.update((state) => { state.quotesInitialLoadTime = updatedQuotesRefreshCount === 1 && this.#quotesFirstFetched ? quotesLastFetched - this.#quotesFirstFetched diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index ddf471b0b16..547a6d50c0e 100644 --- a/packages/bridge-controller/src/constants/bridge.ts +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -49,22 +49,21 @@ export const DEFAULT_FEATURE_FLAG_CONFIG = { chains: {}, }; -export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState['bridgeState'] = - { - bridgeFeatureFlags: { - [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: DEFAULT_FEATURE_FLAG_CONFIG, - [BridgeFeatureFlagsKey.MOBILE_CONFIG]: DEFAULT_FEATURE_FLAG_CONFIG, - }, - quoteRequest: { - srcTokenAddress: AddressZero, - }, - quotesInitialLoadTime: null, - quotes: [], - quotesLastFetched: null, - quotesLoadingStatus: null, - quoteFetchError: null, - quotesRefreshCount: 0, - }; +export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = { + bridgeFeatureFlags: { + [BridgeFeatureFlagsKey.EXTENSION_CONFIG]: DEFAULT_FEATURE_FLAG_CONFIG, + [BridgeFeatureFlagsKey.MOBILE_CONFIG]: DEFAULT_FEATURE_FLAG_CONFIG, + }, + quoteRequest: { + srcTokenAddress: AddressZero, + }, + quotesInitialLoadTime: null, + quotes: [], + quotesLastFetched: null, + quotesLoadingStatus: null, + quoteFetchError: null, + quotesRefreshCount: 0, +}; export const METABRIDGE_CHAIN_TO_ADDRESS_MAP: Record = { [CHAIN_IDS.MAINNET]: METABRIDGE_ETHEREUM_ADDRESS, diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index a1e927f9c57..4f9493fa218 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -280,7 +280,7 @@ export enum BridgeBackgroundAction { GET_BRIDGE_ERC20_ALLOWANCE = 'getBridgeERC20Allowance', } -type BridgeState = { +export type BridgeControllerState = { bridgeFeatureFlags: BridgeFeatureFlags; quoteRequest: Partial; quotes: (QuoteResponse & L1GasFees & SolanaFees)[]; @@ -291,8 +291,6 @@ type BridgeState = { quotesRefreshCount: number; }; -export type BridgeControllerState = { bridgeState: BridgeState }; - export type BridgeControllerAction< FunctionName extends keyof BridgeController, > = { diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 0c1c3a2a82f..3ca657d1cdf 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -6,6 +6,7 @@ import type { CaipChainId } from '@metamask/utils'; import { isCaipChainId, isStrictHexString, type Hex } from '@metamask/utils'; import { + DEFAULT_BRIDGE_CONTROLLER_STATE, ETH_USDT_ADDRESS, METABRIDGE_ETHEREUM_ADDRESS, } from '../constants/bridge'; @@ -14,8 +15,13 @@ import { DEFAULT_SOLANA_TOKEN_ADDRESS, SWAPS_CHAINID_DEFAULT_TOKEN_MAP, } from '../constants/tokens'; +import type { BridgeControllerState } from '../types'; import { ChainId } from '../types'; +export const getDefaultBridgeControllerState = (): BridgeControllerState => { + return DEFAULT_BRIDGE_CONTROLLER_STATE; +}; + /** * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum * From 35f0c2031d1b9fb04dd9ae763296268ca7a2bf75 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 15:16:04 -0700 Subject: [PATCH 06/47] chore: export more constants, utils and types --- packages/bridge-controller/src/index.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 35424b68aa3..375bea60dc1 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -3,6 +3,7 @@ export { BridgeController } from './bridge-controller'; export type { ChainConfiguration, L1GasFees, + SolanaFees, QuoteMetadata, BridgeToken, GasMultiplierByChainId, @@ -10,6 +11,7 @@ export type { BridgeAsset, GenericQuoteRequest, Protocol, + TokenAmountValues, Step, RefuelData, Quote, @@ -40,6 +42,7 @@ export { export { ALLOWED_BRIDGE_CHAIN_IDS, BridgeClientId, + BRIDGE_CONTROLLER_NAME, BRIDGE_QUOTE_MAX_ETA_SECONDS, BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE, BRIDGE_PREFERRED_GAS_ESTIMATE, @@ -55,7 +58,10 @@ export { export type { AllowedBridgeChainIds } from './constants/bridge'; -export type { SwapsTokenObject } from './constants/tokens'; +export { + type SwapsTokenObject, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, +} from './constants/tokens'; export { SWAPS_API_V2_BASE_URL } from './constants/swaps'; @@ -67,3 +73,9 @@ export { } from './utils/bridge'; export { isValidQuoteRequest } from './utils/quote'; + +export { calcLatestSrcBalance } from './utils/balance'; + +export { fetchBridgeTokens } from './utils/fetch'; + +export * from './utils/caip-formatters'; From 5f6df52f72755c966631b1d41aba255b51a3fb35 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Mon, 17 Mar 2025 15:16:21 -0700 Subject: [PATCH 07/47] fix: type definitions --- .../bridge-controller/src/constants/tokens.ts | 4 +++ packages/bridge-controller/src/types.ts | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index bad65f066e2..fc497a707c5 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -23,6 +23,10 @@ export type SwapsTokenObject = { * URL for token icon */ iconUrl: string; + /** + * The CAIP asset ID of the token + */ + assetId?: string; }; const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 4f9493fa218..5d3592045b3 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -69,18 +69,26 @@ export type SolanaFees = { }; /** - * 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; +}; + +/** + * Values derived from the quote response + */ export type QuoteMetadata = { - gasFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; - totalNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // estimatedGasFees + relayerFees - totalMaxNetworkFee: { amount: BigNumber; valueInCurrency: BigNumber | null }; // maxGasFees + relayerFees - toTokenAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; - adjustedReturn: { valueInCurrency: BigNumber | null }; // destTokenAmount - totalNetworkFee - sentAmount: { amount: BigNumber; valueInCurrency: BigNumber | null }; // srcTokenAmount + metabridgeFee + gasFee: TokenAmountValues; + totalNetworkFee: TokenAmountValues; // estimatedGasFees + relayerFees + totalMaxNetworkFee: TokenAmountValues; // maxGasFees + relayerFees + toTokenAmount: TokenAmountValues; + adjustedReturn: Omit; // destTokenAmount - totalNetworkFee + sentAmount: TokenAmountValues; // srcTokenAmount + metabridgeFee swapRate: BigNumber; // destTokenAmount / sentAmount - cost: { valueInCurrency: BigNumber | null }; // sentAmount - adjustedReturn + cost: Omit; // sentAmount - adjustedReturn }; /** @@ -92,7 +100,6 @@ export enum SortOrder { } export type BridgeToken = { - type: AssetType.native | AssetType.token; address: string; symbol: string; image: string; @@ -129,6 +136,7 @@ export type BridgeAsset = { name: string; decimals: number; icon?: string; + iconUrl: string; assetId: string; }; From 42b9e6cf39f573aaad2e107212337a7dcf5f41fa Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 10:44:21 -0700 Subject: [PATCH 08/47] chore: return native and solana tokens in fetchBridgeTokens --- packages/bridge-controller/src/types.ts | 29 +++++++++------ .../bridge-controller/src/utils/bridge.ts | 11 ++++-- packages/bridge-controller/src/utils/fetch.ts | 36 +++++-------------- .../bridge-controller/src/utils/validators.ts | 36 ++++++++----------- 4 files changed, 50 insertions(+), 62 deletions(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 5d3592045b3..a877f553074 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -99,6 +99,25 @@ export enum SortOrder { ETA_ASC = 'time_descending', } +/** + * This is the interface for the asset object returned by the bridge-api + * This type is used in the QuoteResponse and in the fetchBridgeTokens response + */ +export type BridgeAsset = { + chainId: ChainId; + address: string; + symbol: string; + name: string; + decimals: number; + icon?: string; + iconUrl: string; + assetId: string; +}; + +/** + * This is the interface for the token object used in the extension client + * In addition to the {@link BridgeAsset} fields, it includes balance information + */ export type BridgeToken = { address: string; symbol: string; @@ -129,16 +148,6 @@ export type FeatureFlagResponse = { [BridgeFlag.MOBILE_CONFIG]: FeatureFlagResponsePlatformConfig; }; -export type BridgeAsset = { - chainId: ChainId; - address: string; - symbol: string; - name: string; - decimals: number; - icon?: string; - iconUrl: string; - assetId: string; -}; /** * This is the interface for the quote request sent to the bridge-api diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 3ca657d1cdf..0e75bc4232a 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -59,7 +59,10 @@ export const sumHexes = (...hexStrings: string[]): Hex => { * @param chainId - The hex encoded chain ID of the default swaps token to check * @returns Whether the address is the provided chain's default token address */ -export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { +export const isSwapsDefaultTokenAddress = ( + address: string, + chainId: Hex | CaipChainId, +) => { if (!address || !chainId) { return false; } @@ -80,7 +83,10 @@ export const isSwapsDefaultTokenAddress = (address: string, chainId: Hex) => { * @param chainId - The hex encoded chain ID of the default swaps token to check * @returns Whether the symbol is the provided chain's default token symbol */ -export const isSwapsDefaultTokenSymbol = (symbol: string, chainId: Hex) => { +export const isSwapsDefaultTokenSymbol = ( + symbol: string, + chainId: Hex | CaipChainId, +) => { if (!symbol || !chainId) { return false; } @@ -103,6 +109,7 @@ export const isNativeAddress = (address?: string | null) => address === AddressZero || // bridge and swap apis set the native asset address to zero address === '' || // assets controllers set the native asset address to an empty string !address || + address.endsWith('11111111111111111111111111111111') || [DEFAULT_SOLANA_TOKEN_ADDRESS].some( (assetId) => assetId.includes(address) && !isStrictHexString(address), ); // multichain native assets are represented in caip format diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index 49dd720b72c..cb524dbc39e 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -1,10 +1,6 @@ -import type { Hex } from '@metamask/utils'; -import { Duration, hexToNumber } from '@metamask/utils'; +import type { CaipChainId, Hex } from '@metamask/utils'; +import { Duration } from '@metamask/utils'; -import { - isSwapsDefaultTokenAddress, - isSwapsDefaultTokenSymbol, -} from './bridge'; import { formatAddressToString, formatChainIdToCaip, @@ -16,8 +12,6 @@ import { validateSwapsTokenObject, } from './validators'; import { DEFAULT_FEATURE_FLAG_CONFIG } from '../constants/bridge'; -import type { SwapsTokenObject } from '../constants/tokens'; -import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/tokens'; import type { QuoteResponse, BridgeFeatureFlags, @@ -25,6 +19,7 @@ import type { ChainConfiguration, GenericQuoteRequest, QuoteRequest, + BridgeAsset, } from '../types'; import { BridgeFlag, BridgeFeatureFlagsKey } from '../types'; @@ -94,13 +89,13 @@ export async function fetchBridgeFeatureFlags( * @returns A list of enabled (unblocked) tokens */ export async function fetchBridgeTokens( - chainId: Hex, + chainId: Hex | CaipChainId, clientId: string, fetchFn: FetchFunction, bridgeApiBaseUrl: string, -): Promise> { +): Promise> { // TODO make token api v2 call - const url = `${bridgeApiBaseUrl}/getTokens?chainId=${hexToNumber(chainId)}`; + const url = `${bridgeApiBaseUrl}/getTokens?chainId=${formatChainIdToDec(chainId)}`; // TODO we will need to cache these. In Extension fetchWithCache is used. This is due to the following: // If we allow selecting dest networks which the user has not imported, @@ -111,24 +106,9 @@ export async function fetchBridgeTokens( 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; - } - + const transformedTokens: Record = {}; tokens.forEach((token: unknown) => { - if ( - validateSwapsTokenObject(token) && - !( - isSwapsDefaultTokenSymbol(token.symbol, chainId) || - isSwapsDefaultTokenAddress(token.address, chainId) - ) - ) { + if (validateSwapsTokenObject(token)) { transformedTokens[token.address] = token; } }); diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 73891a41070..9de4474d922 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -11,13 +11,11 @@ import { optional, enums, define, - size, union, } from '@metamask/superstruct'; import { isStrictHexString } from '@metamask/utils'; -import type { SwapsTokenObject } from '../constants/tokens'; -import type { FeatureFlagResponse, QuoteResponse } from '../types'; +import type { BridgeAsset, FeatureFlagResponse, QuoteResponse } from '../types'; import { ActionTypes, BridgeFlag, FeeType } from '../types'; const HexAddressSchema = define('HexAddress', (v: unknown) => @@ -35,10 +33,16 @@ const TruthyDigitStringSchema = define( truthyString(v as string) && Boolean((v as string).match(/^\d+$/u)), ); -const SwapsTokenObjectSchema = type({ - decimals: number(), +const ChainIdSchema = number(); + +const BridgeAssetSchema = type({ + chainId: ChainIdSchema, address: string(), - symbol: size(string(), 1, 12), + assetId: string(), + symbol: string(), + name: string(), + decimals: number(), + icon: optional(string()), }); export const validateFeatureFlagsResponse = ( @@ -67,23 +71,11 @@ export const validateFeatureFlagsResponse = ( export const validateSwapsTokenObject = ( data: unknown, -): data is SwapsTokenObject => { - return is(data, SwapsTokenObjectSchema); +): data is BridgeAsset => { + return is(data, BridgeAssetSchema); }; export const validateQuoteResponse = (data: unknown): data is QuoteResponse => { - const ChainIdSchema = number(); - - const BridgeAssetSchema = type({ - chainId: ChainIdSchema, - address: string(), - assetId: string(), - symbol: string(), - name: string(), - decimals: number(), - icon: optional(string()), - }); - const FeeDataSchema = type({ amount: TruthyDigitStringSchema, asset: BridgeAssetSchema, @@ -111,10 +103,10 @@ export const validateQuoteResponse = (data: unknown): data is QuoteResponse => { const QuoteSchema = type({ requestId: string(), srcChainId: ChainIdSchema, - srcAsset: SwapsTokenObjectSchema, + srcAsset: BridgeAssetSchema, srcTokenAmount: string(), destChainId: ChainIdSchema, - destAsset: SwapsTokenObjectSchema, + destAsset: BridgeAssetSchema, destTokenAmount: string(), feeData: record(enums(Object.values(FeeType)), FeeDataSchema), bridgeId: string(), From 79ae2cef4e113b838a4e1a783f41d6bf900d2fe4 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 11:30:29 -0700 Subject: [PATCH 09/47] chore: add comments and rm assetId from SwapsTokenObject --- packages/bridge-controller/src/constants/tokens.ts | 4 ---- packages/bridge-controller/src/utils/bridge.ts | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index fc497a707c5..bad65f066e2 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -23,10 +23,6 @@ export type SwapsTokenObject = { * URL for token icon */ iconUrl: string; - /** - * The CAIP asset ID of the token - */ - assetId?: string; }; const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 0e75bc4232a..07b302f6583 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -109,10 +109,10 @@ export const isNativeAddress = (address?: string | null) => address === AddressZero || // bridge and swap apis set the native asset address to zero address === '' || // assets controllers set the native asset address to an empty string !address || - address.endsWith('11111111111111111111111111111111') || + address.endsWith('11111111111111111111111111111111') || // token-api and bridge-api use this as the solana native assetId [DEFAULT_SOLANA_TOKEN_ADDRESS].some( (assetId) => assetId.includes(address) && !isStrictHexString(address), - ); // multichain native assets are represented in caip format + ); // solana native assetId used in the extension client /** * Checks whether the chainId matches Solana in CaipChainId or number format From f9bbc6eb727e60d3982549a6dac249025f9a4257 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 11:34:17 -0700 Subject: [PATCH 10/47] fix: lint issue --- packages/bridge-controller/src/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index a877f553074..ab94eb14914 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -148,7 +148,6 @@ export type FeatureFlagResponse = { [BridgeFlag.MOBILE_CONFIG]: FeatureFlagResponsePlatformConfig; }; - /** * This is the interface for the quote request sent to the bridge-api * and should only be used by the fetchBridgeQuotes utility function From 17d07186ee3fe08a48ad456006b6d1deb1327d3c Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 13:36:35 -0700 Subject: [PATCH 11/47] fix: build --- package.json | 1 - packages/bridge-controller/package.json | 14 +-- yarn.lock | 155 +++--------------------- 3 files changed, 20 insertions(+), 150 deletions(-) diff --git a/package.json b/package.json index 15f5a8bd014..99adddd6cf8 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "pre-push": "yarn lint" }, "resolutions": { - "@metamask/snaps-sdk": "^6.19.0", "elliptic@6.5.4": "^6.5.7", "fast-xml-parser@^4.3.4": "^4.4.1", "ws@7.4.6": "^7.5.10" diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 1c8ea8595d1..bfd277bfe9c 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -54,21 +54,16 @@ "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^8.0.0", "@metamask/controller-utils": "^11.6.0", - "@metamask/keyring-api": "^17.2.0", "@metamask/metamask-eth-abis": "^3.1.1", - "@metamask/multichain-network-controller": "^0.1.0", "@metamask/polling-controller": "^12.0.3", - "@metamask/snaps-controllers": "^10.0.1", - "@metamask/snaps-utils": "^9.0.1", "@metamask/utils": "^11.2.0" }, "devDependencies": { - "@metamask/accounts-controller": "^26.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", - "@metamask/network-controller": "^22.2.1", + "@metamask/keyring-api": "^17.2.0", + "@metamask/snaps-controllers": "^9.19.0", "@metamask/superstruct": "^3.1.0", - "@metamask/transaction-controller": "^50.0.0", "@ts-bridge/cli": "^0.6.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", @@ -83,8 +78,11 @@ }, "peerDependencies": { "@metamask/accounts-controller": "^26.0.0", + "@metamask/multichain-network-controller": "^0.1.0", "@metamask/network-controller": "^22.0.0", - "@metamask/transaction-controller": "^50.0.0" + "@metamask/snaps-controllers": "^9.19.0", + "@metamask/snaps-utils": "^8.10.0", + "@metamask/transaction-controller": "^49.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/yarn.lock b/yarn.lock index 1abe5d59c2b..0edbdb950df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,20 +2679,15 @@ __metadata: "@ethersproject/constants": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^26.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.6.0" "@metamask/eth-json-rpc-provider": "npm:^4.1.8" "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/multichain-network-controller": "npm:^0.1.0" - "@metamask/network-controller": "npm:^22.2.1" "@metamask/polling-controller": "npm:^12.0.3" - "@metamask/snaps-controllers": "npm:^10.0.1" - "@metamask/snaps-utils": "npm:^9.0.1" + "@metamask/snaps-controllers": "npm:^9.19.0" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/transaction-controller": "npm:^50.0.0" "@metamask/utils": "npm:^11.2.0" "@ts-bridge/cli": "npm:^0.6.3" "@types/jest": "npm:^27.4.1" @@ -2707,8 +2702,11 @@ __metadata: typescript: "npm:~5.2.2" peerDependencies: "@metamask/accounts-controller": ^26.0.0 + "@metamask/multichain-network-controller": ^0.1.0 "@metamask/network-controller": ^22.0.0 - "@metamask/transaction-controller": ^50.0.0 + "@metamask/snaps-controllers": ^9.19.0 + "@metamask/snaps-utils": ^8.10.0 + "@metamask/transaction-controller": ^49.0.0 languageName: unknown linkType: soft @@ -3418,7 +3416,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-middleware-stream@npm:^8.0.6, @metamask/json-rpc-middleware-stream@npm:^8.0.7, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": +"@metamask/json-rpc-middleware-stream@npm:^8.0.6, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": version: 0.0.0-use.local resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" dependencies: @@ -3653,21 +3651,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-network-controller@npm:^0.1.0": - 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.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/4f5423a6de319db540a13e5dc5d637b5e55cf7af901cbe040353912c4aaba640a9454abcecb93fa53ca397f5c40d097835b001f40963f8624c02961dbf29931a - languageName: node - linkType: hard - "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" @@ -3944,7 +3927,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/phishing-controller@npm:^12.3.1, @metamask/phishing-controller@npm:^12.4.0, @metamask/phishing-controller@workspace:packages/phishing-controller": +"@metamask/phishing-controller@npm:^12.3.1, @metamask/phishing-controller@workspace:packages/phishing-controller": version: 0.0.0-use.local resolution: "@metamask/phishing-controller@workspace:packages/phishing-controller" dependencies: @@ -4067,7 +4050,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/providers@npm:^18.1.1": +"@metamask/providers@npm:^18.1.1, @metamask/providers@npm:^18.3.1": version: 18.3.1 resolution: "@metamask/providers@npm:18.3.1" dependencies: @@ -4088,27 +4071,6 @@ __metadata: languageName: node linkType: hard -"@metamask/providers@npm:^20.0.0": - version: 20.0.0 - resolution: "@metamask/providers@npm:20.0.0" - dependencies: - "@metamask/json-rpc-engine": "npm:^10.0.2" - "@metamask/json-rpc-middleware-stream": "npm:^8.0.6" - "@metamask/object-multiplex": "npm:^2.0.0" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/safe-event-emitter": "npm:^3.1.1" - "@metamask/utils": "npm:^11.0.1" - detect-browser: "npm:^5.2.0" - extension-port-stream: "npm:^4.1.0" - fast-deep-equal: "npm:^3.1.3" - is-stream: "npm:^2.0.0" - readable-stream: "npm:^3.6.2" - peerDependencies: - webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/b958d03a9380d86e605db239109a3debcc1ffde90371abe5beb82a5bed46c7718303a2bb92ec269eae16eff145b9ebbfcb3445a2b6bad4f297a590ee725a5bad - languageName: node - linkType: hard - "@metamask/queued-request-controller@workspace:packages/queued-request-controller": version: 0.0.0-use.local resolution: "@metamask/queued-request-controller@workspace:packages/queued-request-controller" @@ -4295,47 +4257,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:^9.19.0": version: 9.19.1 resolution: "@metamask/snaps-controllers@npm:9.19.1" @@ -4406,33 +4327,16 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-rpc-methods@npm:^11.13.0": - version: 11.13.1 - resolution: "@metamask/snaps-rpc-methods@npm:11.13.1" - dependencies: - "@metamask/key-tree": "npm:^10.0.2" - "@metamask/permission-controller": "npm:^11.0.6" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/snaps-sdk": "npm:^6.19.0" - "@metamask/snaps-utils": "npm:^9.0.1" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.2.0" - "@noble/hashes": "npm:^1.3.1" - luxon: "npm:^3.5.0" - checksum: 10/72298b0cb5cf79ffaacb48368590513ec5befa10f9ffb84865ce1eab5dc2ff28f5b0e765a7fb55885e8e0583091af404e879fad8510e9c8cb49ee455cf7b9444 - languageName: node - linkType: hard - -"@metamask/snaps-sdk@npm:^6.19.0": - version: 6.19.0 - resolution: "@metamask/snaps-sdk@npm:6.19.0" +"@metamask/snaps-sdk@npm:^6.17.0, @metamask/snaps-sdk@npm:^6.17.1": + version: 6.17.1 + resolution: "@metamask/snaps-sdk@npm:6.17.1" dependencies: "@metamask/key-tree": "npm:^10.0.2" - "@metamask/providers": "npm:^20.0.0" + "@metamask/providers": "npm:^18.3.1" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.2.0" - checksum: 10/585f56b11e82e835ce104b9c719326c0f009cb87a76dd7c49eb324c7175a3afea0a08d5e8a0bd66bbcd9b25c90a8af6bf15342b830898a736d7b94fa951bd8ff + "@metamask/utils": "npm:^11.0.1" + checksum: 10/05c5170c6250115535bc6d06a417157bb55005dd6fe86e768d70fabfba610ec8114cf45a8a5aad1219b1cfb0bcf5e080974735a0ac9a8c8bd0ac102f5c3cf42f languageName: node linkType: hard @@ -4467,37 +4371,6 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-utils@npm:^9.0.1": - version: 9.0.1 - resolution: "@metamask/snaps-utils@npm:9.0.1" - dependencies: - "@babel/core": "npm:^7.23.2" - "@babel/types": "npm:^7.23.0" - "@metamask/base-controller": "npm:^8.0.0" - "@metamask/key-tree": "npm:^10.0.2" - "@metamask/permission-controller": "npm:^11.0.6" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/slip44": "npm:^4.1.0" - "@metamask/snaps-registry": "npm:^3.2.3" - "@metamask/snaps-sdk": "npm:^6.19.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.2.0" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.1" - chalk: "npm:^4.1.2" - cron-parser: "npm:^4.5.0" - fast-deep-equal: "npm:^3.1.3" - fast-json-stable-stringify: "npm:^2.1.0" - fast-xml-parser: "npm:^4.4.1" - marked: "npm:^12.0.1" - rfdc: "npm:^1.3.0" - semver: "npm:^7.5.4" - ses: "npm:^1.1.0" - validate-npm-package-name: "npm:^5.0.0" - checksum: 10/3031ebda558b3fd78636806a34dbe03b0ff6f0cbbf16fec9b9cb64eba4eb00bd035cea3a07fbc55fe17b75b4f02a6b376918922011df599b94cfc1dea5a9aced - languageName: node - linkType: hard - "@metamask/stake-sdk@npm:^1.0.0": version: 1.0.0 resolution: "@metamask/stake-sdk@npm:1.0.0" From accbf8f17dd8b944d54b2aa1550c7919eb5c898f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 13:51:08 -0700 Subject: [PATCH 12/47] fix: package constraints --- packages/bridge-controller/package.json | 11 ++++++--- yarn.lock | 30 ++++++++----------------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index bfd277bfe9c..146f02d4089 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -59,12 +59,17 @@ "@metamask/utils": "^11.2.0" }, "devDependencies": { + "@metamask/accounts-controller": "^26.1.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/keyring-api": "^17.2.0", + "@metamask/multichain-network-controller": "^0.2.0", + "@metamask/network-controller": "^22.2.1", "@metamask/snaps-controllers": "^9.19.0", + "@metamask/snaps-utils": "^8.10.0", "@metamask/superstruct": "^3.1.0", - "@ts-bridge/cli": "^0.6.3", + "@metamask/transaction-controller": "^50.0.0", + "@ts-bridge/cli": "^0.6.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -78,11 +83,11 @@ }, "peerDependencies": { "@metamask/accounts-controller": "^26.0.0", - "@metamask/multichain-network-controller": "^0.1.0", + "@metamask/multichain-network-controller": "^0.0.0", "@metamask/network-controller": "^22.0.0", "@metamask/snaps-controllers": "^9.19.0", "@metamask/snaps-utils": "^8.10.0", - "@metamask/transaction-controller": "^49.0.0" + "@metamask/transaction-controller": "^50.0.0" }, "engines": { "node": "^18.18 || >=20" diff --git a/yarn.lock b/yarn.lock index 0edbdb950df..9f2175d0bf6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,17 +2679,22 @@ __metadata: "@ethersproject/constants": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.0" + "@metamask/accounts-controller": "npm:^26.1.0" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.0.0" "@metamask/controller-utils": "npm:^11.6.0" "@metamask/eth-json-rpc-provider": "npm:^4.1.8" "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/multichain-network-controller": "npm:^0.2.0" + "@metamask/network-controller": "npm:^22.2.1" "@metamask/polling-controller": "npm:^12.0.3" "@metamask/snaps-controllers": "npm:^9.19.0" + "@metamask/snaps-utils": "npm:^8.10.0" "@metamask/superstruct": "npm:^3.1.0" + "@metamask/transaction-controller": "npm:^50.0.0" "@metamask/utils": "npm:^11.2.0" - "@ts-bridge/cli": "npm:^0.6.3" + "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -2702,11 +2707,11 @@ __metadata: typescript: "npm:~5.2.2" peerDependencies: "@metamask/accounts-controller": ^26.0.0 - "@metamask/multichain-network-controller": ^0.1.0 + "@metamask/multichain-network-controller": ^0.0.0 "@metamask/network-controller": ^22.0.0 "@metamask/snaps-controllers": ^9.19.0 "@metamask/snaps-utils": ^8.10.0 - "@metamask/transaction-controller": ^49.0.0 + "@metamask/transaction-controller": ^50.0.0 languageName: unknown linkType: soft @@ -3651,7 +3656,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": +"@metamask/multichain-network-controller@npm:^0.2.0, @metamask/multichain-network-controller@workspace:packages/multichain-network-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" dependencies: @@ -5110,23 +5115,6 @@ __metadata: languageName: node linkType: hard -"@ts-bridge/cli@npm:^0.6.3": - version: 0.6.3 - resolution: "@ts-bridge/cli@npm:0.6.3" - dependencies: - "@ts-bridge/resolver": "npm:^0.2.0" - chalk: "npm:^5.3.0" - cjs-module-lexer: "npm:^1.3.1" - yargs: "npm:^17.7.2" - peerDependencies: - typescript: ">=4.8.0" - bin: - ts-bridge: ./dist/index.js - tsbridge: ./dist/index.js - checksum: 10/01cba56ff0f097ca0ef15b79cff80a6be07b7f237e7153f63f7a9acf911583d0a410385fa1711d7b14e3a5e95fed63310b6a743700e7ecc0dd3a2d97a0df75b3 - languageName: node - linkType: hard - "@ts-bridge/resolver@npm:^0.2.0": version: 0.2.0 resolution: "@ts-bridge/resolver@npm:0.2.0" From 8b0133a0b5c10991b65d855520061548f0538136 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 15:25:24 -0700 Subject: [PATCH 13/47] fix: add iconUrl to BridgeAssetSchema --- packages/bridge-controller/src/types.ts | 2 +- packages/bridge-controller/src/utils/validators.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index ab94eb14914..082465d9850 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -110,7 +110,7 @@ export type BridgeAsset = { name: string; decimals: number; icon?: string; - iconUrl: string; + iconUrl?: string; assetId: string; }; diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 9de4474d922..8bf40173d72 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -43,6 +43,7 @@ const BridgeAssetSchema = type({ name: string(), decimals: number(), icon: optional(string()), + iconUrl: optional(string()), }); export const validateFeatureFlagsResponse = ( From 504122ef0b22ffc3067601f8d629edb7176513ad Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 15:25:48 -0700 Subject: [PATCH 14/47] test: add assetId to bridge tokens --- .../tests/mock-quotes-erc20-erc20.json | 66 ++++--------------- .../tests/mock-quotes-erc20-native.json | 33 +++++++++- .../tests/mock-quotes-native-erc20-eth.json | 14 ++++ .../tests/mock-quotes-native-erc20.json | 18 +++++ 4 files changed, 75 insertions(+), 56 deletions(-) diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json index 8b589aa85e1..bae328edd4e 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json +++ b/packages/bridge-controller/tests/mock-quotes-erc20-erc20.json @@ -6,6 +6,7 @@ "srcAsset": { "chainId": 10, "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -18,6 +19,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -32,6 +34,7 @@ "asset": { "chainId": 10, "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -56,6 +59,7 @@ "srcAsset": { "chainId": 10, "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -66,6 +70,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -76,33 +81,7 @@ "srcAmount": "14000000", "destAmount": "13984280" } - ], - "refuel": { - "action": "refuel", - "srcChainId": 10, - "destChainId": 137, - "protocol": { - "name": "refuel", - "displayName": "Refuel", - "icon": "" - }, - "srcAsset": { - "chainId": 10, - "address": "0x0000000000000000000000000000000000000000", - "symbol": "ETH", - "name": "Ether", - "decimals": 18 - }, - "destAsset": { - "chainId": 137, - "address": "0x0000000000000000000000000000000000000000", - "symbol": "MATIC", - "name": "Matic", - "decimals": 18 - }, - "srcAmount": "1000000000000000", - "destAmount": "4405865573929566208" - } + ] }, "approval": { "chainId": 10, @@ -129,6 +108,7 @@ "srcAsset": { "chainId": 10, "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -141,6 +121,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -155,6 +136,7 @@ "asset": { "chainId": 10, "address": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -179,6 +161,7 @@ "srcAsset": { "chainId": 10, "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -189,6 +172,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -199,33 +183,7 @@ "srcAmount": "14000000", "destAmount": "13800000" } - ], - "refuel": { - "action": "refuel", - "srcChainId": 10, - "destChainId": 137, - "protocol": { - "name": "refuel", - "displayName": "Refuel", - "icon": "" - }, - "srcAsset": { - "chainId": 10, - "address": "0x0000000000000000000000000000000000000000", - "symbol": "ETH", - "name": "Ether", - "decimals": 18 - }, - "destAsset": { - "chainId": 137, - "address": "0x0000000000000000000000000000000000000000", - "symbol": "MATIC", - "name": "Matic", - "decimals": 18 - }, - "srcAmount": "1000000000000000", - "destAmount": "4405865573929566208" - } + ] }, "approval": { "chainId": 10, diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-native.json b/packages/bridge-controller/tests/mock-quotes-erc20-native.json index cd4a1963c6f..a73a48229cc 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-native.json +++ b/packages/bridge-controller/tests/mock-quotes-erc20-native.json @@ -6,6 +6,7 @@ "srcTokenAmount": "991250000000000000", "srcAsset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -19,6 +20,7 @@ "destTokenAmount": "991225000000000000", "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -33,6 +35,7 @@ "amount": "8750000000000000", "asset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -58,6 +61,7 @@ }, "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "chainId": 10, "symbol": "ETH", "decimals": 18, @@ -68,7 +72,8 @@ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" }, "destAsset": { - "address": "0x0000000000000000000000000000000000000000", + "address": "0x0000000000000000000000000000000000000000", "assetId": "eip155:42161/slip44:614", + "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -107,7 +112,8 @@ "srcChainId": 10, "srcTokenAmount": "991250000000000000", "srcAsset": { - "address": "0x4200000000000000000000000000000000000006", + "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -121,6 +127,7 @@ "destTokenAmount": "991147696728676903", "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -135,6 +142,7 @@ "amount": "8750000000000000", "asset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -160,6 +168,7 @@ }, "srcAsset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -171,6 +180,7 @@ }, "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -210,6 +220,7 @@ "srcTokenAmount": "991250000000000000", "srcAsset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -223,6 +234,7 @@ "destTokenAmount": "991112862890876485", "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -237,6 +249,7 @@ "amount": "8750000000000000", "asset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -262,6 +275,7 @@ }, "srcAsset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -273,6 +287,7 @@ }, "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -312,6 +327,7 @@ "srcTokenAmount": "991250000000000000", "srcAsset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -325,6 +341,7 @@ "destTokenAmount": "990221346602370184", "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -339,6 +356,7 @@ "amount": "8750000000000000", "asset": { "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", "decimals": 18, @@ -364,6 +382,7 @@ }, "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "chainId": 10, "symbol": "ETH", "decimals": 18, @@ -375,6 +394,7 @@ }, "destAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -414,6 +434,7 @@ "srcAsset": { "chainId": 10, "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "symbol": "WETH", "name": "Wrapped Ether", "decimals": 18, @@ -426,6 +447,7 @@ "destAsset": { "chainId": 42161, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -440,6 +462,7 @@ "asset": { "chainId": 10, "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "symbol": "WETH", "name": "Wrapped Ether", "decimals": 18, @@ -464,6 +487,7 @@ "srcAsset": { "chainId": 10, "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "symbol": "WETH", "name": "Wrapped Ether", "decimals": 18, @@ -474,6 +498,7 @@ "destAsset": { "chainId": 42161, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -513,6 +538,7 @@ "id": "10_0x4200000000000000000000000000000000000006", "symbol": "WETH", "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "name": "Wrapped ETH", "decimals": 18, @@ -534,6 +560,7 @@ "id": "42161_0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "symbol": "ETH", "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "name": "ETH", "decimals": 18, @@ -556,6 +583,7 @@ "id": "10_0x4200000000000000000000000000000000000006", "symbol": "WETH", "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "name": "Wrapped ETH", "decimals": 18, @@ -588,6 +616,7 @@ "id": "10_0x4200000000000000000000000000000000000006", "symbol": "WETH", "address": "0x4200000000000000000000000000000000000006", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "name": "Wrapped ETH", "decimals": 18, diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json index 0afd77760e7..00611043858 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json @@ -6,6 +6,7 @@ "srcTokenAmount": "991250000000000000", "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:1/slip44:60", "chainId": 1, "symbol": "ETH", "decimals": 18, @@ -19,6 +20,7 @@ "destTokenAmount": "3104367033", "destAsset": { "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "assetId": "eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "chainId": 42161, "symbol": "USDC", "decimals": 6, @@ -33,6 +35,7 @@ "amount": "8750000000000000", "asset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:1/slip44:60", "chainId": 1, "symbol": "ETH", "decimals": 18, @@ -58,6 +61,7 @@ }, "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:1/slip44:60", "chainId": 1, "symbol": "ETH", "decimals": 18, @@ -69,6 +73,7 @@ }, "destAsset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "assetId":"eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "symbol": "USDC", "decimals": 6, @@ -92,6 +97,7 @@ }, "srcAsset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "assetId":"eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "symbol": "USDC", "decimals": 6, @@ -103,6 +109,7 @@ }, "destAsset": { "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "assetId": "eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "chainId": 42161, "symbol": "USDC", "decimals": 6, @@ -134,6 +141,7 @@ "srcTokenAmount": "991250000000000000", "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:1/slip44:60", "chainId": 1, "symbol": "ETH", "decimals": 18, @@ -147,6 +155,7 @@ "destTokenAmount": "3104601473", "destAsset": { "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "assetId": "eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "chainId": 42161, "symbol": "USDC", "decimals": 6, @@ -161,6 +170,7 @@ "amount": "8750000000000000", "asset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:1/slip44:60", "chainId": 1, "symbol": "ETH", "decimals": 18, @@ -186,6 +196,7 @@ }, "srcAsset": { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:1/slip44:60", "chainId": 1, "symbol": "ETH", "decimals": 18, @@ -197,6 +208,7 @@ }, "destAsset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "assetId": "eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "symbol": "USDC", "decimals": 6, @@ -220,6 +232,7 @@ }, "srcAsset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "assetId":"eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "symbol": "USDC", "decimals": 6, @@ -231,6 +244,7 @@ }, "destAsset": { "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "assetId": "eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "chainId": 42161, "symbol": "USDC", "decimals": 6, diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20.json b/packages/bridge-controller/tests/mock-quotes-native-erc20.json index f7efe7950ba..51ae77d2df8 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20.json +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20.json @@ -6,6 +6,7 @@ "srcAsset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -18,6 +19,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -32,6 +34,7 @@ "asset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -55,6 +58,7 @@ "srcAsset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -65,6 +69,7 @@ "destAsset": { "chainId": 10, "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -87,6 +92,7 @@ "srcAsset": { "chainId": 10, "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -97,6 +103,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -120,6 +127,7 @@ "srcAsset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ether", "decimals": 18 @@ -127,6 +135,7 @@ "destAsset": { "chainId": 137, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:137/slip44:614", "symbol": "MATIC", "name": "Matic", "decimals": 18 @@ -152,6 +161,7 @@ "srcAsset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -164,6 +174,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -178,6 +189,7 @@ "asset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -201,6 +213,7 @@ "srcAsset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ethereum", "decimals": 18, @@ -211,6 +224,7 @@ "destAsset": { "chainId": 10, "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -233,6 +247,7 @@ "srcAsset": { "chainId": 10, "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + "assetId": "eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85", "symbol": "USDC", "name": "USD Coin", "decimals": 6, @@ -243,6 +258,7 @@ "destAsset": { "chainId": 137, "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + "assetId": "eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "symbol": "USDC", "name": "Native USD Coin (POS)", "decimals": 6, @@ -266,6 +282,7 @@ "srcAsset": { "chainId": 10, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:614", "symbol": "ETH", "name": "Ether", "decimals": 18 @@ -273,6 +290,7 @@ "destAsset": { "chainId": 137, "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:137/slip44:614", "symbol": "MATIC", "name": "Matic", "decimals": 18 From 1cbef13eda051a81fec9b362de536eb3cae8725e Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 15:26:30 -0700 Subject: [PATCH 15/47] fix: unit tests --- .../src/bridge-controller.test.ts | 127 ++++++++---------- .../src/utils/bridge.test.ts | 17 +++ .../bridge-controller/src/utils/fetch.test.ts | 117 +++++++++++++--- 3 files changed, 169 insertions(+), 92 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 63b9c8d335e..0ecc9f8366d 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1,4 +1,5 @@ import { Contract } from '@ethersproject/contracts'; +import { SolScope } from '@metamask/keyring-api'; import type { Hex } from '@metamask/utils'; import { bigIntToHex } from '@metamask/utils'; import nock from 'nock'; @@ -9,9 +10,12 @@ import { BRIDGE_PROD_API_BASE_URL, DEFAULT_BRIDGE_CONTROLLER_STATE, } from './constants/bridge'; -import { CHAIN_IDS } from './constants/chains'; import { SWAPS_API_V2_BASE_URL } from './constants/swaps'; -import type { BridgeControllerMessenger, QuoteResponse } from './types'; +import { + ChainId, + type BridgeControllerMessenger, + type QuoteResponse, +} from './types'; import * as balanceUtils from './utils/balance'; import * as fetchUtils from './utils/fetch'; import { flushPromises } from '../../../tests/helpers'; @@ -79,6 +83,10 @@ describe('BridgeController', function () { isActiveSrc: false, isActiveDest: true, }, + [ChainId.SOLANA]: { + isActiveSrc: true, + isActiveDest: true, + }, }, }, 'mobile-config': { @@ -102,6 +110,10 @@ describe('BridgeController', function () { isActiveSrc: false, isActiveDest: true, }, + [ChainId.SOLANA]: { + isActiveSrc: true, + isActiveDest: true, + }, }, }, 'approval-gas-multiplier': { @@ -153,10 +165,14 @@ describe('BridgeController', function () { refreshRate: 3, support: true, chains: { - [CHAIN_IDS.OPTIMISM]: { isActiveSrc: true, isActiveDest: false }, - [CHAIN_IDS.SCROLL]: { isActiveSrc: true, isActiveDest: false }, - [CHAIN_IDS.POLYGON]: { isActiveSrc: false, isActiveDest: true }, - [CHAIN_IDS.ARBITRUM]: { isActiveSrc: false, isActiveDest: true }, + 'eip155:10': { isActiveSrc: true, isActiveDest: false }, + 'eip155:534352': { isActiveSrc: true, isActiveDest: false }, + 'eip155:137': { isActiveSrc: false, isActiveDest: true }, + 'eip155:42161': { isActiveSrc: false, isActiveDest: true }, + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + isActiveSrc: true, + isActiveDest: true, + }, }, }; @@ -194,14 +210,12 @@ describe('BridgeController', function () { await bridgeController.updateBridgeQuoteRequestParams({ srcChainId: 1 }); expect(bridgeController.state.quoteRequest).toStrictEqual({ srcChainId: 1, - slippage: 0.5, srcTokenAddress: '0x0000000000000000000000000000000000000000', }); await bridgeController.updateBridgeQuoteRequestParams({ destChainId: 10 }); expect(bridgeController.state.quoteRequest).toStrictEqual({ destChainId: 10, - slippage: 0.5, srcTokenAddress: '0x0000000000000000000000000000000000000000', }); @@ -210,7 +224,6 @@ describe('BridgeController', function () { }); expect(bridgeController.state.quoteRequest).toStrictEqual({ destChainId: undefined, - slippage: 0.5, srcTokenAddress: '0x0000000000000000000000000000000000000000', }); @@ -218,7 +231,6 @@ describe('BridgeController', function () { srcTokenAddress: undefined, }); expect(bridgeController.state.quoteRequest).toStrictEqual({ - slippage: 0.5, srcTokenAddress: undefined, }); @@ -239,13 +251,11 @@ describe('BridgeController', function () { srcTokenAddress: '0x2ABC', }); expect(bridgeController.state.quoteRequest).toStrictEqual({ - slippage: 0.5, srcTokenAddress: '0x2ABC', }); bridgeController.resetState(); expect(bridgeController.state.quoteRequest).toStrictEqual({ - slippage: 0.5, srcTokenAddress: '0x0000000000000000000000000000000000000000', }); }); @@ -260,6 +270,7 @@ describe('BridgeController', function () { messengerMock.call.mockReturnValue({ address: '0x123', provider: jest.fn(), + selectedNetworkClientId: 'selectedNetworkClientId', } as never); const fetchBridgeQuotesSpy = jest @@ -292,16 +303,16 @@ describe('BridgeController', function () { }); const quoteParams = { - srcChainId: 1, - destChainId: 10, + srcChainId: '0x1', + destChainId: SolScope.Mainnet, srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x123', + destTokenAddress: '123d1', srcTokenAmount: '1000000000000000000', + slippage: 0.5, + walletAddress: '0x123', }; const quoteRequest = { ...quoteParams, - slippage: 0.5, - walletAddress: '0x123', }; await bridgeController.updateBridgeQuoteRequestParams(quoteParams); @@ -309,7 +320,7 @@ describe('BridgeController', function () { expect(startPollingSpy).toHaveBeenCalledTimes(1); expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); expect(startPollingSpy).toHaveBeenCalledWith({ - networkClientId: expect.anything(), + networkClientId: 'selectedNetworkClientId', updatedQuoteRequest: { ...quoteRequest, insufficientBal: false, @@ -318,7 +329,7 @@ describe('BridgeController', function () { expect(bridgeController.state).toStrictEqual( expect.objectContaining({ - quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quoteRequest: { ...quoteRequest, walletAddress: '0x123' }, quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, quotesLoadingStatus: @@ -390,10 +401,7 @@ describe('BridgeController', function () { expect(bridgeController.state).toStrictEqual( expect.objectContaining({ quoteRequest: { ...quoteRequest, insufficientBal: false }, - quotes: [ - ...mockBridgeQuotesNativeErc20Eth, - ...mockBridgeQuotesNativeErc20Eth, - ], + quotes: [], quotesLoadingStatus: 2, quoteFetchError: 'Network error', quotesRefreshCount: 3, @@ -418,6 +426,7 @@ describe('BridgeController', function () { messengerMock.call.mockReturnValue({ address: '0x123', provider: jest.fn(), + selectedNetworkClientId: 'selectedNetworkClientId', } as never); const fetchBridgeQuotesSpy = jest @@ -442,16 +451,16 @@ describe('BridgeController', function () { }); const quoteParams = { - srcChainId: 1, - destChainId: 10, + srcChainId: '0x1', + destChainId: '0x10', srcTokenAddress: '0x0000000000000000000000000000000000000000', destTokenAddress: '0x123', srcTokenAmount: '1000000000000000000', + walletAddress: '0x123', + slippage: 0.5, }; const quoteRequest = { ...quoteParams, - slippage: 0.5, - walletAddress: '0x123', }; await bridgeController.updateBridgeQuoteRequestParams(quoteParams); @@ -459,7 +468,7 @@ describe('BridgeController', function () { expect(startPollingSpy).toHaveBeenCalledTimes(1); expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); expect(startPollingSpy).toHaveBeenCalledWith({ - networkClientId: expect.anything(), + networkClientId: 'selectedNetworkClientId', updatedQuoteRequest: { ...quoteRequest, insufficientBal: true, @@ -468,7 +477,7 @@ describe('BridgeController', function () { expect(bridgeController.state).toStrictEqual( expect.objectContaining({ - quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quoteRequest, quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, quotesInitialLoadTime: null, @@ -547,6 +556,7 @@ describe('BridgeController', function () { destChainId: 10, srcTokenAddress: '0x0000000000000000000000000000000000000000', destTokenAddress: '0x123', + slippage: 0.5, }); expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); @@ -657,6 +667,7 @@ describe('BridgeController', function () { messengerMock.call.mockReturnValue({ address: '0x123', provider: jest.fn(), + selectedNetworkClientId: 'selectedNetworkClientId', } as never); getLayer1GasFeeMock.mockResolvedValue('0x25F63418AA4'); @@ -671,16 +682,16 @@ describe('BridgeController', function () { }); const quoteParams = { - srcChainId: 10, - destChainId: 1, + 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, - walletAddress: '0x123', }; await bridgeController.updateBridgeQuoteRequestParams(quoteParams); @@ -688,7 +699,7 @@ describe('BridgeController', function () { expect(startPollingSpy).toHaveBeenCalledTimes(1); expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); expect(startPollingSpy).toHaveBeenCalledWith({ - networkClientId: expect.anything(), + networkClientId: 'selectedNetworkClientId', updatedQuoteRequest: { ...quoteRequest, insufficientBal: true, @@ -697,7 +708,7 @@ describe('BridgeController', function () { expect(bridgeController.state).toStrictEqual( expect.objectContaining({ - quoteRequest: { ...quoteRequest, walletAddress: undefined }, + quoteRequest, quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes, quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched, quotesLoadingStatus: @@ -705,7 +716,7 @@ describe('BridgeController', function () { }), ); - // // Loading state + // Loading state jest.advanceTimersByTime(500); await flushPromises(); expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); @@ -755,39 +766,6 @@ describe('BridgeController', function () { }, ); - it('should not fetch quotes if source and destination chains are the same', async () => { - jest.useFakeTimers(); - const fetchBridgeQuotesSpy = jest.spyOn(fetchUtils, 'fetchBridgeQuotes'); - messengerMock.call.mockReturnValue({ - address: '0x123', - provider: jest.fn(), - } as never); - - const hasSufficientBalanceSpy = jest - .spyOn(balanceUtils, 'hasSufficientBalance') - .mockResolvedValue(true); - - const quoteParams = { - srcChainId: 1, - destChainId: 1, // Same chain ID - srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x123', - srcTokenAmount: '1000000000000000000', - }; - - await bridgeController.updateBridgeQuoteRequestParams(quoteParams); - - // Advance timers to trigger fetch - jest.advanceTimersByTime(1000); - await flushPromises(); - - expect(fetchBridgeQuotesSpy).not.toHaveBeenCalled(); - expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1); - expect(bridgeController.state.quotesLoadingStatus).toBe( - DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus, - ); - }); - it('should handle abort signals in fetchBridgeQuotes', async () => { jest.useFakeTimers(); const fetchBridgeQuotesSpy = jest.spyOn(fetchUtils, 'fetchBridgeQuotes'); @@ -806,11 +784,12 @@ describe('BridgeController', function () { }); const quoteParams = { - srcChainId: 1, - destChainId: 10, - srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x123', - srcTokenAmount: '1000000000000000000', + srcChainId: '0x10', + destChainId: '0x1', + srcTokenAddress: '0x4200000000000000000000000000000000000006', + destTokenAddress: '0x0000000000000000000000000000000000000000', + srcTokenAmount: '991250000000000000', + walletAddress: 'eip:id/id:id/0x123', }; await bridgeController.updateBridgeQuoteRequestParams(quoteParams); diff --git a/packages/bridge-controller/src/utils/bridge.test.ts b/packages/bridge-controller/src/utils/bridge.test.ts index ddfc652dce5..d203900ffe3 100644 --- a/packages/bridge-controller/src/utils/bridge.test.ts +++ b/packages/bridge-controller/src/utils/bridge.test.ts @@ -1,10 +1,12 @@ import { Contract } from '@ethersproject/contracts'; +import { SolScope } from '@metamask/keyring-api'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Hex } from '@metamask/utils'; import { getEthUsdtResetData, isEthUsdt, + isSolanaChainId, isSwapsDefaultTokenAddress, isSwapsDefaultTokenSymbol, sumHexes, @@ -135,4 +137,19 @@ describe('Bridge utils', () => { expect(isSwapsDefaultTokenSymbol('ETH', '' as Hex)).toBe(false); }); }); + + describe('isSolanaChainId', () => { + it('returns true for ChainId.SOLANA', () => { + expect(isSolanaChainId(1151111081099710)).toBe(true); + }); + + it('returns true for SolScope.Mainnet', () => { + expect(isSolanaChainId(SolScope.Mainnet)).toBe(true); + }); + + it('returns false for other chainIds', () => { + expect(isSolanaChainId(1)).toBe(false); + expect(isSolanaChainId('0x0')).toBe(false); + }); + }); }); diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 4c38695d30a..1f2059bd96e 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -44,6 +44,10 @@ describe('fetch', () => { isActiveSrc: false, isActiveDest: true, }, + '1151111081099710': { + isActiveSrc: true, + isActiveDest: true, + }, }, }; const mockResponse = { @@ -63,6 +67,10 @@ describe('fetch', () => { 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', { headers: { 'X-Client-Id': 'extension' }, + cacheOptions: { + cacheRefreshTime: 600000, + }, + functionName: 'fetchBridgeFeatureFlags', }, ); @@ -71,29 +79,33 @@ describe('fetch', () => { refreshRate: 3, support: true, chains: { - [CHAIN_IDS.MAINNET]: { - isActiveSrc: true, + 'eip155:1': { isActiveDest: true, - }, - [CHAIN_IDS.OPTIMISM]: { isActiveSrc: true, - isActiveDest: false, }, - [CHAIN_IDS.LINEA_MAINNET]: { + 'eip155:10': { + isActiveDest: false, isActiveSrc: true, + }, + 'eip155:11111': { isActiveDest: true, + isActiveSrc: false, }, - '0x78': { - isActiveSrc: true, + 'eip155:120': { isActiveDest: false, + isActiveSrc: true, }, - [CHAIN_IDS.POLYGON]: { + 'eip155:137': { + isActiveDest: true, isActiveSrc: false, + }, + 'eip155:59144': { isActiveDest: true, + isActiveSrc: true, }, - '0x2b67': { - isActiveSrc: false, + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { isActiveDest: true, + isActiveSrc: true, }, }, }; @@ -136,6 +148,10 @@ describe('fetch', () => { expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getAllFeatureFlags', { + cacheOptions: { + cacheRefreshTime: 600000, + }, + functionName: 'fetchBridgeFeatureFlags', headers: { 'X-Client-Id': 'extension' }, }, ); @@ -170,29 +186,60 @@ describe('fetch', () => { describe('fetchBridgeTokens', () => { it('should fetch bridge tokens successfully', async () => { const mockResponse = [ + { + address: '0x0000000000000000000000000000000000000000', + assetId: 'eip155:10/slip44:614', + symbol: 'ETH', + decimals: 18, + name: 'Ether', + coingeckoId: 'ethereum', + aggregators: [], + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/native/614.png', + metadata: { + honeypotStatus: {}, + isContractVerified: false, + erc20Permit: false, + description: {}, + createdAt: '2023-10-31T22:16:37.494Z', + }, + chainId: 10, + }, { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + assetId: 'eip155:10/erc20:0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC', + name: 'ABC', decimals: 16, + chainId: 10, }, { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', + assetId: 'eip155:10/erc20:0x1f9840a85d5af5bf1d1762f925bdaddc4201f985', decimals: 16, + chainId: 10, }, { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + assetId: 'eip155:10/erc20:0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', decimals: 16, symbol: 'DEF', + name: 'DEF', aggregators: ['lifi'], + chainId: 10, }, { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f987', + assetId: 'eip155:10/erc20:0x1f9840a85d5af5bf1d1762f925bdaddc4201f987', symbol: 'DEF', + chainId: 10, }, { address: '0x124', + assetId: 'eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85', symbol: 'JKL', decimals: 16, + chainId: 10, }, ]; @@ -208,6 +255,10 @@ describe('fetch', () => { expect(mockFetchFn).toHaveBeenCalledWith( 'https://bridge.api.cx.metamask.io/getTokens?chainId=10', { + cacheOptions: { + cacheRefreshTime: 600000, + }, + functionName: 'fetchBridgeTokens', headers: { 'X-Client-Id': 'extension' }, }, ); @@ -215,20 +266,38 @@ describe('fetch', () => { expect(result).toStrictEqual({ '0x0000000000000000000000000000000000000000': { address: '0x0000000000000000000000000000000000000000', + aggregators: [], + assetId: 'eip155:10/slip44:614', + chainId: 10, + coingeckoId: 'ethereum', decimals: 18, - iconUrl: '', + iconUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/native/614.png', + metadata: { + createdAt: '2023-10-31T22:16:37.494Z', + description: {}, + erc20Permit: false, + honeypotStatus: {}, + isContractVerified: false, + }, name: 'Ether', symbol: 'ETH', }, '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986': { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + assetId: 'eip155:10/erc20:0x1f9840a85d5af5bf1d1762f925bdaddc4201f986', + chainId: 10, decimals: 16, + name: 'DEF', symbol: 'DEF', aggregators: ['lifi'], }, '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + assetId: 'eip155:10/erc20:0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + chainId: 10, decimals: 16, + name: 'ABC', symbol: 'ABC', }, }); @@ -257,7 +326,7 @@ describe('fetch', () => { const result = await fetchBridgeQuotes( { - walletAddress: '0x123', + walletAddress: '0x388c818ca8b9251b393131c08a736a67ccb19297', srcChainId: 1, destChainId: 10, srcTokenAddress: AddressZero, @@ -272,8 +341,12 @@ describe('fetch', () => { ); expect(mockFetchFn).toHaveBeenCalledWith( - 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x388C818CA8B9251b393131C08a736A67ccB19297&destWalletAddress=0x388C818CA8B9251b393131C08a736A67ccB19297&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&slippage=0.5', { + cacheOptions: { + cacheRefreshTime: 0, + }, + functionName: 'fetchBridgeQuotes', headers: { 'X-Client-Id': 'extension' }, signal, }, @@ -292,7 +365,7 @@ describe('fetch', () => { const result = await fetchBridgeQuotes( { - walletAddress: '0x123', + walletAddress: '0x388c818ca8b9251b393131c08a736a67ccb19297', srcChainId: 1, destChainId: 10, srcTokenAddress: AddressZero, @@ -307,8 +380,12 @@ describe('fetch', () => { ); expect(mockFetchFn).toHaveBeenCalledWith( - 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x388C818CA8B9251b393131C08a736A67ccB19297&destWalletAddress=0x388C818CA8B9251b393131C08a736A67ccB19297&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&slippage=0.5', { + cacheOptions: { + cacheRefreshTime: 0, + }, + functionName: 'fetchBridgeQuotes', headers: { 'X-Client-Id': 'extension' }, signal, }, @@ -346,7 +423,7 @@ describe('fetch', () => { const result = await fetchBridgeQuotes( { - walletAddress: '0x123', + walletAddress: '0x388c818ca8b9251b393131c08a736a67ccb19297', srcChainId: 1, destChainId: 10, srcTokenAddress: AddressZero, @@ -361,8 +438,12 @@ describe('fetch', () => { ); expect(mockFetchFn).toHaveBeenCalledWith( - 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x123&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&slippage=0.5&insufficientBal=false&resetApproval=false', + 'https://bridge.api.cx.metamask.io/getQuote?walletAddress=0x388C818CA8B9251b393131C08a736A67ccB19297&destWalletAddress=0x388C818CA8B9251b393131C08a736A67ccB19297&srcChainId=1&destChainId=10&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0x0000000000000000000000000000000000000000&srcTokenAmount=20000&insufficientBal=false&resetApproval=false&slippage=0.5', { + cacheOptions: { + cacheRefreshTime: 0, + }, + functionName: 'fetchBridgeQuotes', headers: { 'X-Client-Id': 'extension' }, signal, }, From ea221765ae1e70723b0848db39aeec64d3c26471 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 15:29:19 -0700 Subject: [PATCH 16/47] fix: unused var --- packages/bridge-controller/src/utils/fetch.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-controller/src/utils/fetch.test.ts b/packages/bridge-controller/src/utils/fetch.test.ts index 1f2059bd96e..9ccafd14dc2 100644 --- a/packages/bridge-controller/src/utils/fetch.test.ts +++ b/packages/bridge-controller/src/utils/fetch.test.ts @@ -8,7 +8,6 @@ import { import mockBridgeQuotesErc20Erc20 from '../../tests/mock-quotes-erc20-erc20.json'; import mockBridgeQuotesNativeErc20 from '../../tests/mock-quotes-native-erc20.json'; import { BridgeClientId, BRIDGE_PROD_API_BASE_URL } from '../constants/bridge'; -import { CHAIN_IDS } from '../constants/chains'; const mockFetchFn = jest.fn(); From 933615856af3ae966be60f691654d06534a84d37 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 15:33:00 -0700 Subject: [PATCH 17/47] test: update bridge-status test data --- .../bridge-status-controller.test.ts.snap | 10 ++++++++++ .../src/bridge-status-controller.test.ts | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index fdf64b3dbd4..0deb0482bd0 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -20,6 +20,7 @@ Object { ], "destAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -36,6 +37,7 @@ Object { "amount": "8750000000000", "asset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -50,6 +52,7 @@ Object { "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", "srcAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -67,6 +70,7 @@ Object { "destAmount": "990654755978612", "destAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -85,6 +89,7 @@ Object { "srcAmount": "991250000000000", "srcAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -133,6 +138,7 @@ Object { ], "destAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -149,6 +155,7 @@ Object { "amount": "8750000000000", "asset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -163,6 +170,7 @@ Object { "requestId": "197c402f-cb96-4096-9f8c-54aed84ca776", "srcAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, @@ -180,6 +188,7 @@ Object { "destAmount": "990654755978612", "destAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:10/slip44:60", "chainId": 10, "coinKey": "ETH", "decimals": 18, @@ -198,6 +207,7 @@ Object { "srcAmount": "991250000000000", "srcAsset": Object { "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:60", "chainId": 42161, "coinKey": "ETH", "decimals": 18, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index 7ba1b8147ce..7deece524bf 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -131,6 +131,7 @@ const getMockQuote = ({ srcChainId = 42161, destChainId = 10 } = {}) => ({ srcTokenAmount: '991250000000000', srcAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: `eip155:${srcChainId}/slip44:60`, chainId: srcChainId, symbol: 'ETH', decimals: 18, @@ -145,6 +146,7 @@ const getMockQuote = ({ srcChainId = 42161, destChainId = 10 } = {}) => ({ destTokenAmount: '990654755978612', destAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: `eip155:${destChainId}/slip44:60`, chainId: destChainId, symbol: 'ETH', decimals: 18, @@ -160,6 +162,7 @@ const getMockQuote = ({ srcChainId = 42161, destChainId = 10 } = {}) => ({ amount: '8750000000000', asset: { address: '0x0000000000000000000000000000000000000000', + assetId: `eip155:${srcChainId}/slip44:60`, chainId: srcChainId, symbol: 'ETH', decimals: 18, @@ -186,6 +189,7 @@ const getMockQuote = ({ srcChainId = 42161, destChainId = 10 } = {}) => ({ }, srcAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: `eip155:${srcChainId}/slip44:60`, chainId: srcChainId, symbol: 'ETH', decimals: 18, @@ -198,6 +202,7 @@ const getMockQuote = ({ srcChainId = 42161, destChainId = 10 } = {}) => ({ }, destAsset: { address: '0x0000000000000000000000000000000000000000', + assetId: `eip155:${destChainId}/slip44:60`, chainId: destChainId, symbol: 'ETH', decimals: 18, From f95dbb568be7e40e7cc4bb0bcb3e3aeccbaaa1fc Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 15:42:01 -0700 Subject: [PATCH 18/47] test: fix lint issue --- .../bridge-controller/tests/mock-quotes-erc20-native.json | 6 +++--- .../tests/mock-quotes-native-erc20-eth.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bridge-controller/tests/mock-quotes-erc20-native.json b/packages/bridge-controller/tests/mock-quotes-erc20-native.json index a73a48229cc..2501edad4fc 100644 --- a/packages/bridge-controller/tests/mock-quotes-erc20-native.json +++ b/packages/bridge-controller/tests/mock-quotes-erc20-native.json @@ -72,8 +72,8 @@ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png" }, "destAsset": { - "address": "0x0000000000000000000000000000000000000000", "assetId": "eip155:42161/slip44:614", - + "address": "0x0000000000000000000000000000000000000000", + "assetId": "eip155:42161/slip44:614", "chainId": 42161, "symbol": "ETH", "decimals": 18, @@ -112,7 +112,7 @@ "srcChainId": 10, "srcTokenAmount": "991250000000000000", "srcAsset": { - "address": "0x4200000000000000000000000000000000000006", + "address": "0x4200000000000000000000000000000000000006", "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000006", "chainId": 10, "symbol": "WETH", diff --git a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json index 00611043858..bc959e6158a 100644 --- a/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json +++ b/packages/bridge-controller/tests/mock-quotes-native-erc20-eth.json @@ -73,7 +73,7 @@ }, "destAsset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "assetId":"eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "assetId": "eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "symbol": "USDC", "decimals": 6, @@ -97,7 +97,7 @@ }, "srcAsset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "assetId":"eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "assetId": "eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "symbol": "USDC", "decimals": 6, @@ -232,7 +232,7 @@ }, "srcAsset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "assetId":"eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "assetId": "eip155:42161/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "chainId": 1, "symbol": "USDC", "decimals": 6, From b3e69336d7270f498017b9a44fbb7b182a839b5d Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 15:54:59 -0700 Subject: [PATCH 19/47] test: add unit tests for caip-formatters utils --- .../src/utils/caip-formatters.test.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 packages/bridge-controller/src/utils/caip-formatters.test.ts diff --git a/packages/bridge-controller/src/utils/caip-formatters.test.ts b/packages/bridge-controller/src/utils/caip-formatters.test.ts new file mode 100644 index 00000000000..97143559be2 --- /dev/null +++ b/packages/bridge-controller/src/utils/caip-formatters.test.ts @@ -0,0 +1,117 @@ +import { AddressZero } from '@ethersproject/constants'; +import { SolScope } from '@metamask/keyring-api'; + +import { + formatChainIdToCaip, + formatChainIdToDec, + formatChainIdToHex, + formatAddressToString, + formatChainIdToHexOrCaip, +} from './caip-formatters'; +import { ChainId } from '../types'; + +describe('CAIP Formatters', () => { + describe('formatChainIdToCaip', () => { + it('should return the same value if already CAIP format', () => { + expect(formatChainIdToCaip('eip155:1')).toBe('eip155:1'); + }); + + it('should convert hex chainId to CAIP format', () => { + expect(formatChainIdToCaip('0x1')).toBe('eip155:1'); + }); + + it('should convert Solana chainId to SolScope.Mainnet', () => { + expect(formatChainIdToCaip(ChainId.SOLANA)).toBe(SolScope.Mainnet); + expect(formatChainIdToCaip(SolScope.Mainnet)).toBe(SolScope.Mainnet); + }); + + it('should convert number to CAIP format', () => { + expect(formatChainIdToCaip(1)).toBe('eip155:1'); + }); + }); + + describe('formatChainIdToDec', () => { + it('should convert hex chainId to decimal', () => { + expect(formatChainIdToDec('0x1')).toBe(1); + }); + + it('should handle Solana mainnet', () => { + expect(formatChainIdToDec(SolScope.Mainnet)).toBe(ChainId.SOLANA); + }); + + it('should parse CAIP chainId to decimal', () => { + expect(formatChainIdToDec('eip155:1')).toBe(1); + }); + + it('should handle numeric strings', () => { + expect(formatChainIdToDec('1')).toBe(1); + }); + + it('should return same number if number provided', () => { + expect(formatChainIdToDec(1)).toBe(1); + }); + }); + + describe('formatChainIdToHex', () => { + it('should return same value if already hex', () => { + expect(formatChainIdToHex('0x1')).toBe('0x1'); + }); + + it('should convert number to hex', () => { + expect(formatChainIdToHex(1)).toBe('0x1'); + }); + + it('should convert CAIP chainId to hex', () => { + expect(formatChainIdToHex('eip155:1')).toBe('0x1'); + }); + + it('should throw error for invalid chainId', () => { + expect(() => formatChainIdToHex('invalid')).toThrow( + 'Invalid cross-chain swaps chainId: invalid', + ); + }); + }); + + describe('formatAddressToString', () => { + it('should checksum hex addresses', () => { + expect( + formatAddressToString('0x1234567890123456789012345678901234567890'), + ).toBe('0x1234567890123456789012345678901234567890'); + }); + + it('should return zero address for native token addresses', () => { + expect(formatAddressToString(AddressZero)).toStrictEqual(AddressZero); + expect(formatAddressToString('')).toStrictEqual(AddressZero); + expect( + formatAddressToString(`${SolScope.Mainnet}/slip44:501`), + ).toStrictEqual(AddressZero); + }); + + it('should extract address from CAIP format', () => { + expect( + formatAddressToString( + 'eip155:1:0x1234567890123456789012345678901234567890', + ), + ).toBe('0x1234567890123456789012345678901234567890'); + }); + + it('should throw error for invalid address', () => { + expect(() => formatAddressToString('test:')).toThrow('Invalid address'); + }); + }); + + describe('formatChainIdToHexOrCaip', () => { + it('should return SolScope.Mainnet for Solana chainId', () => { + expect(formatChainIdToHexOrCaip(ChainId.SOLANA)).toBe(SolScope.Mainnet); + expect(formatChainIdToHexOrCaip(SolScope.Mainnet)).toBe(SolScope.Mainnet); + }); + + it('should return hex for EVM chainId', () => { + expect(formatChainIdToHexOrCaip(1)).toBe('0x1'); + }); + + it('should handle CAIP chainId', () => { + expect(formatChainIdToHexOrCaip('eip155:1')).toBe('0x1'); + }); + }); +}); From 638a9ab65cfba648b78527fb100a017ec2801a12 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 17:29:17 -0700 Subject: [PATCH 20/47] test: add bridge-controller and quote unit tests --- .../src/bridge-controller.test.ts | 309 ++++++++++++++++++ .../bridge-controller/src/utils/quote.test.ts | 125 +++++++ .../tests/mock-quotes-sol-erc20.json | 296 +++++++++++++++++ 3 files changed, 730 insertions(+) create mode 100644 packages/bridge-controller/src/utils/quote.test.ts create mode 100644 packages/bridge-controller/tests/mock-quotes-sol-erc20.json diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 0ecc9f8366d..3bb90598da5 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1,5 +1,6 @@ import { Contract } from '@ethersproject/contracts'; import { SolScope } from '@metamask/keyring-api'; +import { HandlerType } from '@metamask/snaps-utils'; import type { Hex } from '@metamask/utils'; import { bigIntToHex } from '@metamask/utils'; import nock from 'nock'; @@ -23,6 +24,7 @@ import { handleFetch } from '../../controller-utils/src'; import mockBridgeQuotesErc20Native from '../tests/mock-quotes-erc20-native.json'; import mockBridgeQuotesNativeErc20Eth from '../tests/mock-quotes-native-erc20-eth.json'; import mockBridgeQuotesNativeErc20 from '../tests/mock-quotes-native-erc20.json'; +import mockBridgeQuotesSolErc20 from '../tests/mock-quotes-sol-erc20.json'; const EMPTY_INIT_STATE = DEFAULT_BRIDGE_CONTROLLER_STATE; @@ -543,6 +545,115 @@ describe('BridgeController', function () { expect(getLayer1GasFeeMock).not.toHaveBeenCalled(); }); + it('updateBridgeQuoteRequestParams should set insufficientBal=true if RPC provider is tenderly', 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.mockImplementation( + ( + ...args: Parameters + ): ReturnType => { + const actionType = args[0]; + + // eslint-disable-next-line jest/no-conditional-in-test + if (actionType === 'AccountsController:getSelectedMultichainAccount') { + return { + address: '0x123', + metadata: { + snap: { + id: 'npm:@metamask/solana-snap', + name: 'Solana Snap', + enabled: true, + }, + } as never, + options: { + scope: 'mainnet', + }, + } as never; + } + // eslint-disable-next-line jest/no-conditional-in-test + if (actionType === 'NetworkController:getNetworkClientById') { + return { + configuration: { rpcUrl: 'https://rpc.tenderly.co' }, + } as never; + } + return { + provider: jest.fn() as never, + selectedNetworkClientId: 'selectedNetworkClientId', + } as never; + }, + ); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, '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, + }; + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).not.toHaveBeenCalled(); + expect(startPollingSpy).toHaveBeenCalledWith({ + networkClientId: 'selectedNetworkClientId', + updatedQuoteRequest: { + ...quoteRequest, + insufficientBal: true, + }, + }); + + // Loading state + jest.advanceTimersByTime(1000); + await flushPromises(); + + // After first fetch + jest.advanceTimersByTime(10000); + await flushPromises(); + expect(bridgeController.state).toStrictEqual( + expect.objectContaining({ + quoteRequest: { ...quoteRequest, insufficientBal: true }, + quotes: mockBridgeQuotesNativeErc20Eth, + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + quotesInitialLoadTime: 11000, + }), + ); + const firstFetchTime = bridgeController.state.quotesLastFetched; + expect(firstFetchTime).toBeGreaterThan(0); + }); + it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', async function () { const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); @@ -816,4 +927,202 @@ describe('BridgeController', function () { expect(bridgeController.state.quotesLoadingStatus).toBe(0); expect(bridgeController.state.quotes).toStrictEqual([]); }); + + const getFeeSnapCalls = mockBridgeQuotesSolErc20.map(({ trade }) => [ + 'SnapController:handleRequest', + { + snapId: 'npm:@metamask/solana-snap', + origin: 'metamask', + handler: HandlerType.OnRpcRequest, + request: { + method: 'getFeeForTransaction', + params: { + transaction: trade, + scope: 'mainnet', + }, + }, + }, + ]); + + it.each([ + [ + 'should append solanaFees for Solana quotes', + mockBridgeQuotesSolErc20 as unknown as QuoteResponse[], + '5000', + getFeeSnapCalls, + ], + [ + 'should not append solanaFees if selected account is not a snap', + mockBridgeQuotesSolErc20 as unknown as QuoteResponse[], + undefined, + [], + false, + ], + [ + 'should handle mixed Solana and non-Solana quotes by not appending fees', + [ + ...mockBridgeQuotesSolErc20, + ...mockBridgeQuotesErc20Native, + ] as unknown as QuoteResponse[], + undefined, + [], + ], + ])( + 'updateBridgeQuoteRequestParams: %s', + async ( + _testTitle: string, + quoteResponse: QuoteResponse[], + expectedFees: string | undefined, + expectedSnapCalls: typeof getFeeSnapCalls, + isSnapAccount = true, + ) => { + jest.useFakeTimers(); + const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling'); + const startPollingSpy = jest.spyOn(bridgeController, 'startPolling'); + const hasSufficientBalanceSpy = jest + .spyOn(balanceUtils, 'hasSufficientBalance') + .mockResolvedValue(false); + + messengerMock.call.mockImplementation( + ( + ...args: Parameters + ): ReturnType => { + const actionType = args[0]; + + // eslint-disable-next-line jest/no-conditional-in-test + if ( + // eslint-disable-next-line jest/no-conditional-in-test + actionType === 'AccountsController:getSelectedMultichainAccount' && + isSnapAccount + ) { + return { + address: '0x123', + metadata: { + snap: { + id: 'npm:@metamask/solana-snap', + name: 'Solana Snap', + enabled: true, + }, + } as never, + options: { + scope: 'mainnet', + }, + } as never; + } + // eslint-disable-next-line jest/no-conditional-in-test + if (actionType === 'SnapController:handleRequest') { + return { value: '5000' } as never; + } + return { + provider: jest.fn() as never, + selectedNetworkClientId: 'selectedNetworkClientId', + } as never; + }, + ); + + const fetchBridgeQuotesSpy = jest + .spyOn(fetchUtils, 'fetchBridgeQuotes') + .mockImplementation(async () => { + return await new Promise((resolve) => { + return setTimeout(() => { + resolve(quoteResponse as never); + }, 1000); + }); + }); + + const quoteParams = { + srcChainId: SolScope.Mainnet, + destChainId: '1', + srcTokenAddress: 'NATIVE', + destTokenAddress: '0x0000000000000000000000000000000000000000', + srcTokenAmount: '1000000', + walletAddress: '0x123', + slippage: 0.5, + }; + + await bridgeController.updateBridgeQuoteRequestParams(quoteParams); + + expect(stopAllPollingSpy).toHaveBeenCalledTimes(1); + expect(startPollingSpy).toHaveBeenCalledTimes(1); + expect(hasSufficientBalanceSpy).not.toHaveBeenCalled(); + + // Loading state + jest.advanceTimersByTime(500); + await flushPromises(); + expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1); + + // After fetch completes + jest.advanceTimersByTime(1500); + await flushPromises(); + + const { quotes } = bridgeController.state; + expect(bridgeController.state).toStrictEqual( + expect.objectContaining({ + quotesLoadingStatus: 1, + quotesRefreshCount: 1, + }), + ); + + // Verify Solana fees + quotes.forEach((quote) => { + expect(quote.solanaFeesInLamports).toBe(expectedFees); + }); + + // Verify snap interaction + const snapCalls = messengerMock.call.mock.calls.filter( + ([methodName]) => methodName === 'SnapController:handleRequest', + ); + + expect(snapCalls).toMatchObject(expectedSnapCalls); + }, + ); + // jest.useFakeTimers(); + + // // Mock account without snap metadata + // messengerMock.call.mockImplementation((methodName) => { + // if (methodName === 'AccountsController:getSelectedMultichainAccount') { + // return { + // address: '0x123', + // options: { + // scope: 'mainnet', + // }, + // }; + // } + // return { + // provider: jest.fn(), + // selectedNetworkClientId: 'selectedNetworkClientId', + // }; + // }); + + // const solanaQuotes = [ + // { + // quote: { + // srcChainId: 'solana:101', + // // ... other required quote fields + // }, + // trade: 'base64EncodedSolanaTransaction', + // }, + // ] as QuoteResponse[]; + + // jest + // .spyOn(fetchUtils, 'fetchBridgeQuotes') + // .mockResolvedValueOnce(solanaQuotes as never); + + // await bridgeController.updateBridgeQuoteRequestParams({ + // srcChainId: 'solana:101', + // destChainId: '1', + // srcTokenAmount: '1000000', + // walletAddress: '0x123', + // }); + + // jest.advanceTimersByTime(2000); + // await flushPromises(); + + // const { quotes } = bridgeController.state; + // expect(quotes[0]).not.toHaveProperty('solanaFeesInLamports'); + // expect(messengerMock.call).not.toHaveBeenCalledWith( + // 'SnapController:handleRequest', + // expect.any(Object), + // ); + // }); }); diff --git a/packages/bridge-controller/src/utils/quote.test.ts b/packages/bridge-controller/src/utils/quote.test.ts new file mode 100644 index 00000000000..22978f79a8d --- /dev/null +++ b/packages/bridge-controller/src/utils/quote.test.ts @@ -0,0 +1,125 @@ +import { isValidQuoteRequest } from './quote'; +import type { GenericQuoteRequest } from '../types'; + +describe('Quote Utils', () => { + describe('isValidQuoteRequest', () => { + const validRequest: GenericQuoteRequest = { + srcTokenAddress: '0x123', + destTokenAddress: '0x456', + srcChainId: '1', + destChainId: '137', + walletAddress: '0x789', + srcTokenAmount: '1000', + slippage: 0.5, + }; + + it('should return true for valid request with all required fields', () => { + expect(isValidQuoteRequest(validRequest)).toBe(true); + }); + + it('should return false if any required string field is missing', () => { + const requiredFields = [ + 'srcTokenAddress', + 'destTokenAddress', + 'srcChainId', + 'destChainId', + 'walletAddress', + 'srcTokenAmount', + ]; + + requiredFields.forEach((field) => { + const invalidRequest = { ...validRequest }; + delete invalidRequest[field as keyof GenericQuoteRequest]; + expect(isValidQuoteRequest(invalidRequest)).toBe(false); + }); + }); + + it('should return false if any required string field is empty', () => { + const requiredFields = [ + 'srcTokenAddress', + 'destTokenAddress', + 'srcChainId', + 'destChainId', + 'walletAddress', + 'srcTokenAmount', + ]; + + requiredFields.forEach((field) => { + const invalidRequest = { + ...validRequest, + [field]: '', + }; + expect(isValidQuoteRequest(invalidRequest)).toBe(false); + }); + }); + + it('should return false if any required string field is null', () => { + const invalidRequest = { + ...validRequest, + srcTokenAddress: null, + }; + expect(isValidQuoteRequest(invalidRequest as never)).toBe(false); + }); + + it('should return false if srcTokenAmount is not a valid positive integer', () => { + const invalidAmounts = ['0', '-1', '1.5', 'abc', '01']; + invalidAmounts.forEach((amount) => { + const invalidRequest = { + ...validRequest, + srcTokenAmount: amount, + }; + expect(isValidQuoteRequest(invalidRequest)).toBe(false); + }); + }); + + it('should return true for valid srcTokenAmount values', () => { + const validAmounts = ['1', '100', '999999']; + validAmounts.forEach((amount) => { + const validAmountRequest = { + ...validRequest, + srcTokenAmount: amount, + }; + expect(isValidQuoteRequest(validAmountRequest)).toBe(true); + }); + }); + + it('should validate request without amount when requireAmount is false', () => { + const { srcTokenAmount, ...requestWithoutAmount } = validRequest; + expect(isValidQuoteRequest(requestWithoutAmount, false)).toBe(true); + }); + + describe('slippage validation', () => { + it('should return true when slippage is a valid number', () => { + const requestWithSlippage = { + ...validRequest, + slippage: 1.5, + }; + expect(isValidQuoteRequest(requestWithSlippage)).toBe(true); + }); + + it('should return false when slippage is NaN', () => { + const requestWithInvalidSlippage = { + ...validRequest, + slippage: NaN, + }; + expect(isValidQuoteRequest(requestWithInvalidSlippage)).toBe(false); + }); + + it('should return false when slippage is null', () => { + const requestWithInvalidSlippage = { + ...validRequest, + slippage: null, + }; + expect(isValidQuoteRequest(requestWithInvalidSlippage as never)).toBe( + false, + ); + }); + + it('should return true when slippage is undefined', () => { + const requestWithoutSlippage = { ...validRequest }; + delete requestWithoutSlippage.slippage; + expect(isValidQuoteRequest(requestWithoutSlippage)).toBe(true); + }); + }); + }); +}); diff --git a/packages/bridge-controller/tests/mock-quotes-sol-erc20.json b/packages/bridge-controller/tests/mock-quotes-sol-erc20.json new file mode 100644 index 00000000000..5ecf836a8c4 --- /dev/null +++ b/packages/bridge-controller/tests/mock-quotes-sol-erc20.json @@ -0,0 +1,296 @@ +[ + { + "quote": { + "requestId": "5cb5a527-d4e4-4b5e-b753-136afc3986d3", + "srcChainId": 1151111081099710, + "srcTokenAmount": "1000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111", + "symbol": "SOL", + "decimals": 9, + "name": "SOL", + "aggregators": [], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/11111111111111111111111111111111.png", + "metadata": {}, + "chainId": 1151111081099710, + "price": "124.92" + }, + "destChainId": 10, + "destTokenAmount": "143291269234176100000", + "destAsset": { + "address": "0x4200000000000000000000000000000000000042", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000042", + "symbol": "OP", + "decimals": 18, + "name": "Optimism", + "coingeckoId": "optimism", + "aggregators": [ + "coinGecko", + "openSwap", + "optimism", + "uniswap", + "oneInch", + "liFi", + "xSwap", + "socket", + "rubic", + "squid" + ], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/erc20/0x4200000000000000000000000000000000000042.png", + "metadata": { + "honeypotStatus": { + "goPlus": false + }, + "isContractVerified": false, + "storage": { + "balance": 0, + "approval": 1 + }, + "erc20Permit": true, + "description": { + "en": "OP is the token for the Optimism Collective that governs the Optimism L2 blockchain. The Optimism Collective is a large-scale experiment in digital democratic governance, built to drive rapid and sustainable growth of a decentralized ecosystem, and stewarded by the newly formed Optimism Foundation.OP governs upgrades to the protocol and network parameters, and creates an ongoing system of incentives for projects and users in the Optimism ecosystem. 5.4% of the total token supply will be distributed to projects on Optimism over the next six months via governance. If you're building something in the Ethereum ecosystem, you can consider applying for the grant." + }, + "createdAt": "2023-10-31T22:16:37.494Z" + }, + "chainId": 10, + "price": "0.865" + }, + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111", + "symbol": "SOL", + "decimals": 9, + "name": "SOL", + "aggregators": [], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/11111111111111111111111111111111.png", + "metadata": {}, + "chainId": 1151111081099710, + "price": "124.92" + } + } + }, + "bridgeId": "lifi", + "bridges": ["mayan"], + "steps": [ + { + "action": "bridge", + "srcChainId": 1151111081099710, + "destChainId": 10, + "protocol": { + "name": "mayan", + "displayName": "Mayan (Swift)", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/mayan.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111", + "symbol": "SOL", + "decimals": 9, + "name": "SOL", + "aggregators": [], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/11111111111111111111111111111111.png", + "metadata": {}, + "chainId": 1151111081099710 + }, + "destAsset": { + "address": "0x4200000000000000000000000000000000000042", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000042", + "symbol": "OP", + "decimals": 18, + "name": "Optimism", + "coingeckoId": "optimism", + "aggregators": [ + "coinGecko", + "openSwap", + "optimism", + "uniswap", + "oneInch", + "liFi", + "xSwap", + "socket", + "rubic", + "squid" + ], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/erc20/0x4200000000000000000000000000000000000042.png", + "metadata": { + "honeypotStatus": { + "goPlus": false + }, + "isContractVerified": false, + "storage": { + "balance": 0, + "approval": 1 + }, + "erc20Permit": true, + "description": { + "en": "OP is the token for the Optimism Collective that governs the Optimism L2 blockchain. The Optimism Collective is a large-scale experiment in digital democratic governance, built to drive rapid and sustainable growth of a decentralized ecosystem, and stewarded by the newly formed Optimism Foundation.OP governs upgrades to the protocol and network parameters, and creates an ongoing system of incentives for projects and users in the Optimism ecosystem. 5.4% of the total token supply will be distributed to projects on Optimism over the next six months via governance. If you're building something in the Ethereum ecosystem, you can consider applying for the grant." + }, + "createdAt": "2023-10-31T22:16:37.494Z" + }, + "chainId": 10 + }, + "srcAmount": "991250000", + "destAmount": "143291269234176100000" + } + ], + "bridgePriceData": { + "totalFromAmountUsd": "124.9200", + "totalToAmountUsd": "123.9469", + "priceImpact": "0.007789785462696144" + } + }, + "trade": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHDXLY8oVRIwA8ZdRSGjM5RIZJW8Wv+Twyw3NqU4Hov+OHoHp/dmeDvstKbICW3ezeGR69t3/PTAvdXgZVdJFJXaxkoKXUTWfEAyQyCCG9nwVoDsd10OFdnM9ldSi+9SLqHpqWVDV+zzkmftkF//DpbXxqeH8obNXHFR7pUlxG9uNVOn64oNsFdeUvD139j1M51iRmUY839Y25ET4jDRscT081oGb+rLnywLjLSrIQx6MkqNBhCFbxqY1YmoGZVORW/QMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHnVW/IxwG7udMVuzmgVB/2xst6j9I5RArHNola8E4+0P/on9df2SnTAmx8pWHneSwmrNt/J3VFLMhqns4zl6JmXkZ+niuxMhAGrmKBaBo94uMv2Sl+Xh3i+VOO0m5BdNZ1ElenbwQylHQY+VW1ydG1MaUEeNpG+EVgswzPMwPoLBgAFAsBcFQAGAAkDQA0DAAAAAAAHBgABAhMICQAHBgADABYICQEBCAIAAwwCAAAAUEYVOwAAAAAJAQMBEQoUCQADBAETCgsKFw0ODxARAwQACRQj5RfLl3rjrSoBAAAAQ2QAAVBGFTsAAAAAyYZnBwAAAABkAAAJAwMAAAEJDAkAAAIBBBMVCQjGASBMKQwnooTbKNxdBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUHTKomh4KXvNgA0ovYKS5F8GIOBgAAAAAAAAAAAAAAAAAQgAAAAAAAAAAAAAAAAAAAAAAAEIF7RFOAwAAAAAAAAAAAAAAaAIAAAAAAAC4CwAAAAAAAOAA2mcAAAAAAAAAAAAAAAAAAAAApapuIXG0FuHSfsU8qME9s/kaic0AAwGCsZdSuxV5eCm+Ria4LEQPgTg4bg65gNrTAefEzpAfPQgCABIMAgAAAAAAAAAAAAAACAIABQwCAAAAsIOFAAAAAAADWk6DVOZO8lMFQg2r0dgfltD6tRL/B1hH3u00UzZdgqkAAxEqIPdq2eRt/F6mHNmFe7iwZpdrtGmHNJMFlK7c6Bc6k6kjBezr6u/tAgvu3OGsJSwSElmcOHZ21imqH/rhJ2KgqDJdBPFH4SYIM1kBAAA=", + "estimatedProcessingTimeInSeconds": 12 + }, + { + "quote": { + "requestId": "12c94d29-4b5c-4aee-92de-76eee4172d3d", + "srcChainId": 1151111081099710, + "srcTokenAmount": "1000000000", + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111", + "symbol": "SOL", + "decimals": 9, + "name": "SOL", + "aggregators": [], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/11111111111111111111111111111111.png", + "metadata": {}, + "chainId": 1151111081099710, + "price": "124.92" + }, + "destChainId": 10, + "destTokenAmount": "141450025181571360000", + "destAsset": { + "address": "0x4200000000000000000000000000000000000042", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000042", + "symbol": "OP", + "decimals": 18, + "name": "Optimism", + "coingeckoId": "optimism", + "aggregators": [ + "coinGecko", + "openSwap", + "optimism", + "uniswap", + "oneInch", + "liFi", + "xSwap", + "socket", + "rubic", + "squid" + ], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/erc20/0x4200000000000000000000000000000000000042.png", + "metadata": { + "honeypotStatus": { + "goPlus": false + }, + "isContractVerified": false, + "storage": { + "balance": 0, + "approval": 1 + }, + "erc20Permit": true, + "description": { + "en": "OP is the token for the Optimism Collective that governs the Optimism L2 blockchain. The Optimism Collective is a large-scale experiment in digital democratic governance, built to drive rapid and sustainable growth of a decentralized ecosystem, and stewarded by the newly formed Optimism Foundation.OP governs upgrades to the protocol and network parameters, and creates an ongoing system of incentives for projects and users in the Optimism ecosystem. 5.4% of the total token supply will be distributed to projects on Optimism over the next six months via governance. If you're building something in the Ethereum ecosystem, you can consider applying for the grant." + }, + "createdAt": "2023-10-31T22:16:37.494Z" + }, + "chainId": 10, + "price": "0.865" + }, + "feeData": { + "metabridge": { + "amount": "0", + "asset": { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111", + "symbol": "SOL", + "decimals": 9, + "name": "SOL", + "aggregators": [], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/11111111111111111111111111111111.png", + "metadata": {}, + "chainId": 1151111081099710, + "price": "124.92" + } + } + }, + "bridgeId": "lifi", + "bridges": ["mayanMCTP"], + "steps": [ + { + "action": "bridge", + "srcChainId": 1151111081099710, + "destChainId": 10, + "protocol": { + "name": "mayanMCTP", + "displayName": "Mayan (MCTP)", + "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/mayan.png" + }, + "srcAsset": { + "address": "0x0000000000000000000000000000000000000000", + "assetId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:11111111111111111111111111111111", + "symbol": "SOL", + "decimals": 9, + "name": "SOL", + "aggregators": [], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token/11111111111111111111111111111111.png", + "metadata": {}, + "chainId": 1151111081099710 + }, + "destAsset": { + "address": "0x4200000000000000000000000000000000000042", + "assetId": "eip155:10/erc20:0x4200000000000000000000000000000000000042", + "symbol": "OP", + "decimals": 18, + "name": "Optimism", + "coingeckoId": "optimism", + "aggregators": [ + "coinGecko", + "openSwap", + "optimism", + "uniswap", + "oneInch", + "liFi", + "xSwap", + "socket", + "rubic", + "squid" + ], + "iconUrl": "https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/10/erc20/0x4200000000000000000000000000000000000042.png", + "metadata": { + "honeypotStatus": { + "goPlus": false + }, + "isContractVerified": false, + "storage": { + "balance": 0, + "approval": 1 + }, + "erc20Permit": true, + "description": { + "en": "OP is the token for the Optimism Collective that governs the Optimism L2 blockchain. The Optimism Collective is a large-scale experiment in digital democratic governance, built to drive rapid and sustainable growth of a decentralized ecosystem, and stewarded by the newly formed Optimism Foundation.OP governs upgrades to the protocol and network parameters, and creates an ongoing system of incentives for projects and users in the Optimism ecosystem. 5.4% of the total token supply will be distributed to projects on Optimism over the next six months via governance. If you're building something in the Ethereum ecosystem, you can consider applying for the grant." + }, + "createdAt": "2023-10-31T22:16:37.494Z" + }, + "chainId": 10 + }, + "srcAmount": "991250000", + "destAmount": "141450025181571360000" + } + ], + "bridgePriceData": { + "totalFromAmountUsd": "124.9200", + "totalToAmountUsd": "122.3543", + "priceImpact": "0.020538744796669922" + } + }, + "trade": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAIEnLY8oVRIwA8ZdRSGjM5RIZJW8Wv+Twyw3NqU4Hov+OHz7U6VQBhniAZG564p5JhG+y5+5uEABjxPtimE61bsqsz4TFeaDdmFmlW16xBf2qhUAUla7cIQjqp3HfLznM1aZqWVDV+zzkmftkF//DpbXxqeH8obNXHFR7pUlxG9uNVZ0EED+QHqrBQRqB+cbMfYZjXZcTe9r+CfdbguirL8P49t1pWG6qWtPmFmciR1xbrt4IW+b1nNcz2N5abYbCcsDgByJFz/oyJeNAhYJfn7erTZs6xJHjnuAV0v/cuH6iQNCzB1ajK9lOERjgtFNI8XDODau1kgDlDaRIGFfFNP09KMWgsU3Ye36HzgEdq38sqvZDFOifcDzPxfPOcDxeZgLShtMST0fB39lSGQI7f01fZv+JVg5S4qIF2zdmCAhSAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAACMlyWPTiSJ8bs9ECkUjg2DC1oTmdr/EIQEjnvY2+n4WQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTj1E+LF26QsO9gzDavYNO6ZflUDWJ+gBV9eCQ5OcuzAMStD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5egJanTpAxnCBLW4j9Mn+DAuluhVY4cEgRJ9Pah1VqYQXzWdRJXp28EMpR0GPlVtcnRtTGlBHjaRvhFYLMMzzMD6CQoABQLAXBUACgAJA0ANAwAAAAAACwYAAQIbDA0ACwYAAwAcDA0BAQwCAAMMAgAAAFBGFTsAAAAADQEDAREOKQ0PAAMEBQEcGw4OEA4dDx4SBAYTFBUNBxYICQ4fDwYFFxgZGiAhIiMNKMEgmzNB1pyBAwIAAAAaZAABOGQBAlBGFTsAAAAAP4hnBwAAAABkAAANAwMAAAEJEQUAAgEbDLwBj+v8wtNahk0AAAAAAAAAAAAAAAAUHTKomh4KXvNgA0ovYKS5F8GIOCjcXQcAAAAAAAAAAAAAAACUXhgAAAAAABb1AwAAAAAAGABuuH/gY8j1t421m3ekiET/qFVeKhVA3SJVS5OH/NW+oQMAAAAAAAAAAAAAAABCAAAAAAAAAAAAAAAAAAAAAAAAQrPV80YDAAAACwLaZwAAAAAAAAAAAAAAAAAAAAClqm4hcbQW4dJ+xTyowT2z+RqJzQADWk6DVOZO8lMFQg2r0dgfltD6tRL/B1hH3u00UzZdgqkAARE9whapJMxiYg1Y/S9bROWrjXfldZCFcyME/snbeFkkhAUXFisYKQMaKiVZfTkrqqg0GkW+iGFAaIHEbhkRX4YCBLoWvHI1OH2T2gSmTlKhBREUDA0H", + "estimatedProcessingTimeInSeconds": 120 + } +] From 90fae9239c050ce4e69d970539cfefe8c008a1b7 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Tue, 18 Mar 2025 17:32:54 -0700 Subject: [PATCH 21/47] fix: bridge-status test mocks --- .../bridge-status-controller/src/utils/bridge-status.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/bridge-status-controller/src/utils/bridge-status.test.ts b/packages/bridge-status-controller/src/utils/bridge-status.test.ts index 3430cd6da03..9f23a88617d 100644 --- a/packages/bridge-status-controller/src/utils/bridge-status.test.ts +++ b/packages/bridge-status-controller/src/utils/bridge-status.test.ts @@ -32,6 +32,7 @@ describe('utils', () => { name: 'Ether', decimals: 18, icon: undefined, + assetId: 'eip155:1/erc20:0x123', }, srcTokenAmount: '', destAsset: { @@ -41,6 +42,7 @@ describe('utils', () => { name: 'USD Coin', decimals: 6, icon: undefined, + assetId: 'eip155:137/erc20:0x456', }, destTokenAmount: '', feeData: { @@ -53,6 +55,7 @@ describe('utils', () => { name: 'Ether', decimals: 18, icon: 'eth.jpeg', + assetId: 'eip155:1/erc20:0x123', }, }, }, From bbfcb784527ccf6e9c351815d36f2bc5a271be0a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:29:06 -0700 Subject: [PATCH 22/47] fix: remove new peerDependencies --- packages/bridge-controller/package.json | 3 --- yarn.lock | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 146f02d4089..d54635e1b1d 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -83,10 +83,7 @@ }, "peerDependencies": { "@metamask/accounts-controller": "^26.0.0", - "@metamask/multichain-network-controller": "^0.0.0", "@metamask/network-controller": "^22.0.0", - "@metamask/snaps-controllers": "^9.19.0", - "@metamask/snaps-utils": "^8.10.0", "@metamask/transaction-controller": "^50.0.0" }, "engines": { diff --git a/yarn.lock b/yarn.lock index e3cdb535658..cf24f64d950 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2707,10 +2707,7 @@ __metadata: typescript: "npm:~5.2.2" peerDependencies: "@metamask/accounts-controller": ^26.0.0 - "@metamask/multichain-network-controller": ^0.0.0 "@metamask/network-controller": ^22.0.0 - "@metamask/snaps-controllers": ^9.19.0 - "@metamask/snaps-utils": ^8.10.0 "@metamask/transaction-controller": ^50.0.0 languageName: unknown linkType: soft From 903be6240d2562fc6fe074eb4e3996a4ca5ce95a Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:29:37 -0700 Subject: [PATCH 23/47] test: remove comments from test --- .../src/bridge-controller.test.ts | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.test.ts b/packages/bridge-controller/src/bridge-controller.test.ts index 3bb90598da5..554893df9ac 100644 --- a/packages/bridge-controller/src/bridge-controller.test.ts +++ b/packages/bridge-controller/src/bridge-controller.test.ts @@ -1076,53 +1076,4 @@ describe('BridgeController', function () { expect(snapCalls).toMatchObject(expectedSnapCalls); }, ); - // jest.useFakeTimers(); - - // // Mock account without snap metadata - // messengerMock.call.mockImplementation((methodName) => { - // if (methodName === 'AccountsController:getSelectedMultichainAccount') { - // return { - // address: '0x123', - // options: { - // scope: 'mainnet', - // }, - // }; - // } - // return { - // provider: jest.fn(), - // selectedNetworkClientId: 'selectedNetworkClientId', - // }; - // }); - - // const solanaQuotes = [ - // { - // quote: { - // srcChainId: 'solana:101', - // // ... other required quote fields - // }, - // trade: 'base64EncodedSolanaTransaction', - // }, - // ] as QuoteResponse[]; - - // jest - // .spyOn(fetchUtils, 'fetchBridgeQuotes') - // .mockResolvedValueOnce(solanaQuotes as never); - - // await bridgeController.updateBridgeQuoteRequestParams({ - // srcChainId: 'solana:101', - // destChainId: '1', - // srcTokenAmount: '1000000', - // walletAddress: '0x123', - // }); - - // jest.advanceTimersByTime(2000); - // await flushPromises(); - - // const { quotes } = bridgeController.state; - // expect(quotes[0]).not.toHaveProperty('solanaFeesInLamports'); - // expect(messengerMock.call).not.toHaveBeenCalledWith( - // 'SnapController:handleRequest', - // expect.any(Object), - // ); - // }); }); From 4eaec4492a3916746254f2419830e2c00be95fa3 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:33:17 -0700 Subject: [PATCH 24/47] refactor: rename formatAddressToString -> formatAddressToCaipReference --- .../src/bridge-controller.ts | 8 ++++---- packages/bridge-controller/src/index.ts | 6 +++++- .../src/utils/caip-formatters.test.ts | 20 +++++++++++-------- .../src/utils/caip-formatters.ts | 2 +- packages/bridge-controller/src/utils/fetch.ts | 10 +++++----- 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 2c7317eff7b..be8c213480a 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -39,7 +39,7 @@ import { sumHexes, } from './utils/bridge'; import { - formatAddressToString, + formatAddressToCaipReference, formatChainIdToCaip, formatChainIdToHex, } from './utils/caip-formatters'; @@ -235,20 +235,20 @@ export class BridgeController extends StaticIntervalPollingController { }); }); - describe('formatAddressToString', () => { + describe('formatAddressToCaipReference', () => { it('should checksum hex addresses', () => { expect( - formatAddressToString('0x1234567890123456789012345678901234567890'), + formatAddressToCaipReference( + '0x1234567890123456789012345678901234567890', + ), ).toBe('0x1234567890123456789012345678901234567890'); }); it('should return zero address for native token addresses', () => { - expect(formatAddressToString(AddressZero)).toStrictEqual(AddressZero); - expect(formatAddressToString('')).toStrictEqual(AddressZero); + expect(formatAddressToCaipReference(AddressZero)).toStrictEqual( + AddressZero, + ); + expect(formatAddressToCaipReference('')).toStrictEqual(AddressZero); expect( - formatAddressToString(`${SolScope.Mainnet}/slip44:501`), + formatAddressToCaipReference(`${SolScope.Mainnet}/slip44:501`), ).toStrictEqual(AddressZero); }); it('should extract address from CAIP format', () => { expect( - formatAddressToString( + formatAddressToCaipReference( 'eip155:1:0x1234567890123456789012345678901234567890', ), ).toBe('0x1234567890123456789012345678901234567890'); }); it('should throw error for invalid address', () => { - expect(() => formatAddressToString('test:')).toThrow('Invalid address'); + expect(() => formatAddressToCaipReference('test:')).toThrow('Invalid address'); }); }); diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index d8d133158b4..f8d0ad162f1 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -94,7 +94,7 @@ export const formatChainIdToHex = ( * @param address - The address to convert * @returns The converted address */ -export const formatAddressToString = (address: string) => { +export const formatAddressToCaipReference = (address: string) => { if (isStrictHexString(address)) { return getAddress(address); } diff --git a/packages/bridge-controller/src/utils/fetch.ts b/packages/bridge-controller/src/utils/fetch.ts index cb524dbc39e..4a8b3e8c942 100644 --- a/packages/bridge-controller/src/utils/fetch.ts +++ b/packages/bridge-controller/src/utils/fetch.ts @@ -2,7 +2,7 @@ import type { CaipChainId, Hex } from '@metamask/utils'; import { Duration } from '@metamask/utils'; import { - formatAddressToString, + formatAddressToCaipReference, formatChainIdToCaip, formatChainIdToDec, } from './caip-formatters'; @@ -136,12 +136,12 @@ export async function fetchBridgeQuotes( const destWalletAddress = request.destWalletAddress ?? request.walletAddress; // Transform the generic quote request into QuoteRequest const normalizedRequest: QuoteRequest = { - walletAddress: formatAddressToString(request.walletAddress), - destWalletAddress: formatAddressToString(destWalletAddress), + walletAddress: formatAddressToCaipReference(request.walletAddress), + destWalletAddress: formatAddressToCaipReference(destWalletAddress), srcChainId: formatChainIdToDec(request.srcChainId), destChainId: formatChainIdToDec(request.destChainId), - srcTokenAddress: formatAddressToString(request.srcTokenAddress), - destTokenAddress: formatAddressToString(request.destTokenAddress), + srcTokenAddress: formatAddressToCaipReference(request.srcTokenAddress), + destTokenAddress: formatAddressToCaipReference(request.destTokenAddress), srcTokenAmount: request.srcTokenAmount, insufficientBal: Boolean(request.insufficientBal), resetApproval: Boolean(request.resetApproval), From 8ca9e4aea74d210b2c30713ae1a05ca44c6baba9 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:33:59 -0700 Subject: [PATCH 25/47] chore: add comments for L1 and Solana fees --- packages/bridge-controller/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 082465d9850..f165369c9c3 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -61,11 +61,11 @@ export type ChainConfiguration = { }; export type L1GasFees = { - l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller + l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by BridgeController.#appendL1GasFees }; export type SolanaFees = { - solanaFeesInLamports?: string; // solana fees in lamports, appended by controller + solanaFeesInLamports?: string; // solana fees in lamports, appended by BridgeController.#appendSolanaFees }; /** From befb4781e1b477244fa9111e6dd41ff22bc1cc3d Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:34:40 -0700 Subject: [PATCH 26/47] chore: add solana to AllowedBridgeChainIds --- .../bridge-controller/src/constants/bridge.ts | 4 +++- .../bridge-controller/src/constants/chains.ts | 16 ---------------- .../src/utils/caip-formatters.ts | 2 +- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/bridge-controller/src/constants/bridge.ts b/packages/bridge-controller/src/constants/bridge.ts index 547a6d50c0e..62f1e87c57e 100644 --- a/packages/bridge-controller/src/constants/bridge.ts +++ b/packages/bridge-controller/src/constants/bridge.ts @@ -1,4 +1,5 @@ import { AddressZero } from '@ethersproject/constants'; +import { SolScope } from '@metamask/keyring-api'; import type { Hex } from '@metamask/utils'; import { CHAIN_IDS } from './chains'; @@ -16,7 +17,8 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [ CHAIN_IDS.ARBITRUM, CHAIN_IDS.LINEA_MAINNET, CHAIN_IDS.BASE, -]; + SolScope.Mainnet, +] as const; export type AllowedBridgeChainIds = (typeof ALLOWED_BRIDGE_CHAIN_IDS)[number]; diff --git a/packages/bridge-controller/src/constants/chains.ts b/packages/bridge-controller/src/constants/chains.ts index abf24411276..790e9a8ccdf 100644 --- a/packages/bridge-controller/src/constants/chains.ts +++ b/packages/bridge-controller/src/constants/chains.ts @@ -1,5 +1,3 @@ -import type { AllowedBridgeChainIds } from './bridge'; - /** * An object containing all of the chain ids for networks both built in and * those that we have added custom code to support our feature set. @@ -157,17 +155,3 @@ export const NETWORK_TO_NAME_MAP = { [CHAIN_IDS.LISK]: LISK_DISPLAY_NAME, [CHAIN_IDS.LISK_SEPOLIA]: LISK_SEPOLIA_DISPLAY_NAME, } as const; -export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record< - AllowedBridgeChainIds, - string -> = { - [CHAIN_IDS.MAINNET]: 'Ethereum', - [CHAIN_IDS.LINEA_MAINNET]: 'Linea', - [CHAIN_IDS.POLYGON]: NETWORK_TO_NAME_MAP[CHAIN_IDS.POLYGON], - [CHAIN_IDS.AVALANCHE]: 'Avalanche', - [CHAIN_IDS.BSC]: NETWORK_TO_NAME_MAP[CHAIN_IDS.BSC], - [CHAIN_IDS.ARBITRUM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.ARBITRUM], - [CHAIN_IDS.OPTIMISM]: NETWORK_TO_NAME_MAP[CHAIN_IDS.OPTIMISM], - [CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era', - [CHAIN_IDS.BASE]: 'Base', -}; diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index f8d0ad162f1..5a064f0e48c 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -70,7 +70,7 @@ export const formatChainIdToDec = ( */ export const formatChainIdToHex = ( chainId: Hex | CaipChainId | string | number, -) => { +): Hex => { if (isStrictHexString(chainId)) { return chainId; } From 1275f99099f411d08ed0865dfee099821d385eb6 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:39:59 -0700 Subject: [PATCH 27/47] chore: rm formatChainIdToHexOrCaip --- .../src/utils/caip-formatters.test.ts | 20 +++---------------- .../src/utils/caip-formatters.ts | 15 -------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/packages/bridge-controller/src/utils/caip-formatters.test.ts b/packages/bridge-controller/src/utils/caip-formatters.test.ts index 1b2a6aede00..fc9e4d9f363 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.test.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.test.ts @@ -6,7 +6,6 @@ import { formatChainIdToDec, formatChainIdToHex, formatAddressToCaipReference, - formatChainIdToHexOrCaip, } from './caip-formatters'; import { ChainId } from '../types'; @@ -100,22 +99,9 @@ describe('CAIP Formatters', () => { }); it('should throw error for invalid address', () => { - expect(() => formatAddressToCaipReference('test:')).toThrow('Invalid address'); - }); - }); - - describe('formatChainIdToHexOrCaip', () => { - it('should return SolScope.Mainnet for Solana chainId', () => { - expect(formatChainIdToHexOrCaip(ChainId.SOLANA)).toBe(SolScope.Mainnet); - expect(formatChainIdToHexOrCaip(SolScope.Mainnet)).toBe(SolScope.Mainnet); - }); - - it('should return hex for EVM chainId', () => { - expect(formatChainIdToHexOrCaip(1)).toBe('0x1'); - }); - - it('should handle CAIP chainId', () => { - expect(formatChainIdToHexOrCaip('eip155:1')).toBe('0x1'); + expect(() => formatAddressToCaipReference('test:')).toThrow( + 'Invalid address', + ); }); }); }); diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index 5a064f0e48c..0eea289613f 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -111,18 +111,3 @@ export const formatAddressToCaipReference = (address: string) => { } return addressWithoutPrefix; }; - -/** - * Converts a chainId to a hex string or CaipChainId - * - * @param chainId - The chainId to convert - * @returns The hex string or CaipChainId - */ -export const formatChainIdToHexOrCaip = ( - chainId: number | Hex | CaipChainId, -) => { - if (isSolanaChainId(chainId)) { - return SolScope.Mainnet; - } - return formatChainIdToHex(chainId); -}; From f9b83e6475c69c3dde9571dc7aa775f9af6aa7ff Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:44:51 -0700 Subject: [PATCH 28/47] feat: deprecate SwapsTokenObject and SWAPS_CHAINID_DEFAULT_TOKEN_MAP --- .../bridge-controller/src/constants/tokens.ts | 77 ++++++++++++------- packages/bridge-controller/src/index.ts | 7 ++ packages/bridge-controller/src/types.ts | 21 +++++ .../src/utils/bridge.test.ts | 11 +-- .../bridge-controller/src/utils/bridge.ts | 49 ++++++++---- 5 files changed, 113 insertions(+), 52 deletions(-) diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index bad65f066e2..d8d8fa4de68 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,4 +1,6 @@ +import type { CaipChainId } from '@metamask/keyring-api'; import { SolScope } from '@metamask/keyring-api'; +import { formatChainIdToCaip } from 'src/utils/caip-formatters'; import { CHAIN_IDS } from './chains'; @@ -26,9 +28,9 @@ export type SwapsTokenObject = { }; const DEFAULT_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; -export const DEFAULT_SOLANA_TOKEN_ADDRESS = `${SolScope.Mainnet}/slip44:501`; +const DEFAULT_SOLANA_TOKEN_ADDRESS = `${SolScope.Mainnet}/slip44:501`; -export const CURRENCY_SYMBOLS = { +const CURRENCY_SYMBOLS = { ARBITRUM: 'ETH', AVALANCHE: 'AVAX', BNB: 'BNB', @@ -54,7 +56,7 @@ export const CURRENCY_SYMBOLS = { SOL: 'SOL', } as const; -export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const ETH_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.ETH, name: 'Ether', address: DEFAULT_TOKEN_ADDRESS, @@ -62,7 +64,7 @@ export const ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { iconUrl: '', }; -export const BNB_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const BNB_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.BNB, name: 'Binance Coin', address: DEFAULT_TOKEN_ADDRESS, @@ -70,7 +72,7 @@ export const BNB_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { iconUrl: '', } as const; -export const MATIC_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const MATIC_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.POL, name: 'Polygon', address: DEFAULT_TOKEN_ADDRESS, @@ -78,7 +80,7 @@ export const MATIC_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { iconUrl: '', } as const; -export const AVAX_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const AVAX_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.AVALANCHE, name: 'Avalanche', address: DEFAULT_TOKEN_ADDRESS, @@ -86,7 +88,7 @@ export const AVAX_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { iconUrl: '', } as const; -export const TEST_ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const TEST_ETH_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.TEST_ETH, name: 'Test Ether', address: DEFAULT_TOKEN_ADDRESS, @@ -94,7 +96,7 @@ export const TEST_ETH_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { iconUrl: '', } as const; -export const GOERLI_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const GOERLI_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.ETH, name: 'Ether', address: DEFAULT_TOKEN_ADDRESS, @@ -102,7 +104,7 @@ export const GOERLI_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { iconUrl: '', } as const; -export const SEPOLIA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const SEPOLIA_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.ETH, name: 'Ether', address: DEFAULT_TOKEN_ADDRESS, @@ -110,48 +112,67 @@ export const SEPOLIA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { iconUrl: '', } as const; -export const ARBITRUM_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const ARBITRUM_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT, } as const; -export const OPTIMISM_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const OPTIMISM_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT, } as const; -export const ZKSYNC_ERA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const ZKSYNC_ERA_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT, } as const; -export const LINEA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const LINEA_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT, } as const; -export const BASE_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const BASE_SWAPS_TOKEN_OBJECT = { ...ETH_SWAPS_TOKEN_OBJECT, } as const; -const SOLANA_SWAPS_TOKEN_OBJECT: SwapsTokenObject = { +const SOLANA_SWAPS_TOKEN_OBJECT = { symbol: CURRENCY_SYMBOLS.SOL, name: 'Solana', address: DEFAULT_SOLANA_TOKEN_ADDRESS, decimals: 9, iconUrl: '', -}; +} as const; const SWAPS_TESTNET_CHAIN_ID = '0x539'; export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { - [CHAIN_IDS.MAINNET]: ETH_SWAPS_TOKEN_OBJECT, - [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.BSC]: BNB_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.POLYGON]: MATIC_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.GOERLI]: GOERLI_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.SEPOLIA]: GOERLI_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.AVALANCHE]: AVAX_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.OPTIMISM]: OPTIMISM_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.ARBITRUM]: ARBITRUM_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.LINEA_MAINNET]: LINEA_SWAPS_TOKEN_OBJECT, - [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.MAINNET)]: ETH_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(SWAPS_TESTNET_CHAIN_ID)]: TEST_ETH_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.BSC)]: BNB_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.POLYGON)]: MATIC_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.GOERLI)]: GOERLI_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.SEPOLIA)]: SEPOLIA_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.AVALANCHE)]: AVAX_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.OPTIMISM)]: OPTIMISM_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.ARBITRUM)]: ARBITRUM_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.ZKSYNC_ERA)]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.LINEA_MAINNET)]: LINEA_SWAPS_TOKEN_OBJECT, + [formatChainIdToCaip(CHAIN_IDS.BASE)]: BASE_SWAPS_TOKEN_OBJECT, [SolScope.Mainnet]: SOLANA_SWAPS_TOKEN_OBJECT, } as const; + +export type SupportedSwapsNativeCurrencySymbols = + (typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[CaipChainId]['symbol']; + +/** + * A map of native currency symbols to their SLIP-44 representation + * From {@link https://github.com/satoshilabs/slips/blob/master/slip-0044.md} + */ +export const SYMBOL_TO_SLIP44_MAP: Record< + SupportedSwapsNativeCurrencySymbols, + `${string}:${string}` +> = { + SOL: 'slip44:501', + ETH: 'slip44:60', + POL: 'slip44:966', + BNB: 'slip44:714', + AVAX: 'slip44:9000', + TESTETH: 'slip44:60', +}; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index efcb47526f8..549192446a0 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -59,7 +59,13 @@ export { export type { AllowedBridgeChainIds } from './constants/bridge'; export { + /** + * @deprecated This type should not be used. Use {@link BridgeAsset} instead. + */ type SwapsTokenObject, + /** + * @deprecated This map should not be used. Use getNativeAssetForChainId" } instead. + */ SWAPS_CHAINID_DEFAULT_TOKEN_MAP, } from './constants/tokens'; @@ -70,6 +76,7 @@ export { isEthUsdt, isNativeAddress, isSolanaChainId, + getNativeAssetForChainId, } from './utils/bridge'; export { isValidQuoteRequest } from './utils/quote'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index f165369c9c3..b1aa34caa53 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -104,13 +104,34 @@ export enum SortOrder { * This type is used in the QuoteResponse and in the fetchBridgeTokens response */ export type BridgeAsset = { + /** + * The chainId of the token + */ chainId: ChainId; + /** + * An address that the metaswap-api recognizes as the default token + */ address: string; + /** + * The symbol of token object + */ symbol: string; + /** + * The name for the network + */ name: string; + /** + * Number of digits after decimal point + */ decimals: number; icon?: string; + /** + * URL for token icon + */ iconUrl?: string; + /** + * The assetId of the token + */ assetId: string; }; diff --git a/packages/bridge-controller/src/utils/bridge.test.ts b/packages/bridge-controller/src/utils/bridge.test.ts index d203900ffe3..2c597a69ddb 100644 --- a/packages/bridge-controller/src/utils/bridge.test.ts +++ b/packages/bridge-controller/src/utils/bridge.test.ts @@ -5,6 +5,7 @@ import type { Hex } from '@metamask/utils'; import { getEthUsdtResetData, + getNativeAssetForChainId, isEthUsdt, isSolanaChainId, isSwapsDefaultTokenAddress, @@ -91,10 +92,7 @@ describe('Bridge utils', () => { describe('isSwapsDefaultTokenAddress', () => { it('returns true for default token address of given chain', () => { const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; - const defaultToken = - SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ]; + const defaultToken = getNativeAssetForChainId(chainId); expect(isSwapsDefaultTokenAddress(defaultToken.address, chainId)).toBe( true, @@ -116,10 +114,7 @@ describe('Bridge utils', () => { describe('isSwapsDefaultTokenSymbol', () => { it('returns true for default token symbol of given chain', () => { const chainId = Object.keys(SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[0] as Hex; - const defaultToken = - SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ]; + const defaultToken = getNativeAssetForChainId(chainId); expect(isSwapsDefaultTokenSymbol(defaultToken.symbol, chainId)).toBe( true, diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 07b302f6583..c62dbea152f 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -2,9 +2,10 @@ import { AddressZero } from '@ethersproject/constants'; import { Contract } from '@ethersproject/contracts'; import { SolScope } from '@metamask/keyring-api'; import { abiERC20 } from '@metamask/metamask-eth-abis'; -import type { CaipChainId } from '@metamask/utils'; +import type { CaipAssetType, CaipChainId } from '@metamask/utils'; import { isCaipChainId, isStrictHexString, type Hex } from '@metamask/utils'; +import { formatChainIdToCaip, formatChainIdToDec } from './caip-formatters'; import { DEFAULT_BRIDGE_CONTROLLER_STATE, ETH_USDT_ADDRESS, @@ -12,16 +13,42 @@ import { } from '../constants/bridge'; import { CHAIN_IDS } from '../constants/chains'; import { - DEFAULT_SOLANA_TOKEN_ADDRESS, SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + SYMBOL_TO_SLIP44_MAP, + type SupportedSwapsNativeCurrencySymbols, } from '../constants/tokens'; -import type { BridgeControllerState } from '../types'; +import type { BridgeAsset, BridgeControllerState } from '../types'; import { ChainId } from '../types'; export const getDefaultBridgeControllerState = (): BridgeControllerState => { return DEFAULT_BRIDGE_CONTROLLER_STATE; }; +const getNativeAssetCaipAssetType = ( + chainId: CaipChainId, + nativeCurrencySymbol: SupportedSwapsNativeCurrencySymbols, +): CaipAssetType => { + return `${formatChainIdToCaip(chainId)}/${SYMBOL_TO_SLIP44_MAP[nativeCurrencySymbol]}`; +}; + +/** + * Returns the native swaps or bridge asset for a given chainId + * + * @param chainId - The chainId to get the default token for + * @returns The native asset for the given chainId + */ +export const getNativeAssetForChainId = ( + chainId: string | number | Hex | CaipChainId, +): BridgeAsset => { + const chainIdInCaip = formatChainIdToCaip(chainId); + const nativeToken = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainIdInCaip]; + return { + ...nativeToken, + chainId: formatChainIdToDec(chainId), + assetId: getNativeAssetCaipAssetType(chainIdInCaip, nativeToken.symbol), + }; +}; + /** * A function to return the txParam data for setting allowance to 0 for USDT on Ethereum * @@ -67,12 +94,7 @@ export const isSwapsDefaultTokenAddress = ( return false; } - return ( - address === - SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ]?.address - ); + return address === getNativeAssetForChainId(chainId)?.address; }; /** @@ -91,12 +113,7 @@ export const isSwapsDefaultTokenSymbol = ( return false; } - return ( - symbol === - SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP - ]?.symbol - ); + return symbol === getNativeAssetForChainId(chainId)?.symbol; }; /** @@ -110,7 +127,7 @@ export const isNativeAddress = (address?: string | null) => address === '' || // assets controllers set the native asset address to an empty string !address || address.endsWith('11111111111111111111111111111111') || // token-api and bridge-api use this as the solana native assetId - [DEFAULT_SOLANA_TOKEN_ADDRESS].some( + [getNativeAssetForChainId(ChainId.SOLANA).assetId].some( (assetId) => assetId.includes(address) && !isStrictHexString(address), ); // solana native assetId used in the extension client From e0fde634a704f817fc8d85dd16a2575972fa6b59 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 12:48:07 -0700 Subject: [PATCH 29/47] chore: add comment for removing BridgeToken.string --- packages/bridge-controller/src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index b1aa34caa53..11c59eebe5d 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -146,6 +146,7 @@ export type BridgeToken = { decimals: number; chainId: number | Hex | ChainId | CaipChainId; balance: string; // raw balance + // TODO deprecate this field and use balance instead string: string | undefined; // normalized balance as a stringified number tokenFiatAmount?: number | null; }; From 977fa2c527e84830c63f4a9712e342e12bc18c2e Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 13:13:46 -0700 Subject: [PATCH 30/47] fix: import --- packages/bridge-controller/src/constants/tokens.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index d8d8fa4de68..89610cfce4d 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,8 +1,8 @@ import type { CaipChainId } from '@metamask/keyring-api'; import { SolScope } from '@metamask/keyring-api'; -import { formatChainIdToCaip } from 'src/utils/caip-formatters'; import { CHAIN_IDS } from './chains'; +import { formatChainIdToCaip } from '../utils/caip-formatters'; export type SwapsTokenObject = { /** From 9c4a47fe77df91b3c8ae9184ed35c487e9097eb6 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 13:46:26 -0700 Subject: [PATCH 31/47] fix: types --- .../bridge-controller/src/constants/tokens.ts | 31 ++++++++++--------- .../bridge-controller/src/utils/bridge.ts | 18 +++++++++-- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index 89610cfce4d..950d43e9bf8 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,8 +1,9 @@ import type { CaipChainId } from '@metamask/keyring-api'; import { SolScope } from '@metamask/keyring-api'; +import type { Hex } from '@metamask/utils'; +import type { AllowedBridgeChainIds } from './bridge'; import { CHAIN_IDS } from './chains'; -import { formatChainIdToCaip } from '../utils/caip-formatters'; export type SwapsTokenObject = { /** @@ -143,23 +144,25 @@ const SOLANA_SWAPS_TOKEN_OBJECT = { const SWAPS_TESTNET_CHAIN_ID = '0x539'; export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { - [formatChainIdToCaip(CHAIN_IDS.MAINNET)]: ETH_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(SWAPS_TESTNET_CHAIN_ID)]: TEST_ETH_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.BSC)]: BNB_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.POLYGON)]: MATIC_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.GOERLI)]: GOERLI_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.SEPOLIA)]: SEPOLIA_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.AVALANCHE)]: AVAX_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.OPTIMISM)]: OPTIMISM_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.ARBITRUM)]: ARBITRUM_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.ZKSYNC_ERA)]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.LINEA_MAINNET)]: LINEA_SWAPS_TOKEN_OBJECT, - [formatChainIdToCaip(CHAIN_IDS.BASE)]: BASE_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.MAINNET]: ETH_SWAPS_TOKEN_OBJECT, + [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.BSC]: BNB_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.POLYGON]: MATIC_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.GOERLI]: GOERLI_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.SEPOLIA]: SEPOLIA_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.AVALANCHE]: AVAX_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.OPTIMISM]: OPTIMISM_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.ARBITRUM]: ARBITRUM_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.ZKSYNC_ERA]: ZKSYNC_ERA_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.LINEA_MAINNET]: LINEA_SWAPS_TOKEN_OBJECT, + [CHAIN_IDS.BASE]: BASE_SWAPS_TOKEN_OBJECT, [SolScope.Mainnet]: SOLANA_SWAPS_TOKEN_OBJECT, } as const; export type SupportedSwapsNativeCurrencySymbols = - (typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[CaipChainId]['symbol']; + (typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP)[ + | AllowedBridgeChainIds + | typeof SWAPS_TESTNET_CHAIN_ID]['symbol']; /** * A map of native currency symbols to their SLIP-44 representation diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index c62dbea152f..d8ce97c3476 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -5,7 +5,11 @@ import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { CaipAssetType, CaipChainId } from '@metamask/utils'; import { isCaipChainId, isStrictHexString, type Hex } from '@metamask/utils'; -import { formatChainIdToCaip, formatChainIdToDec } from './caip-formatters'; +import { + formatChainIdToCaip, + formatChainIdToDec, + formatChainIdToHex, +} from './caip-formatters'; import { DEFAULT_BRIDGE_CONTROLLER_STATE, ETH_USDT_ADDRESS, @@ -41,7 +45,17 @@ export const getNativeAssetForChainId = ( chainId: string | number | Hex | CaipChainId, ): BridgeAsset => { const chainIdInCaip = formatChainIdToCaip(chainId); - const nativeToken = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainIdInCaip]; + const nativeToken = + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + formatChainIdToHex( + chainId, + ) as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ] ?? + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ + formatChainIdToCaip( + chainId, + ) as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP + ]; return { ...nativeToken, chainId: formatChainIdToDec(chainId), From f83ca28b8f172f19abfccd681140487f3fe134ec Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 13:50:49 -0700 Subject: [PATCH 32/47] fix: lint --- packages/bridge-controller/src/constants/tokens.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bridge-controller/src/constants/tokens.ts b/packages/bridge-controller/src/constants/tokens.ts index 950d43e9bf8..2e65e12dee1 100644 --- a/packages/bridge-controller/src/constants/tokens.ts +++ b/packages/bridge-controller/src/constants/tokens.ts @@ -1,6 +1,4 @@ -import type { CaipChainId } from '@metamask/keyring-api'; import { SolScope } from '@metamask/keyring-api'; -import type { Hex } from '@metamask/utils'; import type { AllowedBridgeChainIds } from './bridge'; import { CHAIN_IDS } from './chains'; From 84fd84175fd8522430d6cddd74ea750567ec50b1 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:12:29 -0700 Subject: [PATCH 33/47] chore: update changelog --- packages/bridge-controller/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 304abfb86dc..91ef7d4f6a1 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Solana constants, utils and quote-fetching support +- Utilities to convert chainIds between `ChainId`, `Hex`, `string` and `CaipChainId` +- Add `refreshRate` feature flag to enable chain-specific quote refresh intervals +- `isNativeAddress` and `isSolanaChainId` utilities that can be used by both the controller and clients + +### Changed +- Replace QuoteRequest usages with `GenericQuoteRequest` to support both EVM and multichain input parameters +- Make `QuoteRequest.slippage` optional +- Deprecate `SwapsTokenObject` and replace usages with BridgeAsset (compatible with multichain assets) +- Changed `bridgeFeatureFlags.extensionConfig.chains` to key configs by CAIP chainIds + ## [8.0.0] ### Changed From 3cbf01becba9cf1b9c19e63d893990db9d985c25 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:45:02 -0700 Subject: [PATCH 34/47] fix: format changelog --- packages/bridge-controller/CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 91ef7d4f6a1..06700c572ca 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -8,17 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - Solana constants, utils and quote-fetching support - Utilities to convert chainIds between `ChainId`, `Hex`, `string` and `CaipChainId` - Add `refreshRate` feature flag to enable chain-specific quote refresh intervals - `isNativeAddress` and `isSolanaChainId` utilities that can be used by both the controller and clients ### Changed + - Replace QuoteRequest usages with `GenericQuoteRequest` to support both EVM and multichain input parameters - Make `QuoteRequest.slippage` optional -- Deprecate `SwapsTokenObject` and replace usages with BridgeAsset (compatible with multichain assets) +- Deprecate `SwapsTokenObject` and replace usages with multichain BridgeAsset - Changed `bridgeFeatureFlags.extensionConfig.chains` to key configs by CAIP chainIds - + ## [8.0.0] ### Changed From 03c4c9c3e452b1a9dddaef2ae9c0c499111bb8bc Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 14:58:43 -0700 Subject: [PATCH 35/47] chore: update changelog --- packages/bridge-controller/CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 06700c572ca..48450d7b2fd 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,17 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Solana constants, utils and quote-fetching support -- Utilities to convert chainIds between `ChainId`, `Hex`, `string` and `CaipChainId` -- Add `refreshRate` feature flag to enable chain-specific quote refresh intervals -- `isNativeAddress` and `isSolanaChainId` utilities that can be used by both the controller and clients +- Solana constants, utils, quote and token support ([#5486](https://github.com/MetaMask/core/pull/5486)) +- Utilities to convert chainIds between `ChainId`, `Hex`, `string` and `CaipChainId` ([#5486](https://github.com/MetaMask/core/pull/5486)) +- Add `refreshRate` feature flag to enable chain-specific quote refresh intervals ([#5486](https://github.com/MetaMask/core/pull/5486)) +- `isNativeAddress` and `isSolanaChainId` utilities that can be used by both the controller and clients ([#5486](https://github.com/MetaMask/core/pull/5486)) ### Changed -- Replace QuoteRequest usages with `GenericQuoteRequest` to support both EVM and multichain input parameters -- Make `QuoteRequest.slippage` optional -- Deprecate `SwapsTokenObject` and replace usages with multichain BridgeAsset -- Changed `bridgeFeatureFlags.extensionConfig.chains` to key configs by CAIP chainIds +- Replace QuoteRequest usages with `GenericQuoteRequest` to support both EVM and multichain input parameters ([#5486](https://github.com/MetaMask/core/pull/5486)) +- Make `QuoteRequest.slippage` optional ([#5486](https://github.com/MetaMask/core/pull/5486)) +- Deprecate `SwapsTokenObject` and replace usages with multichain BridgeAsset ([#5486](https://github.com/MetaMask/core/pull/5486)) +- Changed `bridgeFeatureFlags.extensionConfig.chains` to key configs by CAIP chainIds ([#5486](https://github.com/MetaMask/core/pull/5486)) ## [8.0.0] From 15656fc54b45cd390ea359a4ff772c4674038f73 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 15:47:45 -0700 Subject: [PATCH 36/47] chore: export getDefaultBridgeControllerState --- packages/bridge-controller/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 549192446a0..d5e75bffc60 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -77,6 +77,7 @@ export { isNativeAddress, isSolanaChainId, getNativeAssetForChainId, + getDefaultBridgeControllerState, } from './utils/bridge'; export { isValidQuoteRequest } from './utils/quote'; From a0cc5699c65a1b12822ca2150e7a84884af15745 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 15:53:14 -0700 Subject: [PATCH 37/47] chore: validate refreshRate FF --- packages/bridge-controller/src/utils/validators.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/bridge-controller/src/utils/validators.ts b/packages/bridge-controller/src/utils/validators.ts index 8bf40173d72..b7acd361519 100644 --- a/packages/bridge-controller/src/utils/validators.ts +++ b/packages/bridge-controller/src/utils/validators.ts @@ -52,6 +52,8 @@ export const validateFeatureFlagsResponse = ( const ChainConfigurationSchema = type({ isActiveSrc: boolean(), isActiveDest: boolean(), + refreshRate: optional(number()), + topAssets: optional(array(string())), }); const ConfigSchema = type({ From 7b3f11819e6494f9b1766b0197034a23a08a3a3f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 16:03:54 -0700 Subject: [PATCH 38/47] fix: constraints --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 826fd9401d3..9ea38b6b6c3 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -63,7 +63,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/keyring-api": "^17.2.0", - "@metamask/multichain-network-controller": "^0.2.0", + "@metamask/multichain-network-controller": "^0.3.0", "@metamask/network-controller": "^23.0.0", "@metamask/snaps-controllers": "^9.19.0", "@metamask/snaps-utils": "^8.10.0", From 72a1a9f5ae973e14e8d539cd02096064e0016e63 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Wed, 19 Mar 2025 16:15:59 -0700 Subject: [PATCH 39/47] fix: yarn.lock --- yarn.lock | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index 554398907ed..50cf14b5861 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2686,7 +2686,7 @@ __metadata: "@metamask/eth-json-rpc-provider": "npm:^4.1.8" "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/multichain-network-controller": "npm:^0.2.0" + "@metamask/multichain-network-controller": "npm:^0.3.0" "@metamask/network-controller": "npm:^23.0.0" "@metamask/polling-controller": "npm:^13.0.0" "@metamask/snaps-controllers": "npm:^9.19.0" @@ -3653,22 +3653,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-network-controller@npm:^0.2.0": - version: 0.2.0 - resolution: "@metamask/multichain-network-controller@npm:0.2.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": ^26.0.0 - "@metamask/network-controller": ^22.0.0 - checksum: 10/ea393effef16371221dc3189438f1b16fe0d3185fb338472758586d87f01d6d79b9723de349afd2912f383a3f1cde3bb48282cdc340002f819673db7220d60eb - languageName: node - linkType: hard - -"@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": +"@metamask/multichain-network-controller@npm:^0.3.0, @metamask/multichain-network-controller@workspace:packages/multichain-network-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" dependencies: From ebfdb5b5415e36c700c78b57f3f9e2ec7d3f48cc Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 09:32:13 -0700 Subject: [PATCH 40/47] Revert "fix: yarn.lock" This reverts commit 72a1a9f5ae973e14e8d539cd02096064e0016e63. --- yarn.lock | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 50cf14b5861..554398907ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2686,7 +2686,7 @@ __metadata: "@metamask/eth-json-rpc-provider": "npm:^4.1.8" "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/multichain-network-controller": "npm:^0.3.0" + "@metamask/multichain-network-controller": "npm:^0.2.0" "@metamask/network-controller": "npm:^23.0.0" "@metamask/polling-controller": "npm:^13.0.0" "@metamask/snaps-controllers": "npm:^9.19.0" @@ -3653,7 +3653,22 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-network-controller@npm:^0.3.0, @metamask/multichain-network-controller@workspace:packages/multichain-network-controller": +"@metamask/multichain-network-controller@npm:^0.2.0": + version: 0.2.0 + resolution: "@metamask/multichain-network-controller@npm:0.2.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": ^26.0.0 + "@metamask/network-controller": ^22.0.0 + checksum: 10/ea393effef16371221dc3189438f1b16fe0d3185fb338472758586d87f01d6d79b9723de349afd2912f383a3f1cde3bb48282cdc340002f819673db7220d60eb + languageName: node + linkType: hard + +"@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" dependencies: From 22bbc02b5a01619f54dc5caf09617cb8022c4027 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 09:32:25 -0700 Subject: [PATCH 41/47] Revert "fix: constraints" This reverts commit 7b3f11819e6494f9b1766b0197034a23a08a3a3f. --- packages/bridge-controller/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 9ea38b6b6c3..826fd9401d3 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -63,7 +63,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/keyring-api": "^17.2.0", - "@metamask/multichain-network-controller": "^0.3.0", + "@metamask/multichain-network-controller": "^0.2.0", "@metamask/network-controller": "^23.0.0", "@metamask/snaps-controllers": "^9.19.0", "@metamask/snaps-utils": "^8.10.0", From 4f747611b7f160eb7366b56e0b15b3a53a412661 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 10:00:36 -0700 Subject: [PATCH 42/47] chore: add comment for assetType/assetId --- packages/bridge-controller/src/utils/bridge.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index d8ce97c3476..7f548143c81 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -28,6 +28,14 @@ export const getDefaultBridgeControllerState = (): BridgeControllerState => { return DEFAULT_BRIDGE_CONTROLLER_STATE; }; +/** + * Returns the native assetType for a given chainId and native currency symbol + * Note that the return value is used as the assetId although it is a CaipAssetType + * + * @param chainId - The chainId to get the native assetType for + * @param nativeCurrencySymbol - The native currency symbol for the given chainId + * @returns The native assetType for the given chainId + */ const getNativeAssetCaipAssetType = ( chainId: CaipChainId, nativeCurrencySymbol: SupportedSwapsNativeCurrencySymbols, From 8c783cb18bd6286e1aff517079dee18f03f34163 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 10:06:11 -0700 Subject: [PATCH 43/47] fix: constraints --- packages/bridge-controller/package.json | 2 +- yarn.lock | 19 ++----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 826fd9401d3..9ea38b6b6c3 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -63,7 +63,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", "@metamask/keyring-api": "^17.2.0", - "@metamask/multichain-network-controller": "^0.2.0", + "@metamask/multichain-network-controller": "^0.3.0", "@metamask/network-controller": "^23.0.0", "@metamask/snaps-controllers": "^9.19.0", "@metamask/snaps-utils": "^8.10.0", diff --git a/yarn.lock b/yarn.lock index 554398907ed..50cf14b5861 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2686,7 +2686,7 @@ __metadata: "@metamask/eth-json-rpc-provider": "npm:^4.1.8" "@metamask/keyring-api": "npm:^17.2.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/multichain-network-controller": "npm:^0.2.0" + "@metamask/multichain-network-controller": "npm:^0.3.0" "@metamask/network-controller": "npm:^23.0.0" "@metamask/polling-controller": "npm:^13.0.0" "@metamask/snaps-controllers": "npm:^9.19.0" @@ -3653,22 +3653,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain-network-controller@npm:^0.2.0": - version: 0.2.0 - resolution: "@metamask/multichain-network-controller@npm:0.2.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": ^26.0.0 - "@metamask/network-controller": ^22.0.0 - checksum: 10/ea393effef16371221dc3189438f1b16fe0d3185fb338472758586d87f01d6d79b9723de349afd2912f383a3f1cde3bb48282cdc340002f819673db7220d60eb - languageName: node - linkType: hard - -"@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": +"@metamask/multichain-network-controller@npm:^0.3.0, @metamask/multichain-network-controller@workspace:packages/multichain-network-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" dependencies: From 3938915bfd48f300a1e9bb694fc4e0281071180f Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 13:43:58 -0700 Subject: [PATCH 44/47] fix: package.json --- packages/bridge-controller/package.json | 8 ++++---- yarn.lock | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index 9ea38b6b6c3..809d503f4ad 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -54,22 +54,21 @@ "@ethersproject/providers": "^5.7.0", "@metamask/base-controller": "^8.0.0", "@metamask/controller-utils": "^11.6.0", + "@metamask/keyring-api": "^17.2.0", "@metamask/metamask-eth-abis": "^3.1.1", + "@metamask/multichain-network-controller": "^0.3.0", "@metamask/polling-controller": "^13.0.0", + "@metamask/snaps-utils": "^8.10.0", "@metamask/utils": "^11.2.0" }, "devDependencies": { "@metamask/accounts-controller": "^27.0.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/eth-json-rpc-provider": "^4.1.8", - "@metamask/keyring-api": "^17.2.0", - "@metamask/multichain-network-controller": "^0.3.0", "@metamask/network-controller": "^23.0.0", "@metamask/snaps-controllers": "^9.19.0", - "@metamask/snaps-utils": "^8.10.0", "@metamask/superstruct": "^3.1.0", "@metamask/transaction-controller": "^51.0.0", - "@ts-bridge/cli": "^0.6.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -84,6 +83,7 @@ "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" }, "engines": { diff --git a/yarn.lock b/yarn.lock index 50cf14b5861..1826dc40f29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2694,7 +2694,6 @@ __metadata: "@metamask/superstruct": "npm:^3.1.0" "@metamask/transaction-controller": "npm:^51.0.0" "@metamask/utils": "npm:^11.2.0" - "@ts-bridge/cli": "npm:^0.6.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -2708,6 +2707,7 @@ __metadata: 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 languageName: unknown linkType: soft From a213df21a6ee1098d6f2f08d22802302ea145626 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 13:49:45 -0700 Subject: [PATCH 45/47] chore: update changelog --- packages/bridge-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index bccc496a1a2..22c35d1078f 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- BREAKING: Bump dependency @metamask/keyring-api to ^17.2.0 ([#5486](https://github.com/MetaMask/core/pull/5486)) +- BREAKING: Bump dependency @metamask/multichain-network-controller to ^0.3.0 ([#5486](https://github.com/MetaMask/core/pull/5486)) +- BREAKING: Bump dependency @metamask/snaps-utils to ^8.10.0 ([#5486](https://github.com/MetaMask/core/pull/5486)) +- BREAKING: Bump peer dependency @metamask/snaps-controllers to ^9.19.0 ([#5486](https://github.com/MetaMask/core/pull/5486)) - Solana constants, utils, quote and token support ([#5486](https://github.com/MetaMask/core/pull/5486)) - Utilities to convert chainIds between `ChainId`, `Hex`, `string` and `CaipChainId` ([#5486](https://github.com/MetaMask/core/pull/5486)) - Add `refreshRate` feature flag to enable chain-specific quote refresh intervals ([#5486](https://github.com/MetaMask/core/pull/5486)) From ec8afda44a7745957e8fd9bb4107f553343b4982 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 16:23:18 -0700 Subject: [PATCH 46/47] chore: replace AddressZero check with isNativeAddress --- packages/bridge-controller/src/utils/balance.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/balance.ts b/packages/bridge-controller/src/utils/balance.ts index 1fada6d9826..56eb212f5f4 100644 --- a/packages/bridge-controller/src/utils/balance.ts +++ b/packages/bridge-controller/src/utils/balance.ts @@ -1,12 +1,13 @@ import { getAddress } from '@ethersproject/address'; import type { BigNumber } from '@ethersproject/bignumber'; -import { AddressZero } from '@ethersproject/constants'; import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import type { Provider } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; +import { isNativeAddress } from './bridge'; + export const fetchTokenBalance = async ( address: string, userAddress: string, @@ -28,7 +29,7 @@ export const calcLatestSrcBalance = async ( chainId: Hex, ): Promise => { if (tokenAddress && chainId) { - if (tokenAddress === AddressZero) { + if (isNativeAddress(tokenAddress)) { const ethersProvider = new Web3Provider(provider); return await ethersProvider.getBalance(getAddress(selectedAddress)); } From 330335c16d0cc4ebe536895b60462316bb5b76f3 Mon Sep 17 00:00:00 2001 From: Micaela Estabillo Date: Thu, 20 Mar 2025 16:26:22 -0700 Subject: [PATCH 47/47] chore: throw an error if native asset is not found for a chain --- .../src/utils/bridge.test.ts | 55 +++++++++++++++++++ .../bridge-controller/src/utils/bridge.ts | 12 +++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/bridge-controller/src/utils/bridge.test.ts b/packages/bridge-controller/src/utils/bridge.test.ts index 2c597a69ddb..72730cc8492 100644 --- a/packages/bridge-controller/src/utils/bridge.test.ts +++ b/packages/bridge-controller/src/utils/bridge.test.ts @@ -147,4 +147,59 @@ describe('Bridge utils', () => { expect(isSolanaChainId('0x0')).toBe(false); }); }); + + describe('getNativeAssetForChainId', () => { + it('should return native asset for hex chainId', () => { + const result = getNativeAssetForChainId('0x1'); + expect(result).toStrictEqual({ + ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP['0x1'], + chainId: 1, + assetId: 'eip155:1/slip44:60', + }); + }); + + it('should return native asset for decimal chainId', () => { + const result = getNativeAssetForChainId(137); + expect(result).toStrictEqual({ + ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP['0x89'], + chainId: 137, + assetId: 'eip155:137/slip44:966', + }); + }); + + it('should return native asset for CAIP chainId', () => { + const result = getNativeAssetForChainId('eip155:1'); + expect(result).toStrictEqual({ + ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP['0x1'], + chainId: 1, + assetId: 'eip155:1/slip44:60', + }); + }); + + it('should return native asset for Solana chainId', () => { + const result = getNativeAssetForChainId(SolScope.Mainnet); + expect(result).toStrictEqual({ + ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[SolScope.Mainnet], + chainId: 1151111081099710, + assetId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501', + }); + }); + + it('should throw error for unsupported chainId', () => { + expect(() => getNativeAssetForChainId('999999')).toThrow( + 'No XChain Swaps native asset found for chainId: 999999', + ); + }); + + it('should handle different chainId formats for the same chain', () => { + const hexResult = getNativeAssetForChainId('0x89'); + const decimalResult = getNativeAssetForChainId(137); + const stringifiedDecimalResult = getNativeAssetForChainId('137'); + const caipResult = getNativeAssetForChainId('eip155:137'); + + expect(hexResult).toStrictEqual(decimalResult); + expect(decimalResult).toStrictEqual(caipResult); + expect(decimalResult).toStrictEqual(stringifiedDecimalResult); + }); + }); }); diff --git a/packages/bridge-controller/src/utils/bridge.ts b/packages/bridge-controller/src/utils/bridge.ts index 7f548143c81..aaeea071ac6 100644 --- a/packages/bridge-controller/src/utils/bridge.ts +++ b/packages/bridge-controller/src/utils/bridge.ts @@ -48,6 +48,7 @@ const getNativeAssetCaipAssetType = ( * * @param chainId - The chainId to get the default token for * @returns The native asset for the given chainId + * @throws If no native asset is defined for the given chainId */ export const getNativeAssetForChainId = ( chainId: string | number | Hex | CaipChainId, @@ -55,15 +56,22 @@ export const getNativeAssetForChainId = ( const chainIdInCaip = formatChainIdToCaip(chainId); const nativeToken = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - formatChainIdToHex( + formatChainIdToCaip( chainId, ) as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP ] ?? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[ - formatChainIdToCaip( + formatChainIdToHex( chainId, ) as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP ]; + + if (!nativeToken) { + throw new Error( + `No XChain Swaps native asset found for chainId: ${chainId}`, + ); + } + return { ...nativeToken, chainId: formatChainIdToDec(chainId),