mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-02 06:07:06 +01:00
6e5c2f03bf
* addding the legacy tokenlist, tuning token detection OFF by default, adding new message while importing tokens updating the controller version and calling detectNewToken on network change fixing rebase error Run yarn lavamoat:auto for updating policies updating lavamoat Deleted node modules and run again lavamoat auto fixing rebase issues updating lavamoat policies updating lavamoat after rebasing policies updating custom token warning and blocking detectedtoken link when tpken detection is off for supported networks to update the token in fetchTosync updating the contract map object Revert build-system lavamoat policy changes Move token list selection logic from components to getTokenList selector updating the tokenList Update lavamoat Fix error updating lavamoat lint fix fix unit test fail fix unit test fail lint fix fixing rebase locale error rebase fix Revert build-system policy changes temp addressing review comments * rebase fix
906 lines
30 KiB
JavaScript
906 lines
30 KiB
JavaScript
import React, { useContext, useEffect, useState, useCallback } from 'react';
|
|
import BigNumber from 'bignumber.js';
|
|
import PropTypes from 'prop-types';
|
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
|
import classnames from 'classnames';
|
|
import { uniqBy, isEqual } from 'lodash';
|
|
import { useHistory } from 'react-router-dom';
|
|
import { getTokenTrackerLink } from '@metamask/etherscan-link';
|
|
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
|
import {
|
|
useTokensToSearch,
|
|
getRenderableTokenData,
|
|
} 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, getConversionRate } from '../../../ducks/metamask/metamask';
|
|
import InfoTooltip from '../../../components/ui/info-tooltip';
|
|
import Popover from '../../../components/ui/popover';
|
|
import Button from '../../../components/ui/button';
|
|
import ActionableMessage from '../../../components/ui/actionable-message/actionable-message';
|
|
import Box from '../../../components/ui/box';
|
|
import Typography from '../../../components/ui/typography';
|
|
import {
|
|
TYPOGRAPHY,
|
|
DISPLAY,
|
|
FLEX_DIRECTION,
|
|
FONT_WEIGHT,
|
|
COLORS,
|
|
} from '../../../helpers/constants/design-system';
|
|
import {
|
|
VIEW_QUOTE_ROUTE,
|
|
LOADING_QUOTES_ROUTE,
|
|
} from '../../../helpers/constants/routes';
|
|
|
|
import {
|
|
fetchQuotesAndSetQuoteState,
|
|
setSwapsFromToken,
|
|
setSwapToToken,
|
|
getFromToken,
|
|
getToToken,
|
|
getBalanceError,
|
|
getTopAssets,
|
|
getFetchParams,
|
|
getQuotes,
|
|
setBalanceError,
|
|
setFromTokenInputValue,
|
|
setFromTokenError,
|
|
setMaxSlippage,
|
|
setReviewSwapClickedTimestamp,
|
|
getSmartTransactionsOptInStatus,
|
|
getSmartTransactionsEnabled,
|
|
getCurrentSmartTransactionsEnabled,
|
|
getFromTokenInputValue,
|
|
getFromTokenError,
|
|
getMaxSlippage,
|
|
getIsFeatureFlagLoaded,
|
|
getCurrentSmartTransactionsError,
|
|
getSmartTransactionFees,
|
|
} from '../../../ducks/swaps/swaps';
|
|
import {
|
|
getSwapsDefaultToken,
|
|
getTokenExchangeRates,
|
|
getCurrentCurrency,
|
|
getCurrentChainId,
|
|
getRpcPrefsForCurrentProvider,
|
|
getUseTokenDetection,
|
|
isHardwareWallet,
|
|
getHardwareWalletType,
|
|
} from '../../../selectors';
|
|
|
|
import {
|
|
getValueFromWeiHex,
|
|
hexToDecimal,
|
|
} from '../../../helpers/utils/conversions.util';
|
|
import { calcTokenAmount } from '../../../helpers/utils/token-util';
|
|
import { getURLHostName } from '../../../helpers/utils/util';
|
|
import { usePrevious } from '../../../hooks/usePrevious';
|
|
import { useTokenTracker } from '../../../hooks/useTokenTracker';
|
|
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
|
import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount';
|
|
|
|
import {
|
|
isSwapsDefaultTokenAddress,
|
|
isSwapsDefaultTokenSymbol,
|
|
} from '../../../../shared/modules/swaps.utils';
|
|
import { EVENT } from '../../../../shared/constants/metametrics';
|
|
import {
|
|
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP,
|
|
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
|
|
TOKEN_BUCKET_PRIORITY,
|
|
} from '../../../../shared/constants/swaps';
|
|
|
|
import {
|
|
resetSwapsPostFetchState,
|
|
ignoreTokens,
|
|
setBackgroundSwapRouteState,
|
|
clearSwapsQuotes,
|
|
stopPollingForQuotes,
|
|
setSmartTransactionsOptInStatus,
|
|
clearSmartTransactionFees,
|
|
} from '../../../store/actions';
|
|
import {
|
|
countDecimals,
|
|
fetchTokenPrice,
|
|
fetchTokenBalance,
|
|
shouldEnableDirectWrapping,
|
|
} from '../swaps.util';
|
|
import SwapsFooter from '../swaps-footer';
|
|
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
|
|
|
|
const fuseSearchKeys = [
|
|
{ name: 'name', weight: 0.499 },
|
|
{ name: 'symbol', weight: 0.499 },
|
|
{ name: 'address', weight: 0.002 },
|
|
];
|
|
|
|
const MAX_ALLOWED_SLIPPAGE = 15;
|
|
|
|
let timeoutIdForQuotesPrefetching;
|
|
|
|
export default function BuildQuote({
|
|
ethBalance,
|
|
selectedAccountAddress,
|
|
shuffledTokensList,
|
|
}) {
|
|
const t = useContext(I18nContext);
|
|
const dispatch = useDispatch();
|
|
const history = useHistory();
|
|
const trackEvent = useContext(MetaMetricsContext);
|
|
|
|
const [fetchedTokenExchangeRate, setFetchedTokenExchangeRate] =
|
|
useState(undefined);
|
|
const [verificationClicked, setVerificationClicked] = useState(false);
|
|
|
|
const isFeatureFlagLoaded = useSelector(getIsFeatureFlagLoaded);
|
|
const balanceError = useSelector(getBalanceError);
|
|
const fetchParams = useSelector(getFetchParams, isEqual);
|
|
const { sourceTokenInfo = {}, destinationTokenInfo = {} } =
|
|
fetchParams?.metaData || {};
|
|
const tokens = useSelector(getTokens, isEqual);
|
|
const topAssets = useSelector(getTopAssets, isEqual);
|
|
const fromToken = useSelector(getFromToken, isEqual);
|
|
const fromTokenInputValue = useSelector(getFromTokenInputValue);
|
|
const fromTokenError = useSelector(getFromTokenError);
|
|
const maxSlippage = useSelector(getMaxSlippage);
|
|
const toToken = useSelector(getToToken, isEqual) || destinationTokenInfo;
|
|
const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual);
|
|
const chainId = useSelector(getCurrentChainId);
|
|
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual);
|
|
const useTokenDetection = useSelector(getUseTokenDetection);
|
|
const quotes = useSelector(getQuotes, isEqual);
|
|
const areQuotesPresent = Object.keys(quotes).length > 0;
|
|
|
|
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
|
|
const conversionRate = useSelector(getConversionRate);
|
|
const hardwareWalletUsed = useSelector(isHardwareWallet);
|
|
const hardwareWalletType = useSelector(getHardwareWalletType);
|
|
const smartTransactionsOptInStatus = useSelector(
|
|
getSmartTransactionsOptInStatus,
|
|
);
|
|
const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled);
|
|
const currentSmartTransactionsEnabled = useSelector(
|
|
getCurrentSmartTransactionsEnabled,
|
|
);
|
|
const smartTransactionFees = useSelector(getSmartTransactionFees);
|
|
const smartTransactionsOptInPopoverDisplayed =
|
|
smartTransactionsOptInStatus !== undefined;
|
|
const currentSmartTransactionsError = useSelector(
|
|
getCurrentSmartTransactionsError,
|
|
);
|
|
const currentCurrency = useSelector(getCurrentCurrency);
|
|
|
|
const showSmartTransactionsOptInPopover =
|
|
smartTransactionsEnabled && !smartTransactionsOptInPopoverDisplayed;
|
|
|
|
const onCloseSmartTransactionsOptInPopover = (e) => {
|
|
e?.preventDefault();
|
|
setSmartTransactionsOptInStatus(false, smartTransactionsOptInStatus);
|
|
};
|
|
|
|
const onEnableSmartTransactionsClick = () =>
|
|
setSmartTransactionsOptInStatus(true, smartTransactionsOptInStatus);
|
|
|
|
const fetchParamsFromToken = isSwapsDefaultTokenSymbol(
|
|
sourceTokenInfo?.symbol,
|
|
chainId,
|
|
)
|
|
? defaultSwapsToken
|
|
: 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 =
|
|
!isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance
|
|
? [fromToken]
|
|
: [];
|
|
const usersTokens = uniqBy(
|
|
[...tokensWithBalances, ...tokens, ...fromTokenArray],
|
|
'address',
|
|
);
|
|
const memoizedUsersTokens = useEqualityCheck(usersTokens);
|
|
|
|
const selectedFromToken = getRenderableTokenData(
|
|
fromToken || fetchParamsFromToken,
|
|
tokenConversionRates,
|
|
conversionRate,
|
|
currentCurrency,
|
|
chainId,
|
|
shuffledTokensList,
|
|
useTokenDetection,
|
|
);
|
|
|
|
const tokensToSearchSwapFrom = useTokensToSearch({
|
|
usersTokens: memoizedUsersTokens,
|
|
topTokens: topAssets,
|
|
shuffledTokensList,
|
|
tokenBucketPriority: TOKEN_BUCKET_PRIORITY.OWNED,
|
|
});
|
|
const tokensToSearchSwapTo = useTokensToSearch({
|
|
usersTokens: memoizedUsersTokens,
|
|
topTokens: topAssets,
|
|
shuffledTokensList,
|
|
tokenBucketPriority: TOKEN_BUCKET_PRIORITY.TOP,
|
|
});
|
|
const selectedToToken =
|
|
tokensToSearchSwapFrom.find(({ address }) =>
|
|
isEqualCaseInsensitive(address, toToken?.address),
|
|
) || toToken;
|
|
const toTokenIsNotDefault =
|
|
selectedToToken?.address &&
|
|
!isSwapsDefaultTokenAddress(selectedToToken?.address, chainId);
|
|
const occurrences = Number(
|
|
selectedToToken?.occurances || selectedToToken?.occurrences || 0,
|
|
);
|
|
const {
|
|
address: fromTokenAddress,
|
|
symbol: fromTokenSymbol,
|
|
string: fromTokenString,
|
|
decimals: fromTokenDecimals,
|
|
balance: rawFromTokenBalance,
|
|
} = selectedFromToken || {};
|
|
const { address: toTokenAddress } = selectedToToken || {};
|
|
|
|
const fromTokenBalance =
|
|
rawFromTokenBalance &&
|
|
calcTokenAmount(rawFromTokenBalance, fromTokenDecimals).toString(10);
|
|
|
|
const prevFromTokenBalance = usePrevious(fromTokenBalance);
|
|
|
|
const swapFromTokenFiatValue = useTokenFiatAmount(
|
|
fromTokenAddress,
|
|
fromTokenInputValue || 0,
|
|
fromTokenSymbol,
|
|
{
|
|
showFiat: true,
|
|
},
|
|
true,
|
|
);
|
|
const swapFromEthFiatValue = useEthFiatAmount(
|
|
fromTokenInputValue || 0,
|
|
{ showFiat: true },
|
|
true,
|
|
);
|
|
const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId)
|
|
? swapFromEthFiatValue
|
|
: swapFromTokenFiatValue;
|
|
|
|
const onInputChange = useCallback(
|
|
(newInputValue, balance) => {
|
|
dispatch(setFromTokenInputValue(newInputValue));
|
|
const newBalanceError = new BigNumber(newInputValue || 0).gt(
|
|
balance || 0,
|
|
);
|
|
// "setBalanceError" is just a warning, a user can still click on the "Review swap" button.
|
|
if (balanceError !== newBalanceError) {
|
|
dispatch(setBalanceError(newBalanceError));
|
|
}
|
|
dispatch(
|
|
setFromTokenError(
|
|
fromToken && countDecimals(newInputValue) > fromToken.decimals
|
|
? 'tooManyDecimals'
|
|
: null,
|
|
),
|
|
);
|
|
},
|
|
[dispatch, fromToken, balanceError],
|
|
);
|
|
|
|
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) =>
|
|
isEqualCaseInsensitive(usersToken.address, token.address),
|
|
)
|
|
) {
|
|
fetchTokenBalance(token.address, selectedAccountAddress).then(
|
|
(fetchedBalance) => {
|
|
if (fetchedBalance?.balance) {
|
|
const balanceAsDecString = fetchedBalance.balance.toString(10);
|
|
const userTokenBalance = calcTokenAmount(
|
|
balanceAsDecString,
|
|
token.decimals,
|
|
);
|
|
dispatch(
|
|
setSwapsFromToken({
|
|
...token,
|
|
string: userTokenBalance.toString(10),
|
|
balance: balanceAsDecString,
|
|
}),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
dispatch(setSwapsFromToken(token));
|
|
onInputChange(
|
|
token?.address ? fromTokenInputValue : '',
|
|
token.string,
|
|
token.decimals,
|
|
);
|
|
};
|
|
|
|
const blockExplorerTokenLink = getTokenTrackerLink(
|
|
selectedToToken.address,
|
|
chainId,
|
|
null, // no networkId
|
|
null, // no holderAddress
|
|
{
|
|
blockExplorerUrl:
|
|
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ?? null,
|
|
},
|
|
);
|
|
|
|
const blockExplorerLabel = rpcPrefs.blockExplorerUrl
|
|
? getURLHostName(blockExplorerTokenLink)
|
|
: t('etherscan');
|
|
|
|
const { destinationTokenAddedForSwap } = fetchParams || {};
|
|
const { address: toAddress } = toToken || {};
|
|
const onToSelect = useCallback(
|
|
(token) => {
|
|
if (destinationTokenAddedForSwap && token.address !== toAddress) {
|
|
dispatch(
|
|
ignoreTokens({
|
|
tokensToIgnore: toAddress,
|
|
dontShowLoadingIndicator: true,
|
|
}),
|
|
);
|
|
}
|
|
dispatch(setSwapToToken(token));
|
|
setVerificationClicked(false);
|
|
},
|
|
[dispatch, destinationTokenAddedForSwap, toAddress],
|
|
);
|
|
|
|
const hideDropdownItemIf = useCallback(
|
|
(item) => isEqualCaseInsensitive(item.address, fromTokenAddress),
|
|
[fromTokenAddress],
|
|
);
|
|
|
|
const tokensWithBalancesFromToken = tokensWithBalances.find((token) =>
|
|
isEqualCaseInsensitive(token.address, fromToken?.address),
|
|
);
|
|
const previousTokensWithBalancesFromToken = usePrevious(
|
|
tokensWithBalancesFromToken,
|
|
);
|
|
|
|
useEffect(() => {
|
|
const notDefault = !isSwapsDefaultTokenAddress(
|
|
tokensWithBalancesFromToken?.address,
|
|
chainId,
|
|
);
|
|
const addressesAreTheSame = isEqualCaseInsensitive(
|
|
tokensWithBalancesFromToken?.address,
|
|
previousTokensWithBalancesFromToken?.address,
|
|
);
|
|
const balanceHasChanged =
|
|
tokensWithBalancesFromToken?.balance !==
|
|
previousTokensWithBalancesFromToken?.balance;
|
|
if (notDefault && addressesAreTheSame && balanceHasChanged) {
|
|
dispatch(
|
|
setSwapsFromToken({
|
|
...fromToken,
|
|
balance: tokensWithBalancesFromToken?.balance,
|
|
string: tokensWithBalancesFromToken?.string,
|
|
}),
|
|
);
|
|
}
|
|
}, [
|
|
dispatch,
|
|
tokensWithBalancesFromToken,
|
|
previousTokensWithBalancesFromToken,
|
|
fromToken,
|
|
chainId,
|
|
]);
|
|
|
|
// If the eth balance changes while on build quote, we update the selected from token
|
|
useEffect(() => {
|
|
if (
|
|
isSwapsDefaultTokenAddress(fromToken?.address, chainId) &&
|
|
fromToken?.balance !== hexToDecimal(ethBalance)
|
|
) {
|
|
dispatch(
|
|
setSwapsFromToken({
|
|
...fromToken,
|
|
balance: hexToDecimal(ethBalance),
|
|
string: getValueFromWeiHex({
|
|
value: ethBalance,
|
|
numberOfDecimals: 4,
|
|
toDenomination: 'ETH',
|
|
}),
|
|
}),
|
|
);
|
|
}
|
|
}, [dispatch, fromToken, ethBalance, chainId]);
|
|
|
|
useEffect(() => {
|
|
if (prevFromTokenBalance !== fromTokenBalance) {
|
|
onInputChange(fromTokenInputValue, fromTokenBalance);
|
|
}
|
|
}, [
|
|
onInputChange,
|
|
prevFromTokenBalance,
|
|
fromTokenInputValue,
|
|
fromTokenBalance,
|
|
]);
|
|
|
|
const trackBuildQuotePageLoadedEvent = useCallback(() => {
|
|
trackEvent({
|
|
event: 'Build Quote Page Loaded',
|
|
category: EVENT.CATEGORIES.SWAPS,
|
|
sensitiveProperties: {
|
|
is_hardware_wallet: hardwareWalletUsed,
|
|
hardware_wallet_type: hardwareWalletType,
|
|
stx_enabled: smartTransactionsEnabled,
|
|
current_stx_enabled: currentSmartTransactionsEnabled,
|
|
stx_user_opt_in: smartTransactionsOptInStatus,
|
|
},
|
|
});
|
|
}, [
|
|
trackEvent,
|
|
hardwareWalletUsed,
|
|
hardwareWalletType,
|
|
smartTransactionsEnabled,
|
|
currentSmartTransactionsEnabled,
|
|
smartTransactionsOptInStatus,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
dispatch(resetSwapsPostFetchState());
|
|
dispatch(setReviewSwapClickedTimestamp());
|
|
trackBuildQuotePageLoadedEvent();
|
|
}, [dispatch, trackBuildQuotePageLoadedEvent]);
|
|
|
|
useEffect(() => {
|
|
if (smartTransactionsEnabled && smartTransactionFees?.tradeTxFees) {
|
|
// We want to clear STX fees, because we only want to use fresh ones on the View Quote page.
|
|
clearSmartTransactionFees();
|
|
}
|
|
}, [smartTransactionsEnabled, smartTransactionFees]);
|
|
|
|
const BlockExplorerLink = () => {
|
|
return (
|
|
<a
|
|
className="build-quote__token-etherscan-link build-quote__underline"
|
|
key="build-quote-etherscan-link"
|
|
onClick={() => {
|
|
trackEvent({
|
|
event: 'Clicked Block Explorer Link',
|
|
category: EVENT.CATEGORIES.SWAPS,
|
|
properties: {
|
|
link_type: 'Token Tracker',
|
|
action: 'Swaps Confirmation',
|
|
block_explorer_domain: getURLHostName(blockExplorerTokenLink),
|
|
},
|
|
});
|
|
global.platform.openTab({
|
|
url: blockExplorerTokenLink,
|
|
});
|
|
}}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{blockExplorerLabel}
|
|
</a>
|
|
);
|
|
};
|
|
|
|
let tokenVerificationDescription = '';
|
|
if (blockExplorerTokenLink) {
|
|
if (occurrences === 1) {
|
|
tokenVerificationDescription = t('verifyThisTokenOn', [
|
|
<BlockExplorerLink key="block-explorer-link" />,
|
|
]);
|
|
} else if (occurrences === 0) {
|
|
tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [
|
|
<BlockExplorerLink key="block-explorer-link" />,
|
|
]);
|
|
}
|
|
}
|
|
|
|
const swapYourTokenBalance = t('swapYourTokenBalance', [
|
|
fromTokenString || '0',
|
|
fromTokenSymbol || SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.symbol || '',
|
|
]);
|
|
|
|
const isDirectWrappingEnabled = shouldEnableDirectWrapping(
|
|
chainId,
|
|
fromTokenAddress,
|
|
selectedToToken.address,
|
|
);
|
|
const isReviewSwapButtonDisabled =
|
|
fromTokenError ||
|
|
!isFeatureFlagLoaded ||
|
|
!Number(fromTokenInputValue) ||
|
|
!selectedToToken?.address ||
|
|
!fromTokenAddress ||
|
|
Number(maxSlippage) < 0 ||
|
|
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
|
|
(toTokenIsNotDefault && occurrences < 2 && !verificationClicked);
|
|
|
|
// It's triggered every time there is a change in form values (token from, token to, amount and slippage).
|
|
useEffect(() => {
|
|
dispatch(clearSwapsQuotes());
|
|
dispatch(stopPollingForQuotes());
|
|
const prefetchQuotesWithoutRedirecting = async () => {
|
|
const pageRedirectionDisabled = true;
|
|
await dispatch(
|
|
fetchQuotesAndSetQuoteState(
|
|
history,
|
|
fromTokenInputValue,
|
|
maxSlippage,
|
|
trackEvent,
|
|
pageRedirectionDisabled,
|
|
),
|
|
);
|
|
};
|
|
// Delay fetching quotes until a user is done typing an input value. If they type a new char in less than a second,
|
|
// we will cancel previous setTimeout call and start running a new one.
|
|
timeoutIdForQuotesPrefetching = setTimeout(() => {
|
|
timeoutIdForQuotesPrefetching = null;
|
|
if (!isReviewSwapButtonDisabled) {
|
|
// Only do quotes prefetching if the Review swap button is enabled.
|
|
prefetchQuotesWithoutRedirecting();
|
|
}
|
|
}, 1000);
|
|
return () => clearTimeout(timeoutIdForQuotesPrefetching);
|
|
}, [
|
|
dispatch,
|
|
history,
|
|
maxSlippage,
|
|
trackEvent,
|
|
isReviewSwapButtonDisabled,
|
|
fromTokenInputValue,
|
|
fromTokenAddress,
|
|
toTokenAddress,
|
|
smartTransactionsOptInStatus,
|
|
]);
|
|
|
|
return (
|
|
<div className="build-quote">
|
|
<div className="build-quote__content">
|
|
{showSmartTransactionsOptInPopover && (
|
|
<Popover
|
|
title={t('stxAreHere')}
|
|
footer={
|
|
<>
|
|
<Button type="primary" onClick={onEnableSmartTransactionsClick}>
|
|
{t('enableSmartTransactions')}
|
|
</Button>
|
|
<Box marginTop={1}>
|
|
<Typography variant={TYPOGRAPHY.H6}>
|
|
<Button
|
|
type="link"
|
|
onClick={onCloseSmartTransactionsOptInPopover}
|
|
className="smart-transactions-popover__no-thanks-link"
|
|
>
|
|
{t('noThanksVariant2')}
|
|
</Button>
|
|
</Typography>
|
|
</Box>
|
|
</>
|
|
}
|
|
footerClassName="smart-transactions-popover__footer"
|
|
className="smart-transactions-popover"
|
|
>
|
|
<Box
|
|
paddingRight={6}
|
|
paddingLeft={6}
|
|
paddingTop={0}
|
|
paddingBottom={0}
|
|
display={DISPLAY.FLEX}
|
|
className="smart-transactions-popover__content"
|
|
>
|
|
<Box
|
|
marginTop={0}
|
|
marginBottom={4}
|
|
display={DISPLAY.FLEX}
|
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
|
>
|
|
<img
|
|
src="./images/logo/smart-transactions-header.png"
|
|
alt={t('swapSwapSwitch')}
|
|
/>
|
|
</Box>
|
|
<Typography variant={TYPOGRAPHY.H7} marginTop={0}>
|
|
{t('stxDescription')}
|
|
</Typography>
|
|
<Typography
|
|
tag="ul"
|
|
variant={TYPOGRAPHY.H7}
|
|
fontWeight={FONT_WEIGHT.BOLD}
|
|
marginTop={3}
|
|
>
|
|
<li>{t('stxBenefit1')}</li>
|
|
<li>{t('stxBenefit2')}</li>
|
|
<li>{t('stxBenefit3')}</li>
|
|
<li>
|
|
{t('stxBenefit4')}
|
|
<Typography
|
|
tag="span"
|
|
fontWeight={FONT_WEIGHT.NORMAL}
|
|
variant={TYPOGRAPHY.H7}
|
|
>
|
|
{' *'}
|
|
</Typography>
|
|
</li>
|
|
</Typography>
|
|
<Typography
|
|
variant={TYPOGRAPHY.H8}
|
|
color={COLORS.TEXT_ALTERNATIVE}
|
|
boxProps={{ marginTop: 3 }}
|
|
>
|
|
{t('stxSubDescription')}
|
|
<Typography
|
|
tag="span"
|
|
fontWeight={FONT_WEIGHT.BOLD}
|
|
variant={TYPOGRAPHY.H8}
|
|
color={COLORS.TEXT_ALTERNATIVE}
|
|
>
|
|
{t('stxYouCanOptOut')}
|
|
</Typography>
|
|
</Typography>
|
|
</Box>
|
|
</Popover>
|
|
)}
|
|
<div className="build-quote__dropdown-input-pair-header">
|
|
<div className="build-quote__input-label">{t('swapSwapFrom')}</div>
|
|
{!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && (
|
|
<div
|
|
className="build-quote__max-button"
|
|
data-testid="build-quote__max-button"
|
|
onClick={() =>
|
|
onInputChange(fromTokenBalance || '0', fromTokenBalance)
|
|
}
|
|
>
|
|
{t('max')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DropdownInputPair
|
|
onSelect={onFromSelect}
|
|
itemsToSearch={tokensToSearchSwapFrom}
|
|
onInputChange={(value) => {
|
|
onInputChange(value, fromTokenBalance);
|
|
}}
|
|
inputValue={fromTokenInputValue}
|
|
leftValue={fromTokenInputValue && swapFromFiatValue}
|
|
selectedItem={selectedFromToken}
|
|
maxListItems={30}
|
|
loading={
|
|
loading &&
|
|
(!tokensToSearchSwapFrom?.length ||
|
|
!topAssets ||
|
|
!Object.keys(topAssets).length)
|
|
}
|
|
selectPlaceHolderText={t('swapSelect')}
|
|
hideItemIf={(item) =>
|
|
isEqualCaseInsensitive(item.address, selectedToToken?.address)
|
|
}
|
|
listContainerClassName="build-quote__open-dropdown"
|
|
autoFocus
|
|
/>
|
|
<div
|
|
className={classnames('build-quote__balance-message', {
|
|
'build-quote__balance-message--error':
|
|
balanceError || fromTokenError,
|
|
})}
|
|
>
|
|
{!fromTokenError &&
|
|
!balanceError &&
|
|
fromTokenSymbol &&
|
|
swapYourTokenBalance}
|
|
{!fromTokenError && balanceError && fromTokenSymbol && (
|
|
<div className="build-quite__insufficient-funds">
|
|
<div className="build-quite__insufficient-funds-first">
|
|
{t('swapsNotEnoughForTx', [fromTokenSymbol])}
|
|
</div>
|
|
<div className="build-quite__insufficient-funds-second">
|
|
{swapYourTokenBalance}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{fromTokenError && (
|
|
<>
|
|
<div className="build-quote__form-error">
|
|
{t('swapTooManyDecimalsError', [
|
|
fromTokenSymbol,
|
|
fromTokenDecimals,
|
|
])}
|
|
</div>
|
|
<div>{swapYourTokenBalance}</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="build-quote__swap-arrows-row">
|
|
<button
|
|
className="build-quote__swap-arrows"
|
|
onClick={() => {
|
|
onToSelect(selectedFromToken);
|
|
onFromSelect(selectedToToken);
|
|
}}
|
|
>
|
|
<i className="fa fa-arrow-up" title={t('swapSwapSwitch')} />
|
|
<i className="fa fa-arrow-down" title={t('swapSwapSwitch')} />
|
|
</button>
|
|
</div>
|
|
<div className="build-quote__dropdown-swap-to-header">
|
|
<div className="build-quote__input-label">{t('swapSwapTo')}</div>
|
|
</div>
|
|
<div className="dropdown-input-pair dropdown-input-pair__to">
|
|
<DropdownSearchList
|
|
startingItem={selectedToToken}
|
|
itemsToSearch={tokensToSearchSwapTo}
|
|
searchPlaceholderText={t('swapSearchForAToken')}
|
|
fuseSearchKeys={fuseSearchKeys}
|
|
selectPlaceHolderText={t('swapSelectAToken')}
|
|
maxListItems={30}
|
|
onSelect={onToSelect}
|
|
loading={
|
|
loading &&
|
|
(!tokensToSearchSwapTo?.length ||
|
|
!topAssets ||
|
|
!Object.keys(topAssets).length)
|
|
}
|
|
externallySelectedItem={selectedToToken}
|
|
hideItemIf={hideDropdownItemIf}
|
|
listContainerClassName="build-quote__open-to-dropdown"
|
|
hideRightLabels
|
|
defaultToAll
|
|
shouldSearchForImports
|
|
/>
|
|
</div>
|
|
{toTokenIsNotDefault &&
|
|
(occurrences < 2 ? (
|
|
<ActionableMessage
|
|
type={occurrences === 1 ? 'warning' : 'danger'}
|
|
message={
|
|
<div className="build-quote__token-verification-warning-message">
|
|
<div className="build-quote__bold">
|
|
{occurrences === 1
|
|
? t('swapTokenVerificationOnlyOneSource')
|
|
: t('swapTokenVerificationAddedManually')}
|
|
</div>
|
|
<div>{tokenVerificationDescription}</div>
|
|
</div>
|
|
}
|
|
primaryAction={
|
|
verificationClicked
|
|
? null
|
|
: {
|
|
label: t('continue'),
|
|
onClick: () => setVerificationClicked(true),
|
|
}
|
|
}
|
|
withRightButton
|
|
infoTooltipText={
|
|
blockExplorerTokenLink &&
|
|
t('swapVerifyTokenExplanation', [blockExplorerLabel])
|
|
}
|
|
/>
|
|
) : (
|
|
<div className="build-quote__token-message">
|
|
<span
|
|
className="build-quote__bold"
|
|
key="token-verification-bold-text"
|
|
>
|
|
{t('swapTokenVerificationSources', [occurrences])}
|
|
</span>
|
|
{blockExplorerTokenLink && (
|
|
<>
|
|
{t('swapTokenVerificationMessage', [
|
|
<a
|
|
className="build-quote__token-etherscan-link"
|
|
key="build-quote-etherscan-link"
|
|
onClick={() => {
|
|
trackEvent({
|
|
event: 'Clicked Block Explorer Link',
|
|
category: EVENT.CATEGORIES.SWAPS,
|
|
properties: {
|
|
link_type: 'Token Tracker',
|
|
action: 'Swaps Confirmation',
|
|
block_explorer_domain: getURLHostName(
|
|
blockExplorerTokenLink,
|
|
),
|
|
},
|
|
});
|
|
global.platform.openTab({
|
|
url: blockExplorerTokenLink,
|
|
});
|
|
}}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{blockExplorerLabel}
|
|
</a>,
|
|
])}
|
|
<InfoTooltip
|
|
position="top"
|
|
contentText={t('swapVerifyTokenExplanation', [
|
|
blockExplorerLabel,
|
|
])}
|
|
containerClassName="build-quote__token-tooltip-container"
|
|
key="token-verification-info-tooltip"
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
))}
|
|
{(smartTransactionsEnabled ||
|
|
(!smartTransactionsEnabled && !isDirectWrappingEnabled)) && (
|
|
<div className="build-quote__slippage-buttons-container">
|
|
<SlippageButtons
|
|
onSelect={(newSlippage) => {
|
|
dispatch(setMaxSlippage(newSlippage));
|
|
}}
|
|
maxAllowedSlippage={MAX_ALLOWED_SLIPPAGE}
|
|
currentSlippage={maxSlippage}
|
|
smartTransactionsEnabled={smartTransactionsEnabled}
|
|
smartTransactionsOptInStatus={smartTransactionsOptInStatus}
|
|
setSmartTransactionsOptInStatus={setSmartTransactionsOptInStatus}
|
|
currentSmartTransactionsError={currentSmartTransactionsError}
|
|
isDirectWrappingEnabled={isDirectWrappingEnabled}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<SwapsFooter
|
|
onSubmit={async () => {
|
|
// We need this to know how long it took to go from clicking on the Review swap button to rendered View Quote page.
|
|
dispatch(setReviewSwapClickedTimestamp(Date.now()));
|
|
// In case that quotes prefetching is waiting to be executed, but hasn't started yet,
|
|
// we want to cancel it and fetch quotes from here.
|
|
if (timeoutIdForQuotesPrefetching) {
|
|
clearTimeout(timeoutIdForQuotesPrefetching);
|
|
dispatch(
|
|
fetchQuotesAndSetQuoteState(
|
|
history,
|
|
fromTokenInputValue,
|
|
maxSlippage,
|
|
trackEvent,
|
|
),
|
|
);
|
|
} else if (areQuotesPresent) {
|
|
// If there are prefetched quotes already, go directly to the View Quote page.
|
|
history.push(VIEW_QUOTE_ROUTE);
|
|
} else {
|
|
// If the "Review swap" button was clicked while quotes are being fetched, go to the Loading Quotes page.
|
|
await dispatch(setBackgroundSwapRouteState('loading'));
|
|
history.push(LOADING_QUOTES_ROUTE);
|
|
}
|
|
}}
|
|
submitText={t('swapReviewSwap')}
|
|
disabled={isReviewSwapButtonDisabled}
|
|
hideCancel
|
|
showTermsOfService
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
BuildQuote.propTypes = {
|
|
ethBalance: PropTypes.string,
|
|
selectedAccountAddress: PropTypes.string,
|
|
shuffledTokensList: PropTypes.array,
|
|
};
|