diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7ddf609b48f0..d07953e6bde4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2971,10 +2971,10 @@ "message": "Low" }, "lowEstimatedReturnTooltipMessage": { - "message": "Either your rate or your fees are less favorable than usual. It looks like you'll get back less than $1% of the amount you’re bridging." + "message": "You’ll pay more than $1% of your starting amount in fees. Check your receiving amount and network fees." }, "lowEstimatedReturnTooltipTitle": { - "message": "Low estimated return" + "message": "High cost" }, "lowGasSettingToolTipMessage": { "message": "Use $1 to wait for a cheaper price. Time estimates are much less accurate as prices are somewhat unpredictable.", diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts index ef7cb7f8a785..39f7ba069de8 100644 --- a/shared/constants/bridge.ts +++ b/shared/constants/bridge.ts @@ -27,7 +27,7 @@ export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7'; export const METABRIDGE_ETHEREUM_ADDRESS = '0x0439e60F02a8900a951603950d8D4527f400C3f1'; export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour -export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.8; // if a quote returns in x times less return than the best quote, ignore it +export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.5; // if a quote returns in x times less return than the best quote, ignore it export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high'; export const BRIDGE_DEFAULT_SLIPPAGE = 0.5; diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts index 0e948ee18784..7da72b5c3606 100644 --- a/ui/ducks/bridge/selectors.test.ts +++ b/ui/ducks/bridge/selectors.test.ts @@ -1212,7 +1212,7 @@ describe('Bridge selectors', () => { ).toStrictEqual(false); }); - it('should return isEstimatedReturnLow=true return value is 20% less than sent funds', () => { + it('should return isEstimatedReturnLow=true return value is 50% less than sent funds', () => { const state = createBridgeMockStore({ featureFlagOverrides: { extensionConfig: { @@ -1228,7 +1228,7 @@ describe('Bridge selectors', () => { toToken: { address: zeroAddress(), symbol: 'TEST' }, fromTokenInputValue: '1', fromTokenExchangeRate: 2524.25, - toTokenExchangeRate: 0.798781, + toTokenExchangeRate: 0.61, }, bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20, @@ -1264,11 +1264,11 @@ describe('Bridge selectors', () => { expect( getBridgeQuotes(state as never).activeQuote?.adjustedReturn .valueInCurrency, - ).toStrictEqual(new BigNumber('16.99676538473491988')); + ).toStrictEqual(new BigNumber('12.38316502627291988')); expect(result.isEstimatedReturnLow).toStrictEqual(true); }); - it('should return isEstimatedReturnLow=false when return value is more than 80% of sent funds', () => { + it('should return isEstimatedReturnLow=false when return value is more than 50% of sent funds', () => { const state = createBridgeMockStore({ featureFlagOverrides: { extensionConfig: { @@ -1283,7 +1283,8 @@ describe('Bridge selectors', () => { fromToken: { address: zeroAddress(), symbol: 'ETH' }, toToken: { address: zeroAddress(), symbol: 'TEST' }, fromTokenExchangeRate: 2524.25, - toTokenExchangeRate: 0.998781, + toTokenExchangeRate: 0.63, + fromTokenInputValue: 1, }, bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20, @@ -1320,7 +1321,7 @@ describe('Bridge selectors', () => { expect( getBridgeQuotes(state as never).activeQuote?.adjustedReturn .valueInCurrency, - ).toStrictEqual(new BigNumber('21.88454578473491988')); + ).toStrictEqual(new BigNumber('12.87194306627291988')); expect(result.isEstimatedReturnLow).toStrictEqual(false); }); diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx index 1734624b7dcc..d809f4174e4f 100644 --- a/ui/pages/bridge/prepare/bridge-input-group.tsx +++ b/ui/pages/bridge/prepare/bridge-input-group.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { BigNumber } from 'bignumber.js'; import { getAddress } from 'ethers/lib/utils'; @@ -7,7 +7,6 @@ import { TextField, TextFieldType, ButtonLink, - PopoverPosition, Button, ButtonSize, } from '../../../components/component-library'; @@ -17,7 +16,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getLocale } from '../../../selectors'; import { getCurrentCurrency } from '../../../ducks/metamask/metamask'; import { formatCurrencyAmount, formatTokenAmount } from '../utils/quote'; -import { Column, Row, Tooltip } from '../layout'; +import { Column, Row } from '../layout'; import { Display, FontWeight, @@ -27,7 +26,6 @@ import { TextColor, } from '../../../helpers/constants/design-system'; import { AssetType } from '../../../../shared/constants/transaction'; -import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge'; import useLatestBalance from '../../../hooks/bridge/useLatestBalance'; import { getBridgeQuotes, @@ -87,8 +85,6 @@ export const BridgeInputGroup = ({ const inputRef = useRef(null); - const [isLowReturnTooltipOpen, setIsLowReturnTooltipOpen] = useState(true); - useEffect(() => { if (inputRef.current) { inputRef.current.value = amountFieldProps?.value?.toString() ?? ''; @@ -189,23 +185,6 @@ export const BridgeInputGroup = ({ - {isAmountReadOnly && - isEstimatedReturnLow && - isLowReturnTooltipOpen && ( - setIsLowReturnTooltipOpen(false)} - triggerElement={} - flip={false} - offset={[0, 80]} - > - {t('lowEstimatedReturnTooltipMessage', [ - BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE * 100, - ])} - - )} ( ); const mockFeatureFlags = { - srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], - destNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET], extensionSupport: true, extensionConfig: { refreshRate: 30000, maxRefreshCount: 5, + support: true, + chains: { + '0x1': { isActiveSrc: true, isActiveDest: true }, + '0xa': { isActiveSrc: true, isActiveDest: true }, + }, }, }; const mockBridgeSlice = { diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx index fc076b73629b..70e5f089a33d 100644 --- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx +++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx @@ -91,6 +91,7 @@ import { useBridgeTokens } from '../../../hooks/bridge/useBridgeTokens'; import { getCurrentKeyring, getLocale } from '../../../selectors'; import { isHardwareKeyring } from '../../../helpers/utils/hardware'; import { SECOND } from '../../../../shared/constants/time'; +import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge'; import { BridgeInputGroup } from './bridge-input-group'; import { BridgeCTAButton } from './bridge-cta-button'; @@ -151,10 +152,12 @@ const PrepareBridgePage = () => { const ticker = useSelector(getNativeCurrency); const { + isEstimatedReturnLow, isNoQuotesAvailable, isInsufficientGasForQuote, isInsufficientBalance, } = useSelector(getValidationErrors); + const { quotesRefreshCount } = useSelector(getBridgeQuotes); const { openBuyCryptoInPdapp } = useRamps(); const { balanceAmount: nativeAssetBalance } = useLatestBalance( @@ -190,6 +193,10 @@ const PrepareBridgePage = () => { const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false); + // Resets the banner visibility when the estimated return is low + const [isLowReturnBannerOpen, setIsLowReturnBannerOpen] = useState(true); + useEffect(() => setIsLowReturnBannerOpen(true), [quotesRefreshCount]); + // Background updates are debounced when the switch button is clicked // To prevent putting the frontend in an unexpected state, prevent the user // from switching tokens within the debounce period @@ -211,16 +218,27 @@ const PrepareBridgePage = () => { dispatch(resetBridgeState()); }, []); - const scrollRef = useRef(null); - + // Scroll to bottom of the page when banners are shown + const insufficientBalanceBannerRef = useRef(null); + const isEstimatedReturnLowRef = useRef(null); useEffect(() => { if (isInsufficientGasForQuote(nativeAssetBalance)) { - scrollRef.current?.scrollIntoView({ + insufficientBalanceBannerRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + if (isEstimatedReturnLow) { + isEstimatedReturnLowRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start', }); } - }, [isInsufficientGasForQuote(nativeAssetBalance)]); + }, [ + isEstimatedReturnLow, + isInsufficientGasForQuote(nativeAssetBalance), + isLowReturnBannerOpen, + ]); const quoteParams = useMemo( () => ({ @@ -605,12 +623,26 @@ const PrepareBridgePage = () => { textAlign={TextAlign.Left} /> )} + {isEstimatedReturnLow && isLowReturnBannerOpen && ( + setIsLowReturnBannerOpen(false)} + /> + )} {!isLoading && activeQuote && !isInsufficientBalance(srcTokenBalance) && isInsufficientGasForQuote(nativeAssetBalance) && (

Network fees

@@ -106,19 +107,10 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = ` class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center" >

- $2.52 -

-

- - -

-

- $2.52 + $2.52 - $2.52

Network fees

@@ -270,19 +263,10 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center" >

- $2.52 -

-

- - -

-

- $2.52 + $2.52 - $2.52