Skip to content

Commit

Permalink
feat(permit): fetch token name from chain (#3374)
Browse files Browse the repository at this point in the history
* chore: add PermitUtils type `unsupported`

* chore: apply PermitInfo type to consumers

* feat: add helper method to only check whether token is permittable useTokenSupportsPermit

* refactor: rename useIsTokenPermittable to usePermitInfo

* feat: add utils fn isSupportedPermitInfo

* refactor: move inner private method getContract to external utils fn

* feat: add minimal erc20 `name` method abi

* feat: add utils fn `getTokenName`

* feat: fetch token name when fetching permit info

* feat: make tokenName optional on GetTokenPermitInfoParams

* feat: migrate preGenerated permit info to new format

* chore: more tokenName optional changes

* chore: add TODOs

* refactor: return error rather than throwing

* chore: bump permit-utils package version

* chore: bump permittableTokens atom version to v2

* refactor: use isSupportedPermitInfo in a few places

* chore: do a type guard in the helper fn so there's no need for casting

* chore: return error when name fetching fails due to network issues

* chore: identify earlier when token is not dai-like and return received error

* chore: only log debug msg if not dai-like

* chore: mark some known error types as permanent errors

* chore: log also tokenName when possible

* chore: use a regex to catch connection issues

* chore: use const for default obj return to avoid re-renders

* refactor: remove redundant check. It's covered by isSupportedPermitInfo

* chore: set permit-utils version to 0.0.1
  • Loading branch information
alfetopito authored Nov 17, 2023
1 parent 24601df commit 578df82
Show file tree
Hide file tree
Showing 28 changed files with 245 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useAppData } from 'modules/appData'
import { useRateImpact } from 'modules/limitOrders/hooks/useRateImpact'
import { TradeFlowContext } from 'modules/limitOrders/services/types'
import { limitOrdersSettingsAtom } from 'modules/limitOrders/state/limitOrdersSettingsAtom'
import { useGeneratePermitHook, useIsTokenPermittable } from 'modules/permit'
import { useGeneratePermitHook, usePermitInfo } from 'modules/permit'
import { useEnoughBalanceAndAllowance } from 'modules/tokens'
import { TradeType } from 'modules/trade'
import { useTradeQuote } from 'modules/tradeQuote'
Expand All @@ -34,7 +34,7 @@ export function useTradeFlowContext(): TradeFlowContext | null {
const quoteState = useTradeQuote()
const rateImpact = useRateImpact()
const settingsState = useAtomValue(limitOrdersSettingsAtom)
const permitInfo = useIsTokenPermittable(state.inputCurrency, TradeType.LIMIT_ORDER)
const permitInfo = usePermitInfo(state.inputCurrency, TradeType.LIMIT_ORDER)

const checkAllowanceAddress = GP_VAULT_RELAYER[chainId]
const { enoughAllowance } = useEnoughBalanceAndAllowance({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OrderClass } from '@cowprotocol/cow-sdk'
import { isSupportedPermitInfo } from '@cowprotocol/permit-utils'
import { Percent } from '@uniswap/sdk-core'

import { PriceImpact } from 'legacy/hooks/usePriceImpact'
Expand Down Expand Up @@ -57,7 +58,7 @@ export async function tradeFlow(

try {
logTradeFlow('LIMIT ORDER FLOW', 'STEP 2: handle permit')
if (permitInfo) beforePermit()
if (isSupportedPermitInfo(permitInfo)) beforePermit()

postOrderParams.appData = await handlePermit({
permitInfo,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useEffect, useState } from 'react'

import { PermitHookData } from '@cowprotocol/permit-utils'
import { isSupportedPermitInfo, PermitHookData } from '@cowprotocol/permit-utils'

import { useDerivedTradeState } from 'modules/trade'

import { useSafeMemo } from 'common/hooks/useSafeMemo'

import { useGeneratePermitHook } from './useGeneratePermitHook'
import { useIsTokenPermittable } from './useIsTokenPermittable'
import { usePermitInfo } from './usePermitInfo'

import { GeneratePermitHookParams } from '../types'

Expand Down Expand Up @@ -41,10 +41,10 @@ function useGeneratePermitHookParams(): GeneratePermitHookParams | undefined {
const { state } = useDerivedTradeState()
const { inputCurrency, tradeType } = state || {}

const permitInfo = useIsTokenPermittable(inputCurrency, tradeType)
const permitInfo = usePermitInfo(inputCurrency, tradeType)

return useSafeMemo(() => {
if (!inputCurrency || !('address' in inputCurrency) || !permitInfo) return undefined
if (!inputCurrency || !('address' in inputCurrency) || !isSupportedPermitInfo(permitInfo)) return undefined

return {
inputToken: { address: inputCurrency.address, name: inputCurrency.name },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function checkHasValidPendingPermit(
const eip2162Utils = getPermitUtilsInstance(chainId, provider, order.owner)

const tokenAddress = order.inputToken.address
const tokenName = order.inputToken.name || tokenAddress
const tokenName = order.inputToken.name

const checkedHooks = await Promise.all(
preHooks.map(({ callData }) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { useAtomValue, useSetAtom } from 'jotai'
import { useCallback } from 'react'

import { GP_VAULT_RELAYER } from '@cowprotocol/common-const'
import { generatePermitHook, getPermitUtilsInstance, PermitHookData } from '@cowprotocol/permit-utils'
import {
generatePermitHook,
getPermitUtilsInstance,
isSupportedPermitInfo,
PermitHookData,
} from '@cowprotocol/permit-utils'
import { useWalletInfo } from '@cowprotocol/wallet'
import { useWeb3React } from '@web3-react/core'

Expand Down Expand Up @@ -38,7 +43,7 @@ export function useGeneratePermitHook(): GeneratePermitHook {
async (params: GeneratePermitHookParams): Promise<PermitHookData | undefined> => {
const { inputToken, account, permitInfo } = params

if (!provider) {
if (!provider || !isSupportedPermitInfo(permitInfo)) {
return
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAtomValue } from 'jotai'
import { useMemo, useRef } from 'react'

import { isSupportedPermitInfo } from '@cowprotocol/permit-utils'
import { useWalletInfo } from '@cowprotocol/wallet'

import { useIsPermitEnabled } from 'common/hooks/featureFlags/useIsPermitEnabled'
Expand Down Expand Up @@ -33,11 +34,11 @@ export function usePermitCompatibleTokens(): PermitCompatibleTokens {
const permitCompatibleTokens: PermitCompatibleTokens = {}

for (const address of Object.keys(preGeneratedPermitInfoRef.current)) {
permitCompatibleTokens[address.toLowerCase()] = !!preGeneratedPermitInfoRef.current[address]
permitCompatibleTokens[address.toLowerCase()] = isSupportedPermitInfo(preGeneratedPermitInfoRef.current[address])
}

for (const address of Object.keys(localPermitInfoRef.current)) {
permitCompatibleTokens[address.toLowerCase()] = !!localPermitInfoRef.current[address]
permitCompatibleTokens[address.toLowerCase()] = isSupportedPermitInfo(localPermitInfoRef.current[address])
}

return permitCompatibleTokens
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEffect, useMemo } from 'react'
import { GP_VAULT_RELAYER } from '@cowprotocol/common-const'
import { getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { getTokenPermitInfo } from '@cowprotocol/permit-utils'
import { getTokenPermitInfo, PermitInfo } from '@cowprotocol/permit-utils'
import { useWalletInfo } from '@cowprotocol/wallet'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
Expand All @@ -21,32 +21,33 @@ import { ORDER_TYPE_SUPPORTS_PERMIT } from '../const'
import { addPermitInfoForTokenAtom, permittableTokensAtom } from '../state/permittableTokensAtom'
import { IsTokenPermittableResult } from '../types'

const UNSUPPORTED: PermitInfo = { type: 'unsupported', name: 'native' }

/**
* Checks whether the token is permittable, and caches the result on localStorage
* Check whether the token is permittable, and returns the permit info for it
* Tries to find it out from the pre-generated list
* If not found, tries to load the info from chain
* The result will be cached on localStorage if a final conclusion is found
*
* When it is, returned type is `{type: 'dai'|'permit', gasLimit: number}
* When it is not, returned type is `false`
* When it is not, returned type is `{type: 'unsupported'}`
* When it is unknown, returned type is `undefined`
*
*/
export function useIsTokenPermittable(
token: Nullish<Currency>,
tradeType: Nullish<TradeType>
): IsTokenPermittableResult {
export function usePermitInfo(token: Nullish<Currency>, tradeType: Nullish<TradeType>): IsTokenPermittableResult {
const { chainId } = useWalletInfo()
const { provider } = useWeb3React()

const lowerCaseAddress = token ? getWrappedToken(token).address?.toLowerCase() : undefined
const isNative = !!token && getIsNativeToken(token)
const tokenName = token?.name || lowerCaseAddress || ''
const tokenName = token?.name

// Avoid building permit info in the first place if order type is not supported
const isPermitSupported = !!tradeType && ORDER_TYPE_SUPPORTS_PERMIT[tradeType]

const isPermitEnabled = useIsPermitEnabled() && isPermitSupported

const addPermitInfo = useAddPermitInfo()
const permitInfo = usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined)
const permitInfo = _usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined)
const { permitInfo: preGeneratedInfo, isLoading: preGeneratedIsLoading } = usePreGeneratedPermitInfoForToken(
isPermitEnabled ? token : undefined
)
Expand All @@ -70,16 +71,13 @@ export function useIsTokenPermittable(
}

getTokenPermitInfo({ spender, tokenAddress: lowerCaseAddress, tokenName, chainId, provider }).then((result) => {
if (!result) {
// When falsy, we know it doesn't support permit. Cache it.
addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: false })
} else if ('error' in result) {
if ('error' in result) {
// When error, we don't know. Log and don't cache.
console.debug(
`useIsTokenPermittable: failed to check whether token ${lowerCaseAddress} is permittable: ${result.error}`
)
} else {
// Otherwise, we know it is permittable. Cache it.
// Otherwise, we know it is permittable or not. Cache it.
addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: result })
}
})
Expand All @@ -98,7 +96,7 @@ export function useIsTokenPermittable(
])

if (isNative) {
return false
return UNSUPPORTED
}

return preGeneratedInfo ?? permitInfo
Expand All @@ -111,14 +109,7 @@ function useAddPermitInfo() {
return useSetAtom(addPermitInfoForTokenAtom)
}

/**
* Returns whether a token is permittable.
*
* When it is, returned type is `{type: 'dai'|'permit', gasLimit: number}`
* When it is not, returned type is `false`
* When it is unknown, returned type is `undefined`
*/
function usePermitInfo(chainId: SupportedChainId, tokenAddress: string | undefined): IsTokenPermittableResult {
function _usePermitInfo(chainId: SupportedChainId, tokenAddress: string | undefined): IsTokenPermittableResult {
const permittableTokens = useAtomValue(permittableTokensAtom)

return useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,33 @@ export function usePreGeneratedPermitInfo(): {

const { data, isLoading } = useSWR(
url,
(url: string): Promise<Record<string, PermitInfo>> => fetch(url).then((r) => r.json()),
(url: string): Promise<Record<string, PermitInfo>> =>
fetch(url)
.then((r) => r.json())
.then(migrateData),
{ ...SWR_NO_REFRESH_OPTIONS, fallbackData: {} }
)

return { allPermitInfo: data, isLoading }
}

type OldPermitInfo = PermitInfo | false

const UNSUPPORTED: PermitInfo = { type: 'unsupported' }

/**
* Handles data migration from former way of storing unsupported tokens to the new one
*/
function migrateData(data: Record<string, OldPermitInfo>): Record<string, PermitInfo> {
const migrated: Record<string, PermitInfo> = {}

for (const [k, v] of Object.entries(data)) {
if (v === false) {
migrated[k] = UNSUPPORTED
} else {
migrated[k] = v
}
}

return migrated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { isSupportedPermitInfo } from '@cowprotocol/permit-utils'
import { Currency } from '@uniswap/sdk-core'

import { Nullish } from 'types'

import { TradeType } from 'modules/trade'

import { usePermitInfo } from './usePermitInfo'

/**
* Whether the token supports permit for given trade type
*
* @param token
* @param tradeType
*/
export function useTokenSupportsPermit(token: Nullish<Currency>, tradeType: Nullish<TradeType>): boolean {
const permitInfo = usePermitInfo(token, tradeType)

return isSupportedPermitInfo(permitInfo)
}
3 changes: 2 additions & 1 deletion apps/cowswap-frontend/src/modules/permit/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from './hooks/useAccountAgnosticPermitHookData'
export * from './hooks/useGeneratePermitHook'
export * from './hooks/useIsTokenPermittable'
export * from './hooks/usePermitInfo'
export * from './hooks/useOrdersPermitStatus'
export * from './hooks/usePermitCompatibleTokens'
export * from './hooks/useTokenSupportsPermit'
export * from './types'
export * from './updaters/PendingPermitUpdater'
export * from './utils/handlePermit'
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import { AddPermitTokenParams, PermittableTokens } from '../types'
* Atom that stores the permittable tokens info for each chain on localStorage.
* It's meant to be shared across different tabs, thus no special storage handling.
*
* Contains either the permit info with `type` and `gasLimit` when supported or
* `false` when not supported
* Contains either the permit info for every token checked locally
*/
export const permittableTokensAtom = atomWithStorage<PermittableTokens>('permittableTokens:v1', {
export const permittableTokensAtom = atomWithStorage<PermittableTokens>('permittableTokens:v2', {
[SupportedChainId.MAINNET]: {},
[SupportedChainId.GOERLI]: {},
[SupportedChainId.GNOSIS_CHAIN]: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isSupportedPermitInfo } from '@cowprotocol/permit-utils'

import { AppDataInfo, buildAppDataHooks, updateHooksOnAppData } from 'modules/appData'

import { HandlePermitParams } from '../types'
Expand All @@ -15,7 +17,7 @@ import { HandlePermitParams } from '../types'
export async function handlePermit(params: HandlePermitParams): Promise<AppDataInfo> {
const { permitInfo, inputToken, account, appData, generatePermitHook } = params

if (permitInfo && 'address' in inputToken) {
if (isSupportedPermitInfo(permitInfo) && 'address' in inputToken) {
// permitInfo will only be set if there's NOT enough allowance

const permitData = await generatePermitHook({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/
import { Field } from 'legacy/state/types'
import { useExpertModeManager } from 'legacy/state/user/hooks'

import { useIsTokenPermittable } from 'modules/permit'
import { useTokenSupportsPermit } from 'modules/permit'
import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState'
import { useEthFlowContext } from 'modules/swap/hooks/useEthFlowContext'
import { useHandleSwap } from 'modules/swap/hooks/useHandleSwap'
Expand Down Expand Up @@ -93,7 +93,7 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext
const isSwapUnsupported = useIsTradeUnsupported(currencyIn, currencyOut)
const isSmartContractWallet = useIsSmartContractWallet()
const isBundlingSupported = useIsBundlingSupported()
const isPermitSupported = !!useIsTokenPermittable(currencyIn, TradeType.SWAP)
const isPermitSupported = useTokenSupportsPermit(currencyIn, TradeType.SWAP)

const swapButtonState = getSwapButtonState({
account,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getWrappedToken } from '@cowprotocol/common-utils'
import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk'
import { TradeType as UniTradeType } from '@uniswap/sdk-core'

import { useGeneratePermitHook, useIsTokenPermittable } from 'modules/permit'
import { useGeneratePermitHook, usePermitInfo } from 'modules/permit'
import { FlowType, getFlowContext, useBaseFlowContextSetup } from 'modules/swap/hooks/useFlowContext'
import { SwapFlowContext } from 'modules/swap/services/types'
import { useEnoughBalanceAndAllowance } from 'modules/tokens'
Expand All @@ -14,7 +14,7 @@ export function useSwapFlowContext(): SwapFlowContext | null {
const contract = useGP2SettlementContract()
const baseProps = useBaseFlowContextSetup()
const sellCurrency = baseProps.trade?.inputAmount?.currency
const permitInfo = useIsTokenPermittable(sellCurrency, TradeType.SWAP)
const permitInfo = usePermitInfo(sellCurrency, TradeType.SWAP)
const generatePermitHook = useGeneratePermitHook()

const checkAllowanceAddress = GP_VAULT_RELAYER[baseProps.chainId || SupportedChainId.MAINNET]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isSupportedPermitInfo } from '@cowprotocol/permit-utils'
import { Percent } from '@uniswap/sdk-core'

import { PriceImpact } from 'legacy/hooks/usePriceImpact'
Expand Down Expand Up @@ -26,7 +27,7 @@ export async function swapFlow(

try {
logTradeFlow('SWAP FLOW', 'STEP 2: handle permit')
if (input.permitInfo) input.swapConfirmManager.requestPermitSignature()
if (isSupportedPermitInfo(input.permitInfo)) input.swapConfirmManager.requestPermitSignature()

input.orderParams.appData = await handlePermit({
appData: input.orderParams.appData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useIsTradeUnsupported } from '@cowprotocol/tokens'
import { useGnosisSafeInfo, useIsBundlingSupported, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet'

import { isUnsupportedTokenInQuote } from 'modules/limitOrders/utils/isUnsupportedTokenInQuote'
import { useIsTokenPermittable } from 'modules/permit'
import { useTokenSupportsPermit } from 'modules/permit'
import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState'
import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap'
import { useTradeQuote } from 'modules/tradeQuote'
Expand All @@ -32,7 +32,7 @@ export function useTradeFormValidationContext(): TradeFormValidationCommonContex

const isSafeReadonlyUser = gnosisSafeInfo?.isReadOnly || false

const isPermitSupported = !!useIsTokenPermittable(inputCurrency, tradeType)
const isPermitSupported = useTokenSupportsPermit(inputCurrency, tradeType)

const commonContext = {
account,
Expand Down
2 changes: 1 addition & 1 deletion libs/permit-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cowprotocol/permit-utils",
"version": "0.0.1-RC.1",
"version": "0.0.1",
"type": "module",
"dependencies": {
"ethers": "^5.7.2",
Expand Down
16 changes: 16 additions & 0 deletions libs/permit-utils/src/abi/erc20.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
Loading

0 comments on commit 578df82

Please sign in to comment.