diff --git a/ui/hooks/bridge/useBridgeAmounts.ts b/ui/hooks/bridge/useBridgeAmounts.ts index 54381f6a050e..a8e129c68ecc 100644 --- a/ui/hooks/bridge/useBridgeAmounts.ts +++ b/ui/hooks/bridge/useBridgeAmounts.ts @@ -4,21 +4,16 @@ import { getToChain, getToToken, } from '../../ducks/bridge/selectors'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils'; import { fetchTokenExchangeRates } from '../../helpers/utils/util'; -import { - CHAIN_ID_TO_CURRENCY_SYMBOL_MAP, - CHAIN_IDS, -} from '../../../shared/constants/network'; +import { CHAIN_ID_TO_CURRENCY_SYMBOL_MAP } from '../../../shared/constants/network'; import { zeroAddress } from '../../__mocks__/ethereumjs-util'; import BigNumber from 'bignumber.js'; import { getConversionRate } from '../../ducks/metamask/metamask'; import { getTokenExchangeRates } from '../../selectors'; import { getRelayerFee, getTotalGasFee } from '../../pages/bridge/utils/quote'; -import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants'; import { toChecksumAddress } from 'ethereumjs-util'; -import { deepEqual } from 'assert'; import { isEqual } from 'lodash'; import { useAsyncResult } from '../useAsyncResult'; @@ -37,115 +32,102 @@ const useBridgeAmounts = () => { getTokenExchangeRates, shallowEqual, ); - const fromNativeExchangeRate = useSelector(getConversionRate); - // Contains toToken to dest native asset exchange rate - const [toTokenExchangeRate, setToTokenExchangeRate] = useState< - number | undefined - >(); - const [toNativeExchangeRate, setToNativeExchangeRate] = useState(1); - - const { pending: isToTokenExchangeRateLoading, value: exchangeRates } = - useAsyncResult(async () => { - if (toChain?.chainId && toToken?.address) { - return await fetchTokenExchangeRates( - CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ - toChain.chainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP - ], - [toToken.address], - toChain.chainId, - ); - } - }, [toChain, toToken]); - - useEffect(() => { - if ( - toChain?.chainId && - toToken?.address && - exchangeRates && - !isToTokenExchangeRateLoading - ) { - setToNativeExchangeRate(exchangeRates[zeroAddress()] ?? 1); - if (toToken.address !== zeroAddress()) { - setToTokenExchangeRate( - exchangeRates[toChecksumAddress(toToken.address)], - ); - } else { - setToTokenExchangeRate(1); - } + // TODO move this elsewhere to avoid duplicate spot-prices calls + // Fetch toToken to dest native asset exchange rates + const { value } = useAsyncResult<{ + toTokenExchangeRate: number | undefined; + toNativeExchangeRate: number; + }>(async () => { + if (toChain?.chainId && toToken?.address) { + return await fetchTokenExchangeRates( + CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[ + toChain.chainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP + ], + [toToken.address], + toChain.chainId, + ).then((exchangeRates) => { + return { + toTokenExchangeRate: + toToken.address !== zeroAddress() + ? exchangeRates?.[toChecksumAddress(toToken.address)] + : 1, + toNativeExchangeRate: exchangeRates?.[zeroAddress()] ?? 1, + }; + }); } - }, [toChain, toToken, exchangeRates]); + + return await { toTokenExchangeRate: undefined, toNativeExchangeRate: 1 }; + }, [toChain, toToken]); + const { toTokenExchangeRate, toNativeExchangeRate } = value ?? {}; const toAmounts: Record = useMemo(() => { - return Object.fromEntries( - quotes.map((quote) => { - const normalizedDestAmount = calcTokenAmount( - quote.quote.destTokenAmount, - quote.quote.destAsset.decimals, - ); - return [ - quote.quote.requestId, - { - raw: normalizedDestAmount, - fiat: - toTokenExchangeRate && toNativeExchangeRate - ? normalizedDestAmount - .mul(toTokenExchangeRate.toString()) - .mul(toNativeExchangeRate.toString()) - : null, - }, - ]; - }), - ); + return quotes.reduce((acc, quote) => { + const normalizedDestAmount = calcTokenAmount( + quote.quote.destTokenAmount, + quote.quote.destAsset.decimals, + ); + acc[quote.quote.requestId] = { + raw: normalizedDestAmount, + fiat: + toTokenExchangeRate && toNativeExchangeRate + ? normalizedDestAmount + .mul(toTokenExchangeRate.toString()) + .mul(toNativeExchangeRate.toString()) + : null, + }; + return acc; + }, {} as Record); }, [toTokenExchangeRate, toNativeExchangeRate, quotes]); const fromAmounts: Record = useMemo(() => { - return Object.fromEntries( - quotes.map(({ quote: { requestId, srcTokenAmount, srcAsset } }) => { + return quotes.reduce( + (acc, { quote: { requestId, srcTokenAmount, srcAsset } }) => { const normalizedTokenAmount = calcTokenAmount( srcTokenAmount, srcAsset.decimals, ); - return [ - requestId, - { - raw: normalizedTokenAmount, - fiat: - fromTokenExchangeRates?.[srcAsset.symbol] && - fromNativeExchangeRate - ? normalizedTokenAmount - .mul(fromTokenExchangeRates[srcAsset.address].toString()) - .mul(fromNativeExchangeRate.toString()) - : null, - }, - ]; - }), + acc[requestId] = { + raw: normalizedTokenAmount, + fiat: + fromTokenExchangeRates?.[srcAsset.symbol] && fromNativeExchangeRate + ? normalizedTokenAmount + .mul(fromTokenExchangeRates[srcAsset.address].toString()) + .mul(fromNativeExchangeRate.toString()) + : null, + }; + return acc; + }, + {} as Record, ); }, [fromTokenExchangeRates, fromNativeExchangeRate, quotes]); const gasFees: Record = useMemo(() => { - return Object.fromEntries( - quotes.map((quote) => { - return getTotalGasFee(quote, fromNativeExchangeRate); - }), - ); + return quotes.reduce((acc, quote) => { + const gasFee = getTotalGasFee(quote, fromNativeExchangeRate); + acc[quote.quote.requestId] = gasFee; + return acc; + }, {} as Record); }, [quotes, fromNativeExchangeRate]); const relayerFees: Record = useMemo(() => { - return Object.fromEntries( - quotes.map((quote) => getRelayerFee(quote, fromNativeExchangeRate)), - ); + return quotes.reduce((acc, quote) => { + const relayerFee = getRelayerFee(quote, fromNativeExchangeRate); + acc[quote.quote.requestId] = relayerFee; + return acc; + }, {} as Record); }, [quotes, fromNativeExchangeRate]); - const swapRate = useMemo(() => { - return Object.fromEntries( - quotes.map(({ quote: { requestId, srcAsset, destAsset } }) => [ - requestId, - `1 ${srcAsset.symbol} = ${toAmounts[requestId].raw.div( - fromAmounts[requestId].raw, - )} ${destAsset.symbol}`, - ]), + const swapRates = useMemo(() => { + return quotes.reduce( + (acc, { quote: { requestId, srcAsset, destAsset } }) => { + acc[requestId] = `1 ${srcAsset.symbol} = ${toAmounts[requestId].raw + .div(fromAmounts[requestId].raw) + .toFixed(4)} ${destAsset.symbol}`; + return acc; + }, + {} as Record, ); }, [fromAmounts, toAmounts]); @@ -153,7 +135,7 @@ const useBridgeAmounts = () => { toAmounts, gasFees, relayerFees, - swapRate, + swapRates, }; }; diff --git a/ui/hooks/bridge/useBridgeQuotes.ts b/ui/hooks/bridge/useBridgeQuotes.ts index 53b11619af34..887b25e9655c 100644 --- a/ui/hooks/bridge/useBridgeQuotes.ts +++ b/ui/hooks/bridge/useBridgeQuotes.ts @@ -12,7 +12,7 @@ enum SortOrder { } const MAXIMUM_ETA_SECONDS = 60 * 60; // 1 hour -const MAXIMUM_RETURN_VALUE_DIFFERENCE_PERCENTAGE = 0.8; // if a quote returns in x less return than the best quote, ignore it +const MAXIMUM_RETURN_VALUE_DIFFERENCE_PERCENTAGE = 0.8; // if a quote returns in x times less return than the best quote, ignore it const useBridgeQuotes = () => { const { quotes } = useSelector(getBridgeQuotes); @@ -27,7 +27,7 @@ const useBridgeQuotes = () => { ); }, [quotes]); - const { toAmounts, gasFees, relayerFees } = useBridgeAmounts(); + const { toAmounts, gasFees, relayerFees, swapRates } = useBridgeAmounts(); // Returns {[requestId]: toAmount - gasFees - relayerFees } in fiat const adjustedReturnByRequestId = useMemo(() => { @@ -108,11 +108,12 @@ const useBridgeQuotes = () => { return { recommendedQuote, - toAmount: toAmount, + toAmount, sortedQuotes: sortedRequestIds.map( (requestId) => quotesByRequestId[requestId], ), setSortOrder, + quoteMetadata: { toAmounts, gasFees, relayerFees, swapRates }, }; }; diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx index 011d07b805a4..17c49b097fea 100644 --- a/ui/pages/bridge/quotes/bridge-quote-card.tsx +++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx @@ -14,15 +14,35 @@ import MascotBackgroundAnimation from '../../swaps/mascot-background-animation/m import { QuoteInfoRow } from './quote-info-row'; import { BridgeQuotesModal } from './bridge-quotes-modal'; import useBridgeQuotes from '../../../hooks/bridge/useBridgeQuotes'; +import { getCurrentCurrency } from '../../../selectors'; +import { getNativeCurrency } from '../../../ducks/metamask/metamask'; export const BridgeQuoteCard = () => { const t = useI18nContext(); - const { recommendedQuote } = useBridgeQuotes(); + const { + recommendedQuote, + quoteMetadata: { gasFees, relayerFees, swapRates }, + } = useBridgeQuotes(); const { isLoading } = useSelector(getBridgeQuotes); - const { etaInMinutes, totalFees, quoteRate } = - getQuoteDisplayData(recommendedQuote); + const currency = useSelector(getCurrentCurrency); + const ticker = useSelector(getNativeCurrency); + + const recommendedSwapRate = recommendedQuote?.quote.requestId + ? swapRates[recommendedQuote.quote.requestId] + : undefined; + const { etaInMinutes, totalFees } = getQuoteDisplayData( + ticker, + currency, + recommendedQuote, + recommendedQuote?.quote.requestId + ? gasFees[recommendedQuote.quote.requestId] + : undefined, + recommendedQuote?.quote.requestId + ? relayerFees[recommendedQuote.quote.requestId] + : undefined, + ); const secondsUntilNextRefresh = useCountdownTimer(); @@ -36,7 +56,7 @@ export const BridgeQuoteCard = () => { ); } - return etaInMinutes && totalFees && quoteRate ? ( + return etaInMinutes && totalFees && recommendedSwapRate ? ( { tooltipText={t('bridgeTimingTooltipText')} description={t('bridgeTimingMinutes', [etaInMinutes])} /> - + { - const { - approval, - trade, - quote: { requestId }, - } = bridgeQuote; + const { approval, trade } = bridgeQuote; - const totalGasLimit = new BigNumber(trade.gasLimit ?? 0).plus( - approval?.gasLimit ?? 0, + const totalGasLimit = calcTokenAmount( + new BigNumber(trade.gasLimit ?? 0).plus(approval?.gasLimit ?? 0), + 18, ); // TODO follow gas calculation in https://github.com/MetaMask/metamask-extension/pull/27612 - return [ - requestId, - { - raw: totalGasLimit, - fiat: fromNativeExchangeRate - ? totalGasLimit.mul(fromNativeExchangeRate) - : null, - }, - ]; + return { + raw: totalGasLimit, + fiat: fromNativeExchangeRate + ? totalGasLimit.mul(fromNativeExchangeRate) + : null, + }; }; export const getRelayerFee = ( @@ -35,7 +35,7 @@ export const getRelayerFee = ( fromNativeExchangeRate?: string, ) => { const { - quote: { srcAsset, srcTokenAmount, feeData, requestId }, + quote: { srcAsset, srcTokenAmount, feeData }, trade, } = bridgeQuote; const relayerFeeInNative = calcTokenAmount( @@ -46,36 +46,34 @@ export const getRelayerFee = ( ), 18, ); - return [ - requestId, - { - raw: relayerFeeInNative, - fiat: fromNativeExchangeRate - ? relayerFeeInNative.mul(fromNativeExchangeRate) - : null, - }, - ]; + return { + raw: relayerFeeInNative, + fiat: fromNativeExchangeRate + ? relayerFeeInNative.mul(fromNativeExchangeRate) + : null, + }; }; -export const getQuoteDisplayData = (quoteResponse?: QuoteResponse) => { +export const getQuoteDisplayData = ( + ticker: string, + currency: string, + quoteResponse?: QuoteResponse, + gasFees?: BridgeQuoteAmount, + relayerFees?: BridgeQuoteAmount, +) => { const { quote, estimatedProcessingTimeInSeconds } = quoteResponse ?? {}; if (!quoteResponse || !quote || !estimatedProcessingTimeInSeconds) return {}; const etaInMinutes = (estimatedProcessingTimeInSeconds / 60).toFixed(); - const quoteRate = `1 ${quote.srcAsset.symbol} = ${calcTokenAmount( - quote.destTokenAmount, - quote.destAsset.decimals, - ) - .div(calcTokenAmount(quote.srcTokenAmount, quote.srcAsset.decimals)) - .toFixed(4) - .toString()} ${quote.destAsset.symbol}`; return { etaInMinutes, totalFees: { - amount: '0.01 ETH', // TODO implement - fiat: '$0.01', + amount: `${gasFees?.raw.plus(relayerFees?.raw ?? 0)} ${ticker}`, + fiat: formatCurrency( + (gasFees?.fiat?.plus(relayerFees?.fiat ?? 0) ?? 0).toString(), + currency, + ), }, - quoteRate, }; };