1
1
import type { ExtendedChain } from '@lifi/types'
2
- import type { Address , Client } from 'viem'
2
+ import {
3
+ encodeAbiParameters ,
4
+ keccak256 ,
5
+ pad ,
6
+ parseAbiParameters ,
7
+ toBytes ,
8
+ toHex ,
9
+ } from 'viem'
10
+ import type { Address , Client , Hex } from 'viem'
11
+ import type { TypedDataDomain } from 'viem'
3
12
import { multicall , readContract } from 'viem/actions'
4
13
import { eip2612Abi } from './abi.js'
5
14
import { getMulticallAddress } from './utils.js'
@@ -9,6 +18,136 @@ export type NativePermitData = {
9
18
version : string
10
19
nonce : bigint
11
20
supported : boolean
21
+ domain : TypedDataDomain
22
+ }
23
+
24
+ /**
25
+ * EIP-712 domain typehash with chainId
26
+ * @link https://eips.ethereum.org/EIPS/eip-712#specification
27
+ *
28
+ * keccak256(toBytes(
29
+ * 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
30
+ * ))
31
+ */
32
+ const EIP712_DOMAIN_TYPEHASH =
33
+ '0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f' as Hex
34
+
35
+ /**
36
+ * EIP-712 domain typehash with salt (e.g. USDC.e on Polygon)
37
+ * @link https://eips.ethereum.org/EIPS/eip-712#specification
38
+ *
39
+ * keccak256(toBytes(
40
+ * 'EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)'
41
+ * ))
42
+ */
43
+ const EIP712_DOMAIN_TYPEHASH_WITH_SALT =
44
+ '0x36c25de3e541d5d970f66e4210d728721220fff5c077cc6cd008b3a0c62adab7' as Hex
45
+
46
+ function makeDomainSeparator ( {
47
+ name,
48
+ version,
49
+ chainId,
50
+ verifyingContract,
51
+ withSalt = false ,
52
+ } : {
53
+ name : string
54
+ version : string
55
+ chainId : bigint
56
+ verifyingContract : Address
57
+ withSalt ?: boolean
58
+ } ) : Hex {
59
+ const nameHash = keccak256 ( toBytes ( name ) )
60
+ const versionHash = keccak256 ( toBytes ( version ) )
61
+
62
+ const encoded = withSalt
63
+ ? encodeAbiParameters (
64
+ parseAbiParameters ( 'bytes32, bytes32, bytes32, address, bytes32' ) ,
65
+ [
66
+ EIP712_DOMAIN_TYPEHASH_WITH_SALT ,
67
+ nameHash ,
68
+ versionHash ,
69
+ verifyingContract ,
70
+ pad ( toHex ( chainId ) , { size : 32 } ) ,
71
+ ]
72
+ )
73
+ : encodeAbiParameters (
74
+ parseAbiParameters ( 'bytes32, bytes32, bytes32, uint256, address' ) ,
75
+ [
76
+ EIP712_DOMAIN_TYPEHASH ,
77
+ nameHash ,
78
+ versionHash ,
79
+ chainId ,
80
+ verifyingContract ,
81
+ ]
82
+ )
83
+
84
+ return keccak256 ( encoded )
85
+ }
86
+
87
+ // TODO: Add support for EIP-5267 when adoption increases
88
+ // This EIP provides a standard way to query domain separator and permit type hash
89
+ // via eip712Domain() function, which would simplify permit validation
90
+ // https://eips.ethereum.org/EIPS/eip-5267
91
+ function validateDomainSeparator ( {
92
+ name,
93
+ version,
94
+ chainId,
95
+ verifyingContract,
96
+ domainSeparator,
97
+ } : {
98
+ name : string
99
+ version : string
100
+ chainId : bigint
101
+ verifyingContract : Address
102
+ domainSeparator : Hex
103
+ } ) : { isValid : boolean ; domain : TypedDataDomain } {
104
+ if ( ! name || ! domainSeparator ) {
105
+ return {
106
+ isValid : false ,
107
+ domain : { } ,
108
+ }
109
+ }
110
+
111
+ for ( const withSalt of [ false , true ] ) {
112
+ const computedDS = makeDomainSeparator ( {
113
+ name,
114
+ version,
115
+ chainId,
116
+ verifyingContract,
117
+ withSalt,
118
+ } )
119
+ if ( domainSeparator . toLowerCase ( ) === computedDS . toLowerCase ( ) ) {
120
+ return {
121
+ isValid : true ,
122
+ domain : withSalt
123
+ ? {
124
+ name,
125
+ version,
126
+ verifyingContract,
127
+ salt : pad ( toHex ( chainId ) , { size : 32 } ) ,
128
+ }
129
+ : {
130
+ name,
131
+ version,
132
+ chainId,
133
+ verifyingContract,
134
+ } ,
135
+ }
136
+ }
137
+ }
138
+
139
+ return {
140
+ isValid : false ,
141
+ domain : { } ,
142
+ }
143
+ }
144
+
145
+ const defaultPermit : NativePermitData = {
146
+ name : '' ,
147
+ version : '1' ,
148
+ nonce : 0n ,
149
+ supported : false ,
150
+ domain : { } ,
12
151
}
13
152
14
153
/**
@@ -27,88 +166,102 @@ export const getNativePermit = async (
27
166
try {
28
167
const multicallAddress = await getMulticallAddress ( chain . id )
29
168
30
- if ( multicallAddress ) {
31
- const [ nameResult , domainSeparatorResult , noncesResult , versionResult ] =
32
- await multicall ( client , {
33
- contracts : [
34
- {
35
- address : tokenAddress ,
36
- abi : eip2612Abi ,
37
- functionName : 'name' ,
38
- } ,
39
- {
40
- address : tokenAddress ,
41
- abi : eip2612Abi ,
42
- functionName : 'DOMAIN_SEPARATOR' ,
43
- } ,
44
- {
45
- address : tokenAddress ,
46
- abi : eip2612Abi ,
47
- functionName : 'nonces' ,
48
- args : [ client . account ! . address ] ,
49
- } ,
50
- {
51
- address : tokenAddress ,
52
- abi : eip2612Abi ,
53
- functionName : 'version' ,
54
- } ,
55
- ] ,
56
- multicallAddress,
57
- } )
58
-
59
- const supported =
60
- nameResult . status === 'success' &&
61
- domainSeparatorResult . status === 'success' &&
62
- noncesResult . status === 'success' &&
63
- ! ! nameResult . result &&
64
- ! ! domainSeparatorResult . result &&
65
- noncesResult . result !== undefined
66
-
67
- return {
68
- name : nameResult . result ! ,
69
- version : versionResult . result ?? '1' ,
70
- nonce : noncesResult . result ! ,
71
- supported,
72
- }
73
- }
74
-
75
- // Fallback to individual calls
76
- const [ name , domainSeparator , nonce , version ] = await Promise . all ( [
77
- readContract ( client , {
169
+ const contractCalls = [
170
+ {
78
171
address : tokenAddress ,
79
172
abi : eip2612Abi ,
80
173
functionName : 'name' ,
81
- } ) ,
82
- readContract ( client , {
174
+ } ,
175
+ {
83
176
address : tokenAddress ,
84
177
abi : eip2612Abi ,
85
178
functionName : 'DOMAIN_SEPARATOR' ,
86
- } ) ,
87
- readContract ( client , {
179
+ } ,
180
+ {
88
181
address : tokenAddress ,
89
182
abi : eip2612Abi ,
90
183
functionName : 'nonces' ,
91
184
args : [ client . account ! . address ] ,
92
- } ) ,
93
- readContract ( client , {
185
+ } ,
186
+ {
94
187
address : tokenAddress ,
95
188
abi : eip2612Abi ,
96
189
functionName : 'version' ,
97
- } ) ,
98
- ] )
190
+ } ,
191
+ ] as const
192
+
193
+ if ( multicallAddress ) {
194
+ const [ nameResult , domainSeparatorResult , noncesResult , versionResult ] =
195
+ await multicall ( client , {
196
+ contracts : contractCalls ,
197
+ multicallAddress,
198
+ } )
199
+
200
+ if (
201
+ nameResult . status !== 'success' ||
202
+ domainSeparatorResult . status !== 'success' ||
203
+ noncesResult . status !== 'success' ||
204
+ ! nameResult . result ||
205
+ ! domainSeparatorResult . result ||
206
+ noncesResult . result === undefined
207
+ ) {
208
+ return defaultPermit
209
+ }
210
+
211
+ const { isValid, domain } = validateDomainSeparator ( {
212
+ name : nameResult . result ,
213
+ version : versionResult . result ?? '1' ,
214
+ chainId : BigInt ( chain . id ) ,
215
+ verifyingContract : tokenAddress ,
216
+ domainSeparator : domainSeparatorResult . result ,
217
+ } )
218
+
219
+ return {
220
+ name : nameResult . result ,
221
+ version : versionResult . result ?? '1' ,
222
+ nonce : noncesResult . result ,
223
+ supported : isValid ,
224
+ domain,
225
+ }
226
+ }
227
+
228
+ const [ nameResult , domainSeparatorResult , noncesResult , versionResult ] =
229
+ ( await Promise . allSettled (
230
+ contractCalls . map ( ( call ) => readContract ( client , call ) )
231
+ ) ) as [
232
+ PromiseSettledResult < string > ,
233
+ PromiseSettledResult < Hex > ,
234
+ PromiseSettledResult < bigint > ,
235
+ PromiseSettledResult < string > ,
236
+ ]
237
+
238
+ if (
239
+ nameResult . status !== 'fulfilled' ||
240
+ domainSeparatorResult . status !== 'fulfilled' ||
241
+ noncesResult . status !== 'fulfilled'
242
+ ) {
243
+ return defaultPermit
244
+ }
245
+
246
+ const name = nameResult . value
247
+ const version =
248
+ versionResult . status === 'fulfilled' ? versionResult . value : '1'
249
+ const { isValid, domain } = validateDomainSeparator ( {
250
+ name,
251
+ version,
252
+ chainId : BigInt ( chain . id ) ,
253
+ verifyingContract : tokenAddress ,
254
+ domainSeparator : domainSeparatorResult . value ,
255
+ } )
99
256
100
257
return {
101
258
name,
102
- version : version ?? '1' ,
103
- nonce,
104
- supported : ! ! name && ! ! domainSeparator && nonce !== undefined ,
259
+ version,
260
+ nonce : noncesResult . value ,
261
+ supported : isValid ,
262
+ domain,
105
263
}
106
264
} catch {
107
- return {
108
- name : '' ,
109
- version : '1' ,
110
- nonce : 0n ,
111
- supported : false ,
112
- }
265
+ return defaultPermit
113
266
}
114
267
}
0 commit comments