@cowprotocol/permit-utils 0.7.0-RC.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +219 -0
- package/LICENSE +674 -0
- package/README.md +6 -7
- package/jest.config.ts +11 -0
- package/package.json +25 -14
- package/project.json +42 -0
- package/src/abi/erc20.json +59 -0
- package/src/abi/versionAbis.ts +37 -0
- package/src/const.ts +20 -0
- package/src/consts/1inchPermitUtils.ts +68 -0
- package/src/imports/1inchPermitUtils.ts +10 -0
- package/src/index.ts +11 -0
- package/src/lib/checkIsCallDataAValidPermit.ts +64 -0
- package/src/lib/generatePermitHook.ts +133 -0
- package/src/lib/getPermitUtilsInstance.ts +51 -0
- package/src/lib/getTokenPermitInfo.ts +281 -0
- package/src/types.ts +61 -0
- package/src/utils/PermitProviderConnector.ts +75 -0
- package/src/utils/buildPermitCallData.ts +26 -0
- package/src/utils/getContract.ts +6 -0
- package/src/utils/getEip712Domain.ts +70 -0
- package/src/utils/getPermitDeadline.ts +7 -0
- package/src/utils/getTokenName.ts +13 -0
- package/src/utils/getTokenPermitVersion.ts +35 -0
- package/src/utils/isSupportedPermitInfo.ts +5 -0
- package/tsconfig.json +22 -0
- package/tsconfig.lib.json +10 -0
- package/tsconfig.spec.json +9 -0
- package/vite.config.mts +51 -0
- package/abi/versionAbis.d.ts +0 -15
- package/const.d.ts +0 -6
- package/index.cjs +0 -1
- package/index.d.ts +0 -7
- package/index.js +0 -489
- package/lib/checkIsCallDataAValidPermit.d.ts +0 -3
- package/lib/generatePermitHook.d.ts +0 -2
- package/lib/getPermitUtilsInstance.d.ts +0 -3
- package/lib/getTokenPermitInfo.d.ts +0 -2
- package/types.d.ts +0 -45
- package/utils/PermitProviderConnector.d.ts +0 -13
- package/utils/buildPermitCallData.d.ts +0 -3
- package/utils/getContract.d.ts +0 -3
- package/utils/getEip712Domain.d.ts +0 -13
- package/utils/getPermitDeadline.d.ts +0 -1
- package/utils/getTokenName.d.ts +0 -2
- package/utils/getTokenPermitVersion.d.ts +0 -2
- package/utils/isSupportedPermitInfo.d.ts +0 -2
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { getTokenId } from '@cowprotocol/cow-sdk'
|
|
2
|
+
import type { JsonRpcProvider } from '@ethersproject/providers'
|
|
3
|
+
|
|
4
|
+
import { getPermitUtilsInstance } from './getPermitUtilsInstance'
|
|
5
|
+
|
|
6
|
+
import { oneInchPermitUtilsConsts } from '..'
|
|
7
|
+
import { DEFAULT_MIN_GAS_LIMIT, DEFAULT_PERMIT_VALUE, PERMIT_SIGNER } from '../const'
|
|
8
|
+
import { GetTokenPermitInfoParams, GetTokenPermitIntoResult, PermitInfo, PermitType } from '../types'
|
|
9
|
+
import { buildDaiLikePermitCallData, buildEip2612PermitCallData } from '../utils/buildPermitCallData'
|
|
10
|
+
import { Eip712Domain, getEip712Domain } from '../utils/getEip712Domain'
|
|
11
|
+
import { getPermitDeadline } from '../utils/getPermitDeadline'
|
|
12
|
+
import { getTokenName } from '../utils/getTokenName'
|
|
13
|
+
import { getTokenPermitVersion } from '../utils/getTokenPermitVersion'
|
|
14
|
+
|
|
15
|
+
import type { Eip2612PermitUtils } from '@1inch/permit-signed-approvals-utils'
|
|
16
|
+
|
|
17
|
+
const EIP_2612_PERMIT_PARAMS = {
|
|
18
|
+
nonce: 0,
|
|
19
|
+
deadline: getPermitDeadline(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DAI_LIKE_PERMIT_PARAMS = {
|
|
23
|
+
allowed: true,
|
|
24
|
+
nonce: 0,
|
|
25
|
+
expiry: getPermitDeadline(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const REQUESTS_CACHE: Record<string, Promise<GetTokenPermitIntoResult>> = {}
|
|
29
|
+
|
|
30
|
+
const UNSUPPORTED: PermitInfo = { type: 'unsupported' }
|
|
31
|
+
|
|
32
|
+
export async function getTokenPermitInfo(params: GetTokenPermitInfoParams): Promise<GetTokenPermitIntoResult> {
|
|
33
|
+
const { tokenAddress, chainId } = params
|
|
34
|
+
const key = getTokenId({ address: tokenAddress, chainId })
|
|
35
|
+
|
|
36
|
+
const cached = REQUESTS_CACHE[key]
|
|
37
|
+
|
|
38
|
+
if (cached) {
|
|
39
|
+
return cached
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const request = actuallyCheckTokenIsPermittable(params)
|
|
43
|
+
|
|
44
|
+
REQUESTS_CACHE[key] = request
|
|
45
|
+
|
|
46
|
+
return request
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// TODO: Break down this large function into smaller functions
|
|
50
|
+
// TODO: Reduce function complexity by extracting logic
|
|
51
|
+
// eslint-disable-next-line max-lines-per-function, complexity
|
|
52
|
+
async function actuallyCheckTokenIsPermittable(params: GetTokenPermitInfoParams): Promise<GetTokenPermitIntoResult> {
|
|
53
|
+
const { spender, tokenAddress, chainId, provider, minGasLimit, amount } = params
|
|
54
|
+
|
|
55
|
+
const eip2612PermitUtils = await getPermitUtilsInstance(chainId, provider)
|
|
56
|
+
|
|
57
|
+
const owner = PERMIT_SIGNER.address
|
|
58
|
+
|
|
59
|
+
let domain: Eip712Domain | undefined = undefined
|
|
60
|
+
// Try to get eip712domain, which contains most of the info we'll need here
|
|
61
|
+
try {
|
|
62
|
+
domain = await getEip712Domain(tokenAddress, chainId, provider)
|
|
63
|
+
} catch {
|
|
64
|
+
console.debug(`[checkTokenIsPermittable] Couldn't fetch eip712domain for token ${tokenAddress}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let tokenName = domain?.name
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (!tokenName) {
|
|
71
|
+
tokenName = await getTokenName(tokenAddress, chainId, provider)
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (/ETIMEDOUT/.test(e) && !tokenName) {
|
|
75
|
+
// Network issue or another temporary failure, return error
|
|
76
|
+
return { error: `Failed to fetch token name from contract. RPC connection error` }
|
|
77
|
+
}
|
|
78
|
+
console.debug(
|
|
79
|
+
`[checkTokenIsPermittable] Couldn't fetch token name from the contract for token ${tokenAddress}, using provided '${tokenName}'`,
|
|
80
|
+
e,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!tokenName) {
|
|
85
|
+
const error = `Token name could not be determined for ${tokenAddress}`
|
|
86
|
+
return { error }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let nonce: number
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
nonce = await eip2612PermitUtils.getTokenNonce(tokenAddress, owner)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e === 'nonce not supported' || e.message === 'nonce is NaN') {
|
|
95
|
+
console.debug(`[checkTokenIsPermittable] Not a permittable token ${tokenAddress} - ${tokenName}`, e?.message || e)
|
|
96
|
+
// Here we know it's not supported, return unsupported
|
|
97
|
+
// See https://github.com/1inch/permit-signed-approvals-utils/blob/b190197a45c3289867ee4e6da93f10dea51ef276/src/eip-2612-permit.utils.ts#L309
|
|
98
|
+
// and https://github.com/1inch/permit-signed-approvals-utils/blob/b190197a45c3289867ee4e6da93f10dea51ef276/src/eip-2612-permit.utils.ts#L325
|
|
99
|
+
return { ...UNSUPPORTED, name: tokenName }
|
|
100
|
+
}
|
|
101
|
+
console.debug(`[checkTokenIsPermittable] Failed to get nonce for ${tokenAddress} - ${tokenName}`, e)
|
|
102
|
+
|
|
103
|
+
// Otherwise, it might have been a network issue or another temporary failure, return error
|
|
104
|
+
return { error: e.message || e.toString() }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let version: string | undefined = domain?.version
|
|
108
|
+
|
|
109
|
+
if (version === undefined) {
|
|
110
|
+
// If the token does not outright fails when calling with the `version` value
|
|
111
|
+
// returned by the contract, fetch it.
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
// Required by USDC-mainnet as its version is `2`.
|
|
115
|
+
// There might be other tokens that need this as well.
|
|
116
|
+
version = await getTokenPermitVersion(tokenAddress, provider)
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Not a problem, we can (try to) continue without it, and will default to `1` (part of the 1inch lib)
|
|
119
|
+
console.debug(`[checkTokenIsPermittable] Failed to get version for ${tokenAddress} - ${tokenName}`, e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const baseParams: BaseParams = {
|
|
124
|
+
chainId,
|
|
125
|
+
eip2612PermitUtils,
|
|
126
|
+
nonce,
|
|
127
|
+
spender,
|
|
128
|
+
tokenAddress,
|
|
129
|
+
tokenName,
|
|
130
|
+
walletAddress: owner,
|
|
131
|
+
version,
|
|
132
|
+
minGasLimit,
|
|
133
|
+
amount,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Try to estimate with eip-2612 first
|
|
138
|
+
return await estimateTokenPermit({ ...baseParams, type: 'eip-2612', provider })
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// Not eip-2612, try dai-like
|
|
141
|
+
try {
|
|
142
|
+
const isDaiLike = await isDaiLikeTypeHash(tokenAddress, eip2612PermitUtils)
|
|
143
|
+
|
|
144
|
+
if (!isDaiLike) {
|
|
145
|
+
// These might be supported, as they have nonces, but we don't know why the permit call fails
|
|
146
|
+
// TODO: further investigate this kind of token
|
|
147
|
+
// For now mark them as unsupported and don't check it again
|
|
148
|
+
if (/invalid signature/.test(e) || e?.code === 'UNPREDICTABLE_GAS_LIMIT') {
|
|
149
|
+
console.debug(
|
|
150
|
+
`[checkTokenIsPermittable] Token ${tokenAddress} - ${tokenName} might be permittable, but it's not supported for now. Reason:`,
|
|
151
|
+
e?.reason,
|
|
152
|
+
)
|
|
153
|
+
return { ...UNSUPPORTED, name: tokenName }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Maybe a temporary failure
|
|
157
|
+
console.debug(
|
|
158
|
+
`[checkTokenIsPermittable] Failed to estimate eip-2612 permit for ${tokenAddress} - ${tokenName}`,
|
|
159
|
+
e,
|
|
160
|
+
)
|
|
161
|
+
return { error: e.message || e.toString() }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return await estimateTokenPermit({ ...baseParams, type: 'dai-like', provider })
|
|
165
|
+
} catch (e) {
|
|
166
|
+
// Not dai-like either, return error
|
|
167
|
+
console.debug(
|
|
168
|
+
`[checkTokenIsPermittable] Failed to estimate dai-like permit for ${tokenAddress} - ${tokenName}`,
|
|
169
|
+
e,
|
|
170
|
+
)
|
|
171
|
+
return { error: e.message || e.toString() }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type BaseParams = {
|
|
177
|
+
tokenAddress: string
|
|
178
|
+
tokenName: string
|
|
179
|
+
chainId: number
|
|
180
|
+
walletAddress: string
|
|
181
|
+
spender: string
|
|
182
|
+
eip2612PermitUtils: Eip2612PermitUtils
|
|
183
|
+
nonce: number
|
|
184
|
+
version: string | undefined
|
|
185
|
+
minGasLimit?: number | undefined
|
|
186
|
+
amount?: bigint
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type EstimateParams = BaseParams & {
|
|
190
|
+
type: PermitType
|
|
191
|
+
provider: JsonRpcProvider
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function estimateTokenPermit(params: EstimateParams): Promise<GetTokenPermitIntoResult> {
|
|
195
|
+
const {
|
|
196
|
+
provider,
|
|
197
|
+
walletAddress,
|
|
198
|
+
tokenAddress,
|
|
199
|
+
tokenName,
|
|
200
|
+
type,
|
|
201
|
+
version,
|
|
202
|
+
minGasLimit = DEFAULT_MIN_GAS_LIMIT,
|
|
203
|
+
} = params
|
|
204
|
+
|
|
205
|
+
const getCallDataFn = type === 'eip-2612' ? getEip2612CallData : getDaiLikeCallData
|
|
206
|
+
|
|
207
|
+
const data = await getCallDataFn(params)
|
|
208
|
+
|
|
209
|
+
if (!data) {
|
|
210
|
+
return { ...UNSUPPORTED, name: tokenName }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const estimatedGas = await provider.estimateGas({
|
|
214
|
+
data,
|
|
215
|
+
from: walletAddress,
|
|
216
|
+
to: tokenAddress,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const gasLimit = estimatedGas.toNumber()
|
|
220
|
+
|
|
221
|
+
return gasLimit > minGasLimit
|
|
222
|
+
? {
|
|
223
|
+
type,
|
|
224
|
+
version,
|
|
225
|
+
name: tokenName,
|
|
226
|
+
}
|
|
227
|
+
: { ...UNSUPPORTED, name: tokenName }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function getEip2612CallData(params: BaseParams): Promise<string> {
|
|
231
|
+
const { eip2612PermitUtils, walletAddress, spender, nonce, chainId, tokenName, tokenAddress, version, amount } =
|
|
232
|
+
params
|
|
233
|
+
return buildEip2612PermitCallData({
|
|
234
|
+
eip2612Utils: eip2612PermitUtils,
|
|
235
|
+
callDataParams: [
|
|
236
|
+
{
|
|
237
|
+
...EIP_2612_PERMIT_PARAMS,
|
|
238
|
+
owner: walletAddress,
|
|
239
|
+
spender,
|
|
240
|
+
nonce,
|
|
241
|
+
value: amount ? amount.toString() : DEFAULT_PERMIT_VALUE.toString(),
|
|
242
|
+
},
|
|
243
|
+
+chainId,
|
|
244
|
+
tokenName,
|
|
245
|
+
tokenAddress,
|
|
246
|
+
version,
|
|
247
|
+
],
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function isDaiLikeTypeHash(tokenAddress: string, eip2612PermitUtils: Eip2612PermitUtils): Promise<boolean> {
|
|
252
|
+
const permitTypeHash = await eip2612PermitUtils.getPermitTypeHash(tokenAddress)
|
|
253
|
+
|
|
254
|
+
return permitTypeHash === oneInchPermitUtilsConsts.DAI_LIKE_PERMIT_TYPEHASH
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function getDaiLikeCallData(params: BaseParams): Promise<string | false> {
|
|
258
|
+
const { eip2612PermitUtils, tokenAddress, walletAddress, spender, nonce, chainId, tokenName, version } = params
|
|
259
|
+
|
|
260
|
+
const permitTypeHash = await eip2612PermitUtils.getPermitTypeHash(tokenAddress)
|
|
261
|
+
|
|
262
|
+
if (permitTypeHash === oneInchPermitUtilsConsts.DAI_LIKE_PERMIT_TYPEHASH) {
|
|
263
|
+
return buildDaiLikePermitCallData({
|
|
264
|
+
eip2612Utils: eip2612PermitUtils,
|
|
265
|
+
callDataParams: [
|
|
266
|
+
{
|
|
267
|
+
...DAI_LIKE_PERMIT_PARAMS,
|
|
268
|
+
holder: walletAddress,
|
|
269
|
+
spender,
|
|
270
|
+
nonce,
|
|
271
|
+
},
|
|
272
|
+
chainId as number,
|
|
273
|
+
tokenName,
|
|
274
|
+
tokenAddress,
|
|
275
|
+
version,
|
|
276
|
+
],
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return false
|
|
281
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { cowAppDataLatestScheme } from '@cowprotocol/cow-sdk'
|
|
2
|
+
import type { JsonRpcProvider } from '@ethersproject/providers'
|
|
3
|
+
|
|
4
|
+
import type { Eip2612PermitUtils } from '@1inch/permit-signed-approvals-utils'
|
|
5
|
+
|
|
6
|
+
export type PermitType = 'dai-like' | 'eip-2612' | 'unsupported'
|
|
7
|
+
|
|
8
|
+
export type PermitInfo = {
|
|
9
|
+
type: PermitType
|
|
10
|
+
// TODO: make it not optional once token-lists is migrated
|
|
11
|
+
name?: string
|
|
12
|
+
version?: string | undefined // Some tokens have it different than `1`, and won't work without it
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Local TokenInfo definition to not depend on external libs just for this
|
|
16
|
+
type TokenInfo = {
|
|
17
|
+
address: string
|
|
18
|
+
// TODO: remove from token info
|
|
19
|
+
name: string | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type PermitHookParams = {
|
|
23
|
+
inputToken: TokenInfo
|
|
24
|
+
spender: string
|
|
25
|
+
chainId: number
|
|
26
|
+
permitInfo: PermitInfo
|
|
27
|
+
provider: JsonRpcProvider
|
|
28
|
+
eip2612Utils: Eip2612PermitUtils
|
|
29
|
+
account?: string
|
|
30
|
+
nonce?: number
|
|
31
|
+
amount?: bigint
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PermitHookData = cowAppDataLatestScheme.CoWHook
|
|
35
|
+
|
|
36
|
+
type FailedToIdentify = { error: string }
|
|
37
|
+
|
|
38
|
+
export type GetTokenPermitIntoResult =
|
|
39
|
+
// When it's a permittable token:
|
|
40
|
+
| PermitInfo
|
|
41
|
+
// When something failed:
|
|
42
|
+
| FailedToIdentify
|
|
43
|
+
|
|
44
|
+
type BasePermitCallDataParams = {
|
|
45
|
+
eip2612Utils: Eip2612PermitUtils
|
|
46
|
+
}
|
|
47
|
+
export type BuildEip2612PermitCallDataParams = BasePermitCallDataParams & {
|
|
48
|
+
callDataParams: Parameters<Eip2612PermitUtils['buildPermitCallData']>
|
|
49
|
+
}
|
|
50
|
+
export type BuildDaiLikePermitCallDataParams = BasePermitCallDataParams & {
|
|
51
|
+
callDataParams: Parameters<Eip2612PermitUtils['buildDaiLikePermitCallData']>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type GetTokenPermitInfoParams = {
|
|
55
|
+
spender: string
|
|
56
|
+
tokenAddress: string
|
|
57
|
+
chainId: number
|
|
58
|
+
provider: JsonRpcProvider
|
|
59
|
+
minGasLimit?: number | undefined
|
|
60
|
+
amount?: bigint
|
|
61
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defaultAbiCoder, ParamType } from '@ethersproject/abi'
|
|
2
|
+
import { TypedDataField } from '@ethersproject/abstract-signer'
|
|
3
|
+
import { BigNumber } from '@ethersproject/bignumber'
|
|
4
|
+
import type { JsonRpcProvider } from '@ethersproject/providers'
|
|
5
|
+
import { Wallet } from '@ethersproject/wallet'
|
|
6
|
+
|
|
7
|
+
import { getContract } from './getContract'
|
|
8
|
+
|
|
9
|
+
import type { AbiInput, AbiItem, EIP712TypedData, ProviderConnector } from '@1inch/permit-signed-approvals-utils'
|
|
10
|
+
|
|
11
|
+
export class PermitProviderConnector implements ProviderConnector {
|
|
12
|
+
constructor(
|
|
13
|
+
private provider: JsonRpcProvider,
|
|
14
|
+
private walletSigner?: Wallet | undefined,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
contractEncodeABI(abi: AbiItem[], address: string | null, methodName: string, methodParams: unknown[]): string {
|
|
18
|
+
const contract = getContract(address || '', abi, this.provider)
|
|
19
|
+
|
|
20
|
+
return contract.interface.encodeFunctionData(methodName, methodParams)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
signTypedData(_walletAddress: string, typedData: EIP712TypedData, _typedDataHash: string): Promise<string> {
|
|
24
|
+
// Removes `EIP712Domain` as it's already part of EIP712 (see https://ethereum.stackexchange.com/a/151930/55204)
|
|
25
|
+
// and EthersJS complains when a type is not needed (see https://github.com/ethers-io/ethers.js/discussions/4000)
|
|
26
|
+
const types = Object.keys(typedData.types).reduce<Record<string, TypedDataField[]>>((acc, type) => {
|
|
27
|
+
if (type !== 'EIP712Domain') {
|
|
28
|
+
acc[type] = typedData.types[type]
|
|
29
|
+
}
|
|
30
|
+
return acc
|
|
31
|
+
}, {})
|
|
32
|
+
|
|
33
|
+
const signer = this.walletSigner || this.provider.getSigner()
|
|
34
|
+
|
|
35
|
+
return signer._signTypedData(typedData.domain, types, typedData.message)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ethCall(contractAddress: string, callData: string): Promise<string> {
|
|
39
|
+
return this.provider.call({
|
|
40
|
+
to: contractAddress,
|
|
41
|
+
data: callData,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
decodeABIParameter<T>(type: string, hex: string): T {
|
|
46
|
+
return defaultAbiCoder.decode([type], hex)[0]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
decodeABIParameters<T>(types: AbiInput[], hex: string): T {
|
|
50
|
+
const decodedValues = defaultAbiCoder.decode(types as unknown as (ParamType | string)[], hex) as T
|
|
51
|
+
|
|
52
|
+
// Ethersjs decodes numbers as BigNumber instances
|
|
53
|
+
// However, 1inch utils do not deal with BigNumber instances,
|
|
54
|
+
// so we need this mess to convert them to hex strings, which 1inch understands
|
|
55
|
+
// TODO: Any way to make this typing mess any cleaner?
|
|
56
|
+
if (decodedValues && typeof decodedValues === 'object') {
|
|
57
|
+
const copy: Record<string, unknown> = {}
|
|
58
|
+
|
|
59
|
+
Object.keys(decodedValues).forEach((key) => {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
const value = decodedValues[key]
|
|
62
|
+
if (BigNumber.isBigNumber(value)) {
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
copy[key] = value.toHexString()
|
|
65
|
+
} else {
|
|
66
|
+
copy[key] = value
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return copy as T
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return decodedValues
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { oneInchPermitUtilsConsts } from '..'
|
|
2
|
+
import { BuildDaiLikePermitCallDataParams, BuildEip2612PermitCallDataParams } from '../types'
|
|
3
|
+
|
|
4
|
+
export async function buildEip2612PermitCallData({
|
|
5
|
+
eip2612Utils,
|
|
6
|
+
callDataParams,
|
|
7
|
+
}: BuildEip2612PermitCallDataParams): Promise<string> {
|
|
8
|
+
const [permitParams, chainId, tokenName, ...rest] = callDataParams
|
|
9
|
+
|
|
10
|
+
const callData = await eip2612Utils.buildPermitCallData(permitParams, chainId, tokenName, ...rest)
|
|
11
|
+
// For some reason, the method above removes the permit selector prefix
|
|
12
|
+
// https://github.com/1inch/permit-signed-approvals-utils/blob/master/src/eip-2612-permit.utils.ts#L92
|
|
13
|
+
// Adding it back
|
|
14
|
+
return callData.replace('0x', oneInchPermitUtilsConsts.EIP_2612_PERMIT_SELECTOR)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function buildDaiLikePermitCallData({
|
|
18
|
+
eip2612Utils,
|
|
19
|
+
callDataParams,
|
|
20
|
+
}: BuildDaiLikePermitCallDataParams): Promise<string> {
|
|
21
|
+
const callData = await eip2612Utils.buildDaiLikePermitCallData(...callDataParams)
|
|
22
|
+
|
|
23
|
+
// Same as above, but for dai like
|
|
24
|
+
// https://github.com/1inch/permit-signed-approvals-utils/blob/master/src/eip-2612-permit.utils.ts#L140
|
|
25
|
+
return callData.replace('0x', oneInchPermitUtilsConsts.DAI_PERMIT_SELECTOR)
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Contract, ContractInterface } from '@ethersproject/contracts'
|
|
2
|
+
import type { JsonRpcProvider } from '@ethersproject/providers'
|
|
3
|
+
|
|
4
|
+
export function getContract(address: string, abi: ContractInterface, provider: JsonRpcProvider): Contract {
|
|
5
|
+
return new Contract(address, abi, provider)
|
|
6
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getAddress } from '@ethersproject/address'
|
|
2
|
+
import type { JsonRpcProvider } from '@ethersproject/providers'
|
|
3
|
+
|
|
4
|
+
import { getContract } from './getContract'
|
|
5
|
+
|
|
6
|
+
import Erc20Abi from '../abi/erc20.json'
|
|
7
|
+
|
|
8
|
+
// See https://eips.ethereum.org/EIPS/eip-5267
|
|
9
|
+
|
|
10
|
+
export async function getEip712Domain(
|
|
11
|
+
tokenAddress: string,
|
|
12
|
+
chainId: number,
|
|
13
|
+
provider: JsonRpcProvider,
|
|
14
|
+
): Promise<Eip712Domain> {
|
|
15
|
+
const formattedAddress = getAddress(tokenAddress)
|
|
16
|
+
const erc20Contract = getContract(formattedAddress, Erc20Abi, provider)
|
|
17
|
+
|
|
18
|
+
const eip5267Domain: Required<Eip5267Return> = await erc20Contract.callStatic['eip712Domain']()
|
|
19
|
+
|
|
20
|
+
return processDomain(eip5267Domain, chainId, tokenAddress)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Eip5267Return = {
|
|
24
|
+
fields: string
|
|
25
|
+
name?: string
|
|
26
|
+
version?: string
|
|
27
|
+
chainId?: string
|
|
28
|
+
verifyingContract?: string
|
|
29
|
+
salt?: string
|
|
30
|
+
extensions?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Eip712Domain = Omit<Eip5267Return, 'fields' | 'extensions'>
|
|
34
|
+
|
|
35
|
+
/** Retrieves the EIP-712 domain of a contract using EIP-5267 without extensions.
|
|
36
|
+
*
|
|
37
|
+
* Derived from https://eips.ethereum.org/EIPS/eip-5267 reference Javascript implementation
|
|
38
|
+
**/
|
|
39
|
+
async function processDomain(domain: Required<Eip5267Return>, chainId: number, address: string): Promise<Eip712Domain> {
|
|
40
|
+
const { extensions, ...rest } = domain
|
|
41
|
+
|
|
42
|
+
if (extensions.length > 0) {
|
|
43
|
+
throw Error('Extensions not implemented')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (rest.chainId && chainId !== parseInt(rest.chainId, 16)) {
|
|
47
|
+
throw Error(`ChainId mismatch. Received: '${rest.chainId}', expected: '${chainId}'`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (rest.verifyingContract && address.toLowerCase() !== rest.verifyingContract.toLowerCase()) {
|
|
51
|
+
throw Error(`Address mismatch. Received: '${rest.verifyingContract}', expected: '${address}'`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return buildBasicDomain(rest)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const FIELD_NAMES = ['name', 'version', 'chainId', 'verifyingContract', 'salt']
|
|
58
|
+
|
|
59
|
+
/** Builds a domain object without extensions based on the return values of `eip712Domain()`. */
|
|
60
|
+
function buildBasicDomain(domain: Required<Omit<Eip5267Return, 'extensions'>>): Eip712Domain {
|
|
61
|
+
// Will be a 5bit number as a hex string
|
|
62
|
+
const fields = parseInt(domain.fields, 16)
|
|
63
|
+
|
|
64
|
+
return FIELD_NAMES.reduce<Eip712Domain>((acc, field, i) => {
|
|
65
|
+
if (fields & (1 << i)) {
|
|
66
|
+
acc[field as keyof Eip712Domain] = domain[field as keyof Omit<Eip5267Return, 'extensions'>]
|
|
67
|
+
}
|
|
68
|
+
return acc
|
|
69
|
+
}, {})
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_PERMIT_DURATION } from '../const'
|
|
2
|
+
|
|
3
|
+
// TODO: Add proper return type annotation
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
5
|
+
export function getPermitDeadline() {
|
|
6
|
+
return Math.ceil((Date.now() + DEFAULT_PERMIT_DURATION) / 1000)
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getAddress } from '@ethersproject/address'
|
|
2
|
+
import type { JsonRpcProvider } from '@ethersproject/providers'
|
|
3
|
+
|
|
4
|
+
import { getContract } from './getContract'
|
|
5
|
+
|
|
6
|
+
import Erc20Abi from '../abi/erc20.json'
|
|
7
|
+
|
|
8
|
+
export async function getTokenName(tokenAddress: string, chainId: number, provider: JsonRpcProvider): Promise<string> {
|
|
9
|
+
const formattedAddress = getAddress(tokenAddress)
|
|
10
|
+
const erc20Contract = getContract(formattedAddress, Erc20Abi, provider)
|
|
11
|
+
|
|
12
|
+
return erc20Contract.callStatic['name']()
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defaultAbiCoder } from '@ethersproject/abi'
|
|
2
|
+
import type { JsonRpcProvider } from '@ethersproject/providers'
|
|
3
|
+
|
|
4
|
+
import { getContract } from './getContract'
|
|
5
|
+
|
|
6
|
+
import { VERSION_ABIS } from '../abi/versionAbis'
|
|
7
|
+
|
|
8
|
+
export async function getTokenPermitVersion(
|
|
9
|
+
tokenAddress: string,
|
|
10
|
+
provider: JsonRpcProvider,
|
|
11
|
+
index = 0,
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
const abi = VERSION_ABIS[index]
|
|
14
|
+
|
|
15
|
+
if (!abi) {
|
|
16
|
+
return '1'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const contract = getContract(tokenAddress, abi, provider)
|
|
21
|
+
const data = contract.interface.encodeFunctionData(abi[0].name)
|
|
22
|
+
const response = await provider.call({ to: tokenAddress, data })
|
|
23
|
+
|
|
24
|
+
if (response === '0x' || Number.isNaN(Number(response))) {
|
|
25
|
+
return '1'
|
|
26
|
+
}
|
|
27
|
+
if (!response.startsWith('0x')) {
|
|
28
|
+
return response
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return defaultAbiCoder.decode(['string'], response).toString()
|
|
32
|
+
} catch {
|
|
33
|
+
return getTokenPermitVersion(tokenAddress, provider, index + 1)
|
|
34
|
+
}
|
|
35
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"forceConsistentCasingInFileNames": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noImplicitOverride": true,
|
|
8
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
9
|
+
"noImplicitReturns": true,
|
|
10
|
+
"noFallthroughCasesInSwitch": true
|
|
11
|
+
},
|
|
12
|
+
"files": [],
|
|
13
|
+
"include": [],
|
|
14
|
+
"references": [
|
|
15
|
+
{
|
|
16
|
+
"path": "./tsconfig.lib.json"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "./tsconfig.spec.json"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
package/vite.config.mts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vite'
|
|
3
|
+
import macrosPlugin from 'vite-plugin-babel-macros'
|
|
4
|
+
import dts from 'vite-plugin-dts'
|
|
5
|
+
import viteTsConfigPaths from 'vite-tsconfig-paths'
|
|
6
|
+
|
|
7
|
+
import * as path from 'path'
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
root: path.resolve(__dirname, './'),
|
|
11
|
+
cacheDir: '../../../node_modules/.vite/permit-utils',
|
|
12
|
+
|
|
13
|
+
plugins: [
|
|
14
|
+
dts({
|
|
15
|
+
entryRoot: 'src',
|
|
16
|
+
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
|
|
17
|
+
}),
|
|
18
|
+
|
|
19
|
+
macrosPlugin(),
|
|
20
|
+
|
|
21
|
+
viteTsConfigPaths({
|
|
22
|
+
root: '../../../',
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
// Uncomment this if you are using workers.
|
|
26
|
+
// worker: {
|
|
27
|
+
// plugins: [
|
|
28
|
+
// viteTsConfigPaths({
|
|
29
|
+
// root: '../../',
|
|
30
|
+
// }),
|
|
31
|
+
// ],
|
|
32
|
+
// },
|
|
33
|
+
|
|
34
|
+
// Configuration for building your library.
|
|
35
|
+
// See: https://vitejs.dev/guide/build.html#library-mode
|
|
36
|
+
build: {
|
|
37
|
+
lib: {
|
|
38
|
+
// Could also be a dictionary or array of multiple entry points.
|
|
39
|
+
entry: 'src/index.ts',
|
|
40
|
+
name: 'permit-utils',
|
|
41
|
+
fileName: 'index',
|
|
42
|
+
// Change this to the formats you want to support.
|
|
43
|
+
// Don't forget to update your package.json as well.
|
|
44
|
+
formats: ['es', 'cjs'],
|
|
45
|
+
},
|
|
46
|
+
rollupOptions: {
|
|
47
|
+
// External packages that should not be bundled into your library.
|
|
48
|
+
external: [/@1inch/, /@cowprotocol/, /@ethersproject/],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|