Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(permit): fetch token name from chain #3374

Merged
merged 27 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3c1144b
chore: add PermitUtils type `unsupported`
alfetopito Nov 3, 2023
631e5d1
chore: apply PermitInfo type to consumers
alfetopito Nov 3, 2023
0652a48
feat: add helper method to only check whether token is permittable us…
alfetopito Nov 3, 2023
540c102
refactor: rename useIsTokenPermittable to usePermitInfo
alfetopito Nov 3, 2023
3656709
feat: add utils fn isSupportedPermitInfo
alfetopito Nov 8, 2023
737fe59
refactor: move inner private method getContract to external utils fn
alfetopito Nov 8, 2023
dee9e97
feat: add minimal erc20 `name` method abi
alfetopito Nov 8, 2023
4c8c262
feat: add utils fn `getTokenName`
alfetopito Nov 8, 2023
4a60c99
feat: fetch token name when fetching permit info
alfetopito Nov 8, 2023
b7e7674
feat: make tokenName optional on GetTokenPermitInfoParams
alfetopito Nov 8, 2023
c78b001
feat: migrate preGenerated permit info to new format
alfetopito Nov 8, 2023
7c76c95
chore: more tokenName optional changes
alfetopito Nov 9, 2023
227016a
chore: add TODOs
alfetopito Nov 9, 2023
4e80ac9
refactor: return error rather than throwing
alfetopito Nov 9, 2023
629fac9
chore: bump permit-utils package version
alfetopito Nov 9, 2023
4d2d666
chore: bump permittableTokens atom version to v2
alfetopito Nov 9, 2023
39db8a6
refactor: use isSupportedPermitInfo in a few places
alfetopito Nov 9, 2023
ec82c5f
chore: do a type guard in the helper fn so there's no need for casting
alfetopito Nov 14, 2023
d3bf9a2
chore: return error when name fetching fails due to network issues
alfetopito Nov 14, 2023
d70baf1
chore: identify earlier when token is not dai-like and return receive…
alfetopito Nov 14, 2023
7dfb0cd
chore: only log debug msg if not dai-like
alfetopito Nov 14, 2023
72f95b7
chore: mark some known error types as permanent errors
alfetopito Nov 14, 2023
f79e97f
chore: log also tokenName when possible
alfetopito Nov 14, 2023
4b25698
chore: use a regex to catch connection issues
alfetopito Nov 14, 2023
77471bf
chore: use const for default obj return to avoid re-renders
alfetopito Nov 16, 2023
d8ed59f
refactor: remove redundant check. It's covered by isSupportedPermitInfo
alfetopito Nov 17, 2023
35fbc52
chore: set permit-utils version to 0.0.1
alfetopito Nov 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Comment on lines +34 to +53
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should only be needed until this hits prod and the token-lists preGenerated version is not updated.

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', {
Comment on lines -15 to +14
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping the version to not use the old formats anymore

[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) {
alfetopito marked this conversation as resolved.
Show resolved Hide resolved
// 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"
}
]
Comment on lines +1 to +16
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a duplication, I know, but I want to have just the minimal ABI possible.
Also, if I would use the one from the abis lib, it would create an inner dependency and I don't want that for this lib

Loading
Loading