Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Commit 6040394

Browse files
authored
feat: permit2 and universal-router integration (#321)
* 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
1 parent 0832a06 commit 6040394

19 files changed

+518
-82
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"@uniswap/sdk-core": "^3.0.1",
7171
"@uniswap/smart-order-router": "^2.10.0",
7272
"@uniswap/token-lists": "^1.0.0-beta.30",
73-
"@uniswap/universal-router-sdk": "^1.2.0",
73+
"@uniswap/universal-router-sdk": "^1.2.1",
7474
"@uniswap/v2-sdk": "^3.0.1",
7575
"@uniswap/v3-sdk": "^3.8.2",
7676
"@web3-react/core": "8.0.35-beta.0",

src/components/ActionButton.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Color, ThemedText } from 'theme'
55

66
import Button from './Button'
77
import Row, { RowProps } from './Row'
8+
import Tooltip from './Tooltip'
89

910
const StyledButton = styled(Button)`
1011
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
@@ -65,6 +66,7 @@ export const Overlay = styled(Row)<{ hasAction: boolean }>`
6566
export interface Action {
6667
message: ReactNode
6768
icon?: Icon
69+
tooltip?: ReactNode
6870
onClick?: () => void
6971
children?: ReactNode
7072
}
@@ -89,16 +91,20 @@ export default function ActionButton({
8991
const textColor = useMemo(() => (color === 'accent' ? 'onAccent' : 'currentColor'), [color])
9092
return (
9193
<Overlay hasAction={Boolean(action)} flex align="stretch" {...wrapperProps}>
92-
{(action ? action.onClick : true) && (
93-
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick} {...rest}>
94-
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
95-
{action?.children || children}
96-
</ThemedText.TransitionButton>
97-
</StyledButton>
98-
)}
94+
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick} {...rest}>
95+
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
96+
{action?.children || children}
97+
</ThemedText.TransitionButton>
98+
</StyledButton>
9999
{action && (
100100
<ActionRow gap={0.5}>
101-
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
101+
{action.tooltip ? (
102+
<Tooltip icon={LargeIcon} iconProps={{ color: 'currentColor', icon: action.icon || AlertTriangle }}>
103+
{action.tooltip}
104+
</Tooltip>
105+
) : (
106+
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
107+
)}
102108
<ThemedText.Subhead2>{action?.message}</ThemedText.Subhead2>
103109
</ActionRow>
104110
)}

src/components/Popover.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const PopoverContainer = styled.div<{ show: boolean }>`
2222
`
2323

2424
const Reference = styled.div`
25-
align-self: flex-start;
2625
display: inline-block;
2726
height: 1em;
2827
`

src/components/Swap/Summary/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ function ConfirmButton({
138138
<ActionButton
139139
onClick={onClick}
140140
action={action}
141+
disabled={isPending}
141142
wrapperProps={{
142143
style: {
143144
bottom: '0.25em',
@@ -146,7 +147,7 @@ function ConfirmButton({
146147
},
147148
}}
148149
>
149-
<Trans>Confirm swap</Trans>
150+
{isPending ? <Trans>Confirm</Trans> : <Trans>Confirm swap</Trans>}
150151
</ActionButton>
151152
)
152153
}

src/components/Swap/SwapActionButton/ApproveButton.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { TransactionResponse } from '@ethersproject/providers'
22
import { Trans } from '@lingui/macro'
3+
import { useWeb3React } from '@web3-react/core'
34
import ActionButton from 'components/ActionButton'
45
import EtherscanLink from 'components/EtherscanLink'
6+
import { SWAP_ROUTER_ADDRESSES } from 'constants/addresses'
57
import { SwapApprovalState } from 'hooks/swap/useSwapApproval'
68
import { usePendingApproval } from 'hooks/transactions'
79
import { Spinner } from 'icons'
@@ -30,18 +32,23 @@ export default function ApproveButton({
3032
tokenAddress: string
3133
spenderAddress: string
3234
} | void>
33-
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | undefined>) => Promise<boolean>
35+
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | void>) => Promise<void>
3436
}) {
3537
const [isPending, setIsPending] = useState(false)
3638
const onApprove = useCallback(async () => {
3739
setIsPending(true)
38-
await onSubmit(async () => {
39-
const info = await approve?.()
40-
if (!info) return
40+
try {
41+
await onSubmit(async () => {
42+
const info = await approve?.()
43+
if (!info) return
4144

42-
return { type: TransactionType.APPROVAL, ...info }
43-
})
44-
setIsPending(false)
45+
return { type: TransactionType.APPROVAL, ...info }
46+
})
47+
} catch (e) {
48+
console.error(e) // ignore error
49+
} finally {
50+
setIsPending(false)
51+
}
4552
}, [approve, onSubmit])
4653

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

53-
const pendingApprovalHash = usePendingApproval(currency?.isToken ? currency : undefined)
60+
const { chainId } = useWeb3React()
61+
const spender = chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined
62+
const pendingApprovalHash = usePendingApproval(currency?.isToken ? currency : undefined, spender)
5463

5564
const actionProps = useMemo(() => {
5665
switch (state) {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { t, Trans } from '@lingui/macro'
2+
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
3+
import ActionButton from 'components/ActionButton'
4+
import EtherscanLink from 'components/EtherscanLink'
5+
import { usePendingApproval } from 'hooks/transactions'
6+
import { PermitState } from 'hooks/usePermit2'
7+
import { Spinner } from 'icons'
8+
import { useCallback, useEffect, useMemo, useState } from 'react'
9+
import { InterfaceTrade } from 'state/routing/types'
10+
import { ApprovalTransactionInfo } from 'state/transactions'
11+
import { Colors } from 'theme'
12+
import { ExplorerDataType } from 'utils/getExplorerLink'
13+
14+
/**
15+
* An approving PermitButton.
16+
* Should only be rendered if a valid trade exists that is not yet permitted.
17+
*/
18+
export default function PermitButton({
19+
color,
20+
trade,
21+
state,
22+
callback,
23+
onSubmit,
24+
}: {
25+
color: keyof Colors
26+
trade?: InterfaceTrade
27+
state: PermitState
28+
callback?: () => Promise<ApprovalTransactionInfo | void>
29+
onSubmit: (submit?: () => Promise<ApprovalTransactionInfo | void>) => Promise<void>
30+
}) {
31+
const currency = trade?.inputAmount?.currency
32+
const [isPending, setIsPending] = useState(false)
33+
const [isFailed, setIsFailed] = useState(false)
34+
useEffect(() => {
35+
// Reset pending/failed state if currency changes.
36+
setIsPending(false)
37+
setIsFailed(false)
38+
}, [currency])
39+
40+
const onClick = useCallback(async () => {
41+
setIsPending(true)
42+
try {
43+
await onSubmit(callback)
44+
setIsFailed(false)
45+
} catch (e) {
46+
console.error(e)
47+
setIsFailed(true)
48+
} finally {
49+
setIsPending(false)
50+
}
51+
}, [callback, onSubmit])
52+
53+
const pendingApproval = usePendingApproval(currency?.isToken ? currency : undefined, PERMIT2_ADDRESS)
54+
55+
const actionProps = useMemo(() => {
56+
switch (state) {
57+
case PermitState.UNKNOWN:
58+
case PermitState.PERMITTED:
59+
return
60+
case PermitState.APPROVAL_NEEDED:
61+
case PermitState.PERMIT_NEEDED:
62+
}
63+
64+
if (isPending) {
65+
return {
66+
icon: Spinner,
67+
message: t`Approve in your wallet`,
68+
}
69+
} else if (pendingApproval) {
70+
return {
71+
icon: Spinner,
72+
message: (
73+
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={pendingApproval}>
74+
<Trans>Approval pending</Trans>
75+
</EtherscanLink>
76+
),
77+
}
78+
} else if (isFailed) {
79+
return {
80+
message: t`Approval failed`,
81+
onClick,
82+
}
83+
} else {
84+
return {
85+
tooltip: t`Permission is required for Uniswap to swap each token. This will expire after one month for your security.`,
86+
message: `Approve use of ${currency?.symbol ?? 'token'}`,
87+
onClick,
88+
}
89+
}
90+
}, [currency?.symbol, isFailed, isPending, onClick, pendingApproval, state])
91+
92+
return (
93+
<ActionButton color={color} disabled={!actionProps?.onClick} action={actionProps}>
94+
{isFailed ? t`Try again` : t`Approve`}
95+
</ActionButton>
96+
)
97+
}

src/components/Swap/SwapActionButton/SwapButton.tsx

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { SwapApprovalState } from 'hooks/swap/useSwapApproval'
55
import { useSwapCallback } from 'hooks/swap/useSwapCallback'
66
import { useConditionalHandler } from 'hooks/useConditionalHandler'
77
import { useSetOldestValidBlock } from 'hooks/useIsValidBlock'
8+
import { PermitState } from 'hooks/usePermit2'
9+
import { usePermit2 } from 'hooks/useSyncFlags'
810
import useTransactionDeadline from 'hooks/useTransactionDeadline'
11+
import { useUniversalRouterSwapCallback } from 'hooks/useUniversalRouter'
912
import { useAtomValue } from 'jotai/utils'
1013
import { useCallback, useEffect, useState } from 'react'
1114
import { feeOptionsAtom, Field, swapEventHandlersAtom } from 'state/swap'
@@ -17,6 +20,7 @@ import ActionButton from '../../ActionButton'
1720
import Dialog from '../../Dialog'
1821
import { SummaryDialog } from '../Summary'
1922
import ApproveButton from './ApproveButton'
23+
import PermitButton from './Permit2Button'
2024

2125
/**
2226
* A swapping ActionButton.
@@ -29,28 +33,37 @@ export default function SwapButton({
2933
}: {
3034
color: keyof Colors
3135
disabled: boolean
32-
onSubmit: (submit: () => Promise<ApprovalTransactionInfo | SwapTransactionInfo | undefined>) => Promise<boolean>
36+
onSubmit: (submit?: () => Promise<ApprovalTransactionInfo | SwapTransactionInfo | void>) => Promise<void>
3337
}) {
3438
const { account, chainId } = useWeb3React()
3539
const {
3640
[Field.INPUT]: { usdc: inputUSDC },
3741
[Field.OUTPUT]: { usdc: outputUSDC },
3842
trade: { trade, gasUseEstimateUSD },
43+
approval,
44+
permit,
3945
slippage,
4046
impact,
41-
approval,
4247
} = useSwapInfo()
4348
const deadline = useTransactionDeadline()
4449
const feeOptions = useAtomValue(feeOptionsAtom)
4550

46-
const { callback: swapCallback } = useSwapCallback({
47-
trade,
51+
const permit2 = usePermit2()
52+
const { callback: swapRouterCallback } = useSwapCallback({
53+
trade: permit2 ? undefined : trade,
4854
allowedSlippage: slippage.allowed,
4955
recipientAddressOrName: account ?? null,
5056
signatureData: approval?.signatureData,
5157
deadline,
5258
feeOptions,
5359
})
60+
const universalRouterCallback = useUniversalRouterSwapCallback(permit2 ? trade : undefined, {
61+
slippageTolerance: slippage.allowed,
62+
deadline,
63+
permit: permit.signature,
64+
feeOptions,
65+
})
66+
const swapCallback = permit2 ? universalRouterCallback : swapRouterCallback
5467

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

6174
const setOldestValidBlock = useSetOldestValidBlock()
6275
const onSwap = useCallback(async () => {
63-
const submitted = await onSubmit(async () => {
64-
const response = await swapCallback?.()
65-
if (!response) return
76+
try {
77+
await onSubmit(async () => {
78+
const response = await swapCallback?.()
79+
if (!response) return
6680

67-
// Set the block containing the response to the oldest valid block to ensure that the
68-
// completed trade's impact is reflected in future fetched trades.
69-
response.wait(1).then((receipt) => {
70-
setOldestValidBlock(receipt.blockNumber)
71-
})
81+
// Set the block containing the response to the oldest valid block to ensure that the
82+
// completed trade's impact is reflected in future fetched trades.
83+
response.wait(1).then((receipt) => {
84+
setOldestValidBlock(receipt.blockNumber)
85+
})
7286

73-
invariant(trade)
74-
return {
75-
type: TransactionType.SWAP,
76-
response,
77-
tradeType: trade.tradeType,
78-
trade,
79-
slippageTolerance: slippage.allowed,
80-
}
81-
})
87+
invariant(trade)
88+
return {
89+
type: TransactionType.SWAP,
90+
response,
91+
tradeType: trade.tradeType,
92+
trade,
93+
slippageTolerance: slippage.allowed,
94+
}
95+
})
8296

83-
// Only close the review modal if the transaction has submitted.
84-
if (submitted) {
97+
// Only close the review modal if the swap submitted (ie no-throw).
8598
setOpen(false)
99+
} catch (e) {
100+
console.error(e) // ignore error
86101
}
87102
}, [onSubmit, setOldestValidBlock, slippage.allowed, swapCallback, trade])
88103

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

94-
if (approval.state !== SwapApprovalState.APPROVED && !disabled) {
95-
return <ApproveButton color={color} onSubmit={onSubmit} trade={trade} {...approval} />
109+
if (usePermit2()) {
110+
if (![PermitState.UNKNOWN, PermitState.PERMITTED].includes(permit.state)) {
111+
return <PermitButton color={color} onSubmit={onSubmit} trade={trade} {...permit} />
112+
}
113+
} else {
114+
if (approval.state !== SwapApprovalState.APPROVED && !disabled) {
115+
return <ApproveButton color={color} onSubmit={onSubmit} trade={trade} {...approval} />
116+
}
96117
}
97118

98119
return (

src/components/Swap/SwapActionButton/WrapButton.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function WrapButton({
2121
}: {
2222
color: keyof Colors
2323
disabled: boolean
24-
onSubmit: (submit: () => Promise<WrapTransactionInfo | UnwrapTransactionInfo | undefined>) => Promise<boolean>
24+
onSubmit: (submit: () => Promise<WrapTransactionInfo | UnwrapTransactionInfo | void>) => Promise<void>
2525
}) {
2626
const { type: wrapType, callback: wrapCallback } = useWrapCallback()
2727

@@ -33,17 +33,20 @@ export default function WrapButton({
3333
const inputCurrency = wrapType === TransactionType.WRAP ? native : native.wrapped
3434
const onWrap = useCallback(async () => {
3535
setIsPending(true)
36-
await onSubmit(async () => {
37-
const response = await wrapCallback()
38-
if (!response) return
36+
try {
37+
await onSubmit(async () => {
38+
const response = await wrapCallback()
39+
if (!response) return
3940

40-
invariant(wrapType !== undefined) // if response is valid, then so is wrapType
41-
const amount = CurrencyAmount.fromRawAmount(native, response.value?.toString() ?? '0')
42-
return { response, type: wrapType, amount }
43-
})
44-
45-
// Whether or not the transaction submits, reset the pending state.
46-
setIsPending(false)
41+
invariant(wrapType !== undefined) // if response is valid, then so is wrapType
42+
const amount = CurrencyAmount.fromRawAmount(native, response.value?.toString() ?? '0')
43+
return { response, type: wrapType, amount }
44+
})
45+
} catch (e) {
46+
console.error(e) // ignore error
47+
} finally {
48+
setIsPending(false)
49+
}
4750
}, [native, onSubmit, wrapCallback, wrapType])
4851

4952
const actionProps = useMemo(

0 commit comments

Comments
 (0)