import EventEmitter from 'events'; import React, { useContext, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import { createCustomExplorerLink } from '@metamask/etherscan-link'; import { I18nContext } from '../../../contexts/i18n'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { MetaMetricsContext } from '../../../contexts/metametrics.new'; import { getCurrentChainId, getCurrentCurrency, getRpcPrefsForCurrentProvider, getUSDConversionRate, } from '../../../selectors'; import { getUsedQuote, getFetchParams, getApproveTxParams, getUsedSwapsGasPrice, fetchQuotesAndSetQuoteState, navigateBackToBuildQuote, prepareForRetryGetQuotes, prepareToLeaveSwaps, } from '../../../ducks/swaps/swaps'; import Mascot from '../../../components/ui/mascot'; import { QUOTES_EXPIRED_ERROR, SWAP_FAILED_ERROR, ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, OFFLINE_FOR_MAINTENANCE, SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, } from '../../../../../shared/constants/swaps'; import { CHAIN_ID_TO_TYPE_MAP as VALID_INFURA_CHAIN_IDS } from '../../../../../shared/constants/network'; import { isSwapsDefaultTokenSymbol } from '../../../../../shared/modules/swaps.utils'; import PulseLoader from '../../../components/ui/pulse-loader'; import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getRenderableNetworkFeesForQuote } from '../swaps.util'; import SwapsFooter from '../swaps-footer'; import { getBlockExplorerUrlForTx } from '../../../../../shared/modules/transaction.utils'; 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, txHash, tokensReceived, submittingSwap, 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 swapsGasPrice = useSelector(getUsedSwapsGasPrice); const currentCurrency = useSelector(getCurrentCurrency); const usdConversionRate = useSelector(getUSDConversionRate); const chainId = useSelector(getCurrentChainId); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const [trackedQuotesExpiredEvent, setTrackedQuotesExpiredEvent] = useState( false, ); let feeinUnformattedFiat; if (usedQuote && swapsGasPrice) { const renderableNetworkFees = getRenderableNetworkFeesForQuote({ tradeGas: usedQuote.gasEstimateWithRefund || usedQuote.averageGas, approveGas: approveTxParams?.gas || '0x0', gasPrice: swapsGasPrice, currentCurrency, conversionRate: usdConversionRate, tradeValue: usedQuote?.trade?.value, sourceSymbol: sourceTokenInfo?.symbol, sourceAmount: usedQuote.sourceAmount, chainId, }); feeinUnformattedFiat = renderableNetworkFees.rawNetworkFees; } const quotesExpiredEvent = useNewMetricEvent({ event: 'Quotes Timed Out', sensitiveProperties: { 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: feeinUnformattedFiat, }, category: 'swaps', }); let blockExplorerUrl; if (txHash && rpcPrefs.blockExplorerUrl) { blockExplorerUrl = getBlockExplorerUrlForTx({ hash: txHash }, rpcPrefs); } else if (txHash && SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) { blockExplorerUrl = createCustomExplorerLink( txHash, SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], ); } else if (txHash && VALID_INFURA_CHAIN_IDS[chainId]) { blockExplorerUrl = getBlockExplorerUrlForTx({ chainId, hash: txHash }); } const isCustomBlockExplorerUrl = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] || rpcPrefs.blockExplorerUrl; 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('swapFailedErrorDescriptionWithSupportLink', [ support.metamask.io , ]); 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(); } } 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) { headerText = t('swapProcessing'); statusImage = ; submitText = t('swapsViewInActivity'); descriptionText = t('swapOnceTransactionHasProcess', [ {destinationTokenInfo.symbol} , ]); content = blockExplorerUrl && ( ); } else if (!errorKey && swapComplete) { headerText = t('swapTransactionComplete'); statusImage = ; submitText = t('swapViewToken', [destinationTokenInfo.symbol]); descriptionText = t('swapTokenAvailable', [ {`${tokensReceived || ''} ${destinationTokenInfo.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 ( isSwapsDefaultTokenSymbol(destinationTokenInfo?.symbol, chainId) ) { 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, txHash: PropTypes.string, tokensReceived: PropTypes.string, errorKey: PropTypes.oneOf([ QUOTES_EXPIRED_ERROR, SWAP_FAILED_ERROR, ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, OFFLINE_FOR_MAINTENANCE, ]), submittingSwap: PropTypes.bool, inputValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), maxSlippage: PropTypes.number, };