import React, { useState, useEffect, useRef, useContext } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Switch, Route, useLocation, useHistory, Redirect, } from 'react-router-dom'; import BigNumber from 'bignumber.js'; import { shuffle } from 'lodash'; import { I18nContext } from '../../contexts/i18n'; import { getSelectedAccount, getCurrentChainId, getIsSwapsChain, isHardwareWallet, getHardwareWalletType, getTokenList, } from '../../selectors/selectors'; import { getQuotes, clearSwapsState, getTradeTxId, getApproveTxId, getFetchingQuotes, setBalanceError, setTopAssets, getFetchParams, setAggregatorMetadata, getAggregatorMetadata, getBackgroundSwapRouteState, getSwapsErrorKey, getSwapsFeatureIsLive, prepareToLeaveSwaps, fetchAndSetSwapsGasPriceInfo, fetchSwapsLiveness, getUseNewSwapsApi, getFromToken, getReviewSwapClickedTimestamp, } from '../../ducks/swaps/swaps'; import { checkNetworkAndAccountSupports1559, currentNetworkTxListSelector, } from '../../selectors'; import { AWAITING_SIGNATURES_ROUTE, AWAITING_SWAP_ROUTE, BUILD_QUOTE_ROUTE, VIEW_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, DEFAULT_ROUTE, SWAPS_MAINTENANCE_ROUTE, } from '../../helpers/constants/routes'; import { ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, SWAP_FAILED_ERROR, CONTRACT_DATA_DISABLED_ERROR, OFFLINE_FOR_MAINTENANCE, } from '../../../shared/constants/swaps'; import { resetBackgroundSwapsState, setSwapsTokens, removeToken, setBackgroundSwapRouteState, setSwapsErrorKey, } from '../../store/actions'; import { useNewMetricEvent } from '../../hooks/useMetricEvent'; import { useGasFeeEstimates } from '../../hooks/useGasFeeEstimates'; import FeatureToggledRoute from '../../helpers/higher-order-components/feature-toggled-route'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; import { fetchTokens, fetchTopAssets, getSwapsTokensReceivedFromTxMeta, fetchAggregatorMetadata, countDecimals, } from './swaps.util'; import AwaitingSignatures from './awaiting-signatures'; import AwaitingSwap from './awaiting-swap'; import LoadingQuote from './loading-swaps-quotes'; import BuildQuote from './build-quote'; import ViewQuote from './view-quote'; export default function Swap() { const t = useContext(I18nContext); const history = useHistory(); const dispatch = useDispatch(); const { pathname } = useLocation(); const isAwaitingSwapRoute = pathname === AWAITING_SWAP_ROUTE; const isAwaitingSignaturesRoute = pathname === AWAITING_SIGNATURES_ROUTE; const isSwapsErrorRoute = pathname === SWAPS_ERROR_ROUTE; const isLoadingQuotesRoute = pathname === LOADING_QUOTES_ROUTE; const fetchParams = useSelector(getFetchParams); const { destinationTokenInfo = {} } = fetchParams?.metaData || {}; const [inputValue, setInputValue] = useState(fetchParams?.value || ''); const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 3); const [isFeatureFlagLoaded, setIsFeatureFlagLoaded] = useState(false); const [tokenFromError, setTokenFromError] = useState(null); const routeState = useSelector(getBackgroundSwapRouteState); const selectedAccount = useSelector(getSelectedAccount); const quotes = useSelector(getQuotes); const txList = useSelector(currentNetworkTxListSelector); const tradeTxId = useSelector(getTradeTxId); const approveTxId = useSelector(getApproveTxId); const aggregatorMetadata = useSelector(getAggregatorMetadata); const fetchingQuotes = useSelector(getFetchingQuotes); let swapsErrorKey = useSelector(getSwapsErrorKey); const swapsEnabled = useSelector(getSwapsFeatureIsLive); const chainId = useSelector(getCurrentChainId); const isSwapsChain = useSelector(getIsSwapsChain); const useNewSwapsApi = useSelector(getUseNewSwapsApi); const prevUseNewSwapsApi = useRef(useNewSwapsApi); const networkAndAccountSupports1559 = useSelector( checkNetworkAndAccountSupports1559, ); const fromToken = useSelector(getFromToken); const tokenList = useSelector(getTokenList); const listTokenValues = shuffle(Object.values(tokenList)); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp); if (networkAndAccountSupports1559) { // This will pre-load gas fees before going to the View Quote page. // eslint-disable-next-line react-hooks/rules-of-hooks useGasFeeEstimates(); } const { balance: ethBalance, address: selectedAccountAddress, } = selectedAccount; const { destinationTokenAddedForSwap } = fetchParams || {}; const approveTxData = approveTxId && txList.find(({ id }) => approveTxId === id); const tradeTxData = tradeTxId && txList.find(({ id }) => tradeTxId === id); const tokensReceived = tradeTxData?.txReceipt && getSwapsTokensReceivedFromTxMeta( destinationTokenInfo?.symbol, tradeTxData, destinationTokenInfo?.address, selectedAccountAddress, destinationTokenInfo?.decimals, approveTxData, chainId, ); const tradeConfirmed = tradeTxData?.status === TRANSACTION_STATUSES.CONFIRMED; const approveError = approveTxData?.status === TRANSACTION_STATUSES.FAILED || approveTxData?.txReceipt?.status === '0x0'; const tradeError = tradeTxData?.status === TRANSACTION_STATUSES.FAILED || tradeTxData?.txReceipt?.status === '0x0'; const conversionError = approveError || tradeError; if (conversionError && swapsErrorKey !== CONTRACT_DATA_DISABLED_ERROR) { swapsErrorKey = SWAP_FAILED_ERROR; } const clearTemporaryTokenRef = useRef(); useEffect(() => { clearTemporaryTokenRef.current = () => { if ( destinationTokenAddedForSwap && (!isAwaitingSwapRoute || conversionError) ) { dispatch(removeToken(destinationTokenInfo?.address)); } }; }, [ conversionError, dispatch, destinationTokenAddedForSwap, destinationTokenInfo, fetchParams, isAwaitingSwapRoute, ]); useEffect(() => { return () => { clearTemporaryTokenRef.current(); }; }, []); // eslint-disable-next-line useEffect(() => { if (isFeatureFlagLoaded && prevUseNewSwapsApi.current === useNewSwapsApi) { fetchTokens(chainId, useNewSwapsApi) .then((tokens) => { dispatch(setSwapsTokens(tokens)); }) .catch((error) => console.error(error)); fetchTopAssets(chainId, useNewSwapsApi).then((topAssets) => { dispatch(setTopAssets(topAssets)); }); fetchAggregatorMetadata(chainId, useNewSwapsApi).then( (newAggregatorMetadata) => { dispatch(setAggregatorMetadata(newAggregatorMetadata)); }, ); if (!networkAndAccountSupports1559) { dispatch(fetchAndSetSwapsGasPriceInfo(chainId)); } return () => { dispatch(prepareToLeaveSwaps()); }; } prevUseNewSwapsApi.current = useNewSwapsApi; }, [ dispatch, chainId, isFeatureFlagLoaded, useNewSwapsApi, networkAndAccountSupports1559, ]); const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const exitedSwapsEvent = useNewMetricEvent({ event: 'Exited Swaps', category: 'swaps', sensitiveProperties: { token_from: fetchParams?.sourceTokenInfo?.symbol, token_from_amount: fetchParams?.value, request_type: fetchParams?.balanceError, token_to: fetchParams?.destinationTokenInfo?.symbol, slippage: fetchParams?.slippage, custom_slippage: fetchParams?.slippage !== 2, current_screen: pathname.match(/\/swaps\/(.+)/u)[1], is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, }, }); const exitEventRef = useRef(); useEffect(() => { exitEventRef.current = () => { exitedSwapsEvent(); }; }); useEffect(() => { const fetchSwapsLivenessWrapper = async () => { await dispatch(fetchSwapsLiveness()); setIsFeatureFlagLoaded(true); }; fetchSwapsLivenessWrapper(); return () => { exitEventRef.current(); }; }, [dispatch]); useEffect(() => { // If there is a swapsErrorKey and reviewSwapClicked is false, there was an error in silent quotes prefetching // and we don't want to show the error page in that case, because another API call for quotes can be successful. if (swapsErrorKey && !isSwapsErrorRoute && reviewSwapClicked) { history.push(SWAPS_ERROR_ROUTE); } }, [history, swapsErrorKey, isSwapsErrorRoute, reviewSwapClicked]); const beforeUnloadEventAddedRef = useRef(); useEffect(() => { const fn = () => { clearTemporaryTokenRef.current(); if (isLoadingQuotesRoute) { dispatch(prepareToLeaveSwaps()); } return null; }; if (isLoadingQuotesRoute && !beforeUnloadEventAddedRef.current) { beforeUnloadEventAddedRef.current = true; window.addEventListener('beforeunload', fn); } return () => window.removeEventListener('beforeunload', fn); }, [dispatch, isLoadingQuotesRoute]); if (!isSwapsChain) { return ; } return (
{t('swap')}
{!isAwaitingSwapRoute && !isAwaitingSignaturesRoute && (
{ clearTemporaryTokenRef.current(); dispatch(clearSwapsState()); await dispatch(resetBackgroundSwapsState()); history.push(DEFAULT_ROUTE); }} > {t('cancel')}
)}
{ if (tradeTxData && !conversionError) { return ; } else if (tradeTxData && routeState) { return ; } else if (routeState === 'loading' && aggregatorMetadata) { return ; } const onInputChange = (newInputValue, balance) => { setInputValue(newInputValue); const balanceError = new BigNumber(newInputValue || 0).gt( balance || 0, ); // "setBalanceError" is just a warning, a user can still click on the "Review Swap" button. dispatch(setBalanceError(balanceError)); setTokenFromError( fromToken && countDecimals(newInputValue) > fromToken.decimals ? 'tooManyDecimals' : null, ); }; return ( ); }} /> { if (Object.values(quotes).length) { return ( ); } else if (fetchParams) { return ; } return ; }} /> { if (swapsErrorKey) { return ( ); } return ; }} /> { return aggregatorMetadata ? ( { await dispatch(setBackgroundSwapRouteState('')); if ( swapsErrorKey === ERROR_FETCHING_QUOTES || swapsErrorKey === QUOTES_NOT_AVAILABLE_ERROR ) { dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)); history.push(SWAPS_ERROR_ROUTE); } else { history.push(VIEW_QUOTE_ROUTE); } }} aggregatorMetadata={aggregatorMetadata} /> ) : ( ); }} /> { return swapsEnabled === false ? ( ) : ( ); }} /> { return ; }} /> { return routeState === 'awaiting' || tradeTxData ? ( ) : ( ); }} />
); }