Skip to content

Commit

Permalink
fix: compute insufficient balance and approval off of input (#3312)
Browse files Browse the repository at this point in the history
  • Loading branch information
zzmp authored Feb 16, 2022
1 parent b152b11 commit ae664dc
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 64 deletions.
29 changes: 21 additions & 8 deletions src/lib/components/Swap/Input.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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 (
<InputColumn gap={0.5} approved={mockApproved}>
<TokenInput
currency={swapInputCurrency}
amount={(swapInputAmount !== undefined ? swapInputAmount : inputCurrencyAmount?.toSignificant(6)) ?? ''}
amount={(swapInputAmount !== undefined ? swapInputAmount : swapInputCurrencyAmount?.toSignificant(6)) ?? ''}
disabled={disabled}
onMax={onMax}
onChangeInput={updateSwapInputAmount}
Expand All @@ -87,7 +100,7 @@ export default function Input({ disabled, focused }: InputProps) {
<Row>
<LoadingRow $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingRow>
{balance && (
<Balance color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined} focused={focused}>
<Balance color={balanceColor} focused={focused}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</Balance>
)}
Expand Down
6 changes: 3 additions & 3 deletions src/lib/components/Swap/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 4 additions & 5 deletions src/lib/components/Swap/Summary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -160,14 +159,14 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
</DetailsColumn>
<Estimate color="secondary">
<Trans>Output is estimated.</Trans>
{independentField === Field.INPUT && (
{tradeType === TradeType.EXACT_INPUT && (
<Trans>
You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
or the transaction will revert.
</Trans>
)}
{independentField === Field.OUTPUT && (
{tradeType === TradeType.EXACT_OUTPUT && (
<Trans>
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
{inputCurrency.symbol} or the transaction will revert.
Expand Down
62 changes: 38 additions & 24 deletions src/lib/components/Swap/SwapButton.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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'
Expand Down Expand Up @@ -50,7 +49,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
feeOptions,
} = useSwapInfo()

const independentField = useAtomValue(independentFieldAtom)
const tradeType = useSwapTradeType()

const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
useEffect(() => {
Expand All @@ -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)
Expand All @@ -77,11 +83,17 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
}, [addTransaction, getApproval])

const actionProps = useMemo((): Partial<ActionButtonProps> | 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: <Trans>Approve {currency.symbol} first</Trans>,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} else if (approval === ApprovalState.PENDING) {
return {
disabled: true,
Expand All @@ -100,20 +112,22 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
children: <Trans>Approve</Trans>,
},
}
} else if (approval === ApprovalState.NOT_APPROVED) {
return {
action: {
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} 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)
Expand All @@ -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,
})
Expand All @@ -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 (
<>
Expand Down
47 changes: 38 additions & 9 deletions src/lib/hooks/swap/index.ts
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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) =>
Expand All @@ -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<Currency> | undefined {
const isFieldIndependent = useIsSwapFieldIndependent(field)
const isAmountPopulated = useIsAmountPopulated()
const [swapAmount] = useSwapAmount(field)
const [swapCurrency] = useSwapCurrency(field)
if (isFieldIndependent && isAmountPopulated) {
return tryParseCurrencyAmount(swapAmount, swapCurrency)
}
return
}
9 changes: 5 additions & 4 deletions src/lib/hooks/swap/useSwapApproval.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -63,11 +63,12 @@ export default function useSwapApproval(
| Trade<Currency, Currency, TradeType>
| undefined,
allowedSlippage: Percent,
useIsPendingApproval: (token?: Token, spender?: string) => boolean
useIsPendingApproval: (token?: Token, spender?: string) => boolean,
amount?: CurrencyAmount<Currency> // 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)

Expand Down
12 changes: 4 additions & 8 deletions src/lib/hooks/swap/useSwapInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -153,9 +151,7 @@ const swapInfoAtom = atom<SwapInfo>({
export function SwapInfoUpdater() {
const setSwapInfo = useUpdateAtom(swapInfoAtom)
const swapInfo = useComputeSwapInfo()
useEffect(() => {
setSwapInfo(swapInfo)
}, [swapInfo, setSwapInfo])
useEffect(() => setSwapInfo(swapInfo), [swapInfo, setSwapInfo])
return null
}

Expand Down
3 changes: 0 additions & 3 deletions src/lib/state/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -24,8 +23,6 @@ export const swapAtom = atomWithImmer<Swap>({
[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<string | undefined>(undefined)

Expand Down

0 comments on commit ae664dc

Please sign in to comment.