Skip to content

Commit

Permalink
feat(permit): refactor permit-utils package (#3258)
Browse files Browse the repository at this point in the history
* feat: add permit-utils lib

Generated using `nx g @nx/js:lib` command

* chore: remove default files

* feat: add utils to permit-utils package

* feat: remove utils from modules/permit in favor of permit-utils pkg

* chore: move PermitProviderConnector to utils

* chore: updated eslint rules similar to main app

* chore: remove external dependency on DAI

* chore: remove types that are not used in the lib

* chore: remove dependency on GP_VAULT_RELAYER

* chore: remove dependency on @cowprotocol/common-utils

* refactor: sorting imports

* chore: update project name

* chore: add readme

* refactor: move buildPermitCallData to utils

* refactor: rename checkIsTokenPermittable to getTokenPermitInfo

* chore: move IsTokenPermittableResult back to modules/permit

* chore: set permit-utils pkg version to 0.0.1-RC.0

* refactor: export fns individually rather than everything from the file

* fix: typo on project name 🤦

* chore: fix config with `nx repair`

* fix: project name has to match path I think. Now it builds

* chore: pick the version from package.json when publishing a package

* chore: add otp arg to all projects

* chore: copy README on publish to dist folder so it gets added to npm

* chore: remove dependency on cow-sdk package

* fix: must have macrosPlugin as part of vite.config

* chore: change provider type to JsonRpcProvider

* chore: expose GetTokenPermitIntoResult

* chore: remove stuff added automatically which are not needed

* fix: js code style

* chore: remove commands which are not relevant in the NPM context

* chore: add somewhat complete usage example

* chore: remove another dep added automatically jest-environment-node

* chore: import AbiInput from @1inch lib instead of directly from web3

* chore: remove @ethersproject/bignumber dep. Already part of ethers

* refactor: removed extra comma

* refactor: remove dependency on @uniswap/core

* chore: exclude all external dependencies from the build

* chore: change package type from commonjs to module

* chore: add required dependencies to lib package.json

* feat: set different entry points for different module styles

* chore: update some package dependencies

* fix: exclusions MUST not be the whole /node_modules/ as it breaks the generated bundle

* fix: no longer using Token type on permit-utils

* fix: uploaded local registry path by mistake
  • Loading branch information
alfetopito authored Oct 27, 2023
1 parent 08c9b8b commit 28ea672
Show file tree
Hide file tree
Showing 40 changed files with 937 additions and 258 deletions.
30 changes: 0 additions & 30 deletions apps/cowswap-frontend/src/modules/permit/const.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,11 @@
import { DAI } from '@cowprotocol/common-const'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { MaxUint256 } from '@ethersproject/constants'
import { Wallet } from '@ethersproject/wallet'

import ms from 'ms.macro'

import { TradeType } from '../trade'

// PK used only for signing permit requests for quoting and identifying token 'permittability'
// Do not use or try to send funds to it. Or do. It'll be your funds 🤷
const PERMIT_PK = '0xc58a2a421ca71ca57ae698f1c32feeb0b0ccb434da0b8089d88d80fb918f3f9d' // address: 0xFf65D1DfCF256cf4A8D5F2fb8e70F936606B7474

export const PERMIT_SIGNER = new Wallet(PERMIT_PK)

export const PERMIT_GAS_LIMIT_MIN: Record<SupportedChainId, number> = {
1: 55_000,
100: 55_000,
5: 36_000,
}

export const DEFAULT_PERMIT_GAS_LIMIT = '80000'

export const DEFAULT_PERMIT_VALUE = MaxUint256.toString()

export const DEFAULT_PERMIT_DURATION = ms`5 years`

export const ORDER_TYPE_SUPPORTS_PERMIT: Record<TradeType, boolean> = {
[TradeType.SWAP]: true,
[TradeType.LIMIT_ORDER]: true,
[TradeType.ADVANCED_ORDERS]: false,
}

export const PENDING_ORDER_PERMIT_CHECK_INTERVAL = ms`1min`

// DAI's mainnet contract (https://etherscan.io/address/0x6b175474e89094c44da98b954eedeac495271d0f#readContract) returns
// `1` for the version, while when calling the contract method returns `2`.
// Also, if we use the version returned by the contract, it simply doesn't work
// Thus, do not call it for DAI.
// TODO: figure out whether more tokens behave the same way
export const TOKENS_TO_SKIP_VERSION = new Set([DAI.address.toLowerCase()])
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'

import { Token } from '@uniswap/sdk-core'
import { PermitHookData } from '@cowprotocol/permit-utils'

import { useDerivedTradeState } from 'modules/trade'

Expand All @@ -9,7 +9,7 @@ import { useSafeMemo } from 'common/hooks/useSafeMemo'
import { useGeneratePermitHook } from './useGeneratePermitHook'
import { useIsTokenPermittable } from './useIsTokenPermittable'

import { GeneratePermitHookParams, PermitHookData } from '../types'
import { GeneratePermitHookParams } from '../types'

/**
* Returns PermitHookData using an account agnostic signer if inputCurrency is permittable
Expand Down Expand Up @@ -44,10 +44,10 @@ function useGeneratePermitHookParams(): GeneratePermitHookParams | undefined {
const permitInfo = useIsTokenPermittable(inputCurrency, tradeType)

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

return {
inputToken: inputCurrency as Token,
inputToken: { address: inputCurrency.address, name: inputCurrency.name },
permitInfo,
}
}, [inputCurrency, permitInfo])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { useCallback } from 'react'

import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { checkIsCallDataAValidPermit, getPermitUtilsInstance, PermitInfo } from '@cowprotocol/permit-utils'
import { useWalletInfo } from '@cowprotocol/wallet'
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'

import { DAI_PERMIT_SELECTOR, Eip2612PermitUtils, EIP_2612_PERMIT_SELECTOR } from '@1inch/permit-signed-approvals-utils'

import { getAppDataHooks } from 'modules/appData'

import { ParsedOrder } from 'utils/orderUtils/parseOrder'

import { useGetPermitInfo } from './useGetPermitInfo'

import { CheckHasValidPendingPermit, PermitInfo, SupportedPermitInfo } from '../types'
import { fixTokenName } from '../utils/fixTokenName'
import { getPermitUtilsInstance } from '../utils/getPermitUtilsInstance'
import { CheckHasValidPendingPermit } from '../types'

export function useCheckHasValidPendingPermit(): CheckHasValidPendingPermit {
const { chainId } = useWalletInfo()
Expand All @@ -38,7 +35,7 @@ export function useCheckHasValidPendingPermit(): CheckHasValidPendingPermit {

return checkHasValidPendingPermit(order, provider, chainId, permitInfo)
},
[chainId, provider]
[chainId, getPermitInfo, provider]
)
}

Expand Down Expand Up @@ -71,7 +68,7 @@ async function checkHasValidPendingPermit(

const checkedHooks = await Promise.all(
preHooks.map(({ callData }) =>
checkIsSingleCallDataAValidPermit(order, chainId, eip2162Utils, tokenAddress, tokenName, callData, permitInfo)
checkIsCallDataAValidPermit(order.owner, chainId, eip2162Utils, tokenAddress, tokenName, callData, permitInfo)
)
)

Expand All @@ -85,52 +82,3 @@ async function checkHasValidPendingPermit(
// Only when all permits are valid, then the order permits are still valid
return validPermits.every(Boolean)
}

async function checkIsSingleCallDataAValidPermit(
order: ParsedOrder,
chainId: SupportedChainId,
eip2162Utils: Eip2612PermitUtils,
tokenAddress: string,
tokenName: string,
callData: string,
{ version }: SupportedPermitInfo
): Promise<boolean | undefined> {
const params = { chainId, tokenName: fixTokenName(tokenName), tokenAddress, callData, version }

let recoverPermitOwnerPromise: Promise<string> | undefined = undefined

// If pre-hook doesn't start with either selector, it's not a permit
if (callData.startsWith(EIP_2612_PERMIT_SELECTOR)) {
recoverPermitOwnerPromise = eip2162Utils.recoverPermitOwnerFromCallData({
...params,
// I don't know why this was removed, ok?
// We added it back on buildPermitCallData.ts
// But it looks like this is needed 🤷
// Check the test for this method https://github.com/1inch/permit-signed-approvals-utils/blob/master/src/eip-2612-permit.test.ts#L85-L106
callData: callData.replace(EIP_2612_PERMIT_SELECTOR, '0x'),
})
} else if (callData.startsWith(DAI_PERMIT_SELECTOR)) {
recoverPermitOwnerPromise = eip2162Utils.recoverDaiLikePermitOwnerFromCallData({
...params,
callData: callData.replace(DAI_PERMIT_SELECTOR, '0x'),
})
}

if (!recoverPermitOwnerPromise) {
// The callData doesn't match any known permit type
return undefined
}

try {
const recoveredOwner = await recoverPermitOwnerPromise

// Permit is valid when recovered owner matches order owner
return recoveredOwner.toLowerCase() === order.owner.toLowerCase()
} catch (e) {
console.debug(
`[checkHasValidPendingPermit] Failed to check permit validity for order ${order.id} with callData ${callData}`,
e
)
return false
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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 { useWalletInfo } from '@cowprotocol/wallet'
import { useWeb3React } from '@web3-react/core'

Expand All @@ -10,9 +12,7 @@ import {
storePermitCacheAtom,
userPermitCacheAtom,
} from '../state/permitCacheAtom'
import { GeneratePermitHook, GeneratePermitHookParams, PermitHookData } from '../types'
import { generatePermitHook } from '../utils/generatePermitHook'
import { getPermitUtilsInstance } from '../utils/getPermitUtilsInstance'
import { GeneratePermitHook, GeneratePermitHookParams } from '../types'

/**
* Hook that returns callback to generate permit hook data
Expand All @@ -32,6 +32,8 @@ export function useGeneratePermitHook(): GeneratePermitHook {
const { chainId } = useWalletInfo()
const { provider } = useWeb3React()

const spender = GP_VAULT_RELAYER[chainId]

return useCallback(
async (params: GeneratePermitHookParams): Promise<PermitHookData | undefined> => {
const { inputToken, account, permitInfo } = params
Expand All @@ -57,6 +59,7 @@ export function useGeneratePermitHook(): GeneratePermitHook {
const hookData = await generatePermitHook({
chainId,
inputToken,
spender,
provider,
permitInfo,
eip2162Utils,
Expand All @@ -68,6 +71,6 @@ export function useGeneratePermitHook(): GeneratePermitHook {

return hookData
},
[storePermit, chainId, getCachedPermit, provider]
[provider, chainId, getCachedPermit, spender, storePermit]
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useAtomValue, useSetAtom } from 'jotai'
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 { useWalletInfo } from '@cowprotocol/wallet'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
Expand All @@ -16,7 +18,6 @@ import { useIsPermitEnabled } from 'common/hooks/featureFlags/useIsPermitEnabled
import { ORDER_TYPE_SUPPORTS_PERMIT } from '../const'
import { addPermitInfoForTokenAtom, permittableTokensAtom } from '../state/permittableTokensAtom'
import { IsTokenPermittableResult } from '../types'
import { checkIsTokenPermittable } from '../utils/checkIsTokenPermittable'

/**
* Checks whether the token is permittable, and caches the result on localStorage
Expand Down Expand Up @@ -45,12 +46,14 @@ export function useIsTokenPermittable(
const addPermitInfo = useAddPermitInfo()
const permitInfo = usePermitInfo(chainId, isPermitEnabled ? lowerCaseAddress : undefined)

const spender = GP_VAULT_RELAYER[chainId]

useEffect(() => {
if (!chainId || !isPermitEnabled || !lowerCaseAddress || !provider || permitInfo !== undefined || isNative) {
return
}

checkIsTokenPermittable({ tokenAddress: lowerCaseAddress, tokenName, chainId, provider }).then((result) => {
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 })
Expand All @@ -64,7 +67,7 @@ export function useIsTokenPermittable(
addPermitInfo({ chainId, tokenAddress: lowerCaseAddress, permitInfo: result })
}
})
}, [addPermitInfo, chainId, isNative, isPermitEnabled, lowerCaseAddress, permitInfo, provider, tokenName])
}, [addPermitInfo, chainId, isNative, isPermitEnabled, lowerCaseAddress, permitInfo, provider, spender, tokenName])

if (isNative) {
return false
Expand Down
60 changes: 5 additions & 55 deletions apps/cowswap-frontend/src/modules/permit/types.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,30 @@
import { latest } from '@cowprotocol/app-data'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { Web3Provider } from '@ethersproject/providers'
import { Token } from '@uniswap/sdk-core'

import { Eip2612PermitUtils } from '@1inch/permit-signed-approvals-utils'
import { PermitHookData, PermitHookParams, PermitInfo } from '@cowprotocol/permit-utils'
import { Currency } from '@uniswap/sdk-core'

import { AppDataInfo } from 'modules/appData'

import { ParsedOrder } from 'utils/orderUtils/parseOrder'

export type PermitType = 'dai-like' | 'eip-2612'

export type SupportedPermitInfo = {
type: PermitType
version: string | undefined // Some tokens have it different than `1`, and won't work without it
}
type UnsupportedPermitInfo = false
export type PermitInfo = SupportedPermitInfo | UnsupportedPermitInfo
export type IsTokenPermittableResult = PermitInfo | undefined

export type PermittableTokens = Record<SupportedChainId, Record<string, PermitInfo>>

export type IsTokenPermittableResult = PermitInfo | undefined

export type AddPermitTokenParams = {
chainId: SupportedChainId
tokenAddress: string
permitInfo: PermitInfo
}

export type PermitHookParams = {
inputToken: Token
chainId: SupportedChainId
permitInfo: SupportedPermitInfo
provider: Web3Provider
eip2162Utils: Eip2612PermitUtils
account?: string | undefined
nonce?: number | undefined
}

export type GeneratePermitHookParams = Pick<PermitHookParams, 'inputToken' | 'permitInfo' | 'account'>

export type GeneratePermitHook = (params: GeneratePermitHookParams) => Promise<PermitHookData | undefined>

export type HandlePermitParams = Omit<GeneratePermitHookParams, 'permitInfo'> & {
export type HandlePermitParams = Omit<GeneratePermitHookParams, 'permitInfo' | 'inputToken'> & {
permitInfo: IsTokenPermittableResult
appData: AppDataInfo
generatePermitHook: GeneratePermitHook
}

export type PermitHookData = latest.CoWHook

type FailedToIdentify = { error: string }

export type EstimatePermitResult =
// When it's a permittable token:
| SupportedPermitInfo
// When something failed:
| FailedToIdentify
// When it's not permittable:
| UnsupportedPermitInfo

type BasePermitCallDataParams = {
eip2162Utils: Eip2612PermitUtils
}
export type BuildEip2162PermitCallDataParams = BasePermitCallDataParams & {
callDataParams: Parameters<Eip2612PermitUtils['buildPermitCallData']>
}
export type BuildDaiLikePermitCallDataParams = BasePermitCallDataParams & {
callDataParams: Parameters<Eip2612PermitUtils['buildDaiLikePermitCallData']>
}

export type CheckIsTokenPermittableParams = {
tokenAddress: string
tokenName: string
chainId: SupportedChainId
provider: Web3Provider
inputToken: Currency
}

export type PermitCache = Record<string, string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import { HandlePermitParams } from '../types'
export async function handlePermit(params: HandlePermitParams): Promise<AppDataInfo> {
const { permitInfo, inputToken, account, appData, generatePermitHook } = params

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

const permitData = await generatePermitHook({
inputToken,
inputToken: { address: inputToken.address, name: inputToken.name },
account,
permitInfo,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Percent, Token } from '@uniswap/sdk-core'
import { Percent } from '@uniswap/sdk-core'

import { PriceImpact } from 'legacy/hooks/usePriceImpact'
import { partialOrderUpdate } from 'legacy/state/orders/utils'
Expand Down Expand Up @@ -30,7 +30,7 @@ export async function swapFlow(

input.orderParams.appData = await handlePermit({
appData: input.orderParams.appData,
inputToken: input.context.trade.inputAmount.currency as Token,
inputToken: input.context.trade.inputAmount.currency,
account: input.orderParams.account,
permitInfo: input.permitInfo,
generatePermitHook: input.generatePermitHook,
Expand Down
3 changes: 2 additions & 1 deletion apps/cowswap-frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"@cowprotocol/common-hooks": ["../../../libs/common-hooks/src/index.ts"],
"@cowprotocol/ens": ["../../../libs/ens/src/index.ts"],
"@cowprotocol/core": ["../../../libs/core/src/index.ts"],
"@cowprotocol/analytics": ["../../../libs/analytics/src/index.ts"]
"@cowprotocol/analytics": ["../../../libs/analytics/src/index.ts"],
"@cowprotocol/permit-utils": ["../../../libs/permit-utils/src/index.ts"]
}
},
"files": [],
Expand Down
2 changes: 1 addition & 1 deletion apps/widget-configurator/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["coverage/apps/widget-configurator"],
"outputs": ["{workspaceRoot}/coverage/apps/widget-configurator"],
"options": {
"passWithNoTests": true,
"reportsDirectory": "../../coverage/apps/widget-configurator"
Expand Down
5 changes: 5 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getJestProjects } from '@nx/jest'

export default {
projects: getJestProjects(),
}
Loading

0 comments on commit 28ea672

Please sign in to comment.