feat: use permit when valid on approvals (#3354)

* use permit when valid on approvals

* fix broken check for permit sig

* update conditionals

* update text

* remove unneeded else

* move permit and approve logic to combined hook

* update comment

* split txn and approval state, code clean

* organize disable conditions

* small changes

* update conditional check
This commit is contained in:
Ian Lapham 2022-03-03 12:28:36 -05:00 committed by GitHub
parent 4f6173675d
commit 8703013b2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 138 additions and 55 deletions

@ -1,18 +1,17 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
import { useUpdateAtom } from 'jotai/utils'
import { WrapErrorText } from 'lib/components/Swap/WrapErrorText'
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
import useSwapApproval, {
ApprovalState,
import {
ApproveOrPermitState,
useApproveOrPermit,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction } from 'lib/hooks/transactions'
import { usePendingApproval } from 'lib/hooks/transactions'
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Spinner } from 'lib/icons'
@ -68,79 +67,86 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
const [approval, getApproval] = useSwapApproval(
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
optimizedTrade,
allowedSlippage,
useIsPendingApproval,
approvalCurrencyAmount
)
const approvalHash = usePendingApproval(
inputCurrency?.isToken ? inputCurrency : undefined,
useSwapRouterAddress(optimizedTrade)
)
const addTransaction = useAddTransaction()
const addApprovalTransaction = useCallback(() => {
getApproval().then((transaction) => {
const onApprove = useCallback(() => {
handleApproveOrPermit().then((transaction) => {
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
})
}, [addTransaction, getApproval])
}, [addTransaction, handleApproveOrPermit])
const { type: wrapType, callback: wrapCallback, error: wrapError, loading: wrapLoading } = useWrapCallback()
const disableSwap = useMemo(
() =>
disabled ||
!chainId ||
wrapLoading ||
(wrapType !== WrapType.NOT_APPLICABLE && wrapError) ||
approvalState === ApproveOrPermitState.PENDING_SIGNATURE ||
!(inputTradeCurrencyAmount && inputCurrencyBalance) ||
inputCurrencyBalance.lessThan(inputTradeCurrencyAmount),
[disabled, chainId, wrapLoading, wrapType, wrapError, approvalState, inputTradeCurrencyAmount, inputCurrencyBalance]
)
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disabled || wrapLoading) return { disabled: true }
if (!disabled && chainId) {
const hasSufficientInputForTrade =
inputTradeCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputTradeCurrencyAmount)
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,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
if (disableSwap) {
return { disabled: true }
}
if (
wrapType === WrapType.NOT_APPLICABLE &&
(approvalState === ApproveOrPermitState.REQUIRES_APPROVAL ||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE)
) {
const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency)
return {
action: {
message:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? (
<Trans>Allow {currency.symbol} first</Trans>
) : (
<Trans>Approve {currency.symbol} first</Trans>
),
icon: Spinner,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} else if (hasSufficientInputForTrade || (wrapType !== WrapType.NOT_APPLICABLE && !wrapError)) {
return {}
onClick: onApprove,
children:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? <Trans>Allow</Trans> : <Trans>Approve</Trans>,
},
}
}
return { disabled: true }
}, [
addApprovalTransaction,
approval,
approvalCurrencyAmount?.currency,
approvalHash,
chainId,
disabled,
inputCurrency,
inputCurrencyBalance,
inputTradeCurrencyAmount,
wrapError,
wrapLoading,
wrapType,
])
if (approvalState === ApproveOrPermitState.PENDING_APPROVAL) {
return {
disabled: true,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
icon: Spinner,
onClick: () => void 0, // @TODO: should not require an onclick
children: <Trans>Approve</Trans>,
},
}
}
return {}
}, [approvalCurrencyAmount?.currency, approvalHash, approvalState, disableSwap, inputCurrency, onApprove, wrapType])
const deadline = useTransactionDeadline()
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
// the callback to execute the swap
const { callback: swapCallback } = useSwapCallback({

@ -4,7 +4,9 @@ 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 { useERC20PermitFromTrade, UseERC20PermitState } from 'hooks/useERC20Permit'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { useCallback, useMemo } from 'react'
import invariant from 'tiny-invariant'
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
@ -134,3 +136,78 @@ export function useSwapApprovalOptimizedTrade(
}
}, [trade, optimizedSwapRouter])
}
export enum ApproveOrPermitState {
REQUIRES_APPROVAL,
PENDING_APPROVAL,
REQUIRES_SIGNATURE,
PENDING_SIGNATURE,
APPROVED,
}
/**
* Returns all relevant statuses and callback functions for approvals.
* Considers both standard approval and ERC20 permit.
*/
export const useApproveOrPermit = (
trade:
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined,
allowedSlippage: Percent,
useIsPendingApproval: (token?: Token, spender?: string) => boolean,
amount?: CurrencyAmount<Currency> // defaults to trade.maximumAmountIn(allowedSlippage)
) => {
const deadline = useTransactionDeadline()
// Check approvals on ERC20 contract based on amount.
const [approval, getApproval] = useSwapApproval(trade, allowedSlippage, useIsPendingApproval, amount)
// Check status of permit and whether token supports it.
const {
state: signatureState,
signatureData,
gatherPermitSignature,
} = useERC20PermitFromTrade(trade, allowedSlippage, deadline)
const notApproved = approval === ApprovalState.NOT_APPROVED && !(signatureState === UseERC20PermitState.SIGNED)
// If permit is supported, trigger a signature, if not create approval transaction.
const handleApproveOrPermit = useCallback(async () => {
if (signatureState === UseERC20PermitState.NOT_SIGNED && gatherPermitSignature) {
try {
return await gatherPermitSignature()
} catch (error) {
// Try to approve if gatherPermitSignature failed for any reason other than the user rejecting it.
if (error?.code !== 4001) {
return getApproval()
}
}
} else {
return getApproval()
}
}, [signatureState, gatherPermitSignature, getApproval])
const approvalState = useMemo(() => {
if (approval === ApprovalState.PENDING) {
return ApproveOrPermitState.PENDING_APPROVAL
}
if (signatureState === UseERC20PermitState.LOADING) {
return ApproveOrPermitState.PENDING_SIGNATURE
}
if (notApproved && Boolean(gatherPermitSignature)) {
return ApproveOrPermitState.REQUIRES_SIGNATURE
}
if (notApproved) {
return ApproveOrPermitState.REQUIRES_APPROVAL
}
return ApproveOrPermitState.APPROVED
}, [approval, gatherPermitSignature, notApproved, signatureState])
return {
approvalState,
signatureData,
handleApproveOrPermit,
}
}