import React, { useContext, useEffect, useState, useCallback } from 'react' import PropTypes from 'prop-types' import { useDispatch, useSelector } from 'react-redux' import classnames from 'classnames' import { uniqBy } from 'lodash' import { useHistory } from 'react-router-dom' import { MetaMetricsContext } from '../../../contexts/metametrics.new' import { useTokensToSearch } from '../../../hooks/useTokensToSearch' import { useEqualityCheck } from '../../../hooks/useEqualityCheck' import { I18nContext } from '../../../contexts/i18n' import DropdownInputPair from '../dropdown-input-pair' import DropdownSearchList from '../dropdown-search-list' import SlippageButtons from '../slippage-buttons' import { getTokens } from '../../../ducks/metamask/metamask' import { fetchQuotesAndSetQuoteState, setSwapsFromToken, setSwapToToken, getFromToken, getToToken, getBalanceError, getTopAssets, getFetchParams, } from '../../../ducks/swaps/swaps' import { getValueFromWeiHex } from '../../../helpers/utils/conversions.util' import { calcTokenAmount } from '../../../helpers/utils/token-util' import { usePrevious } from '../../../hooks/usePrevious' import { useTokenTracker } from '../../../hooks/useTokenTracker' import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount' import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount' import { ETH_SWAPS_TOKEN_OBJECT } from '../../../helpers/constants/swaps' import { setMaxMode, resetSwapsPostFetchState, removeToken } from '../../../store/actions' import { fetchTokenPrice, fetchTokenBalance } from '../swaps.util' import SwapsFooter from '../swaps-footer' const fuseSearchKeys = [{ name: 'name', weight: 0.499 }, { name: 'symbol', weight: 0.499 }, { name: 'address', weight: 0.002 }] export default function BuildQuote ({ inputValue, onInputChange, ethBalance, setMaxSlippage, maxSlippage, selectedAccountAddress, }) { const t = useContext(I18nContext) const dispatch = useDispatch() const history = useHistory() const metaMetricsEvent = useContext(MetaMetricsContext) const [fetchedTokenExchangeRate, setFetchedTokenExchangeRate] = useState(undefined) const balanceError = useSelector(getBalanceError) const fetchParams = useSelector(getFetchParams) const { sourceTokenInfo = {}, destinationTokenInfo = {} } = fetchParams?.metaData || {} const tokens = useSelector(getTokens) const topAssets = useSelector(getTopAssets) const fromToken = useSelector(getFromToken) const toToken = useSelector(getToToken) || destinationTokenInfo const fetchParamsFromToken = sourceTokenInfo?.symbol === 'ETH' ? { ...ETH_SWAPS_TOKEN_OBJECT, string: getValueFromWeiHex({ value: ethBalance, numberOfDecimals: 4, toDenomination: 'ETH' }), balance: ethBalance } : sourceTokenInfo const { loading, tokensWithBalances } = useTokenTracker(tokens) // If the fromToken was set in a call to `onFromSelect` (see below), and that from token has a balance // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that // the balance of the token can appear in the from token selection dropdown const fromTokenArray = fromToken?.symbol !== 'ETH' && fromToken?.balance ? [fromToken] : [] const usersTokens = uniqBy([...tokensWithBalances, ...tokens, ...fromTokenArray], 'address') const memoizedUsersTokens = useEqualityCheck(usersTokens) const selectedFromToken = useTokensToSearch({ providedTokens: fromToken || fetchParamsFromToken ? [fromToken || fetchParamsFromToken] : [], rawEthBalance: ethBalance, usersTokens: memoizedUsersTokens, onlyEth: (fromToken || fetchParamsFromToken)?.symbol === 'ETH', singleToken: true, })[0] const tokensToSearch = useTokensToSearch({ rawEthBalance: ethBalance, usersTokens: memoizedUsersTokens, topTokens: topAssets, }) const selectedToToken = tokensToSearch.find(({ address }) => address === toToken?.address) || toToken const { address: fromTokenAddress, symbol: fromTokenSymbol, string: fromTokenString, decimals: fromTokenDecimals, balance: rawFromTokenBalance, } = selectedFromToken || {} const fromTokenBalance = rawFromTokenBalance && calcTokenAmount(rawFromTokenBalance, fromTokenDecimals).toString(10) const prevFromTokenBalance = usePrevious(fromTokenBalance) const swapFromTokenFiatValue = useTokenFiatAmount( fromTokenAddress, inputValue || 0, fromTokenSymbol, { showFiat: true, }, true, ) const swapFromEthFiatValue = useEthFiatAmount(inputValue || 0, { showFiat: true }, true) const swapFromFiatValue = fromTokenSymbol === 'ETH' ? swapFromEthFiatValue : swapFromTokenFiatValue const onFromSelect = (token) => { if (token?.address && !swapFromFiatValue && fetchedTokenExchangeRate !== null) { fetchTokenPrice(token.address) .then((rate) => { if (rate !== null && rate !== undefined) { setFetchedTokenExchangeRate(rate) } }) } else { setFetchedTokenExchangeRate(null) } if (token?.address && !memoizedUsersTokens.find((usersToken) => usersToken.address === token.address)) { fetchTokenBalance(token.address, selectedAccountAddress) .then((fetchedBalance) => { if (fetchedBalance?.balance) { const balanceAsDecString = fetchedBalance.balance.toString(10) const balanceAsHexString = fetchedBalance.balance.toString(16) const userTokenBalance = calcTokenAmount(balanceAsDecString, token.decimals) dispatch(setSwapsFromToken({ ...token, string: userTokenBalance.toString(10), balance: balanceAsHexString })) } }) } dispatch(setSwapsFromToken(token)) dispatch(setMaxMode(false)) onInputChange(token?.address ? inputValue : '', token.string, token.decimals) } const { destinationTokenAddedForSwap } = fetchParams || {} const { address: toAddress } = toToken || {} const onToSelect = useCallback((token) => { if (destinationTokenAddedForSwap && token.address !== toAddress) { dispatch(removeToken(toAddress)) } dispatch(setSwapToToken(token)) }, [dispatch, destinationTokenAddedForSwap, toAddress]) const hideDropdownItemIf = useCallback((item) => item.address === fromTokenAddress, [fromTokenAddress]) useEffect(() => { if (fromToken?.address === ETH_SWAPS_TOKEN_OBJECT.address && (fromToken?.balance !== ethBalance)) { dispatch(setSwapsFromToken({ ...fromToken, balance: ethBalance, string: getValueFromWeiHex({ value: ethBalance, numberOfDecimals: 4, toDenomination: 'ETH' }) })) } }, [dispatch, fromToken, ethBalance]) useEffect(() => { if (prevFromTokenBalance !== fromTokenBalance) { onInputChange(inputValue, fromTokenBalance) } }, [onInputChange, prevFromTokenBalance, inputValue, fromTokenBalance]) useEffect(() => { dispatch(resetSwapsPostFetchState()) }, [dispatch]) return (
{t('swapSwapFrom')}
{ dispatch(setMaxMode(true)) onInputChange(fromTokenBalance || '0', fromTokenBalance) }} >{t('max')}
{ dispatch(setMaxMode(false)) onInputChange(value, fromTokenBalance) }} inputValue={inputValue} leftValue={inputValue && swapFromFiatValue} selectedItem={selectedFromToken} maxListItems={30} loading={loading && (!tokensToSearch?.length || !topAssets || !Object.keys(topAssets).length)} selectPlaceHolderText="Select" hideItemIf={(item) => item.address === selectedToToken?.address} listContainerClassName="build-quote__open-dropdown" />
{!balanceError && fromTokenSymbol && t('swapYourTokenBalance', [fromTokenString || '0', fromTokenSymbol])} {balanceError && fromTokenSymbol && (
{t('swapsNotEnoughForTx', [fromTokenSymbol])}
{t('swapYourTokenBalance', [fromTokenString || '0', fromTokenSymbol])}
)}
{ onToSelect(selectedFromToken) onFromSelect(selectedToToken) }} />
{t('swapSwapTo')}
{ setMaxSlippage(newSlippage) }} />
{ dispatch(fetchQuotesAndSetQuoteState(history, inputValue, maxSlippage, metaMetricsEvent)) }} submitText={t('swapGetQuotes')} disabled={((!Number(inputValue) || !selectedToToken?.address) || (Number(maxSlippage) === 0))} hideCancel />
) } BuildQuote.propTypes = { maxSlippage: PropTypes.number, inputValue: PropTypes.string, onInputChange: PropTypes.func, ethBalance: PropTypes.string, setMaxSlippage: PropTypes.func, selectedAccountAddress: PropTypes.string, }