diff --git a/src/lib/components/Swap/Input.tsx b/src/lib/components/Swap/Input.tsx index 644c3fc8f9..422a139720 100644 --- a/src/lib/components/Swap/Input.tsx +++ b/src/lib/components/Swap/Input.tsx @@ -1,10 +1,15 @@ import { useLingui } from '@lingui/react' import { useUSDCValue } from 'hooks/useUSDCPrice' -import { useAtomValue } from 'jotai/utils' import { loadingOpacityCss } from 'lib/css/loading' -import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap' +import { + useIsSwapFieldIndependent, + useSwapAmount, + useSwapCurrency, + useSwapCurrencyAmount, + useSwapInfo, +} from 'lib/hooks/swap' import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor' -import { Field, independentFieldAtom } from 'lib/state/swap' +import { Field } from 'lib/state/swap' import styled, { ThemedText } from 'lib/theme' import { useMemo } from 'react' import { TradeState } from 'state/routing/types' @@ -45,18 +50,19 @@ export default function Input({ disabled, focused }: InputProps) { const { trade: { state: tradeState }, currencyBalances: { [Field.INPUT]: balance }, - currencyAmounts: { [Field.INPUT]: inputCurrencyAmount }, + currencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount }, } = useSwapInfo() - const inputUSDC = useUSDCValue(inputCurrencyAmount) + const inputUSDC = useUSDCValue(swapInputCurrencyAmount) const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT) const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT) + const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT) // extract eagerly in case of reversal usePrefetchCurrencyColor(swapInputCurrency) const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING - const isDependentField = useAtomValue(independentFieldAtom) !== Field.INPUT + const isDependentField = !useIsSwapFieldIndependent(Field.INPUT) const isLoading = isRouteLoading && isDependentField //TODO(ianlapham): mimic logic from app swap page @@ -72,11 +78,18 @@ export default function Input({ disabled, focused }: InputProps) { return }, [maxAmount, updateSwapInputAmount]) + const balanceColor = useMemo(() => { + const insufficientBalance = + balance && + (inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : swapInputCurrencyAmount?.greaterThan(balance)) + return insufficientBalance ? 'error' : undefined + }, [balance, inputCurrencyAmount, swapInputCurrencyAmount]) + return ( {inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'} {balance && ( - + Balance: {formatCurrencyAmount(balance, 4, i18n.locale)} )} diff --git a/src/lib/components/Swap/Output.tsx b/src/lib/components/Swap/Output.tsx index 46a55265e6..12095014ee 100644 --- a/src/lib/components/Swap/Output.tsx +++ b/src/lib/components/Swap/Output.tsx @@ -4,9 +4,9 @@ import { useUSDCValue } from 'hooks/useUSDCPrice' import { atom } from 'jotai' import { useAtomValue } from 'jotai/utils' import BrandedFooter from 'lib/components/BrandedFooter' -import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap' +import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap' import useCurrencyColor from 'lib/hooks/useCurrencyColor' -import { Field, independentFieldAtom } from 'lib/state/swap' +import { Field } from 'lib/state/swap' import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme' import { PropsWithChildren, useMemo } from 'react' import { TradeState } from 'state/routing/types' @@ -50,7 +50,7 @@ export default function Output({ disabled, focused, children }: PropsWithChildre const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT) const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING - const isDependentField = useAtomValue(independentFieldAtom) !== Field.OUTPUT + const isDependentField = !useIsSwapFieldIndependent(Field.OUTPUT) const isLoading = isRouteLoading && isDependentField const overrideColor = useAtomValue(colorAtom) diff --git a/src/lib/components/Swap/Summary/index.tsx b/src/lib/components/Swap/Summary/index.tsx index 9a836e200d..29820d4aee 100644 --- a/src/lib/components/Swap/Summary/index.tsx +++ b/src/lib/components/Swap/Summary/index.tsx @@ -2,12 +2,11 @@ import { Trans } from '@lingui/macro' import { useLingui } from '@lingui/react' import { Trade } from '@uniswap/router-sdk' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' -import { useAtomValue } from 'jotai/utils' import { IconButton } from 'lib/components/Button' +import { useSwapTradeType } from 'lib/hooks/swap' import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage' import useScrollbar from 'lib/hooks/useScrollbar' import { AlertTriangle, BarChart, Expando, Info } from 'lib/icons' -import { Field, independentFieldAtom } from 'lib/state/swap' import styled, { ThemedText } from 'lib/theme' import formatLocaleNumber from 'lib/utils/formatLocaleNumber' import { useMemo, useState } from 'react' @@ -89,7 +88,7 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial const inputCurrency = inputAmount.currency const outputCurrency = outputAmount.currency const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade]) - const independentField = useAtomValue(independentFieldAtom) + const tradeType = useSwapTradeType() const { i18n } = useLingui() const [open, setOpen] = useState(false) @@ -160,14 +159,14 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial Output is estimated. - {independentField === Field.INPUT && ( + {tradeType === TradeType.EXACT_INPUT && ( You will receive at least{' '} {formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '} or the transaction will revert. )} - {independentField === Field.OUTPUT && ( + {tradeType === TradeType.EXACT_OUTPUT && ( You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '} {inputCurrency.symbol} or the transaction will revert. diff --git a/src/lib/components/Swap/SwapButton.tsx b/src/lib/components/Swap/SwapButton.tsx index d87b342f45..cf2a9aab63 100644 --- a/src/lib/components/Swap/SwapButton.tsx +++ b/src/lib/components/Swap/SwapButton.tsx @@ -1,9 +1,8 @@ import { Trans } from '@lingui/macro' -import { Token, TradeType } from '@uniswap/sdk-core' +import { Token } from '@uniswap/sdk-core' import { useERC20PermitFromTrade } from 'hooks/useERC20Permit' import { useUpdateAtom } from 'jotai/utils' -import { useAtomValue } from 'jotai/utils' -import { useSwapInfo } from 'lib/hooks/swap' +import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap' import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade, @@ -15,7 +14,7 @@ import { usePendingApproval } from 'lib/hooks/transactions' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useTransactionDeadline from 'lib/hooks/useTransactionDeadline' import { Link, Spinner } from 'lib/icons' -import { displayTxHashAtom, Field, independentFieldAtom } from 'lib/state/swap' +import { displayTxHashAtom, Field } from 'lib/state/swap' import { TransactionType } from 'lib/state/transactions' import { useTheme } from 'lib/theme' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -50,7 +49,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) { feeOptions, } = useSwapInfo() - const independentField = useAtomValue(independentFieldAtom) + const tradeType = useSwapTradeType() const [activeTrade, setActiveTrade] = useState() useEffect(() => { @@ -61,7 +60,14 @@ export default function SwapButton({ disabled }: SwapButtonProps) { const optimizedTrade = // Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending. useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade - const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval) + + const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT) + const [approval, getApproval] = useSwapApproval( + optimizedTrade, + allowedSlippage, + useIsPendingApproval, + approvalCurrencyAmount + ) const approvalHash = usePendingApproval( inputCurrency?.isToken ? inputCurrency : undefined, useSwapRouterAddress(optimizedTrade) @@ -77,11 +83,17 @@ export default function SwapButton({ disabled }: SwapButtonProps) { }, [addTransaction, getApproval]) const actionProps = useMemo((): Partial | undefined => { - if (disabled) return { disabled: true } - - if (chainId && inputCurrencyAmount) { - if (!inputCurrencyBalance || inputCurrencyBalance.lessThan(inputCurrencyAmount)) { - return { disabled: true } + if (!disabled && chainId) { + if (approval === ApprovalState.NOT_APPROVED) { + const currency = inputCurrency || approvalCurrencyAmount?.currency + invariant(currency) + return { + action: { + message: Approve {currency.symbol} first, + onClick: addApprovalTransaction, + children: Approve, + }, + } } else if (approval === ApprovalState.PENDING) { return { disabled: true, @@ -100,20 +112,22 @@ export default function SwapButton({ disabled }: SwapButtonProps) { children: Approve, }, } - } else if (approval === ApprovalState.NOT_APPROVED) { - return { - action: { - message: Approve {inputCurrencyAmount.currency.symbol} first, - onClick: addApprovalTransaction, - children: Approve, - }, - } + } else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) { + return {} } - return {} } - return { disabled: true } - }, [addApprovalTransaction, approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance]) + }, [ + addApprovalTransaction, + approval, + approvalCurrencyAmount?.currency, + approvalHash, + chainId, + disabled, + inputCurrency, + inputCurrencyAmount, + inputCurrencyBalance, + ]) const deadline = useTransactionDeadline() const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline) @@ -139,7 +153,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) { addTransaction({ response, type: TransactionType.SWAP, - tradeType: independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, + tradeType, inputCurrencyAmount, outputCurrencyAmount, }) @@ -151,7 +165,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) { .finally(() => { setActiveTrade(undefined) }) - }, [addTransaction, independentField, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback]) + }, [addTransaction, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback, tradeType]) return ( <> diff --git a/src/lib/hooks/swap/index.ts b/src/lib/hooks/swap/index.ts index b86f5a3841..5468d76b86 100644 --- a/src/lib/hooks/swap/index.ts +++ b/src/lib/hooks/swap/index.ts @@ -1,13 +1,12 @@ -import { Currency } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { useAtom } from 'jotai' import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { pickAtom } from 'lib/state/atoms' -import { Field, independentFieldAtom, swapAtom } from 'lib/state/swap' +import { Field, swapAtom } from 'lib/state/swap' +import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { useCallback, useMemo } from 'react' export { default as useSwapInfo } from './useSwapInfo' -export const amountAtom = pickAtom(swapAtom, 'amount') - function otherField(field: Field) { switch (field) { case Field.INPUT: @@ -57,10 +56,34 @@ export function useSwapCurrency(field: Field): [Currency | undefined, (currency? return [currency, setOrSwitchCurrency] } +const independentFieldAtom = pickAtom(swapAtom, 'independentField') + +export function useIsSwapFieldIndependent(field: Field): boolean { + const independentField = useAtomValue(independentFieldAtom) + return independentField === field +} + +export function useSwapTradeType(): TradeType { + const independentField = useAtomValue(independentFieldAtom) + switch (independentField) { + case Field.INPUT: + return TradeType.EXACT_INPUT + case Field.OUTPUT: + return TradeType.EXACT_OUTPUT + } +} + +const amountAtom = pickAtom(swapAtom, 'amount') + +// check if any amount has been entered by user +export function useIsAmountPopulated() { + return Boolean(useAtomValue(amountAtom)) +} + export function useSwapAmount(field: Field): [string | undefined, (amount: string) => void] { const amount = useAtomValue(amountAtom) - const independentField = useAtomValue(independentFieldAtom) - const value = useMemo(() => (independentField === field ? amount : undefined), [amount, independentField, field]) + const isFieldIndependent = useIsSwapFieldIndependent(field) + const value = useMemo(() => (isFieldIndependent ? amount : undefined), [amount, isFieldIndependent]) const updateSwap = useUpdateAtom(swapAtom) const updateAmount = useCallback( (amount: string) => @@ -73,7 +96,13 @@ export function useSwapAmount(field: Field): [string | undefined, (amount: strin return [value, updateAmount] } -// check if any amount has been entered by user -export function useIsAmountPopulated() { - return Boolean(useAtomValue(amountAtom)) +export function useSwapCurrencyAmount(field: Field): CurrencyAmount | undefined { + const isFieldIndependent = useIsSwapFieldIndependent(field) + const isAmountPopulated = useIsAmountPopulated() + const [swapAmount] = useSwapAmount(field) + const [swapCurrency] = useSwapCurrency(field) + if (isFieldIndependent && isAmountPopulated) { + return tryParseCurrencyAmount(swapAmount, swapCurrency) + } + return } diff --git a/src/lib/hooks/swap/useSwapApproval.ts b/src/lib/hooks/swap/useSwapApproval.ts index ed975a6c10..c81eb7667d 100644 --- a/src/lib/hooks/swap/useSwapApproval.ts +++ b/src/lib/hooks/swap/useSwapApproval.ts @@ -1,5 +1,5 @@ import { Protocol, Trade } from '@uniswap/router-sdk' -import { Currency, Percent, Token, TradeType } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { Pair, Route as V2Route, Trade as V2Trade } from '@uniswap/v2-sdk' import { Pool, Route as V3Route, Trade as V3Trade } from '@uniswap/v3-sdk' import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from 'constants/addresses' @@ -63,11 +63,12 @@ export default function useSwapApproval( | Trade | undefined, allowedSlippage: Percent, - useIsPendingApproval: (token?: Token, spender?: string) => boolean + useIsPendingApproval: (token?: Token, spender?: string) => boolean, + amount?: CurrencyAmount // defaults to trade.maximumAmountIn(allowedSlippage) ) { const amountToApprove = useMemo( - () => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined), - [trade, allowedSlippage] + () => amount || (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined), + [amount, trade, allowedSlippage] ) const spender = useSwapRouterAddress(trade) diff --git a/src/lib/hooks/swap/useSwapInfo.tsx b/src/lib/hooks/swap/useSwapInfo.tsx index ec8e35da2c..add5e85921 100644 --- a/src/lib/hooks/swap/useSwapInfo.tsx +++ b/src/lib/hooks/swap/useSwapInfo.tsx @@ -57,8 +57,6 @@ function useComputeSwapInfo(): SwapInfo { () => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined), [inputCurrency, isExactIn, outputCurrency, amount] ) - const parsedAmountIn = isExactIn ? parsedAmount : undefined - const parsedAmountOut = isExactIn ? undefined : parsedAmount //@TODO(ianlapham): this would eventually be replaced with routing api logic. const trade = useBestTrade( @@ -85,10 +83,10 @@ function useComputeSwapInfo(): SwapInfo { const currencyAmounts = useMemo( () => ({ - [Field.INPUT]: parsedAmountIn || trade.trade?.inputAmount, - [Field.OUTPUT]: parsedAmountOut || trade.trade?.outputAmount, + [Field.INPUT]: trade.trade?.inputAmount, + [Field.OUTPUT]: trade.trade?.outputAmount, }), - [parsedAmountIn, parsedAmountOut, trade.trade?.inputAmount, trade.trade?.outputAmount] + [trade.trade?.inputAmount, trade.trade?.outputAmount] ) const allowedSlippage = useAllowedSlippage(trade.trade) @@ -153,9 +151,7 @@ const swapInfoAtom = atom({ export function SwapInfoUpdater() { const setSwapInfo = useUpdateAtom(swapInfoAtom) const swapInfo = useComputeSwapInfo() - useEffect(() => { - setSwapInfo(swapInfo) - }, [swapInfo, setSwapInfo]) + useEffect(() => setSwapInfo(swapInfo), [swapInfo, setSwapInfo]) return null } diff --git a/src/lib/state/swap.ts b/src/lib/state/swap.ts index 7707049763..2a2b0efe6e 100644 --- a/src/lib/state/swap.ts +++ b/src/lib/state/swap.ts @@ -4,7 +4,6 @@ import { SupportedChainId } from 'constants/chains' import { nativeOnChain } from 'constants/tokens' import { atom } from 'jotai' import { atomWithImmer } from 'jotai/immer' -import { pickAtom } from 'lib/state/atoms' export enum Field { INPUT = 'INPUT', @@ -24,8 +23,6 @@ export const swapAtom = atomWithImmer({ [Field.INPUT]: nativeOnChain(SupportedChainId.MAINNET), }) -export const independentFieldAtom = pickAtom(swapAtom, 'independentField') - // If set to a transaction hash, that transaction will display in a status dialog. export const displayTxHashAtom = atom(undefined)