1
1
defmodule Ethers.NameService do
2
2
@ moduledoc """
3
- Name Service resolution implementation
3
+ Name Service resolution implementation for ENS (Ethereum Name Service).
4
+ Supports both forward and reverse resolution plus reverse lookups.
5
+
6
+ This module implements [Cross Chain / Offchain Resolvers](https://docs.ens.domains/resolvers/ccip-read)
7
+ (is CCIP-Read aware), allowing it to resolve names that are stored:
8
+ - On-chain (traditional L1 ENS resolution on Ethereum)
9
+ - Off-chain (via CCIP-Read gateway servers)
10
+ - Cross-chain (on other L2s and EVM-compatible blockchains)
11
+
12
+ The resolution process automatically handles these different scenarios transparently,
13
+ following the ENS standards for name resolution including ENSIP-10 and ENSIP-11.
4
14
"""
5
15
6
16
import Ethers , only: [ keccak_module: 0 ]
7
17
18
+ alias Ethers.CcipRead
8
19
alias Ethers.Contracts.ENS
20
+ alias Ethers.Contracts.ERC165
21
+ alias Ethers.Utils
9
22
10
23
@ zero_address Ethers.Types . default ( :address )
11
24
@@ -15,6 +28,8 @@ defmodule Ethers.NameService do
15
28
## Parameters
16
29
- name: Domain name to resolve. (Example: `foo.eth`)
17
30
- opts: Resolve options.
31
+ - resolve_call: TxData for resolution (Defaults to
32
+ `Ethers.Contracts.ENS.Resolver.addr(Ethers.NameService.name_hash(name))`)
18
33
- to: Resolver contract address. Defaults to ENS
19
34
- Accepts all other Execution options from `Ethers.call/2`.
20
35
@@ -26,16 +41,69 @@ defmodule Ethers.NameService do
26
41
```
27
42
"""
28
43
@ spec resolve ( String . t ( ) , Keyword . t ( ) ) ::
29
- { :ok , Ethers.Types . t_address ( ) } | { :error , :domain_not_found | term ( ) }
44
+ { :ok , Ethers.Types . t_address ( ) }
45
+ | { :error , :domain_not_found | :record_not_found | term ( ) }
30
46
def resolve ( name , opts \\ [ ] ) do
31
- name_hash = name_hash ( name )
47
+ with { :ok , resolver } <- get_last_resolver ( name , opts ) do
48
+ do_resolve ( resolver , name , opts )
49
+ end
50
+ end
51
+
52
+ defp do_resolve ( resolver , name , opts ) do
53
+ { resolve_call , opts } =
54
+ Keyword . pop_lazy ( opts , :resolve_call , fn ->
55
+ name
56
+ |> name_hash ( )
57
+ |> ENS.Resolver . addr ( )
58
+ end )
59
+
60
+ case supports_extended_resolver ( resolver , opts ) do
61
+ { :ok , true } ->
62
+ # ENSIP-10 support
63
+ opts = Keyword . put ( opts , :to , resolver )
64
+
65
+ resolve_call
66
+ |> ensip10_resolve ( name , opts )
67
+ |> handle_result ( )
68
+
69
+ { :ok , false } ->
70
+ opts = Keyword . put ( opts , :to , resolver )
71
+
72
+ resolve_call
73
+ |> Ethers . call ( opts )
74
+ |> handle_result ( )
75
+
76
+ { :error , reason } ->
77
+ { :error , reason }
78
+ end
79
+ end
80
+
81
+ defp handle_result ( result ) do
82
+ case result do
83
+ { :ok , @ zero_address } -> { :error , :record_not_found }
84
+ { :ok , address } -> { :ok , address }
85
+ { :error , reason } -> { :error , reason }
86
+ end
87
+ end
88
+
89
+ defp ensip10_resolve ( resolve_call , name , opts ) do
90
+ resolve_call_data = Utils . hex_decode! ( resolve_call . data )
91
+ dns_encoded_name = dns_encode ( name )
92
+ wildcard_call = ENS.ExtendedResolver . resolve ( dns_encoded_name , resolve_call_data )
32
93
33
- with { :ok , resolver } <- get_resolver ( name_hash , opts ) do
34
- opts = Keyword . put ( opts , :to , resolver )
35
- Ethers . call ( ENS.Resolver . addr ( name_hash ) , opts )
94
+ with { :ok , result } <- CcipRead . call ( wildcard_call , opts ) do
95
+ Ethers.TxData . abi_decode ( result , resolve_call )
36
96
end
37
97
end
38
98
99
+ defp supports_extended_resolver ( resolver , opts ) do
100
+ opts = Keyword . put ( opts , :to , resolver )
101
+
102
+ call = ERC165 . supports_interface ( ENS.ExtendedResolver )
103
+
104
+ Ethers . call ( call , opts )
105
+ end
106
+
39
107
@ doc """
40
108
Same as `resolve/2` but raises on errors.
41
109
@@ -46,7 +114,7 @@ defmodule Ethers.NameService do
46
114
"0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
47
115
```
48
116
"""
49
- @ spec resolve! ( String . t ( ) , Keyword . t ( ) ) :: Ethers.Types . t_address ( ) | no_return
117
+ @ spec resolve! ( String . t ( ) , Keyword . t ( ) ) :: Ethers.Types . t_address ( ) | no_return ( )
50
118
def resolve! ( name , opts \\ [ ] ) do
51
119
case resolve ( name , opts ) do
52
120
{ :ok , addr } -> addr
@@ -60,7 +128,8 @@ defmodule Ethers.NameService do
60
128
## Parameters
61
129
- address: Address to resolve.
62
130
- opts: Resolve options.
63
- - to: Resolver contract address. Defaults to ENS
131
+ - to: Resolver contract address. Defaults to ENS.
132
+ - chain_id: Chain ID of the target chain Defaults to `1`.
64
133
- Accepts all other Execution options from `Ethers.call/2`.
65
134
66
135
## Examples
@@ -71,18 +140,55 @@ defmodule Ethers.NameService do
71
140
```
72
141
"""
73
142
@ spec reverse_resolve ( Ethers.Types . t_address ( ) , Keyword . t ( ) ) ::
74
- { :ok , String . t ( ) } | { :error , :domain_not_found | term ( ) }
143
+ { :ok , String . t ( ) }
144
+ | { :error , :domain_not_found | :invalid_name | :forward_resolution_mismatch | term ( ) }
75
145
def reverse_resolve ( address , opts \\ [ ] ) do
76
- "0x" <> address_hash = Ethers.Utils . to_checksum_address ( address )
146
+ address = String . downcase ( address )
147
+ chain_id = Keyword . get ( opts , :chain_id , 1 )
148
+
149
+ { reverse_name , coin_type } = get_reverse_name ( address , chain_id )
150
+ name_hash = name_hash ( reverse_name )
151
+
152
+ with { :ok , resolver } <- get_resolver ( name_hash , opts ) ,
153
+ { :ok , name } <- resolve_name ( resolver , name_hash , opts ) ,
154
+ # Return early if no name found and we're not on default
155
+ { :ok , name } <- handle_empty_name ( name , coin_type , address , opts ) ,
156
+ # Verify forward resolution matches
157
+ :ok <- verify_forward_resolution ( name , address , opts ) do
158
+ { :ok , name }
159
+ end
160
+ end
161
+
162
+ defp get_reverse_name ( "0x" <> address , 1 ) , do: { "#{ address } .addr.reverse" , 60 }
163
+
164
+ defp get_reverse_name ( "0x" <> address , chain_id ) do
165
+ # ENSIP-11: coinType = 0x80000000 | chainId
166
+ coin_type = Bitwise . bor ( 0x80000000 , chain_id )
167
+ coin_type_hex = Integer . to_string ( coin_type , 16 )
168
+ { "#{ address } .#{ coin_type_hex } .reverse" , coin_type }
169
+ end
77
170
78
- name_hash =
79
- address_hash
80
- |> Kernel . <> ( ".addr.reverse" )
81
- |> name_hash ( )
171
+ defp handle_empty_name ( "" , coin_type , address , opts ) when coin_type != 0 do
172
+ "0x" <> address = address
173
+ # Try default reverse name
174
+ reverse_name = "#{ address } .default.reverse"
175
+ name_hash = name_hash ( reverse_name )
82
176
83
- with { :ok , resolver } <- get_resolver ( name_hash , opts ) do
84
- opts = Keyword . put ( opts , :to , resolver )
85
- Ethers . call ( ENS.Resolver . name ( name_hash ) , opts )
177
+ case get_resolver ( name_hash , [ ] ) do
178
+ { :ok , resolver } -> resolve_name ( resolver , name_hash , opts )
179
+ { :error , reason } -> { :error , reason }
180
+ end
181
+ end
182
+
183
+ defp handle_empty_name ( name , _coin_type , _address_hash , _opts ) , do: { :ok , name }
184
+
185
+ defp verify_forward_resolution ( name , address , opts ) do
186
+ with { :ok , resolved_addr } <- resolve ( name , opts ) do
187
+ if String . downcase ( resolved_addr ) == String . downcase ( address ) do
188
+ :ok
189
+ else
190
+ { :error , :forward_resolution_mismatch }
191
+ end
86
192
end
87
193
end
88
194
@@ -96,7 +202,7 @@ defmodule Ethers.NameService do
96
202
"vitalik.eth"
97
203
```
98
204
"""
99
- @ spec reverse_resolve! ( Ethers.Types . t_address ( ) , Keyword . t ( ) ) :: String . t ( ) | no_return
205
+ @ spec reverse_resolve! ( Ethers.Types . t_address ( ) , Keyword . t ( ) ) :: String . t ( ) | no_return ( )
100
206
def reverse_resolve! ( address , opts \\ [ ] ) do
101
207
case reverse_resolve ( address , opts ) do
102
208
{ :ok , name } -> name
@@ -120,9 +226,7 @@ defmodule Ethers.NameService do
120
226
@ spec name_hash ( String . t ( ) ) :: << _ :: 256 >>
121
227
def name_hash ( name ) do
122
228
name
123
- |> String . to_charlist ( )
124
- |> :idna . encode ( transitional: false , std3_rules: true , uts46: true )
125
- |> to_string ( )
229
+ |> normalize_dns_name ( )
126
230
|> String . split ( "." )
127
231
|> do_name_hash ( )
128
232
end
@@ -133,6 +237,30 @@ defmodule Ethers.NameService do
133
237
134
238
defp do_name_hash ( [ ] ) , do: << 0 :: 256 >>
135
239
240
+ defp get_last_resolver ( name , opts ) do
241
+ # HACK: get all resolvers at once using Multicall
242
+ name
243
+ |> name_hash ( )
244
+ |> ENS . resolver ( )
245
+ |> Ethers . call ( opts )
246
+ |> case do
247
+ { :ok , @ zero_address } ->
248
+ parent = get_name_parent ( name )
249
+
250
+ if parent != name do
251
+ get_last_resolver ( parent , opts )
252
+ else
253
+ :error
254
+ end
255
+
256
+ { :ok , resolver } ->
257
+ { :ok , resolver }
258
+
259
+ { :error , reason } ->
260
+ { :error , reason }
261
+ end
262
+ end
263
+
136
264
defp get_resolver ( name_hash , opts ) do
137
265
params = ENS . resolver ( name_hash )
138
266
@@ -142,4 +270,51 @@ defmodule Ethers.NameService do
142
270
{ :error , reason } -> { :error , reason }
143
271
end
144
272
end
273
+
274
+ defp resolve_name ( resolver , name_hash , opts ) do
275
+ opts = Keyword . put ( opts , :to , resolver )
276
+
277
+ name_hash
278
+ |> ENS.Resolver . name ( )
279
+ |> Ethers . call ( opts )
280
+ end
281
+
282
+ defp normalize_dns_name ( name ) do
283
+ name
284
+ |> String . to_charlist ( )
285
+ |> :idna . encode ( transitional: false , std3_rules: true , uts46: true )
286
+ |> to_string ( )
287
+ end
288
+
289
+ # Encodes a DNS name according to section 3.1 of RFC1035.
290
+ defp dns_encode ( name ) when is_binary ( name ) do
291
+ name
292
+ |> normalize_dns_name ( )
293
+ |> to_fqdn ( )
294
+ |> String . split ( "." )
295
+ |> encode_labels ( )
296
+ end
297
+
298
+ defp to_fqdn ( dns_name ) do
299
+ if String . ends_with? ( dns_name , "." ) do
300
+ dns_name
301
+ else
302
+ dns_name <> "."
303
+ end
304
+ end
305
+
306
+ defp encode_labels ( labels ) do
307
+ labels
308
+ |> Enum . reduce ( << >> , fn label , acc ->
309
+ label_length = byte_size ( label )
310
+ acc <> << label_length >> <> label
311
+ end )
312
+ end
313
+
314
+ defp get_name_parent ( name ) do
315
+ case String . split ( name , "." , parts: 2 ) do
316
+ [ _ , parent ] -> parent
317
+ [ tld ] -> tld
318
+ end
319
+ end
145
320
end
0 commit comments