Skip to content

Commit

Permalink
feat: permit2 and universal-router integration (Uniswap#321)
Browse files Browse the repository at this point in the history
* build: bump to valid version of universal-router-sdk

* fix: check for zero

* refactor: look up pending approval by spender

* fix: style nits

* feat: add tooltip to ActionButton

* fix: disable confirm button while pending

* fix: improve onSubmit typing

* fix: avoid setting controller when uncontrolled

* feat: add token allowance callback

* feat: permit2 logic

* feat: universal-router logic

* feat: integrate permit2

* chore: initial PR review

* fix: do not err on unsupported chain

* refactor: try-catch each swap action

* refactor: use async-await for ur call
  • Loading branch information
zzmp authored Dec 6, 2022
1 parent 0832a06 commit 6040394
Show file tree
Hide file tree
Showing 19 changed files with 518 additions and 82 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.10.0",
"@uniswap/token-lists": "^1.0.0-beta.30",
"@uniswap/universal-router-sdk": "^1.2.0",
"@uniswap/universal-router-sdk": "^1.2.1",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.8.2",
"@web3-react/core": "8.0.35-beta.0",
Expand Down
22 changes: 14 additions & 8 deletions src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Color, ThemedText } from 'theme'

import Button from './Button'
import Row, { RowProps } from './Row'
import Tooltip from './Tooltip'

const StyledButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
Expand Down Expand Up @@ -65,6 +66,7 @@ export const Overlay = styled(Row)<{ hasAction: boolean }>`
export interface Action {
message: ReactNode
icon?: Icon
tooltip?: ReactNode
onClick?: () => void
children?: ReactNode
}
Expand All @@ -89,16 +91,20 @@ export default function ActionButton({
const textColor = useMemo(() => (color === 'accent' ? 'onAccent' : 'currentColor'), [color])
return (
<Overlay hasAction={Boolean(action)} flex align="stretch" {...wrapperProps}>
{(action ? action.onClick : true) && (
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick} {...rest}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action?.children || children}
</ThemedText.TransitionButton>
</StyledButton>
)}
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick} {...rest}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action?.children || children}
</ThemedText.TransitionButton>
</StyledButton>
{action && (
<ActionRow gap={0.5}>
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
{action.tooltip ? (
<Tooltip icon={LargeIcon} iconProps={{ color: 'currentColor', icon: action.icon || AlertTriangle }}>
{action.tooltip}
</Tooltip>
) : (
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
)}
<ThemedText.Subhead2>{action?.message}</ThemedText.Subhead2>
</ActionRow>
)}
Expand Down
1 change: 0 additions & 1 deletion src/components/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const PopoverContainer = styled.div<{ show: boolean }>`
`

const Reference = styled.div`
align-self: flex-start;
display: inline-block;
height: 1em;
`
Expand Down
3 changes: 2 additions & 1 deletion src/components/Swap/Summary/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ function ConfirmButton({
<ActionButton
onClick={onClick}
action={action}
disabled={isPending}
wrapperProps={{
style: {
bottom: '0.25em',
Expand All @@ -146,7 +147,7 @@ function ConfirmButton({
},
}}
>
<Trans>Confirm swap</Trans>
{isPending ? <Trans>Confirm</Trans> : <Trans>Confirm swap</Trans>}
</ActionButton>
)
}
Expand Down
25 changes: 17 additions & 8 deletions src/components/Swap/SwapActionButton/ApproveButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import ActionButton from 'components/ActionButton'
import EtherscanLink from 'components/EtherscanLink'
import { SWAP_ROUTER_ADDRESSES } from 'constants/addresses'
import { SwapApprovalState } from 'hooks/swap/useSwapApproval'
import { usePendingApproval } from 'hooks/transactions'
import { Spinner } from 'icons'
Expand Down Expand Up @@ -30,18 +32,23 @@ export default function ApproveButton({
tokenAddress: string
spenderAddress: string
} | void>
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | undefined>) => Promise<boolean>
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | void>) => Promise<void>
}) {
const [isPending, setIsPending] = useState(false)
const onApprove = useCallback(async () => {
setIsPending(true)
await onSubmit(async () => {
const info = await approve?.()
if (!info) return
try {
await onSubmit(async () => {
const info = await approve?.()
if (!info) return

return { type: TransactionType.APPROVAL, ...info }
})
setIsPending(false)
return { type: TransactionType.APPROVAL, ...info }
})
} catch (e) {
console.error(e) // ignore error
} finally {
setIsPending(false)
}
}, [approve, onSubmit])

const currency = trade?.inputAmount?.currency
Expand All @@ -50,7 +57,9 @@ export default function ApproveButton({
// Reset the pending state if currency changes.
useEffect(() => setIsPending(false), [currency])

const pendingApprovalHash = usePendingApproval(currency?.isToken ? currency : undefined)
const { chainId } = useWeb3React()
const spender = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
const pendingApprovalHash = usePendingApproval(currency?.isToken ? currency : undefined, spender)

const actionProps = useMemo(() => {
switch (state) {
Expand Down
97 changes: 97 additions & 0 deletions src/components/Swap/SwapActionButton/Permit2Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { t, Trans } from '@lingui/macro'
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import ActionButton from 'components/ActionButton'
import EtherscanLink from 'components/EtherscanLink'
import { usePendingApproval } from 'hooks/transactions'
import { PermitState } from 'hooks/usePermit2'
import { Spinner } from 'icons'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { ApprovalTransactionInfo } from 'state/transactions'
import { Colors } from 'theme'
import { ExplorerDataType } from 'utils/getExplorerLink'

/**
* An approving PermitButton.
* Should only be rendered if a valid trade exists that is not yet permitted.
*/
export default function PermitButton({
color,
trade,
state,
callback,
onSubmit,
}: {
color: keyof Colors
trade?: InterfaceTrade
state: PermitState
callback?: () => Promise<ApprovalTransactionInfo | void>
onSubmit: (submit?: () => Promise<ApprovalTransactionInfo | void>) => Promise<void>
}) {
const currency = trade?.inputAmount?.currency
const [isPending, setIsPending] = useState(false)
const [isFailed, setIsFailed] = useState(false)
useEffect(() => {
// Reset pending/failed state if currency changes.
setIsPending(false)
setIsFailed(false)
}, [currency])

const onClick = useCallback(async () => {
setIsPending(true)
try {
await onSubmit(callback)
setIsFailed(false)
} catch (e) {
console.error(e)
setIsFailed(true)
} finally {
setIsPending(false)
}
}, [callback, onSubmit])

const pendingApproval = usePendingApproval(currency?.isToken ? currency : undefined, PERMIT2_ADDRESS)

const actionProps = useMemo(() => {
switch (state) {
case PermitState.UNKNOWN:
case PermitState.PERMITTED:
return
case PermitState.APPROVAL_NEEDED:
case PermitState.PERMIT_NEEDED:
}

if (isPending) {
return {
icon: Spinner,
message: t`Approve in your wallet`,
}
} else if (pendingApproval) {
return {
icon: Spinner,
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={pendingApproval}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
}
} else if (isFailed) {
return {
message: t`Approval failed`,
onClick,
}
} else {
return {
tooltip: t`Permission is required for Uniswap to swap each token. This will expire after one month for your security.`,
message: `Approve use of ${currency?.symbol ?? 'token'}`,
onClick,
}
}
}, [currency?.symbol, isFailed, isPending, onClick, pendingApproval, state])

return (
<ActionButton color={color} disabled={!actionProps?.onClick} action={actionProps}>
{isFailed ? t`Try again` : t`Approve`}
</ActionButton>
)
}
71 changes: 46 additions & 25 deletions src/components/Swap/SwapActionButton/SwapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { SwapApprovalState } from 'hooks/swap/useSwapApproval'
import { useSwapCallback } from 'hooks/swap/useSwapCallback'
import { useConditionalHandler } from 'hooks/useConditionalHandler'
import { useSetOldestValidBlock } from 'hooks/useIsValidBlock'
import { PermitState } from 'hooks/usePermit2'
import { usePermit2 } from 'hooks/useSyncFlags'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import { useUniversalRouterSwapCallback } from 'hooks/useUniversalRouter'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useEffect, useState } from 'react'
import { feeOptionsAtom, Field, swapEventHandlersAtom } from 'state/swap'
Expand All @@ -17,6 +20,7 @@ import ActionButton from '../../ActionButton'
import Dialog from '../../Dialog'
import { SummaryDialog } from '../Summary'
import ApproveButton from './ApproveButton'
import PermitButton from './Permit2Button'

/**
* A swapping ActionButton.
Expand All @@ -29,28 +33,37 @@ export default function SwapButton({
}: {
color: keyof Colors
disabled: boolean
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | SwapTransactionInfo | undefined>) => Promise<boolean>
onSubmit: (submit?: () => Promise<ApprovalTransactionInfo | SwapTransactionInfo | void>) => Promise<void>
}) {
const { account, chainId } = useWeb3React()
const {
[Field.INPUT]: { usdc: inputUSDC },
[Field.OUTPUT]: { usdc: outputUSDC },
trade: { trade, gasUseEstimateUSD },
approval,
permit,
slippage,
impact,
approval,
} = useSwapInfo()
const deadline = useTransactionDeadline()
const feeOptions = useAtomValue(feeOptionsAtom)

const { callback: swapCallback } = useSwapCallback({
trade,
const permit2 = usePermit2()
const { callback: swapRouterCallback } = useSwapCallback({
trade: permit2 ? undefined : trade,
allowedSlippage: slippage.allowed,
recipientAddressOrName: account ?? null,
signatureData: approval?.signatureData,
deadline,
feeOptions,
})
const universalRouterCallback = useUniversalRouterSwapCallback(permit2 ? trade : undefined, {
slippageTolerance: slippage.allowed,
deadline,
permit: permit.signature,
feeOptions,
})
const swapCallback = permit2 ? universalRouterCallback : swapRouterCallback

const [open, setOpen] = useState(false)
// Close the review modal if there is no available trade.
Expand All @@ -60,29 +73,31 @@ export default function SwapButton({

const setOldestValidBlock = useSetOldestValidBlock()
const onSwap = useCallback(async () => {
const submitted = await onSubmit(async () => {
const response = await swapCallback?.()
if (!response) return
try {
await onSubmit(async () => {
const response = await swapCallback?.()
if (!response) return

// Set the block containing the response to the oldest valid block to ensure that the
// completed trade's impact is reflected in future fetched trades.
response.wait(1).then((receipt) => {
setOldestValidBlock(receipt.blockNumber)
})
// Set the block containing the response to the oldest valid block to ensure that the
// completed trade's impact is reflected in future fetched trades.
response.wait(1).then((receipt) => {
setOldestValidBlock(receipt.blockNumber)
})

invariant(trade)
return {
type: TransactionType.SWAP,
response,
tradeType: trade.tradeType,
trade,
slippageTolerance: slippage.allowed,
}
})
invariant(trade)
return {
type: TransactionType.SWAP,
response,
tradeType: trade.tradeType,
trade,
slippageTolerance: slippage.allowed,
}
})

// Only close the review modal if the transaction has submitted.
if (submitted) {
// Only close the review modal if the swap submitted (ie no-throw).
setOpen(false)
} catch (e) {
console.error(e) // ignore error
}
}, [onSubmit, setOldestValidBlock, slippage.allowed, swapCallback, trade])

Expand All @@ -91,8 +106,14 @@ export default function SwapButton({
setOpen(await onReviewSwapClick())
}, [onReviewSwapClick])

if (approval.state !== SwapApprovalState.APPROVED && !disabled) {
return <ApproveButton color={color} onSubmit={onSubmit} trade={trade} {...approval} />
if (usePermit2()) {
if (![PermitState.UNKNOWN, PermitState.PERMITTED].includes(permit.state)) {
return <PermitButton color={color} onSubmit={onSubmit} trade={trade} {...permit} />
}
} else {
if (approval.state !== SwapApprovalState.APPROVED && !disabled) {
return <ApproveButton color={color} onSubmit={onSubmit} trade={trade} {...approval} />
}
}

return (
Expand Down
25 changes: 14 additions & 11 deletions src/components/Swap/SwapActionButton/WrapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function WrapButton({
}: {
color: keyof Colors
disabled: boolean
onSubmit: (submit: () => Promise<WrapTransactionInfo | UnwrapTransactionInfo | undefined>) => Promise<boolean>
onSubmit: (submit: () => Promise<WrapTransactionInfo | UnwrapTransactionInfo | void>) => Promise<void>
}) {
const { type: wrapType, callback: wrapCallback } = useWrapCallback()

Expand All @@ -33,17 +33,20 @@ export default function WrapButton({
const inputCurrency = wrapType === TransactionType.WRAP ? native : native.wrapped
const onWrap = useCallback(async () => {
setIsPending(true)
await onSubmit(async () => {
const response = await wrapCallback()
if (!response) return
try {
await onSubmit(async () => {
const response = await wrapCallback()
if (!response) return

invariant(wrapType !== undefined) // if response is valid, then so is wrapType
const amount = CurrencyAmount.fromRawAmount(native, response.value?.toString() ?? '0')
return { response, type: wrapType, amount }
})

// Whether or not the transaction submits, reset the pending state.
setIsPending(false)
invariant(wrapType !== undefined) // if response is valid, then so is wrapType
const amount = CurrencyAmount.fromRawAmount(native, response.value?.toString() ?? '0')
return { response, type: wrapType, amount }
})
} catch (e) {
console.error(e) // ignore error
} finally {
setIsPending(false)
}
}, [native, onSubmit, wrapCallback, wrapType])

const actionProps = useMemo(
Expand Down
Loading

0 comments on commit 6040394

Please sign in to comment.