import EventEmitter from 'events' import React, { useContext, useRef, useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import PropTypes from 'prop-types' import classnames from 'classnames' import { useHistory } from 'react-router-dom' import { I18nContext } from '../../../contexts/i18n' import { useNewMetricEvent } from '../../../hooks/useMetricEvent' import { MetaMetricsContext } from '../../../contexts/metametrics.new' import { getCurrentCurrency, conversionRateSelector } from '../../../selectors' import { getUsedQuote, getFetchParams, getApproveTxParams, getSwapsTradeTxParams, fetchQuotesAndSetQuoteState, navigateBackToBuildQuote, prepareForRetryGetQuotes, prepareToLeaveSwaps, } from '../../../ducks/swaps/swaps' import { useTransactionTimeRemaining } from '../../../hooks/useTransactionTimeRemaining' import { usePrevious } from '../../../hooks/usePrevious' import Mascot from '../../../components/ui/mascot' import PulseLoader from '../../../components/ui/pulse-loader' import { getBlockExplorerUrlForTx, getStatusKey } from '../../../helpers/utils/transactions.util' import CountdownTimer from '../countdown-timer' import { QUOTES_EXPIRED_ERROR, SWAP_FAILED_ERROR, ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, OFFLINE_FOR_MAINTENANCE, } from '../../../helpers/constants/swaps' import { SUBMITTED_STATUS } from '../../../helpers/constants/transactions' import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes' import { getRenderableGasFeesForQuote } from '../swaps.util' import SwapsFooter from '../swaps-footer' import SwapFailureIcon from './swap-failure-icon' import SwapSuccessIcon from './swap-success-icon' import QuotesTimeoutIcon from './quotes-timeout-icon' import ViewOnEtherScanLink from './view-on-ether-scan-link' export default function AwaitingSwap ({ swapComplete, errorKey, symbol, txHash, networkId, tokensReceived, rpcPrefs, submittingSwap, tradeTxData, usedGasPrice, inputValue, maxSlippage, }) { const t = useContext(I18nContext) const metaMetricsEvent = useContext(MetaMetricsContext) const history = useHistory() const dispatch = useDispatch() const animationEventEmitter = useRef(new EventEmitter()) const fetchParams = useSelector(getFetchParams) const { destinationTokenInfo, sourceTokenInfo } = fetchParams?.metaData || {} const usedQuote = useSelector(getUsedQuote) const approveTxParams = useSelector(getApproveTxParams) const tradeTxParams = useSelector(getSwapsTradeTxParams) const currentCurrency = useSelector(getCurrentCurrency) const conversionRate = useSelector(conversionRateSelector) const [timeRemainingExpired, setTimeRemainingExpired] = useState(false) const [trackedQuotesExpiredEvent, setTrackedQuotesExpiredEvent] = useState(false) let feeinFiat if (usedQuote && tradeTxParams) { const renderableGasFees = getRenderableGasFeesForQuote(usedQuote.gasEstimateWithRefund || usedQuote.averageGas, approveTxParams?.gas || '0x0', tradeTxParams.gasPrice, currentCurrency, conversionRate) feeinFiat = renderableGasFees.feeinFiat?.slice(1) } const quotesExpiredEvent = useNewMetricEvent({ event: 'Quotes Timed Out', excludeMetaMetricsId: true, properties: { token_from: sourceTokenInfo?.symbol, token_from_amount: fetchParams?.value, token_to: destinationTokenInfo?.symbol, request_type: fetchParams?.balanceError ? 'Quote' : 'Order', slippage: fetchParams?.slippage, custom_slippage: fetchParams?.slippage === 2, gas_fees: feeinFiat, }, category: 'swaps', }) const anonymousQuotesExpiredEvent = useNewMetricEvent({ event: 'Quotes Timed Out', category: 'swaps' }) const blockExplorerUrl = txHash && getBlockExplorerUrlForTx( networkId, txHash, rpcPrefs, ) const statusKey = tradeTxData && getStatusKey(tradeTxData) const timeRemaining = useTransactionTimeRemaining( statusKey === SUBMITTED_STATUS, true, tradeTxData?.submittedTime, usedGasPrice, true, true, ) const previousTimeRemaining = usePrevious(timeRemaining) const timeRemainingIsNumber = typeof timeRemaining === 'number' && !isNaN(timeRemaining) const previousTimeRemainingIsNumber = typeof previousTimeRemaining === 'number' && !isNaN(previousTimeRemaining) const estimatedTransactionWaitTime = timeRemaining * 1000 * 60 useEffect(() => { if (!timeRemainingIsNumber && previousTimeRemainingIsNumber && !timeRemainingExpired) { setTimeRemainingExpired(true) } }, [timeRemainingIsNumber, previousTimeRemainingIsNumber, timeRemainingExpired]) let countdownText if (timeRemainingIsNumber && !timeRemainingExpired && tradeTxData?.submittedTime) { countdownText = ( ) } else if (tradeTxData?.submittedTime) { countdownText = t('swapsAlmostDone') } else { countdownText = t('swapEstimatedTimeCalculating') } let headerText let statusImage let descriptionText let submitText let content if (errorKey === OFFLINE_FOR_MAINTENANCE) { headerText = t('offlineForMaintenance') descriptionText = t('metamaskSwapsOfflineDescription') submitText = t('close') statusImage = } else if (errorKey === SWAP_FAILED_ERROR) { headerText = t('swapFailedErrorTitle') descriptionText = t('swapFailedErrorDescription') submitText = t('tryAgain') statusImage = content = blockExplorerUrl && ( ) } else if (errorKey === QUOTES_EXPIRED_ERROR) { headerText = t('swapQuotesExpiredErrorTitle') descriptionText = t('swapQuotesExpiredErrorDescription') submitText = t('tryAgain') statusImage = if (!trackedQuotesExpiredEvent) { setTrackedQuotesExpiredEvent(true) quotesExpiredEvent() anonymousQuotesExpiredEvent() } } else if (errorKey === ERROR_FETCHING_QUOTES) { headerText = t('swapFetchingQuotesErrorTitle') descriptionText = t('swapFetchingQuotesErrorDescription') submitText = t('back') statusImage = } else if (errorKey === QUOTES_NOT_AVAILABLE_ERROR) { headerText = t('swapQuotesNotAvailableErrorTitle') descriptionText = t('swapQuotesNotAvailableErrorDescription') submitText = t('tryAgain') statusImage = } else if (!errorKey && !swapComplete) { /** * only show estimated time if the transaction has a submitted time, the swap has * not yet completed and there isn't an error. If the swap has not completed and * there is no error, but also has no submitted time (has not yet been published), * then we apply the invisible class to the time estimate div. This creates consistent * spacing before and after display of the time estimate. */ headerText = t('swapProcessing') statusImage = submitText = t('swapsViewInActivity') descriptionText = t('swapOnceTransactionHasProcess', [{symbol}]) content = ( <>
{t('swapEstimatedTimeFull', [ {t('swapEstimatedTime')}, countdownText, ])}
{blockExplorerUrl && ( )} ) } else if (!errorKey && swapComplete) { headerText = t('swapTransactionComplete') statusImage = submitText = t('swapViewToken', [symbol]) descriptionText = t('swapTokenAvailable', [{`${tokensReceived || ''} ${symbol}`}]) content = blockExplorerUrl && ( ) } return (
{!(swapComplete || errorKey) && ( )}
{statusImage}
{headerText}
{descriptionText}
{content}
{ if (errorKey === OFFLINE_FOR_MAINTENANCE) { await dispatch(prepareToLeaveSwaps()) history.push(DEFAULT_ROUTE) } else if (errorKey === QUOTES_EXPIRED_ERROR) { dispatch(prepareForRetryGetQuotes()) await dispatch(fetchQuotesAndSetQuoteState(history, inputValue, maxSlippage, metaMetricsEvent)) } else if (errorKey) { await dispatch(navigateBackToBuildQuote(history)) } else if (destinationTokenInfo?.symbol === 'ETH') { history.push(DEFAULT_ROUTE) } else { history.push(`${ASSET_ROUTE}/${destinationTokenInfo?.address}`) } }} onCancel={async () => await dispatch(navigateBackToBuildQuote(history))} submitText={submitText} disabled={submittingSwap} hideCancel={errorKey !== QUOTES_EXPIRED_ERROR} />
) } AwaitingSwap.propTypes = { swapComplete: PropTypes.bool, symbol: PropTypes.string.isRequired, networkId: PropTypes.string.isRequired, txHash: PropTypes.string, tokensReceived: PropTypes.string, rpcPrefs: PropTypes.object.isRequired, errorKey: PropTypes.oneOf([ QUOTES_EXPIRED_ERROR, SWAP_FAILED_ERROR, ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, OFFLINE_FOR_MAINTENANCE, ]), submittingSwap: PropTypes.bool, tradeTxData: PropTypes.object, usedGasPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), inputValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), maxSlippage: PropTypes.number, }