import { NativeStackScreenProps } from '@react-navigation/native-stack' import { PayloadAction, createSlice } from '@reduxjs/toolkit' import BigNumber from 'bignumber.js' import React, { useEffect, useMemo, useReducer, useRef } from 'react' import { Trans, useTranslation } from 'react-i18next' import { StyleSheet, Text, View } from 'react-native' import { ScrollView } from 'react-native-gesture-handler' import { getNumberFormatSettings } from 'react-native-localize' import { SafeAreaView } from 'react-native-safe-area-context' import { showError } from 'src/alert/actions' import AppAnalytics from 'src/analytics/AppAnalytics' import { SwapEvents } from 'src/analytics/Events' import { ErrorMessages } from 'src/app/ErrorMessages' import BackButton from 'src/components/BackButton' import BottomSheet, { BottomSheetModalRefType } from 'src/components/BottomSheet' import Button, { BtnSizes, BtnTypes } from 'src/components/Button' import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification' import Toast from 'src/components/Toast' import TokenBottomSheet, { TokenPickerOrigin } from 'src/components/TokenBottomSheet' import Touchable from 'src/components/Touchable' import CustomHeader from 'src/components/header/CustomHeader' import ArrowDown from 'src/icons/ArrowDown' import CircledIcon from 'src/icons/CircledIcon' import CrossChainIndicator from 'src/icons/CrossChainIndicator' import { getLocalCurrencyCode } from 'src/localCurrency/selectors' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' import { useDispatch, useSelector } from 'src/redux/hooks' import EnterAmountOptions from 'src/send/EnterAmountOptions' import { NETWORK_NAMES } from 'src/shared/conts' import { getDynamicConfigParams, getFeatureGate } from 'src/statsig' import { DynamicConfigs } from 'src/statsig/constants' import { StatsigDynamicConfigs, StatsigFeatureGates } from 'src/statsig/types' import colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' import variables from 'src/styles/variables' import FeeInfoBottomSheet from 'src/swap/FeeInfoBottomSheet' import SwapAmountInput from 'src/swap/SwapAmountInput' import SwapTransactionDetails from 'src/swap/SwapTransactionDetails' import getCrossChainFee from 'src/swap/getCrossChainFee' import { getSwapTxsAnalyticsProperties } from 'src/swap/getSwapTxsAnalyticsProperties' import { currentSwapSelector, priceImpactWarningThresholdSelector } from 'src/swap/selectors' import { swapStart } from 'src/swap/slice' import { AppFeeAmount, Field, SwapAmount, SwapFeeAmount } from 'src/swap/types' import useFilterChips from 'src/swap/useFilterChips' import useSwapQuote, { NO_QUOTE_ERROR_MESSAGE, QuoteResult } from 'src/swap/useSwapQuote' import { useSwappableTokens, useTokenInfo } from 'src/tokens/hooks' import { feeCurrenciesSelector, feeCurrenciesWithPositiveBalancesSelector, tokensByIdSelector, } from 'src/tokens/selectors' import { TokenBalance } from 'src/tokens/slice' import { getSupportedNetworkIdsForSwap } from 'src/tokens/utils' import { NetworkId } from 'src/transactions/types' import Logger from 'src/utils/Logger' import { parseInputAmount } from 'src/utils/parsing' import { getFeeCurrencyAndAmounts } from 'src/viem/prepareTransactions' import { getSerializablePreparedTransactions } from 'src/viem/preparedTransactionSerialization' import networkConfig from 'src/web3/networkConfig' import { v4 as uuidv4 } from 'uuid' const TAG = 'SwapScreen' const FETCH_UPDATED_QUOTE_DEBOUNCE_TIME = 200 const DEFAULT_INPUT_SWAP_AMOUNT: SwapAmount = { [Field.FROM]: '', [Field.TO]: '', } type SelectingNoUsdPriceToken = TokenBalance & { tokenPositionInList: number } interface SwapState { fromTokenId: string | undefined toTokenId: string | undefined // Raw input values (can contain region specific decimal separators) inputSwapAmount: SwapAmount selectingField: Field | null selectingNoUsdPriceToken: SelectingNoUsdPriceToken | null confirmingSwap: boolean // Keep track of which swap is currently being executed from this screen // This is because there could be multiple swaps happening at the same time startedSwapId: string | null switchedToNetworkId: NetworkId | null selectedPercentage: number | null } function getInitialState(fromTokenId?: string, toTokenId?: string): SwapState { return { fromTokenId, toTokenId, inputSwapAmount: DEFAULT_INPUT_SWAP_AMOUNT, selectingField: null, selectingNoUsdPriceToken: null, confirmingSwap: false, startedSwapId: null, switchedToNetworkId: null, selectedPercentage: null, } } const swapSlice = createSlice({ name: 'swapSlice', initialState: getInitialState, reducers: { changeAmount: (state, action: PayloadAction<{ value: string }>) => { const { value } = action.payload state.confirmingSwap = false state.startedSwapId = null if (!value) { state.inputSwapAmount = DEFAULT_INPUT_SWAP_AMOUNT return } // Regex to match only numbers and one decimal separator const sanitizedValue = value.match(/^(?:\d+[.,]?\d*|[.,]\d*|[.,])$/)?.join('') if (!sanitizedValue) { return } state.inputSwapAmount[Field.FROM] = sanitizedValue state.selectedPercentage = null }, chooseFromAmountPercentage: ( state, action: PayloadAction<{ fromTokenBalance: BigNumber; percentage: number }> ) => { const { fromTokenBalance, percentage } = action.payload state.confirmingSwap = false state.startedSwapId = null state.selectedPercentage = percentage // If the max percentage is selected, try the current balance first, and we will prompt the user if it's too high state.inputSwapAmount[Field.FROM] = fromTokenBalance.multipliedBy(percentage).toFormat({ decimalSeparator: getNumberFormatSettings().decimalSeparator, }) }, startSelectToken: (state, action: PayloadAction<{ fieldType: Field }>) => { state.selectingField = action.payload.fieldType state.confirmingSwap = false }, selectNoUsdPriceToken: ( state, action: PayloadAction<{ token: SelectingNoUsdPriceToken }> ) => { state.selectingNoUsdPriceToken = action.payload.token }, unselectNoUsdPriceToken: (state) => { state.selectingNoUsdPriceToken = null }, selectTokens: ( state, action: PayloadAction<{ fromTokenId: string | undefined toTokenId: string | undefined switchedToNetworkId: NetworkId | null }> ) => { const { fromTokenId, toTokenId, switchedToNetworkId } = action.payload state.confirmingSwap = false if (fromTokenId !== state.fromTokenId || toTokenId !== state.toTokenId) { state.startedSwapId = null } state.fromTokenId = fromTokenId state.toTokenId = toTokenId state.switchedToNetworkId = switchedToNetworkId state.selectingNoUsdPriceToken = null state.selectedPercentage = null }, quoteUpdated: (state, action: PayloadAction<{ quote: QuoteResult | null }>) => { const { quote } = action.payload state.confirmingSwap = false if (!quote) { state.inputSwapAmount[Field.TO] = '' return } const { decimalSeparator } = getNumberFormatSettings() const parsedAmount = parseInputAmount(state.inputSwapAmount[Field.FROM], decimalSeparator) const newAmount = parsedAmount.multipliedBy(new BigNumber(quote.price)) state.inputSwapAmount[Field.TO] = newAmount.toFormat({ decimalSeparator, }) }, // When the user presses the confirm swap button startConfirmSwap: (state) => { state.confirmingSwap = true }, // When the swap is ready to be executed startSwap: (state, action: PayloadAction<{ swapId: string }>) => { state.startedSwapId = action.payload.swapId }, }, }) const { changeAmount, chooseFromAmountPercentage, startSelectToken, selectTokens, quoteUpdated, startConfirmSwap, startSwap, selectNoUsdPriceToken, unselectNoUsdPriceToken, } = swapSlice.actions const swapStateReducer = swapSlice.reducer function getNetworkFee(quote: QuoteResult | null) { const { feeCurrency, maxFeeAmount, estimatedFeeAmount } = getFeeCurrencyAndAmounts( quote?.preparedTransactions ) return feeCurrency && estimatedFeeAmount ? { token: feeCurrency, maxAmount: maxFeeAmount, amount: estimatedFeeAmount, } : undefined } type Props = NativeStackScreenProps export function SwapScreen({ route }: Props) { const { t } = useTranslation() const dispatch = useDispatch() const tokenBottomSheetFromRef = useRef(null) const tokenBottomSheetToRef = useRef(null) const tokenBottomSheetRefs = { [Field.FROM]: tokenBottomSheetFromRef, [Field.TO]: tokenBottomSheetToRef, } const exchangeRateInfoBottomSheetRef = useRef(null) const feeInfoBottomSheetRef = useRef(null) const slippageInfoBottomSheetRef = useRef(null) const estimatedDurationBottomSheetRef = useRef(null) const allowCrossChainSwaps = getFeatureGate(StatsigFeatureGates.ALLOW_CROSS_CHAIN_SWAPS) const showUKCompliantVariant = getFeatureGate(StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT) const { decimalSeparator } = getNumberFormatSettings() const { maxSlippagePercentage, enableAppFee } = getDynamicConfigParams( DynamicConfigs[StatsigDynamicConfigs.SWAP_CONFIG] ) const { links } = getDynamicConfigParams(DynamicConfigs[StatsigDynamicConfigs.APP_CONFIG]) const parsedSlippagePercentage = new BigNumber(maxSlippagePercentage).toFormat() const { swappableFromTokens, swappableToTokens, areSwapTokensShuffled } = useSwappableTokens() const priceImpactWarningThreshold = useSelector(priceImpactWarningThresholdSelector) const tokensById = useSelector((state) => tokensByIdSelector(state, getSupportedNetworkIdsForSwap()) ) const initialFromTokenId = route.params?.fromTokenId const initialToTokenId = route.params?.toTokenId const initialToTokenNetworkId = route.params?.toTokenNetworkId const [state, localDispatch] = useReducer( swapStateReducer, getInitialState(initialFromTokenId, initialToTokenId) ) const { fromTokenId, toTokenId, inputSwapAmount, selectingField, selectingNoUsdPriceToken, confirmingSwap, switchedToNetworkId, startedSwapId, selectedPercentage, } = state const filterChipsFrom = useFilterChips(Field.FROM) const filterChipsTo = useFilterChips(Field.TO, initialToTokenNetworkId) const { fromToken, toToken } = useMemo(() => { const fromToken = swappableFromTokens.find((token) => token.tokenId === fromTokenId) const toToken = swappableToTokens.find((token) => token.tokenId === toTokenId) return { fromToken, toToken } }, [fromTokenId, toTokenId, swappableFromTokens, swappableToTokens]) const fromTokenBalance = useTokenInfo(fromToken?.tokenId)?.balance ?? new BigNumber(0) const currentSwap = useSelector(currentSwapSelector) const swapStatus = startedSwapId === currentSwap?.id ? currentSwap.status : null const feeCurrenciesWithPositiveBalances = useSelector((state) => feeCurrenciesWithPositiveBalancesSelector( state, fromToken?.networkId || networkConfig.defaultNetworkId ) ) const localCurrency = useSelector(getLocalCurrencyCode) const { quote, refreshQuote, fetchSwapQuoteError, fetchingSwapQuote, clearQuote } = useSwapQuote({ networkId: fromToken?.networkId || networkConfig.defaultNetworkId, slippagePercentage: maxSlippagePercentage, enableAppFee: enableAppFee, }) // Parsed swap amounts (BigNumber) const parsedSwapAmount = useMemo( () => ({ [Field.FROM]: parseInputAmount(inputSwapAmount[Field.FROM], decimalSeparator), [Field.TO]: parseInputAmount(inputSwapAmount[Field.TO], decimalSeparator), }), [inputSwapAmount] ) const shouldShowMaxSwapAmountWarning = feeCurrenciesWithPositiveBalances.length === 1 && fromToken?.tokenId === feeCurrenciesWithPositiveBalances[0].tokenId && fromTokenBalance.gt(0) && parsedSwapAmount[Field.FROM].gte(fromTokenBalance) const fromSwapAmountError = confirmingSwap && parsedSwapAmount[Field.FROM].gt(fromTokenBalance) const quoteUpdatePending = (quote && (quote.fromTokenId !== fromToken?.tokenId || quote.toTokenId !== toToken?.tokenId || !quote.swapAmount.eq(parsedSwapAmount[Field.FROM]))) || fetchingSwapQuote const confirmSwapIsLoading = swapStatus === 'started' const confirmSwapFailed = swapStatus === 'error' useEffect(() => { AppAnalytics.track(SwapEvents.swap_screen_open) }, []) useEffect(() => { if (fetchSwapQuoteError) { if (!fetchSwapQuoteError.message.includes(NO_QUOTE_ERROR_MESSAGE)) { dispatch(showError(ErrorMessages.FETCH_SWAP_QUOTE_FAILED)) } } }, [fetchSwapQuoteError]) useEffect(() => { // since we use the quote to update the parsedSwapAmount, // this hook will be triggered after the quote is first updated. this // variable prevents the quote from needlessly being fetched again. const quoteKnown = fromToken && toToken && quote && quote.toTokenId === toToken.tokenId && quote.fromTokenId === fromToken.tokenId && quote.swapAmount.eq(parsedSwapAmount[Field.FROM]) const debouncedRefreshQuote = setTimeout(() => { if (fromToken && toToken && parsedSwapAmount[Field.FROM].gt(0) && !quoteKnown) { void refreshQuote(fromToken, toToken, parsedSwapAmount, Field.FROM) } }, FETCH_UPDATED_QUOTE_DEBOUNCE_TIME) return () => { clearTimeout(debouncedRefreshQuote) } }, [fromToken, toToken, parsedSwapAmount, quote]) useEffect(() => { localDispatch(quoteUpdated({ quote })) }, [quote]) const handleConfirmSwap = () => { if (!quote) { return // this should never happen, because the button must be disabled in that cases } const fromToken = tokensById[quote.fromTokenId] const toToken = tokensById[quote.toTokenId] if (!fromToken || !toToken) { // Should never happen return } localDispatch(startConfirmSwap()) const userInput = { toTokenId: toToken.tokenId, fromTokenId: fromToken.tokenId, swapAmount: { [Field.FROM]: parsedSwapAmount[Field.FROM].toString(), [Field.TO]: parsedSwapAmount[Field.TO].toString(), }, updatedField: Field.FROM, } const { estimatedPriceImpact, price, allowanceTarget, appFeePercentageIncludedInPrice } = quote const resultType = quote.preparedTransactions.type switch (resultType) { case 'need-decrease-spend-amount-for-gas': // fallthrough on purpose case 'not-enough-balance-for-gas': // This should never actually happen, since the user should not be able // to confirm the swap in this case. break case 'possible': const swapId = uuidv4() AppAnalytics.track(SwapEvents.swap_review_submit, { swapId, toToken: toToken.address, toTokenId: toToken.tokenId, toTokenNetworkId: toToken.networkId, toTokenIsImported: !!toToken.isManuallyImported, fromToken: fromToken.address, fromTokenId: fromToken.tokenId, fromTokenNetworkId: fromToken.networkId, fromTokenIsImported: !!fromToken.isManuallyImported, amount: inputSwapAmount[Field.FROM], amountType: 'sellAmount', allowanceTarget, estimatedPriceImpact, price, appFeePercentageIncludedInPrice, provider: quote.provider, swapType: quote.swapType, web3Library: 'viem', ...getSwapTxsAnalyticsProperties( quote.preparedTransactions.transactions, fromToken.networkId, tokensById ), }) localDispatch(startSwap({ swapId })) dispatch( swapStart({ swapId, quote: { preparedTransactions: getSerializablePreparedTransactions( quote.preparedTransactions.transactions ), receivedAt: quote.receivedAt, price: quote.price, appFeePercentageIncludedInPrice, provider: quote.provider, estimatedPriceImpact, allowanceTarget, swapType: quote.swapType, }, userInput, areSwapTokensShuffled, }) ) break default: // To catch any missing cases at compile time const assertNever: never = resultType return assertNever } } const handleSwitchTokens = () => { AppAnalytics.track(SwapEvents.swap_switch_tokens, { fromTokenId, toTokenId }) localDispatch( selectTokens({ fromTokenId: toTokenId, toTokenId: fromTokenId, switchedToNetworkId: null, }) ) } const handleShowTokenSelect = (fieldType: Field) => () => { AppAnalytics.track(SwapEvents.swap_screen_select_token, { fieldType }) localDispatch(startSelectToken({ fieldType })) // use requestAnimationFrame so that the bottom sheet open animation is done // after the selectingField value is updated, so that the title of the // bottom sheet (which depends on selectingField) does not change on the // screen requestAnimationFrame(() => { tokenBottomSheetRefs[fieldType].current?.snapToIndex(0) }) } const handleConfirmSelectToken = (selectedToken: TokenBalance, tokenPositionInList: number) => { if (!selectingField) { // Should never happen Logger.error(TAG, 'handleSelectToken called without selectingField') return } let newSwitchedToNetworkId: NetworkId | null = null let newFromToken = fromToken let newToToken = toToken if ( (selectingField === Field.FROM && toToken?.tokenId === selectedToken.tokenId) || (selectingField === Field.TO && fromToken?.tokenId === selectedToken.tokenId) ) { newFromToken = toToken newToToken = fromToken } else if (selectingField === Field.FROM) { newFromToken = selectedToken newSwitchedToNetworkId = toToken && toToken.networkId !== newFromToken.networkId && !allowCrossChainSwaps ? newFromToken.networkId : null if (newSwitchedToNetworkId) { // reset the toToken if the user is switching networks newToToken = undefined } } else if (selectingField === Field.TO) { newToToken = selectedToken newSwitchedToNetworkId = fromToken && fromToken.networkId !== newToToken.networkId && !allowCrossChainSwaps ? newToToken.networkId : null if (newSwitchedToNetworkId) { // reset the fromToken if the user is switching networks newFromToken = undefined } } AppAnalytics.track(SwapEvents.swap_screen_confirm_token, { fieldType: selectingField, tokenSymbol: selectedToken.symbol, tokenId: selectedToken.tokenId, tokenNetworkId: selectedToken.networkId, fromTokenSymbol: newFromToken?.symbol, fromTokenId: newFromToken?.tokenId, fromTokenNetworkId: newFromToken?.networkId, toTokenSymbol: newToToken?.symbol, toTokenId: newToToken?.tokenId, toTokenNetworkId: newToToken?.networkId, switchedNetworkId: !!newSwitchedToNetworkId, areSwapTokensShuffled, tokenPositionInList, }) localDispatch( selectTokens({ fromTokenId: newFromToken?.tokenId, toTokenId: newToToken?.tokenId, switchedToNetworkId: allowCrossChainSwaps ? null : newSwitchedToNetworkId, }) ) if (newSwitchedToNetworkId) { clearQuote() } // use requestAnimationFrame so that the bottom sheet and keyboard dismiss // animation can be synchronised and starts after the state changes above. // without this, the keyboard animation lags behind the state updates while // the bottom sheet does not requestAnimationFrame(() => { tokenBottomSheetRefs[selectingField].current?.close() }) } const handleConfirmSelectTokenNoUsdPrice = () => { if (selectingNoUsdPriceToken) { handleConfirmSelectToken( selectingNoUsdPriceToken, selectingNoUsdPriceToken.tokenPositionInList ) } } const handleDismissSelectTokenNoUsdPrice = () => { localDispatch(unselectNoUsdPriceToken()) } const handleSelectToken = (selectedToken: TokenBalance, tokenPositionInList: number) => { if (!selectedToken.priceUsd && selectingField === Field.TO) { localDispatch(selectNoUsdPriceToken({ token: { ...selectedToken, tokenPositionInList } })) return } handleConfirmSelectToken(selectedToken, tokenPositionInList) } const handleChangeAmount = (value: string) => { localDispatch(changeAmount({ value })) if (!value) { clearQuote() } } const handleSelectAmountPercentage = (percentage: number) => { localDispatch(chooseFromAmountPercentage({ fromTokenBalance, percentage })) if (!fromToken) { // Should never happen return } AppAnalytics.track(SwapEvents.swap_screen_percentage_selected, { tokenSymbol: fromToken.symbol, tokenId: fromToken.tokenId, tokenNetworkId: fromToken.networkId, percentage, }) } const onPressLearnMore = () => { AppAnalytics.track(SwapEvents.swap_learn_more) navigate(Screens.WebViewScreen, { uri: links.swapLearnMore }) } const onPressLearnMoreFees = () => { AppAnalytics.track(SwapEvents.swap_gas_fees_learn_more) navigate(Screens.WebViewScreen, { uri: links.transactionFeesLearnMore }) } const switchedToNetworkName = switchedToNetworkId && NETWORK_NAMES[switchedToNetworkId] const showCrossChainSwapNotification = toToken && fromToken && toToken.networkId !== fromToken.networkId && allowCrossChainSwaps const crossChainFeeCurrency = useSelector((state) => feeCurrenciesSelector(state, fromToken?.networkId || networkConfig.defaultNetworkId) ).find((token) => token.isNative) const crossChainFee = quote?.swapType === 'cross-chain' ? getCrossChainFee({ feeCurrency: crossChainFeeCurrency, preparedTransactions: quote.preparedTransactions, fromTokenId: quote.fromTokenId, sellAmount: quote.sellAmount, estimatedCrossChainFee: quote.estimatedCrossChainFee, maxCrossChainFee: quote.maxCrossChainFee, }) : undefined const getWarningStatuses = () => { // NOTE: If a new condition is added here, make sure to update `allowSwap` below if // the condition should prevent the user from swapping. const checks = { showSwitchedToNetworkWarning: !!switchedToNetworkId, showUnsupportedTokensWarning: !quoteUpdatePending && fetchSwapQuoteError?.message.includes(NO_QUOTE_ERROR_MESSAGE), showInsufficientBalanceWarning: parsedSwapAmount[Field.FROM].gt(fromTokenBalance), showCrossChainFeeWarning: !quoteUpdatePending && crossChainFee?.nativeTokenBalanceDeficit.lt(0), showDecreaseSpendForGasWarning: !quoteUpdatePending && quote?.preparedTransactions.type === 'need-decrease-spend-amount-for-gas', showNotEnoughBalanceForGasWarning: !quoteUpdatePending && quote?.preparedTransactions.type === 'not-enough-balance-for-gas', showMaxSwapAmountWarning: shouldShowMaxSwapAmountWarning && !confirmSwapFailed, showNoUsdPriceWarning: !confirmSwapFailed && !quoteUpdatePending && toToken && !toToken.priceUsd, showPriceImpactWarning: !confirmSwapFailed && !quoteUpdatePending && (quote?.estimatedPriceImpact ? new BigNumber(quote.estimatedPriceImpact).gte(priceImpactWarningThreshold) : false), showMissingPriceImpactWarning: !quoteUpdatePending && quote && !quote.estimatedPriceImpact, } // Only ever show a single warning, according to precedence as above. // Warnings that prevent the user from confirming the swap should // take higher priority over others. return Object.entries(checks).reduce( (acc, [name, status]) => { acc[name] = Object.values(acc).some(Boolean) ? false : !!status return acc }, {} as Record ) } const { showCrossChainFeeWarning, showDecreaseSpendForGasWarning, showNotEnoughBalanceForGasWarning, showInsufficientBalanceWarning, showSwitchedToNetworkWarning, showMaxSwapAmountWarning, showNoUsdPriceWarning, showPriceImpactWarning, showUnsupportedTokensWarning, showMissingPriceImpactWarning, } = getWarningStatuses() const allowSwap = useMemo( () => !showDecreaseSpendForGasWarning && !showNotEnoughBalanceForGasWarning && !showInsufficientBalanceWarning && !showCrossChainFeeWarning && !confirmSwapIsLoading && !quoteUpdatePending && Object.values(parsedSwapAmount).every((amount) => amount.gt(0)), [ parsedSwapAmount, quoteUpdatePending, confirmSwapIsLoading, showInsufficientBalanceWarning, showDecreaseSpendForGasWarning, showNotEnoughBalanceForGasWarning, showCrossChainFeeWarning, ] ) const networkFee: SwapFeeAmount | undefined = useMemo(() => { return getNetworkFee(quote) }, [fromToken, quote]) const feeToken = networkFee?.token ? tokensById[networkFee.token.tokenId] : undefined const appFee: AppFeeAmount | undefined = useMemo(() => { if (!quote || !fromToken) { return undefined } const percentage = new BigNumber(quote.appFeePercentageIncludedInPrice || 0) return { amount: parsedSwapAmount[Field.FROM].multipliedBy(percentage).dividedBy(100), token: fromToken, percentage, } }, [quote, parsedSwapAmount, fromToken]) useEffect(() => { if (showPriceImpactWarning || showMissingPriceImpactWarning) { if (!quote) { return } const fromToken = tokensById[quote.fromTokenId] const toToken = tokensById[quote.toTokenId] if (!fromToken || !toToken) { // Should never happen Logger.error(TAG, 'fromToken or toToken not found') return } AppAnalytics.track(SwapEvents.swap_price_impact_warning_displayed, { toToken: toToken.address, toTokenId: toToken.tokenId, toTokenNetworkId: toToken.networkId, toTokenIsImported: !!toToken.isManuallyImported, fromToken: fromToken.address, fromTokenId: fromToken.tokenId, fromTokenNetworkId: fromToken?.networkId, fromTokenIsImported: !!fromToken.isManuallyImported, amount: parsedSwapAmount[Field.FROM].toString(), amountType: 'sellAmount', priceImpact: quote.estimatedPriceImpact, provider: quote.provider, }) } }, [showPriceImpactWarning || showMissingPriceImpactWarning]) const feeCurrencies = quote && quote.preparedTransactions.type === 'not-enough-balance-for-gas' ? quote.preparedTransactions.feeCurrencies.map((feeCurrency) => feeCurrency.symbol).join(', ') : '' const tokenBottomSheetsConfig = [ { fieldType: Field.FROM, tokens: swappableFromTokens, filterChips: filterChipsFrom, origin: TokenPickerOrigin.SwapFrom, }, { fieldType: Field.TO, tokens: swappableToTokens, filterChips: filterChipsTo, origin: TokenPickerOrigin.SwapTo, }, ] return ( } title={t('swapScreen.title')} /> {showCrossChainSwapNotification && ( {t('swapScreen.crossChainNotification')} )} {showCrossChainFeeWarning && ( )} {showDecreaseSpendForGasWarning && ( { if ( !quote || quote.preparedTransactions.type !== 'need-decrease-spend-amount-for-gas' ) return handleChangeAmount(quote.preparedTransactions.decreasedSpendAmount.toString()) }} ctaLabel={t('swapScreen.decreaseSwapAmountForGasWarning.cta')} style={styles.warning} /> )} {showNotEnoughBalanceForGasWarning && ( )} {showInsufficientBalanceWarning && ( )} {showUnsupportedTokensWarning && ( )} {showSwitchedToNetworkWarning && ( )} {showMaxSwapAmountWarning && ( )} {showPriceImpactWarning && ( )} {showNoUsdPriceWarning && ( )} {showMissingPriceImpactWarning && ( )} {confirmSwapFailed && ( )}