import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { isEqual } from 'lodash'; import { I18nContext } from '../../../contexts/i18n'; import { getFetchParams, prepareToLeaveSwaps, getCurrentSmartTransactions, getSelectedQuote, getTopQuote, getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, getCurrentSmartTransactionsEnabled, getSwapsNetworkConfig, cancelSwapsSmartTransaction, } from '../../../ducks/swaps/swaps'; import { isHardwareWallet, getHardwareWalletType, getCurrentChainId, getUSDConversionRate, conversionRateSelector, getCurrentCurrency, getRpcPrefsForCurrentProvider, } from '../../../selectors'; import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { DEFAULT_ROUTE, BUILD_QUOTE_ROUTE, } from '../../../helpers/constants/routes'; import Typography from '../../../components/ui/typography'; import Box from '../../../components/ui/box'; import UrlIcon from '../../../components/ui/url-icon'; import { BLOCK_SIZES, COLORS, TYPOGRAPHY, JUSTIFY_CONTENT, DISPLAY, FONT_WEIGHT, ALIGN_ITEMS, } from '../../../helpers/constants/design-system'; import { stopPollingForQuotes, setBackgroundSwapRouteState, } from '../../../store/actions'; import { EVENT } from '../../../../shared/constants/metametrics'; import { SMART_TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; import SwapsFooter from '../swaps-footer'; import { calcTokenAmount } from '../../../helpers/utils/token-util'; import { showRemainingTimeInMinAndSec, getFeeForSmartTransaction, } from '../swaps.util'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import CreateNewSwap from '../create-new-swap'; import ViewOnBlockExplorer from '../view-on-block-explorer'; import SuccessIcon from './success-icon'; import RevertedIcon from './reverted-icon'; import CanceledIcon from './canceled-icon'; import UnknownIcon from './unknown-icon'; import ArrowIcon from './arrow-icon'; import TimerIcon from './timer-icon'; export default function SmartTransactionStatus() { const [cancelSwapLinkClicked, setCancelSwapLinkClicked] = useState(false); const t = useContext(I18nContext); const history = useHistory(); const dispatch = useDispatch(); const fetchParams = useSelector(getFetchParams, isEqual) || {}; const { destinationTokenInfo = {}, sourceTokenInfo = {} } = fetchParams?.metaData || {}; const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const needsTwoConfirmations = true; const selectedQuote = useSelector(getSelectedQuote, isEqual); const topQuote = useSelector(getTopQuote, isEqual); const usedQuote = selectedQuote || topQuote; const currentSmartTransactions = useSelector( getCurrentSmartTransactions, isEqual, ); const smartTransactionsOptInStatus = useSelector( getSmartTransactionsOptInStatus, ); const chainId = useSelector(getCurrentChainId); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual); const swapsNetworkConfig = useSelector(getSwapsNetworkConfig, shallowEqual); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); const baseNetworkUrl = rpcPrefs.blockExplorerUrl ?? SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null; const nativeCurrencySymbol = useSelector(getNativeCurrency); const conversionRate = useSelector(conversionRateSelector); const USDConversionRate = useSelector(getUSDConversionRate); const currentCurrency = useSelector(getCurrentCurrency); let smartTransactionStatus = SMART_TRANSACTION_STATUSES.PENDING; let latestSmartTransaction = {}; let latestSmartTransactionUuid; let cancellationFeeWei; if (currentSmartTransactions && currentSmartTransactions.length > 0) { latestSmartTransaction = currentSmartTransactions[currentSmartTransactions.length - 1]; latestSmartTransactionUuid = latestSmartTransaction?.uuid; smartTransactionStatus = latestSmartTransaction?.status || SMART_TRANSACTION_STATUSES.PENDING; cancellationFeeWei = latestSmartTransaction?.statusMetadata?.cancellationFeeWei; } const [timeLeftForPendingStxInSec, setTimeLeftForPendingStxInSec] = useState( swapsNetworkConfig.stxStatusDeadline, ); const sensitiveProperties = { needs_two_confirmations: needsTwoConfirmations, 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, is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, }; let destinationValue; if (usedQuote?.destinationAmount) { destinationValue = calcTokenAmount( usedQuote?.destinationAmount, destinationTokenInfo.decimals, ).toPrecision(8); } const trackEvent = useContext(MetaMetricsContext); const isSmartTransactionPending = smartTransactionStatus === SMART_TRANSACTION_STATUSES.PENDING; const showCloseButtonOnly = isSmartTransactionPending || smartTransactionStatus === SMART_TRANSACTION_STATUSES.SUCCESS; const txHash = latestSmartTransaction?.statusMetadata?.minedHash; useEffect(() => { trackEvent({ event: 'STX Status Page Loaded', category: EVENT.CATEGORIES.SWAPS, sensitiveProperties, }); // eslint-disable-next-line }, []); useEffect(() => { let intervalId; if (isSmartTransactionPending && latestSmartTransactionUuid) { const calculateRemainingTime = () => { const secondsAfterStxSubmission = Math.round( (Date.now() - latestSmartTransaction.time) / 1000, ); if (secondsAfterStxSubmission > swapsNetworkConfig.stxStatusDeadline) { setTimeLeftForPendingStxInSec(0); clearInterval(intervalId); return; } setTimeLeftForPendingStxInSec( swapsNetworkConfig.stxStatusDeadline - secondsAfterStxSubmission, ); }; intervalId = setInterval(calculateRemainingTime, 1000); calculateRemainingTime(); } return () => clearInterval(intervalId); }, [ dispatch, isSmartTransactionPending, latestSmartTransactionUuid, latestSmartTransaction.time, swapsNetworkConfig.stxStatusDeadline, ]); useEffect(() => { dispatch(setBackgroundSwapRouteState('smartTransactionStatus')); setTimeout(() => { // We don't need to poll for quotes on the status page. dispatch(stopPollingForQuotes()); }, 1000); // Stop polling for quotes after 1s. }, [dispatch]); let headerText = t('stxPendingPrivatelySubmittingSwap'); let description; let subDescription; let icon; let blockExplorerUrl; if (isSmartTransactionPending) { if (cancelSwapLinkClicked) { headerText = t('stxTryingToCancel'); } else if (cancellationFeeWei > 0) { headerText = t('stxPendingPubliclySubmittingSwap'); } } if (smartTransactionStatus === SMART_TRANSACTION_STATUSES.SUCCESS) { headerText = t('stxSuccess'); if (destinationTokenInfo?.symbol) { description = t('stxSuccessDescription', [destinationTokenInfo.symbol]); } icon = ; } else if ( smartTransactionStatus === 'cancelled_user_cancelled' || latestSmartTransaction?.statusMetadata?.minedTx === SMART_TRANSACTION_STATUSES.CANCELLED ) { headerText = t('stxUserCancelled'); description = t('stxUserCancelledDescription'); icon = ; } else if ( smartTransactionStatus.startsWith('cancelled') || smartTransactionStatus.includes('deadline_missed') ) { headerText = t('stxCancelled'); description = t('stxCancelledDescription'); subDescription = t('stxCancelledSubDescription'); icon = ; } else if (smartTransactionStatus === 'unknown') { headerText = t('stxUnknown'); description = t('stxUnknownDescription'); icon = ; } else if (smartTransactionStatus === 'reverted') { headerText = t('stxFailure'); description = t('stxFailureDescription', [ {t('customerSupport')} , ]); icon = ; } if (txHash && latestSmartTransactionUuid) { blockExplorerUrl = getBlockExplorerLink( { hash: txHash, chainId }, { blockExplorerUrl: baseNetworkUrl }, ); } const showCancelSwapLink = latestSmartTransaction.cancellable && !cancelSwapLinkClicked; const CancelSwap = () => { let feeInFiat; if (cancellationFeeWei > 0) { ({ feeInFiat } = getFeeForSmartTransaction({ chainId, currentCurrency, conversionRate, USDConversionRate, nativeCurrencySymbol, feeInWeiDec: cancellationFeeWei, })); } return ( { e?.preventDefault(); setCancelSwapLinkClicked(true); // We want to hide it after a user clicks on it. trackEvent({ event: 'Cancel STX', category: EVENT.CATEGORIES.SWAPS, sensitiveProperties, }); dispatch(cancelSwapsSmartTransaction(latestSmartTransactionUuid)); }} > {feeInFiat ? t('cancelSwapForFee', [feeInFiat]) : t('cancelSwapForFree')} ); }; return (
{`${fetchParams?.value && Number(fetchParams.value).toFixed(5)} `} {sourceTokenInfo?.symbol} {`~${destinationValue && Number(destinationValue).toFixed(5)} `} {destinationTokenInfo?.symbol} {icon && ( {icon} )} {isSmartTransactionPending && ( {`${t('stxSwapCompleteIn')} `} {showRemainingTimeInMinAndSec(timeLeftForPendingStxInSec)} )} {headerText} {isSmartTransactionPending && (
)} {description && ( {description} )} {blockExplorerUrl && ( )} {subDescription && ( {subDescription} )} {showCancelSwapLink && latestSmartTransactionUuid && isSmartTransactionPending && } {smartTransactionStatus === SMART_TRANSACTION_STATUSES.SUCCESS ? ( ) : null} { if (showCloseButtonOnly) { await dispatch(prepareToLeaveSwaps()); history.push(DEFAULT_ROUTE); } else { history.push(BUILD_QUOTE_ROUTE); } }} onCancel={async () => { await dispatch(prepareToLeaveSwaps()); history.push(DEFAULT_ROUTE); }} submitText={showCloseButtonOnly ? t('close') : t('tryAgain')} hideCancel={showCloseButtonOnly} cancelText={t('close')} className="smart-transaction-status__swaps-footer" />
); }