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

feat: adds permit2 and universal router integration (#5554)

* feat: disable approval if permit2 is enabled

* feat: add permit through permit2

* fix: include analytics

* chore: pass permit to useSwapCallback

* feat: use universal router

* fix: remove unused import + update param formmating

* fix: suppress eslint error on restricted imports

* fix: lint issues

* fix: do not disable unapproved permit2 swap

* Revert "fix: do not disable unapproved permit2 swap"

This reverts commit be3f758e36db90edf205cbe35d091da3c12c1737.

* fix: do not disable unapproved permit2 swap

* fix: allow error for permit2 swap

* fix: better sequencing to handle rejections

* build: upgrade universal router sdk to include goerli address

* fix: mv block time into const

* fix: rm unnecessary id

* fix: cast swap error to string

* chore: parity with widgets

* test: rm old feature flags

* fix: gate permit2 on chain deployment

* fix: import for gate
Co-authored-by: default avatarYannie Yip <yannie.yip@uniswap.org>
parent a96bdaad
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { getTestSelector } from '../utils' import { getTestSelector } from '../utils'
describe('Wallet Dropdown', () => { describe('Wallet Dropdown', () => {
before(() => { before(() => {
cy.visit('/', { featureFlags: [FeatureFlag.navBar, FeatureFlag.tokenSafety] }) cy.visit('/')
}) })
it('should change the theme', () => { it('should change the theme', () => {
......
...@@ -10,7 +10,7 @@ import { ...@@ -10,7 +10,7 @@ import {
SwapWidgetSkeleton, SwapWidgetSkeleton,
} from '@uniswap/widgets' } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2' import { usePermit2Enabled } from 'featureFlags/flags/permit2'
import { useActiveLocale } from 'hooks/useActiveLocale' import { useActiveLocale } from 'hooks/useActiveLocale'
import { import {
formatPercentInBasisPointsNumber, formatPercentInBasisPointsNumber,
...@@ -132,7 +132,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg ...@@ -132,7 +132,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
[initialQuoteDate, trace] [initialQuoteDate, trace]
) )
const permit2Enabled = usePermit2Flag() === Permit2Variant.Enabled const permit2Enabled = usePermit2Enabled()
if (!(inputs.value.INPUT || inputs.value.OUTPUT)) { if (!(inputs.value.INPUT || inputs.value.OUTPUT)) {
return <WidgetSkeleton /> return <WidgetSkeleton />
......
...@@ -11,11 +11,12 @@ import { darkTheme } from 'theme/colors' ...@@ -11,11 +11,12 @@ import { darkTheme } from 'theme/colors'
import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains' import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains'
import { ARBITRUM_LIST, CELO_LIST, OPTIMISM_LIST } from './lists' import { ARBITRUM_LIST, CELO_LIST, OPTIMISM_LIST } from './lists'
export const AVERAGE_L1_BLOCK_TIME = ms`12s`
export enum NetworkType { export enum NetworkType {
L1, L1,
L2, L2,
} }
interface BaseChainInfo { interface BaseChainInfo {
readonly networkType: NetworkType readonly networkType: NetworkType
readonly blockWaitMsBeforeWarning?: number readonly blockWaitMsBeforeWarning?: number
......
...@@ -3,16 +3,11 @@ import { deepCopy } from '@ethersproject/properties' ...@@ -3,16 +3,11 @@ import { deepCopy } from '@ethersproject/properties'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { StaticJsonRpcProvider } from '@ethersproject/providers' import { StaticJsonRpcProvider } from '@ethersproject/providers'
import { isPlain } from '@reduxjs/toolkit' import { isPlain } from '@reduxjs/toolkit'
import ms from 'ms.macro'
import { AVERAGE_L1_BLOCK_TIME } from './chainInfo'
import { CHAIN_IDS_TO_NAMES, SupportedChainId } from './chains' import { CHAIN_IDS_TO_NAMES, SupportedChainId } from './chains'
import { RPC_URLS } from './networks' import { RPC_URLS } from './networks'
// NB: Third-party providers (eg MetaMask) will have their own polling intervals,
// which should be left as-is to allow operations (eg transaction confirmation) to resolve faster.
// Network providers (eg AppJsonRpcProvider) need to update less frequently to be considered responsive.
export const POLLING_INTERVAL = ms`12s` // mainnet block frequency - ok for other chains as it is a sane refresh rate
class AppJsonRpcProvider extends StaticJsonRpcProvider { class AppJsonRpcProvider extends StaticJsonRpcProvider {
private _blockCache = new Map<string, Promise<any>>() private _blockCache = new Map<string, Promise<any>>()
get blockCache() { get blockCache() {
...@@ -27,7 +22,11 @@ class AppJsonRpcProvider extends StaticJsonRpcProvider { ...@@ -27,7 +22,11 @@ class AppJsonRpcProvider extends StaticJsonRpcProvider {
constructor(chainId: SupportedChainId) { constructor(chainId: SupportedChainId) {
// Including networkish allows ethers to skip the initial detectNetwork call. // Including networkish allows ethers to skip the initial detectNetwork call.
super(RPC_URLS[chainId][0], /* networkish= */ { chainId, name: CHAIN_IDS_TO_NAMES[chainId] }) super(RPC_URLS[chainId][0], /* networkish= */ { chainId, name: CHAIN_IDS_TO_NAMES[chainId] })
this.pollingInterval = POLLING_INTERVAL
// NB: Third-party providers (eg MetaMask) will have their own polling intervals,
// which should be left as-is to allow operations (eg transaction confirmation) to resolve faster.
// Network providers (eg AppJsonRpcProvider) need to update less frequently to be considered responsive.
this.pollingInterval = AVERAGE_L1_BLOCK_TIME
} }
send(method: string, params: Array<any>): Promise<any> { send(method: string, params: Array<any>): Promise<any> {
......
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core'
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index' import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function usePermit2Flag(): BaseVariant { export function usePermit2Flag(): BaseVariant {
return useBaseFlag(FeatureFlag.permit2) return useBaseFlag(FeatureFlag.permit2)
} }
export function usePermit2Enabled(): boolean {
const flagEnabled = usePermit2Flag() === BaseVariant.Enabled
const { chainId } = useWeb3React()
try {
// Detect if the Universal Router is not yet deployed to chainId.
// This is necessary so that we can fallback correctly on chains without a Universal Router deployment.
// It will be removed once Universal Router is deployed on all supported chains.
chainId && UNIVERSAL_ROUTER_ADDRESS(chainId)
return flagEnabled
} catch {
return false
}
}
export { BaseVariant as Permit2Variant } export { BaseVariant as Permit2Variant }
import { ContractTransaction } from '@ethersproject/contracts'
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ApproveTransactionInfo } from 'state/transactions/types'
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance'
import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance'
export enum PermitState {
UNKNOWN,
PERMIT_NEEDED,
PERMITTED,
}
export interface Permit {
state: PermitState
signature?: PermitSignature
callback?: () => Promise<{
response: ContractTransaction
info: ApproveTransactionInfo
} | void>
}
export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit {
const { account } = useWeb3React()
const tokenAllowance = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS)
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
const permitAllowance = usePermitAllowance(amount?.currency, spender)
const [permitAllowanceAmount, setPermitAllowanceAmount] = useState(permitAllowance?.amount)
useEffect(() => setPermitAllowanceAmount(permitAllowance?.amount), [permitAllowance?.amount])
const [signature, setSignature] = useState<PermitSignature>()
const updatePermitAllowance = useUpdatePermitAllowance(
amount?.currency,
spender,
permitAllowance?.nonce,
setSignature
)
const updateTokenAndPermitAllowance = useCallback(async () => {
const info = await updateTokenAllowance()
await updatePermitAllowance()
return info
}, [updatePermitAllowance, updateTokenAllowance])
// Trigger a re-render if either tokenAllowance or signature expire.
useInterval(
() => {
// Calculate now such that the signature will still be valid for the next block.
const now = (Date.now() - AVERAGE_L1_BLOCK_TIME) / 1000
if (signature && signature.sigDeadline < now) {
setSignature(undefined)
}
if (permitAllowance && permitAllowance.expiration < now) {
setPermitAllowanceAmount(undefined)
}
},
AVERAGE_L1_BLOCK_TIME,
true
)
return useMemo(() => {
if (!amount || !tokenAllowance) {
return { state: PermitState.UNKNOWN }
} else if (tokenAllowance.greaterThan(amount) || tokenAllowance.equalTo(amount)) {
if (permitAllowanceAmount?.gte(amount.quotient.toString())) {
return { state: PermitState.PERMITTED }
} else if (signature?.details.token === amount.currency.address && signature?.spender === spender) {
return { state: PermitState.PERMITTED, signature }
} else {
return { state: PermitState.PERMIT_NEEDED, callback: updatePermitAllowance }
}
} else {
return { state: PermitState.PERMIT_NEEDED, callback: updateTokenAndPermitAllowance }
}
}, [
amount,
permitAllowanceAmount,
signature,
spender,
tokenAllowance,
updatePermitAllowance,
updateTokenAndPermitAllowance,
])
}
import {
AllowanceData,
AllowanceProvider,
AllowanceTransfer,
MaxAllowanceTransferAmount,
PERMIT2_ADDRESS,
PermitSingle,
} from '@uniswap/permit2-sdk'
import { Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import ms from 'ms.macro'
import { useCallback, useEffect, useMemo, useState } from 'react'
const PERMIT_EXPIRATION = ms`30d`
const PERMIT_SIG_EXPIRATION = ms`30m`
function toDeadline(expiration: number): number {
return Math.floor((Date.now() + expiration) / 1000)
}
export function usePermitAllowance(token?: Token, spender?: string) {
const { account, provider } = useWeb3React()
const allowanceProvider = useMemo(() => provider && new AllowanceProvider(provider, PERMIT2_ADDRESS), [provider])
const [allowanceData, setAllowanceData] = useState<AllowanceData>()
useEffect(() => {
if (!account || !token || !spender) return
allowanceProvider
?.getAllowanceData(token.address, account, spender)
.then((data) => {
if (stale) return
setAllowanceData(data)
})
.catch((e) => {
console.warn(`Failed to fetch allowance data: ${e}`)
})
let stale = false
return () => {
stale = true
}
}, [account, allowanceProvider, spender, token])
return allowanceData
}
interface Permit extends PermitSingle {
sigDeadline: number
}
export interface PermitSignature extends Permit {
signature: string
}
export function useUpdatePermitAllowance(
token: Token | undefined,
spender: string | undefined,
nonce: number | undefined,
onPermitSignature: (signature: PermitSignature) => void
) {
const { account, chainId, provider } = useWeb3React()
return useCallback(async () => {
try {
if (!chainId) throw new Error('missing chainId')
if (!provider) throw new Error('missing provider')
if (!token) throw new Error('missing token')
if (!spender) throw new Error('missing spender')
if (nonce === undefined) throw new Error('missing nonce')
const permit: Permit = {
details: {
token: token.address,
amount: MaxAllowanceTransferAmount,
expiration: toDeadline(PERMIT_EXPIRATION),
nonce,
},
spender,
sigDeadline: toDeadline(PERMIT_SIG_EXPIRATION),
}
const { domain, types, values } = AllowanceTransfer.getPermitData(permit, PERMIT2_ADDRESS, chainId)
const signature = await provider.getSigner(account)._signTypedData(domain, types, values)
onPermitSignature?.({ ...permit, signature })
return
} catch (e: unknown) {
const symbol = token?.symbol ?? 'Token'
throw new Error(`${symbol} permit failed: ${e instanceof Error ? e.message : e}`)
}
}, [account, chainId, nonce, onPermitSignature, provider, spender, token])
}
// eslint-disable-next-line no-restricted-imports
import { Trade } from '@uniswap/router-sdk' import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback' import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
import { ReactNode, useMemo } from 'react' import { ReactNode, useMemo } from 'react'
...@@ -10,7 +10,9 @@ import { TransactionType } from '../state/transactions/types' ...@@ -10,7 +10,9 @@ import { TransactionType } from '../state/transactions/types'
import { currencyId } from '../utils/currencyId' import { currencyId } from '../utils/currencyId'
import useENS from './useENS' import useENS from './useENS'
import { SignatureData } from './useERC20Permit' import { SignatureData } from './useERC20Permit'
import { Permit } from './usePermit2'
import useTransactionDeadline from './useTransactionDeadline' import useTransactionDeadline from './useTransactionDeadline'
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
// returns a function that will execute a swap, if the parameters are all valid // returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade // and the user has approved the slippage adjusted input amount for the trade
...@@ -18,7 +20,8 @@ export function useSwapCallback( ...@@ -18,7 +20,8 @@ export function useSwapCallback(
trade: Trade<Currency, Currency, TradeType> | undefined, // trade to execute, required trade: Trade<Currency, Currency, TradeType> | undefined, // trade to execute, required
allowedSlippage: Percent, // in bips allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | undefined | null signatureData: SignatureData | undefined | null,
permit: Permit | undefined
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } { ): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
const { account } = useWeb3React() const { account } = useWeb3React()
...@@ -29,24 +32,29 @@ export function useSwapCallback( ...@@ -29,24 +32,29 @@ export function useSwapCallback(
const { address: recipientAddress } = useENS(recipientAddressOrName) const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress const recipient = recipientAddressOrName === null ? account : recipientAddress
const permit2Enabled = usePermit2Enabled()
const { const {
state, state,
callback: libCallback, callback: libCallback,
error, error,
} = useLibSwapCallBack({ } = useLibSwapCallBack({
trade, trade: permit2Enabled ? undefined : trade,
allowedSlippage, allowedSlippage,
recipientAddressOrName: recipient, recipientAddressOrName: recipient,
signatureData, signatureData,
deadline, deadline,
}) })
const universalRouterSwapCallback = useUniversalRouterSwapCallback(permit2Enabled ? trade : undefined, {
slippageTolerance: allowedSlippage,
deadline,
permit: permit?.signature,
})
const swapCallback = permit2Enabled ? universalRouterSwapCallback : libCallback
const callback = useMemo(() => { const callback = useMemo(() => {
if (!libCallback || !trade) { if (!trade || !swapCallback) return null
return null
}
return () => return () =>
libCallback().then((response) => { swapCallback().then((response) => {
addTransaction( addTransaction(
response, response,
trade.tradeType === TradeType.EXACT_INPUT trade.tradeType === TradeType.EXACT_INPUT
...@@ -71,7 +79,7 @@ export function useSwapCallback( ...@@ -71,7 +79,7 @@ export function useSwapCallback(
) )
return response.hash return response.hash
}) })
}, [addTransaction, allowedSlippage, libCallback, trade]) }, [addTransaction, allowedSlippage, swapCallback, trade])
return { return {
state, state,
......
import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { BigNumberish } from '@ethersproject/bignumber'
import { ContractTransaction } from '@ethersproject/contracts'
import { CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core'
import { useSingleCallResult } from 'lib/hooks/multicall' import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { ApproveTransactionInfo, TransactionType } from 'state/transactions/types'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import { useTokenContract } from './useContract' import { useTokenContract } from './useContract'
...@@ -15,3 +19,39 @@ export function useTokenAllowance(token?: Token, owner?: string, spender?: strin ...@@ -15,3 +19,39 @@ export function useTokenAllowance(token?: Token, owner?: string, spender?: strin
[token, allowance] [token, allowance]
) )
} }
export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefined, spender: string) {
const contract = useTokenContract(amount?.currency.address)
return useCallback(async (): Promise<{
response: ContractTransaction
info: ApproveTransactionInfo
}> => {
try {
if (!amount) throw new Error('missing amount')
if (!contract) throw new Error('missing contract')
if (!spender) throw new Error('missing spender')
let allowance: BigNumberish = MaxUint256.toString()
const estimatedGas = await contract.estimateGas.approve(spender, allowance).catch(() => {
// Fallback for tokens which restrict approval amounts:
allowance = amount.quotient.toString()
return contract.estimateGas.approve(spender, allowance)
})
const gasLimit = calculateGasMargin(estimatedGas)
const response = await contract.approve(spender, allowance, { gasLimit })
return {
response,
info: {
type: TransactionType.APPROVAL,
tokenAddress: contract.address,
spender,
},
}
} catch (e: unknown) {
const symbol = amount?.currency.symbol ?? 'Token'
throw new Error(`${symbol} approval failed: ${e instanceof Error ? e.message : e}`)
}
}, [amount, contract, spender])
}
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { BigNumber } from '@ethersproject/bignumber'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { FeeOptions } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { useCallback } from 'react'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import isZero from 'utils/isZero'
import { swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage'
import { PermitSignature } from './usePermitAllowance'
interface SwapOptions {
slippageTolerance: Percent
deadline?: BigNumber
permit?: PermitSignature
feeOptions?: FeeOptions
}
export function useUniversalRouterSwapCallback(
trade: Trade<Currency, Currency, TradeType> | undefined,
options: SwapOptions
) {
const { account, chainId, provider } = useWeb3React()
return useCallback(async (): Promise<TransactionResponse> => {
try {
if (!account) throw new Error('missing account')
if (!chainId) throw new Error('missing chainId')
if (!provider) throw new Error('missing provider')
if (!trade) throw new Error('missing trade')
const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, {
slippageTolerance: options.slippageTolerance,
deadlineOrPreviousBlockhash: options.deadline?.toString(),
inputTokenPermit: options.permit,
fee: options.feeOptions,
})
const tx =
value && !isZero(value)
? {
from: account,
to: UNIVERSAL_ROUTER_ADDRESS(chainId),
data,
value,
}
: {
from: account,
to: UNIVERSAL_ROUTER_ADDRESS(chainId),
data,
}
let gasEstimate: BigNumber
try {
gasEstimate = await provider.estimateGas(tx)
} catch (gasError) {
await provider.call(tx) // this should throw the actual error
throw new Error('unexpected issue with gas estimation; please try again')
}
const gasLimit = calculateGasMargin(gasEstimate)
const response = await provider.getSigner().sendTransaction({ ...tx, gasLimit })
return response
} catch (swapError: unknown) {
const message = swapErrorToUserReadableMessage(swapError)
throw new Error(`Trade failed: ${message}`)
}
}, [
account,
chainId,
options.deadline,
options.feeOptions,
options.permit,
options.slippageTolerance,
provider,
trade,
])
}
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent } from '@uniswap/analytics' import { sendAnalyticsEvent, Trace, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, PageName, SectionName } from '@uniswap/analytics-events' import { BrowserEvent, ElementName, EventName, PageName, SectionName } from '@uniswap/analytics-events'
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { Trade } from '@uniswap/router-sdk' import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { sendEvent } from 'components/analytics' import { sendEvent } from 'components/analytics'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert' import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
...@@ -13,23 +15,26 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal' ...@@ -13,23 +15,26 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage' import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
import usePermit, { PermitState } from 'hooks/usePermit2'
import { useSwapCallback } from 'hooks/useSwapCallback' import { useSwapCallback } from 'hooks/useSwapCallback'
import useTransactionDeadline from 'hooks/useTransactionDeadline' import useTransactionDeadline from 'hooks/useTransactionDeadline'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics' import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { ArrowDown, CheckCircle, HelpCircle } from 'react-feather' import { AlertTriangle, ArrowDown, CheckCircle, HelpCircle, Info } from 'react-feather'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { useToggleWalletModal } from 'state/application/hooks' import { useToggleWalletModal } from 'state/application/hooks'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { TradeState } from 'state/routing/types' import { TradeState } from 'state/routing/types'
import { useHasPendingApproval, useTransactionAdder } from 'state/transactions/hooks'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers' import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
import AddressInputPanel from '../../components/AddressInputPanel' import AddressInputPanel from '../../components/AddressInputPanel'
import { ButtonConfirmed, ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button' import { ButtonConfirmed, ButtonError, ButtonLight, ButtonPrimary, ButtonYellow } from '../../components/Button'
import { GrayCard } from '../../components/Card' import { GrayCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column' import { AutoColumn } from '../../components/Column'
import SwapCurrencyInputPanel from '../../components/CurrencyInputPanel/SwapCurrencyInputPanel' import SwapCurrencyInputPanel from '../../components/CurrencyInputPanel/SwapCurrencyInputPanel'
...@@ -287,14 +292,53 @@ export default function Swap() { ...@@ -287,14 +292,53 @@ export default function Swap() {
currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0)) currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
) )
const permit2Enabled = usePermit2Enabled()
const maximumAmountIn = useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
}, [allowedSlippage, trade])
const permit = usePermit(
permit2Enabled ? maximumAmountIn : undefined,
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
)
const [isPermitPending, setIsPermitPending] = useState(false)
const [isPermitFailed, setIsPermitFailed] = useState(false)
const addTransaction = useTransactionAdder()
const isApprovalPending = useHasPendingApproval(maximumAmountIn?.currency, PERMIT2_ADDRESS)
const updatePermit = useCallback(async () => {
setIsPermitPending(true)
try {
const approval = await permit.callback?.()
if (approval) {
sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, {
chain_id: chainId,
token_symbol: maximumAmountIn?.currency.symbol,
token_address: maximumAmountIn?.currency.address,
})
const { response, info } = approval
addTransaction(response, info)
}
setIsPermitFailed(false)
} catch (e) {
console.error(e)
setIsPermitFailed(true)
} finally {
setIsPermitPending(false)
}
}, [addTransaction, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, permit])
// check whether the user has approved the router on the input token // check whether the user has approved the router on the input token
const [approvalState, approveCallback] = useApproveCallbackFromTrade(trade, allowedSlippage) const [approvalState, approveCallback] = useApproveCallbackFromTrade(
permit2Enabled ? undefined : trade,
allowedSlippage
)
const transactionDeadline = useTransactionDeadline() const transactionDeadline = useTransactionDeadline()
const { const {
state: signatureState, state: signatureState,
signatureData, signatureData,
gatherPermitSignature, gatherPermitSignature,
} = useERC20PermitFromTrade(trade, allowedSlippage, transactionDeadline) } = useERC20PermitFromTrade(permit2Enabled ? undefined : trade, allowedSlippage, transactionDeadline)
const [approvalPending, setApprovalPending] = useState<boolean>(false) const [approvalPending, setApprovalPending] = useState<boolean>(false)
const handleApprove = useCallback(async () => { const handleApprove = useCallback(async () => {
...@@ -344,7 +388,8 @@ export default function Swap() { ...@@ -344,7 +388,8 @@ export default function Swap() {
trade, trade,
allowedSlippage, allowedSlippage,
recipient, recipient,
signatureData signatureData,
permit
) )
const handleSwap = useCallback(() => { const handleSwap = useCallback(() => {
...@@ -413,6 +458,7 @@ export default function Swap() { ...@@ -413,6 +458,7 @@ export default function Swap() {
// show approve flow when: no error on inputs, not approved or pending, or approved in current session // show approve flow when: no error on inputs, not approved or pending, or approved in current session
// never show if price impact is above threshold in non expert mode // never show if price impact is above threshold in non expert mode
const showApproveFlow = const showApproveFlow =
!permit2Enabled &&
!isArgentWallet && !isArgentWallet &&
!swapInputError && !swapInputError &&
(approvalState === ApprovalState.NOT_APPROVED || (approvalState === ApprovalState.NOT_APPROVED ||
...@@ -752,6 +798,53 @@ export default function Swap() { ...@@ -752,6 +798,53 @@ export default function Swap() {
</ButtonError> </ButtonError>
</AutoColumn> </AutoColumn>
</AutoRow> </AutoRow>
) : permit.state === PermitState.PERMIT_NEEDED ? (
<ButtonYellow
onClick={updatePermit}
disabled={isPermitPending || isApprovalPending}
style={{ gap: 14 }}
>
{isPermitPending ? (
<>
<Loader size="20px" stroke={theme.accentWarning} />
<ThemedText.SubHeader color="accentWarning">
<Trans>Approve in your wallet</Trans>
</ThemedText.SubHeader>
</>
) : isPermitFailed ? (
<>
<AlertTriangle size={20} stroke={theme.accentWarning} />
<ThemedText.SubHeader color="accentWarning">
<Trans>Approval failed. Try again.</Trans>
</ThemedText.SubHeader>
</>
) : isApprovalPending ? (
<>
<Loader size="20px" stroke={theme.accentWarning} />
<ThemedText.SubHeader color="accentWarning">
<Trans>Approval pending</Trans>
</ThemedText.SubHeader>
</>
) : (
<>
<div style={{ height: 20 }}>
<MouseoverTooltip
text={
<Trans>
Permission is required for Uniswap to swap each token. This will expire after one month
for your security.
</Trans>
}
>
<Info size={20} color={theme.accentWarning} />
</MouseoverTooltip>
</div>
<ThemedText.SubHeader color="accentWarning">
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
</ThemedText.SubHeader>
</>
)}
</ButtonYellow>
) : ( ) : (
<ButtonError <ButtonError
onClick={() => { onClick={() => {
...@@ -768,8 +861,14 @@ export default function Swap() { ...@@ -768,8 +861,14 @@ export default function Swap() {
} }
}} }}
id="swap-button" id="swap-button"
disabled={!isValid || routeIsSyncing || routeIsLoading || priceImpactTooHigh || !!swapCallbackError} disabled={
error={isValid && priceImpactSeverity > 2 && !swapCallbackError} !isValid ||
routeIsSyncing ||
routeIsLoading ||
priceImpactTooHigh ||
Boolean(!permit2Enabled && swapCallbackError)
}
error={isValid && priceImpactSeverity > 2 && (permit2Enabled || !swapCallbackError)}
> >
<Text fontSize={20} fontWeight={600}> <Text fontSize={20} fontWeight={600}>
{swapInputError ? ( {swapInputError ? (
......
...@@ -2,7 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query/react' ...@@ -2,7 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router' import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router'
import { sendTiming } from 'components/analytics' import { sendTiming } from 'components/analytics'
import { POLLING_INTERVAL } from 'constants/providers' import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice' import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice'
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
import useIsValidBlock from 'lib/hooks/useIsValidBlock' import useIsValidBlock from 'lib/hooks/useIsValidBlock'
...@@ -46,7 +46,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -46,7 +46,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
const { isLoading, isError, data, currentData } = useGetQuoteQuery(queryArgs ?? skipToken, { const { isLoading, isError, data, currentData } = useGetQuoteQuery(queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently. // Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === RouterPreference.PRICE ? ms`2m` : POLLING_INTERVAL, pollingInterval: routerPreference === RouterPreference.PRICE ? ms`2m` : AVERAGE_L1_BLOCK_TIME,
}) })
const quoteResult: GetQuoteResult | undefined = useIsValidBlock(Number(data?.blockNumber) || 0) ? data : undefined const quoteResult: GetQuoteResult | undefined = useIsValidBlock(Number(data?.blockNumber) || 0) ? data : undefined
......
import { Trans } from '@lingui/macro' // eslint-disable-next-line no-restricted-imports
import { ReactNode } from 'react' import { t } from '@lingui/macro'
/** /**
* This is hacking out the revert reason from the ethers provider thrown error however it can. * This is hacking out the revert reason from the ethers provider thrown error however it can.
* This object seems to be undocumented by ethers. * This object seems to be undocumented by ethers.
* @param error an error from the ethers provider * @param error an error from the ethers provider
*/ */
export function swapErrorToUserReadableMessage(error: any): ReactNode { export function swapErrorToUserReadableMessage(error: any): string {
let reason: string | undefined let reason: string | undefined
while (Boolean(error)) { while (Boolean(error)) {
reason = error.reason ?? error.message ?? reason reason = error.reason ?? error.message ?? reason
...@@ -16,62 +16,28 @@ export function swapErrorToUserReadableMessage(error: any): ReactNode { ...@@ -16,62 +16,28 @@ export function swapErrorToUserReadableMessage(error: any): ReactNode {
switch (reason) { switch (reason) {
case 'UniswapV2Router: EXPIRED': case 'UniswapV2Router: EXPIRED':
return ( return t`The transaction could not be sent because the deadline has passed. Please check that your transaction deadline is not too low.`
<Trans>
The transaction could not be sent because the deadline has passed. Please check that your transaction deadline
is not too low.
</Trans>
)
case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT': case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT':
case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT': case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT':
return ( return t`This transaction will not succeed either due to price movement or fee on transfer. Try increasing your slippage tolerance.`
<Trans>
This transaction will not succeed either due to price movement or fee on transfer. Try increasing your
slippage tolerance.
</Trans>
)
case 'TransferHelper: TRANSFER_FROM_FAILED': case 'TransferHelper: TRANSFER_FROM_FAILED':
return <Trans>The input token cannot be transferred. There may be an issue with the input token.</Trans> return t`The input token cannot be transferred. There may be an issue with the input token.`
case 'UniswapV2: TRANSFER_FAILED': case 'UniswapV2: TRANSFER_FAILED':
return <Trans>The output token cannot be transferred. There may be an issue with the output token.</Trans> return t`The output token cannot be transferred. There may be an issue with the output token.`
case 'UniswapV2: K': case 'UniswapV2: K':
return ( return t`The Uniswap invariant x*y=k was not satisfied by the swap. This usually means one of the tokens you are swapping incorporates custom behavior on transfer.`
<Trans>
The Uniswap invariant x*y=k was not satisfied by the swap. This usually means one of the tokens you are
swapping incorporates custom behavior on transfer.
</Trans>
)
case 'Too little received': case 'Too little received':
case 'Too much requested': case 'Too much requested':
case 'STF': case 'STF':
return ( return t`This transaction will not succeed due to price movement. Try increasing your slippage tolerance. Note: fee on transfer and rebase tokens are incompatible with Uniswap V3.`
<Trans>
This transaction will not succeed due to price movement. Try increasing your slippage tolerance. Note: fee on
transfer and rebase tokens are incompatible with Uniswap V3.
</Trans>
)
case 'TF': case 'TF':
return ( return t`The output token cannot be transferred. There may be an issue with the output token. Note: fee on transfer and rebase tokens are incompatible with Uniswap V3.`
<Trans>
The output token cannot be transferred. There may be an issue with the output token. Note: fee on transfer and
rebase tokens are incompatible with Uniswap V3.
</Trans>
)
default: default:
if (reason?.indexOf('undefined is not an object') !== -1) { if (reason?.indexOf('undefined is not an object') !== -1) {
return ( return t`An error occurred when trying to execute this swap. You may need to increase your slippage tolerance. If that does not work, there may be an incompatibility with the token you are trading. Note: fee on transfer and rebase tokens are incompatible with Uniswap V3.`
<Trans>
An error occurred when trying to execute this swap. You may need to increase your slippage tolerance. If
that does not work, there may be an incompatibility with the token you are trading. Note: fee on transfer
and rebase tokens are incompatible with Uniswap V3.
</Trans>
)
} }
return ( return t`Unknown error${
<Trans> reason ? `: "${reason}"` : ''
Unknown error{reason ? `: "${reason}"` : ''}. Try increasing your slippage tolerance. Note: fee on transfer }. Try increasing your slippage tolerance. Note: fee on transfer and rebase tokens are incompatible with Uniswap V3.`
and rebase tokens are incompatible with Uniswap V3.
</Trans>
)
} }
} }
...@@ -4329,10 +4329,10 @@ ...@@ -4329,10 +4329,10 @@
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.30.tgz#2103ca23b8007c59ec71718d34cdc97861c409e5" resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.30.tgz#2103ca23b8007c59ec71718d34cdc97861c409e5"
integrity sha512-HwY2VvkQ8lNR6ks5NqQfAtg+4IZqz3KV1T8d2DlI8emIn9uMmaoFbIOg0nzjqAVKKnZSbMTRRtUoAh6mmjRvog== integrity sha512-HwY2VvkQ8lNR6ks5NqQfAtg+4IZqz3KV1T8d2DlI8emIn9uMmaoFbIOg0nzjqAVKKnZSbMTRRtUoAh6mmjRvog==
"@uniswap/universal-router-sdk@1.2.1", "@uniswap/universal-router-sdk@^1.2.1": "@uniswap/universal-router-sdk@^1.2.1", "@uniswap/universal-router-sdk@1.2.2":
version "1.2.1" version "1.2.2"
resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-1.2.1.tgz#6ebd2830cf5abc4e132df4458e92bb33c171b604" resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-1.2.2.tgz#26ece606279d1cd9823277b74cd43575caf160e9"
integrity sha512-NntAR9owpWy0zlDpaOiMqGHR+UK50R9pIOQBa8TwRL4cRdQpXTs9/hjynTMZYDri/fYiW/+111hv4BPwvh7RFQ== integrity sha512-L+MkvMiLyG1vfpEoLbe9xquSApSrfe1m0iLuIjgkZ4qR2Kb0NU8nZXNqeyTJmTZhNbYYIJA0AwRawAN+90NKIw==
dependencies: dependencies:
"@uniswap/permit2-sdk" "^1.2.0" "@uniswap/permit2-sdk" "^1.2.0"
"@uniswap/router-sdk" "^1.4.0" "@uniswap/router-sdk" "^1.4.0"
......
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