Commit 52360657 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

refactor: isolate approval callback hooks (#3172)

* refactor: isolate approval callback hooks

* fix: use approval callback from trade
parent 52128a2d
import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers'
import { Protocol, Trade } from '@uniswap/router-sdk'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, 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 useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback, useMemo } from 'react'
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import useSwapApproval, { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
import { ApprovalState, useApproval } from 'lib/hooks/useApproval'
import { useCallback } from 'react'
import invariant from 'tiny-invariant'
import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from '../constants/addresses'
import { TransactionType } from '../state/transactions/actions'
import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin } from '../utils/calculateGasMargin'
import { useTokenContract } from './useContract'
import { useTokenAllowance } from './useTokenAllowance'
export enum ApprovalState {
UNKNOWN = 'UNKNOWN',
NOT_APPROVED = 'NOT_APPROVED',
PENDING = 'PENDING',
APPROVED = 'APPROVED',
}
export function useApprovalState(amountToApprove?: CurrencyAmount<Currency>, spender?: string) {
const { account } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(token?.address, spender)
return useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve
if (!currentAllowance) return ApprovalState.UNKNOWN
// amountToApprove will be defined if currentAllowance is
return currentAllowance.lessThan(amountToApprove)
? pendingApproval
? ApprovalState.PENDING
: ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender])
}
/** Returns approval state for all known swap routers */
export function useAllApprovalStates(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent
) {
const { chainId } = useActiveWeb3React()
const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage]
)
const v2ApprovalState = useApprovalState(amountToApprove, chainId ? V2_ROUTER_ADDRESS[chainId] : undefined)
const v3ApprovalState = useApprovalState(amountToApprove, chainId ? V3_ROUTER_ADDRESS[chainId] : undefined)
const v2V3ApprovalState = useApprovalState(amountToApprove, chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined)
return useMemo(
() => ({ v2: v2ApprovalState, v3: v3ApprovalState, v2V3: v2V3ApprovalState }),
[v2ApprovalState, v2V3ApprovalState, v3ApprovalState]
)
}
export { ApprovalState } from 'lib/hooks/useApproval'
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
export function useApproveCallback(
amountToApprove?: CurrencyAmount<Currency>,
spender?: string
): [ApprovalState, () => Promise<void>] {
const { chainId } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
// check the current approval status
const approvalState = useApprovalState(amountToApprove, spender)
const tokenContract = useTokenContract(token?.address)
const addTransaction = useTransactionAdder()
const [approval, approvalCallback] = useApproval(amountToApprove, spender, useHasPendingApproval)
const approve = useCallback(async (): Promise<void> => {
if (approvalState !== ApprovalState.NOT_APPROVED) {
console.error('approve was called unnecessarily')
return
}
if (!chainId) {
console.error('no chainId')
return
}
if (!token) {
console.error('no token')
return
}
if (!tokenContract) {
console.error('tokenContract is null')
return
}
if (!amountToApprove) {
console.error('missing amount to approve')
return
}
if (!spender) {
console.error('no spender')
return
}
let useExact = false
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
// general fallback for tokens who restrict approval amounts
useExact = true
return tokenContract.estimateGas.approve(spender, amountToApprove.quotient.toString())
const approveCallback = useCallback(() => {
return approvalCallback().then((response?: TransactionResponse) => {
if (response) {
invariant(token && spender)
addTransaction(response, { type: TransactionType.APPROVAL, tokenAddress: token.address, spender })
}
})
}, [approvalCallback, token, spender, addTransaction])
return tokenContract
.approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas),
})
.then((response: TransactionResponse) => {
addTransaction(response, { type: TransactionType.APPROVAL, tokenAddress: token.address, spender })
})
.catch((error: Error) => {
console.debug('Failed to approve token', error)
throw error
})
}, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction, chainId])
return [approval, approveCallback]
}
return [approvalState, approve]
export function useApprovalOptimizedTrade(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent
) {
return useSwapApprovalOptimizedTrade(trade, allowedSlippage, useHasPendingApproval)
}
// wraps useApproveCallback in the context of a swap
export function useApproveCallbackFromTrade(
trade:
| V2Trade<Currency, Currency, TradeType>
......@@ -142,84 +48,5 @@ export function useApproveCallbackFromTrade(
| undefined,
allowedSlippage: Percent
) {
const { chainId } = useActiveWeb3React()
const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage]
)
const approveCallback = useApproveCallback(
amountToApprove,
chainId
? trade instanceof V2Trade
? V2_ROUTER_ADDRESS[chainId]
: trade instanceof V3Trade
? V3_ROUTER_ADDRESS[chainId]
: SWAP_ROUTER_ADDRESSES[chainId]
: undefined
)
// TODO: remove L162-168 after testing is done. This error will help detect mistakes in the logic.
if (
(Trade instanceof V2Trade && approveCallback[0] !== ApprovalState.APPROVED) ||
(trade instanceof V3Trade && approveCallback[0] !== ApprovalState.APPROVED)
) {
throw new Error('Trying to approve legacy router')
}
return approveCallback
}
export function useApprovalOptimizedTrade(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent
):
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined {
const onlyV2Routes = trade?.routes.every((route) => route.protocol === Protocol.V2)
const onlyV3Routes = trade?.routes.every((route) => route.protocol === Protocol.V3)
const tradeHasSplits = (trade?.routes.length ?? 0) > 1
const approvalStates = useAllApprovalStates(trade, allowedSlippage)
const optimizedSwapRouter = useMemo(
() => getTxOptimizedSwapRouter({ onlyV2Routes, onlyV3Routes, tradeHasSplits, approvalStates }),
[approvalStates, tradeHasSplits, onlyV2Routes, onlyV3Routes]
)
return useMemo(() => {
if (!trade) return undefined
try {
switch (optimizedSwapRouter) {
case SwapRouterVersion.V2V3:
return trade
case SwapRouterVersion.V2:
const pairs = trade.swaps[0].route.pools.filter((pool) => pool instanceof Pair) as Pair[]
const v2Route = new V2Route(pairs, trade.inputAmount.currency, trade.outputAmount.currency)
return new V2Trade(v2Route, trade.inputAmount, trade.tradeType)
case SwapRouterVersion.V3:
return V3Trade.createUncheckedTradeWithMultipleRoutes({
routes: trade.swaps.map(({ route, inputAmount, outputAmount }) => ({
route: new V3Route(
route.pools.filter((p) => p instanceof Pool) as Pool[],
inputAmount.currency,
outputAmount.currency
),
inputAmount,
outputAmount,
})),
tradeType: trade.tradeType,
})
default:
return undefined
}
} catch (e) {
// TODO(#2989): remove try-catch
console.debug(e)
return undefined
}
}, [trade, optimizedSwapRouter])
return useSwapApproval(trade, allowedSlippage, useHasPendingApproval)
}
......@@ -102,7 +102,7 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
<SummaryColumn gap={0.75} flex justify="center">
<Summary input={inputAmount} output={outputAmount} usdc={true} />
<ThemedText.Caption>
1 {inputCurrency.symbol} = {price} {outputCurrency.symbol}
1 {inputCurrency.symbol} = {price?.toSignificant(6)} {outputCurrency.symbol}
</ThemedText.Caption>
</SummaryColumn>
<Rule />
......
import { Trans } from '@lingui/macro'
import { useSwapInfo } from 'lib/hooks/swap'
import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
import { Field } from 'lib/state/swap'
import { useCallback, useMemo, useState } from 'react'
import ActionButton from '../ActionButton'
......@@ -6,49 +9,60 @@ import Dialog from '../Dialog'
import { StatusDialog } from './Status'
import { SummaryDialog } from './Summary'
const mockBalance = 123.45
const mockInputAmount = 10
const mockApproved = true
enum Mode {
NONE,
SWAP,
SUMMARY,
STATUS,
}
export default function SwapButton() {
const [mode, setMode] = useState(Mode.NONE)
const [mode, setMode] = useState(Mode.SWAP)
const {
trade,
allowedSlippage,
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo()
// TODO(zzmp): Track pending approval
const useIsPendingApproval = () => false
const optimizedTrade = useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval)
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
// TODO(zzmp): Pass optimized trade to SummaryDialog
//@TODO(ianlapham): update this to refer to balances and use real symbol
const actionProps = useMemo(() => {
if (mockInputAmount < mockBalance) {
if (mockApproved) {
return {}
} else {
if (inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
if (approval === ApprovalState.NOT_APPROVED) {
return {
updated: { message: <Trans>Approve symbol first</Trans>, action: <Trans>Approve</Trans> },
updated: {
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
action: <Trans>Approve</Trans>,
},
}
}
if (approval === ApprovalState.PENDING) {
return { disabled: true }
}
return {}
}
return { disabled: true }
}, [])
}, [approval, inputCurrencyAmount, inputCurrencyBalance])
const onConfirm = useCallback(() => {
// TODO: Send the tx to the connected wallet.
setMode(Mode.STATUS)
}, [])
return (
<>
<ActionButton color="interactive" onClick={() => setMode(Mode.SUMMARY)} onUpdate={() => void 0} {...actionProps}>
<ActionButton color="interactive" onClick={() => setMode(Mode.SUMMARY)} onUpdate={getApproval} {...actionProps}>
<Trans>Review swap</Trans>
</ActionButton>
{mode >= Mode.SUMMARY && (
<Dialog color="dialog" onClose={() => setMode(Mode.NONE)}>
<Dialog color="dialog" onClose={() => setMode(Mode.SWAP)}>
<SummaryDialog onConfirm={onConfirm} />
</Dialog>
)}
{mode >= Mode.STATUS && (
<Dialog color="dialog">
<StatusDialog onClose={() => setMode(Mode.NONE)} />
<StatusDialog onClose={() => setMode(Mode.SWAP)} />
</Dialog>
)}
</>
......
import { Protocol, Trade } from '@uniswap/router-sdk'
import { Currency, 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'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react'
import invariant from 'tiny-invariant'
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
import { ApprovalState, useApproval, useApprovalStateForSpender } from '../useApproval'
export { ApprovalState } from '../useApproval'
/** Returns approval state for all known swap routers */
function useSwapApprovalStates(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent,
useIsPendingApproval: (token?: Token, spender?: string) => boolean
): { v2: ApprovalState; v3: ApprovalState; v2V3: ApprovalState } {
const { chainId } = useActiveWeb3React()
const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage]
)
const v2RouterAddress = chainId ? V2_ROUTER_ADDRESS[chainId] : undefined
const v3RouterAddress = chainId ? V3_ROUTER_ADDRESS[chainId] : undefined
const swapRouterAddress = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
const v2 = useApprovalStateForSpender(amountToApprove, v2RouterAddress, useIsPendingApproval)
const v3 = useApprovalStateForSpender(amountToApprove, v3RouterAddress, useIsPendingApproval)
const v2V3 = useApprovalStateForSpender(amountToApprove, swapRouterAddress, useIsPendingApproval)
return useMemo(() => ({ v2, v3, v2V3 }), [v2, v2V3, v3])
}
// wraps useApproveCallback in the context of a swap
export default function useSwapApproval(
trade:
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined,
allowedSlippage: Percent,
useIsPendingApproval: (token?: Token, spender?: string) => boolean
) {
const { chainId } = useActiveWeb3React()
const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage]
)
const spender = useMemo(
() =>
chainId
? trade instanceof V2Trade
? V2_ROUTER_ADDRESS[chainId]
: trade instanceof V3Trade
? V3_ROUTER_ADDRESS[chainId]
: SWAP_ROUTER_ADDRESSES[chainId]
: undefined,
[chainId, trade]
)
const approval = useApproval(amountToApprove, spender, useIsPendingApproval)
if (trade instanceof V2Trade || trade instanceof V3Trade) {
const approvalState = approval[0]
invariant(approvalState === ApprovalState.APPROVED, 'Trying to approve legacy router')
}
return approval
}
export function useSwapApprovalOptimizedTrade(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent,
useIsPendingApproval: (token?: Token, spender?: string) => boolean
):
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined {
const onlyV2Routes = trade?.routes.every((route) => route.protocol === Protocol.V2)
const onlyV3Routes = trade?.routes.every((route) => route.protocol === Protocol.V3)
const tradeHasSplits = (trade?.routes.length ?? 0) > 1
const approvalStates = useSwapApprovalStates(trade, allowedSlippage, useIsPendingApproval)
const optimizedSwapRouter = useMemo(
() => getTxOptimizedSwapRouter({ onlyV2Routes, onlyV3Routes, tradeHasSplits, approvalStates }),
[approvalStates, tradeHasSplits, onlyV2Routes, onlyV3Routes]
)
return useMemo(() => {
if (!trade) return undefined
try {
switch (optimizedSwapRouter) {
case SwapRouterVersion.V2V3:
return trade
case SwapRouterVersion.V2:
const pairs = trade.swaps[0].route.pools.filter((pool) => pool instanceof Pair) as Pair[]
const v2Route = new V2Route(pairs, trade.inputAmount.currency, trade.outputAmount.currency)
return new V2Trade(v2Route, trade.inputAmount, trade.tradeType)
case SwapRouterVersion.V3:
return V3Trade.createUncheckedTradeWithMultipleRoutes({
routes: trade.swaps.map(({ route, inputAmount, outputAmount }) => ({
route: new V3Route(
route.pools.filter((p) => p instanceof Pool) as Pool[],
inputAmount.currency,
outputAmount.currency
),
inputAmount,
outputAmount,
})),
tradeType: trade.tradeType,
})
default:
return undefined
}
} catch (e) {
// TODO(#2989): remove try-catch
console.debug(e)
return undefined
}
}, [trade, optimizedSwapRouter])
}
import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useTokenContract } from 'hooks/useContract'
import { useTokenAllowance } from 'hooks/useTokenAllowance'
import { useCallback, useMemo } from 'react'
import { calculateGasMargin } from 'utils/calculateGasMargin'
export enum ApprovalState {
UNKNOWN = 'UNKNOWN',
NOT_APPROVED = 'NOT_APPROVED',
PENDING = 'PENDING',
APPROVED = 'APPROVED',
}
export function useApprovalStateForSpender(
amountToApprove: CurrencyAmount<Currency> | undefined,
spender: string | undefined,
useIsPendingApproval: (token?: Token, spender?: string) => boolean
): ApprovalState {
const { account } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useIsPendingApproval(token, spender)
return useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve
if (!currentAllowance) return ApprovalState.UNKNOWN
// amountToApprove will be defined if currentAllowance is
return currentAllowance.lessThan(amountToApprove)
? pendingApproval
? ApprovalState.PENDING
: ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender])
}
export function useApproval(
amountToApprove: CurrencyAmount<Currency> | undefined,
spender: string | undefined,
useIsPendingApproval: (token?: Token, spender?: string) => boolean
): [ApprovalState, () => Promise<TransactionResponse | undefined>] {
const { chainId } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
// check the current approval status
const approvalState = useApprovalStateForSpender(amountToApprove, spender, useIsPendingApproval)
const tokenContract = useTokenContract(token?.address)
const approve = useCallback(async (): Promise<TransactionResponse | undefined> => {
function logFailure(error: Error | string): undefined {
console.warn(`${token?.symbol || 'Token'} approval failed:`, error)
return
}
// Bail early if there is an issue.
if (approvalState !== ApprovalState.NOT_APPROVED) {
return logFailure('approve was called unnecessarily')
} else if (!chainId) {
return logFailure('no chainId')
} else if (!token) {
return logFailure('no token')
} else if (!tokenContract) {
return logFailure('tokenContract is null')
} else if (!amountToApprove) {
return logFailure('missing amount to approve')
} else if (!spender) {
return logFailure('no spender')
}
let useExact = false
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
// general fallback for tokens which restrict approval amounts
useExact = true
return tokenContract.estimateGas.approve(spender, amountToApprove.quotient.toString())
})
return tokenContract
.approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas),
})
.catch((error: Error) => {
logFailure(error)
throw error
})
}, [approvalState, token, tokenContract, amountToApprove, spender, chainId])
return [approvalState, approve]
}
import { TransactionResponse } from '@ethersproject/providers'
import { Token } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useTransactionMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useCallback, useMemo } from 'react'
......@@ -75,11 +76,11 @@ export function isTransactionRecent(tx: TransactionDetails): boolean {
}
// returns whether a token has a pending approval transaction
export function useHasPendingApproval(tokenAddress: string | undefined, spender: string | undefined): boolean {
export function useHasPendingApproval(token?: Token, spender?: string): boolean {
const allTransactions = useAllTransactions()
return useMemo(
() =>
typeof tokenAddress === 'string' &&
typeof token?.address === 'string' &&
typeof spender === 'string' &&
Object.keys(allTransactions).some((hash) => {
const tx = allTransactions[hash]
......@@ -88,10 +89,10 @@ export function useHasPendingApproval(tokenAddress: string | undefined, spender:
return false
} else {
if (tx.info.type !== TransactionType.APPROVAL) return false
return tx.info.spender === spender && tx.info.tokenAddress === tokenAddress && isTransactionRecent(tx)
return tx.info.spender === spender && tx.info.tokenAddress === token.address && isTransactionRecent(tx)
}
}),
[allTransactions, spender, tokenAddress]
[allTransactions, spender, token?.address]
)
}
......
import { ApprovalState } from 'hooks/useApproveCallback'
import { ApprovalState } from 'lib/hooks/useApproval'
import { getTxOptimizedSwapRouter, SwapRouterVersion } from './getTxOptimizedSwapRouter'
......
import { ApprovalState } from 'hooks/useApproveCallback'
import { ApprovalState } from 'lib/hooks/useApproval'
export enum SwapRouterVersion {
V2,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment