diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 0dae1f432de..7c05c2d8a91 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -562,30 +562,6 @@ "count": 1 } }, - "packages/bridge-controller/src/bridge-controller.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 18 - }, - "@typescript-eslint/naming-convention": { - "count": 4 - } - }, - "packages/bridge-controller/src/selectors.test.ts": { - "@typescript-eslint/prefer-nullish-coalescing": { - "count": 1 - } - }, - "packages/bridge-controller/src/selectors.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 25 - }, - "@typescript-eslint/naming-convention": { - "count": 1 - }, - "no-negated-condition": { - "count": 1 - } - }, "packages/bridge-controller/src/types.ts": { "@typescript-eslint/naming-convention": { "count": 12 @@ -612,11 +588,6 @@ "count": 1 } }, - "packages/bridge-controller/src/utils/caip-formatters.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 2 - } - }, "packages/bridge-controller/src/utils/feature-flags.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 3 @@ -676,14 +647,6 @@ "count": 2 } }, - "packages/bridge-controller/src/utils/quote.ts": { - "@typescript-eslint/explicit-function-return-type": { - "count": 15 - }, - "@typescript-eslint/prefer-nullish-coalescing": { - "count": 4 - } - }, "packages/bridge-controller/src/utils/slippage.ts": { "@typescript-eslint/explicit-function-return-type": { "count": 1 diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 201442d8dc9..a3d48a45d10 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING** Use `gasEstimatesByChainId` instead of `gasEstimates` to remove reference to the global selected network. Clients need to replace gasEstimates with the `gasEstimatesByChainId` state from the GasFeeController when using the `selectBridgeQuotes` selector ([#7826](https://github.com/MetaMask/core/pull/7826)) - Bump `@metamask/transaction-controller` from `^62.13.0` to `^62.14.0` ([#7832](https://github.com/MetaMask/core/pull/7832)) ## [65.2.0] diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 0d0ce7787be..4e57dac02d3 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { BigNumber } from '@ethersproject/bignumber'; import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; @@ -174,11 +175,11 @@ export class BridgeController extends StaticIntervalPollingController( - eventName: T, - properties: CrossChainSwapsEventProperties, + eventName: EventName, + properties: CrossChainSwapsEventProperties, ) => void; readonly #trace: TraceCallback; @@ -208,11 +209,11 @@ export class BridgeController extends StaticIntervalPollingController( - eventName: T, - properties: CrossChainSwapsEventProperties, + eventName: EventName, + properties: CrossChainSwapsEventProperties, ) => void; traceFn?: TraceCallback; }) { @@ -874,12 +875,15 @@ export class BridgeController extends StaticIntervalPollingController( - eventName: T, - propertiesFromClient: Pick[T], - ): CrossChainSwapsEventProperties => { + eventName: EventName, + propertiesFromClient: Pick< + RequiredEventContextFromClient, + EventName + >[EventName], + ): CrossChainSwapsEventProperties => { const baseProperties = { ...propertiesFromClient, action_type: MetricsActionType.SWAPBRIDGE_V1, @@ -1000,14 +1004,17 @@ export class BridgeController extends StaticIntervalPollingController( - eventName: T, - propertiesFromClient: Pick[T], + eventName: EventName, + propertiesFromClient: Pick< + RequiredEventContextFromClient, + EventName + >[EventName], ) => { try { - const combinedPropertiesForEvent = this.#getEventProperties( + const combinedPropertiesForEvent = this.#getEventProperties( eventName, propertiesFromClient, ); diff --git a/packages/bridge-controller/src/selectors.test.ts b/packages/bridge-controller/src/selectors.test.ts index ebe08be0cb1..f0a065c6d7a 100644 --- a/packages/bridge-controller/src/selectors.test.ts +++ b/packages/bridge-controller/src/selectors.test.ts @@ -18,6 +18,7 @@ import { import type { BridgeAsset, QuoteResponse } from './types'; import { SortOrder, RequestStatus, ChainId } from './types'; import { isNativeAddress } from './utils/bridge'; +import { formatChainIdToHex } from './utils/caip-formatters'; describe('Bridge Selectors', () => { describe('selectExchangeRateByChainIdAndAddress', () => { @@ -206,15 +207,19 @@ describe('Bridge Selectors', () => { marketData: {}, conversionRates: {}, participateInMetaMetrics: true, - gasFeeEstimates: { - estimatedBaseFee: '50', - medium: { - suggestedMaxPriorityFeePerGas: '75', - suggestedMaxFeePerGas: '77', - }, - high: { - suggestedMaxPriorityFeePerGas: '100', - suggestedMaxFeePerGas: '102', + gasFeeEstimatesByChainId: { + '0x1': { + gasFeeEstimates: { + estimatedBaseFee: '50', + medium: { + suggestedMaxPriorityFeePerGas: '75', + suggestedMaxFeePerGas: '77', + }, + high: { + suggestedMaxPriorityFeePerGas: '100', + suggestedMaxFeePerGas: '102', + }, + }, }, }, } as unknown as BridgeAppState; @@ -355,55 +360,62 @@ describe('Bridge Selectors', () => { }, }; - const mockState = { - quotes: [ - mockQuote, - { ...mockQuote, quote: { ...mockQuote.quote, requestId: '456' } }, - ], - quoteRequest: { - srcChainId: '1', - destChainId: '137', - srcTokenAddress: '0x0000000000000000000000000000000000000000', - destTokenAddress: '0x0000000000000000000000000000000000000000', - insufficientBal: false, - }, - quotesLastFetched: Date.now(), - quotesLoadingStatus: RequestStatus.FETCHED, - quoteFetchError: null, - quotesRefreshCount: 0, - quotesInitialLoadTime: Date.now(), - remoteFeatureFlags: { - bridgeConfig: { - minimumVersion: '0.0.0', - maxRefreshCount: 5, - refreshRate: 30000, - chainRanking: [], - chains: {}, - support: true, + const getMockState = (chainId: string): BridgeAppState => + ({ + quotes: [ + mockQuote, + { ...mockQuote, quote: { ...mockQuote.quote, requestId: '456' } }, + ], + quoteRequest: { + srcChainId: '1', + destChainId: '137', + srcTokenAddress: '0x0000000000000000000000000000000000000000', + destTokenAddress: '0x0000000000000000000000000000000000000000', + insufficientBal: false, }, - }, - assetExchangeRates: {}, - currencyRates: { - ETH: { - conversionRate: 1800, - usdConversionRate: 1800, + quotesLastFetched: Date.now(), + quotesLoadingStatus: RequestStatus.FETCHED, + quoteFetchError: null, + quotesRefreshCount: 0, + quotesInitialLoadTime: Date.now(), + remoteFeatureFlags: { + bridgeConfig: { + minimumVersion: '0.0.0', + maxRefreshCount: 5, + refreshRate: 30000, + chainRanking: [], + chains: {}, + support: true, + }, }, - }, - marketData: {}, - conversionRates: {}, - participateInMetaMetrics: true, - gasFeeEstimates: { - estimatedBaseFee: '0', - medium: { - suggestedMaxPriorityFeePerGas: '.1', - suggestedMaxFeePerGas: '.1', + assetExchangeRates: {}, + currencyRates: { + ETH: { + conversionRate: 1800, + usdConversionRate: 1800, + }, }, - high: { - suggestedMaxPriorityFeePerGas: '.1', - suggestedMaxFeePerGas: '.2', + marketData: {}, + conversionRates: {}, + participateInMetaMetrics: true, + gasFeeEstimatesByChainId: { + [formatChainIdToHex(chainId)]: { + gasFeeEstimates: { + estimatedBaseFee: '0', + medium: { + suggestedMaxPriorityFeePerGas: '.1', + suggestedMaxFeePerGas: '.1', + }, + high: { + suggestedMaxPriorityFeePerGas: '.1', + suggestedMaxFeePerGas: '.2', + }, + }, + }, }, - }, - } as unknown as BridgeAppState; + }) as unknown as BridgeAppState; + + const mockState = getMockState(mockQuote.quote.srcChainId); const mockClientParams = { sortOrder: SortOrder.COST_ASC, @@ -433,6 +445,7 @@ describe('Bridge Selectors', () => { asset: Pick; }, gasIncluded7702?: boolean, + gasEstimatesChainId?: number, ): BridgeAppState => { const chainId = 56; const currencyRates = { @@ -464,7 +477,9 @@ describe('Bridge Selectors', () => { .multipliedBy(10 ** srcAsset.decimals) .toFixed(0); return { - ...mockState, + ...getMockState( + gasEstimatesChainId?.toString() ?? chainId.toString(), + ), quotes: [ { quote: { @@ -507,7 +522,7 @@ describe('Bridge Selectors', () => { value: isNativeAddress(srcAsset.address) ? toHex( new BigNumber(srcTokenAmount) - .plus(txFee?.amount || '0') + .plus(txFee?.amount ?? '0') .toString(), ) : '0x0', @@ -694,6 +709,92 @@ describe('Bridge Selectors', () => { `); }); + it('erc20 -> native but gas estimates are not available', () => { + const newState = getMockSwapState( + { + address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + decimals: 18, + assetId: + 'eip155:1/erc20:0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + }, + { + address: '0x0000000000000000000000000000000000000000', + decimals: 18, + assetId: + 'eip155:1/erc20:0x0000000000000000000000000000000000000000', + }, + undefined, + undefined, + 1, + ); + + const { sortedQuotes } = selectBridgeQuotes(newState, mockClientParams); + + const { + quote, + trade, + approval, + estimatedProcessingTimeInSeconds, + ...quoteMetadata + } = sortedQuotes[0]; + expect(quoteMetadata).toMatchInlineSnapshot(` + Object { + "adjustedReturn": Object { + "usd": "10.51864197978187625472", + "valueInCurrency": "9.00000000000000008538", + }, + "cost": Object { + "usd": "1.168737997753541695202677292586583974912", + "valueInCurrency": "0.999999999999999914617394921816007289298", + }, + "gasFee": Object { + "effective": Object { + "amount": "0", + "usd": "0", + "valueInCurrency": "0", + }, + "max": Object { + "amount": "0", + "usd": "0", + "valueInCurrency": "0", + }, + "total": Object { + "amount": "0", + "usd": "0", + "valueInCurrency": "0", + }, + }, + "includedTxFees": null, + "minToTokenAmount": Object { + "amount": "0.015489691655494764", + "usd": "9.99270988079278215168", + "valueInCurrency": "8.54999999999999983272", + }, + "sentAmount": Object { + "amount": "11.689344272882887843", + "usd": "11.687379977535417949922677292586583974912", + "valueInCurrency": "9.999999999999999999997394921816007289298", + }, + "swapRate": "0.00139485485277012214", + "toTokenAmount": Object { + "amount": "0.016304938584731331", + "usd": "10.51864197978187625472", + "valueInCurrency": "9.00000000000000008538", + }, + "totalMaxNetworkFee": Object { + "amount": "0", + "usd": "0", + "valueInCurrency": "0", + }, + "totalNetworkFee": Object { + "amount": "0", + "usd": "0", + "valueInCurrency": "0", + }, + } + `); + }); + it('when gas is included and is taken from dest token', () => { const newState = getMockSwapState( { diff --git a/packages/bridge-controller/src/selectors.ts b/packages/bridge-controller/src/selectors.ts index 86bffb17572..3c9a9da568d 100644 --- a/packages/bridge-controller/src/selectors.ts +++ b/packages/bridge-controller/src/selectors.ts @@ -1,10 +1,14 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { AddressZero } from '@ethersproject/constants'; import type { CurrencyRateState, MultichainAssetsRatesControllerState, TokenRatesControllerState, } from '@metamask/assets-controllers'; -import type { GasFeeEstimates } from '@metamask/gas-fee-controller'; +import type { + GasFeeEstimates, + GasFeeEstimatesByChainId, +} from '@metamask/gas-fee-controller'; import type { CaipAssetType } from '@metamask/utils'; import { isStrictHexString } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -66,7 +70,7 @@ type RemoteFeatureFlagControllerState = { }; }; export type BridgeAppState = BridgeControllerState & { - gasFeeEstimates: GasFeeEstimates; + gasFeeEstimatesByChainId: GasFeeEstimatesByChainId; } & ExchangeRateControllerState & { participateInMetaMetrics: boolean; } & RemoteFeatureFlagControllerState; @@ -220,16 +224,39 @@ export const selectIsAssetExchangeRateInState = ( * Selects the gas fee estimates from the gas fee controller. All potential networks * support EIP1559 gas fees so assume that gasFeeEstimates is of type GasFeeEstimates * + * @param state - The state of the bridge controller and its dependency controllers + * @param state.gasFeeEstimatesByChainId - gasEstimates by Hex ChainId + * @param state.quotes - Fetched bridge/swap quotes * @returns The gas fee estimates in decGWEI */ -const selectBridgeFeesPerGas = createStructuredBridgeSelector({ - estimatedBaseFeeInDecGwei: ({ gasFeeEstimates }) => - gasFeeEstimates?.estimatedBaseFee, - feePerGasInDecGwei: ({ gasFeeEstimates }) => - gasFeeEstimates?.[BRIDGE_PREFERRED_GAS_ESTIMATE]?.suggestedMaxFeePerGas, - maxFeePerGasInDecGwei: ({ gasFeeEstimates }) => - gasFeeEstimates?.high?.suggestedMaxFeePerGas, -}); +const selectBridgeFeesPerGas = createBridgeSelector( + [ + (state) => state.gasFeeEstimatesByChainId, + (state) => state.quotes?.[0]?.quote.srcChainId, + ], + (gasFeeEstimatesByChainId, srcChainId) => { + if (!srcChainId) { + return null; + } + if (isNonEvmChainId(srcChainId)) { + return null; + } + // @ts-expect-error - all supported networks use this type of estimates + const gasFeeEstimates: GasFeeEstimates | undefined = + gasFeeEstimatesByChainId?.[ + formatChainIdToHex(srcChainId) as keyof typeof gasFeeEstimatesByChainId + ]?.gasFeeEstimates; + if (!gasFeeEstimates) { + return null; + } + return { + estimatedBaseFeeInDecGwei: gasFeeEstimates.estimatedBaseFee, + feePerGasInDecGwei: + gasFeeEstimates[BRIDGE_PREFERRED_GAS_ESTIMATE]?.suggestedMaxFeePerGas, + maxFeePerGasInDecGwei: gasFeeEstimates.high?.suggestedMaxFeePerGas, + }; + }, +); // Selects cross-chain swap quotes including their metadata const selectBridgeQuotesWithMetadata = createBridgeSelector( @@ -289,19 +316,7 @@ const selectBridgeQuotesWithMetadata = createBridgeSelector( relayerFee, gasFee: QuoteMetadata['gasFee']; - if (!isEvmQuoteResponse(quote)) { - // Use the new generic function for all non-EVM chains - totalEstimatedNetworkFee = calcNonEvmTotalNetworkFee( - quote, - nativeExchangeRate, - ); - gasFee = { - effective: totalEstimatedNetworkFee, - total: totalEstimatedNetworkFee, - max: totalEstimatedNetworkFee, - }; - totalMaxNetworkFee = totalEstimatedNetworkFee; - } else { + if (isEvmQuoteResponse(quote)) { relayerFee = calcRelayerFee(quote, nativeExchangeRate); gasFee = calcEstimatedAndMaxTotalGasFee({ bridgeQuote: quote, @@ -314,6 +329,18 @@ const selectBridgeQuotesWithMetadata = createBridgeSelector( relayerFee, ); totalMaxNetworkFee = calcTotalMaxNetworkFee(gasFee, relayerFee); + } else { + // Use the new generic function for all non-EVM chains + totalEstimatedNetworkFee = calcNonEvmTotalNetworkFee( + quote, + nativeExchangeRate, + ); + gasFee = { + effective: totalEstimatedNetworkFee, + total: totalEstimatedNetworkFee, + max: totalEstimatedNetworkFee, + }; + totalMaxNetworkFee = totalEstimatedNetworkFee; } const adjustedReturn = calcAdjustedReturn( @@ -413,7 +440,7 @@ export const selectIsQuoteExpired = createBridgeSelector( selectIsQuoteGoingToRefresh, ({ quotesLastFetched }) => quotesLastFetched, selectQuoteRefreshRate, - (_, __, currentTimeInMs: number) => currentTimeInMs, + (_, _ignoredParam, currentTimeInMs: number) => currentTimeInMs, ], (isQuoteGoingToRefresh, quotesLastFetched, refreshRate, currentTimeInMs) => Boolean( diff --git a/packages/bridge-controller/src/utils/caip-formatters.ts b/packages/bridge-controller/src/utils/caip-formatters.ts index 498366c7e75..6bdb3386987 100644 --- a/packages/bridge-controller/src/utils/caip-formatters.ts +++ b/packages/bridge-controller/src/utils/caip-formatters.ts @@ -1,6 +1,7 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { getAddress } from '@ethersproject/address'; import { AddressZero } from '@ethersproject/constants'; -import { convertHexToDecimal } from '@metamask/controller-utils'; +import { convertHexToDecimal, toHex } from '@metamask/controller-utils'; import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { @@ -8,7 +9,6 @@ import { isStrictHexString, parseCaipChainId, isCaipReference, - numberToHex, isCaipAssetType, CaipAssetTypeStruct, } from '@metamask/utils'; @@ -48,7 +48,7 @@ export const formatChainIdToCaip = ( if (isTronChainId(chainId)) { return TrxScope.Mainnet; } - return toEvmCaipChainId(numberToHex(Number(chainId))); + return toEvmCaipChainId(toHex(chainId)); }; /** @@ -95,12 +95,12 @@ export const formatChainIdToHex = ( return chainId; } if (typeof chainId === 'number' || parseInt(chainId, 10)) { - return numberToHex(Number(chainId)); + return toHex(chainId); } if (isCaipChainId(chainId)) { const { reference } = parseCaipChainId(chainId); if (isCaipReference(reference) && !isNaN(Number(reference))) { - return numberToHex(Number(reference)); + return toHex(reference); } } // Throw an error if a non-evm chainId is passed to this function diff --git a/packages/bridge-controller/src/utils/quote.ts b/packages/bridge-controller/src/utils/quote.ts index 16c0a336284..29490aa9e35 100644 --- a/packages/bridge-controller/src/utils/quote.ts +++ b/packages/bridge-controller/src/utils/quote.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ import { convertHexToDecimal, toHex, @@ -198,7 +199,7 @@ const calcTotalGasFee = ({ resetApprovalGasLimit?: number | null; tradeGasLimit?: number | null; l1GasFeesInHexWei?: string | null; - feePerGasInDecGwei: string; + feePerGasInDecGwei?: string; nativeToDisplayCurrencyExchangeRate?: string; nativeToUsdExchangeRate?: string; }) => { @@ -208,7 +209,7 @@ const calcTotalGasFee = ({ const l1GasFeesInDecGWei = weiHexToGweiDec(toHex(l1GasFeesInHexWei ?? '0')); const gasFeesInDecGwei = totalGasLimitInDec - .times(feePerGasInDecGwei) + .times(feePerGasInDecGwei ?? '0') .plus(l1GasFeesInDecGWei); const gasFeesInDecEth = gasFeesInDecGwei.times(new BigNumber(10).pow(-9)); @@ -234,8 +235,8 @@ export const calcEstimatedAndMaxTotalGasFee = ({ usdExchangeRate: nativeToUsdExchangeRate, }: { bridgeQuote: QuoteResponse & L1GasFees; - maxFeePerGasInDecGwei: string; - feePerGasInDecGwei: string; + maxFeePerGasInDecGwei?: string; + feePerGasInDecGwei?: string; } & ExchangeRate): QuoteMetadata['gasFee'] => { // Estimated gas fees spent after receiving refunds, this is shown to the user const { @@ -317,12 +318,12 @@ export const calcTotalEstimatedNetworkFee = ( .toString(), valueInCurrency: gasFeeToDisplay?.valueInCurrency ? new BigNumber(gasFeeToDisplay.valueInCurrency) - .plus(relayerFee.valueInCurrency || '0') + .plus(relayerFee.valueInCurrency ?? '0') .toString() : null, usd: gasFeeToDisplay?.usd ? new BigNumber(gasFeeToDisplay.usd) - .plus(relayerFee.usd || '0') + .plus(relayerFee.usd ?? '0') .toString() : null, }; @@ -336,11 +337,11 @@ export const calcTotalMaxNetworkFee = ( amount: new BigNumber(gasFee.max.amount).plus(relayerFee.amount).toString(), valueInCurrency: gasFee.max.valueInCurrency ? new BigNumber(gasFee.max.valueInCurrency) - .plus(relayerFee.valueInCurrency || '0') + .plus(relayerFee.valueInCurrency ?? '0') .toString() : null, usd: gasFee.max.usd - ? new BigNumber(gasFee.max.usd).plus(relayerFee.usd || '0').toString() + ? new BigNumber(gasFee.max.usd).plus(relayerFee.usd ?? '0').toString() : null, }; };