6
6
)
7
7
from typing import (
8
8
TYPE_CHECKING ,
9
+ Any ,
9
10
Optional ,
10
11
Sequence ,
11
12
Tuple ,
17
18
Address ,
18
19
ChecksumAddress ,
19
20
HexAddress ,
21
+ HexStr ,
20
22
)
21
23
from eth_utils import (
24
+ is_address ,
22
25
is_binary_address ,
23
26
is_checksum_address ,
24
27
to_checksum_address ,
34
37
from ens .constants import (
35
38
EMPTY_ADDR_HEX ,
36
39
ENS_MAINNET_ADDR ,
40
+ EXTENDED_RESOLVER_INTERFACE_ID ,
41
+ GET_TEXT_INTERFACE_ID ,
37
42
REVERSE_REGISTRAR_DOMAIN ,
38
43
)
39
44
from ens .exceptions import (
40
45
AddressMismatch ,
46
+ ResolverNotFound ,
41
47
UnauthorizedError ,
42
48
UnownedName ,
49
+ UnsupportedFunction ,
43
50
)
44
51
from ens .utils import (
45
52
address_in ,
46
53
address_to_reverse_domain ,
47
54
default ,
55
+ ens_encode_name ,
56
+ get_abi_output_types ,
48
57
init_web3 ,
58
+ is_empty_name ,
49
59
is_none_or_zero_address ,
50
60
is_valid_name ,
51
61
label_to_hash ,
@@ -119,7 +129,8 @@ def __init__(
119
129
120
130
ens_addr = addr if addr else ENS_MAINNET_ADDR
121
131
self .ens = self .w3 .eth .contract (abi = abis .ENS , address = ens_addr )
122
- self ._resolverContract = self .w3 .eth .contract (abi = abis .RESOLVER )
132
+ self ._resolver_contract = self .w3 .eth .contract (abi = abis .RESOLVER )
133
+ self ._reverse_resolver_contract = self .w3 .eth .contract (abi = abis .REVERSE_RESOLVER )
123
134
124
135
@classmethod
125
136
def fromWeb3 (cls , w3 : 'Web3' , addr : ChecksumAddress = None ) -> 'ENS' :
@@ -152,18 +163,40 @@ def name(self, address: ChecksumAddress) -> Optional[str]:
152
163
:type address: hex-string
153
164
"""
154
165
reversed_domain = address_to_reverse_domain (address )
155
- name = self .resolve (reversed_domain , get = 'name' )
166
+ name = self .resolve (reversed_domain , fn_name = 'name' )
156
167
157
168
# To be absolutely certain of the name, via reverse resolution, the address must match in
158
169
# the forward resolution
159
170
return name if to_checksum_address (address ) == self .address (name ) else None
160
171
172
+ @staticmethod
173
+ def parent (name : str ) -> str :
174
+ """
175
+ Part of ENSIP-10. Returns the parent of a given ENS name, or the empty string if the ENS
176
+ name does not have a parent.
177
+
178
+ e.g.
179
+ - parent('1.foo.bar.eth') = 'foo.bar.eth'
180
+ - parent('foo.bar.eth') = 'bar.eth'
181
+ - parent('foo.eth') = 'eth'
182
+ - parent('eth') is defined as the empty string ''
183
+
184
+ :param name: an ENS name
185
+ :return: the parent for the provided ENS name
186
+ :rtype: str
187
+ """
188
+ if not name :
189
+ return ''
190
+
191
+ labels = name .split ('.' )
192
+ return '' if len (labels ) == 1 else '.' .join (labels [1 :])
193
+
161
194
def setup_address (
162
195
self ,
163
196
name : str ,
164
197
address : Union [Address , ChecksumAddress , HexAddress ] = cast (ChecksumAddress , default ),
165
198
transact : Optional ["TxParams" ] = None
166
- ) -> HexBytes :
199
+ ) -> Optional [ HexBytes ] :
167
200
"""
168
201
Set up the name to point to the supplied address.
169
202
The sender of the transaction must own the name, or
@@ -252,24 +285,38 @@ def setup_name(
252
285
self .setup_address (name , address , transact = transact )
253
286
return self ._setup_reverse (name , address , transact = transact )
254
287
255
- def resolve (self , name : str , get : str = 'addr' ) -> Optional [Union [ChecksumAddress , str ]]:
288
+ def resolver (self , normal_name : str ) -> Optional ['Contract' ]:
289
+ return self ._get_resolver (normal_name )[0 ]
290
+
291
+ def resolve (self , name : str , fn_name : str = 'addr' ) -> Optional [Union [ChecksumAddress , str ]]:
256
292
normal_name = normalize_name (name )
257
- resolver = self .resolver (normal_name )
258
- if resolver :
259
- lookup_function = getattr (resolver .functions , get )
260
- namehash = normal_name_to_hash (normal_name )
261
- address = lookup_function (namehash ).call ()
262
- if is_none_or_zero_address (address ):
263
- return None
264
- return address
265
- else :
266
- return None
267
293
268
- def resolver (self , normal_name : str ) -> Optional ['Contract' ]:
269
- resolver_addr = self .ens .caller .resolver (normal_name_to_hash (normal_name ))
270
- if is_none_or_zero_address (resolver_addr ):
294
+ resolver , current_name = self ._get_resolver (normal_name , fn_name )
295
+ if not resolver :
271
296
return None
272
- return self ._resolverContract (address = resolver_addr )
297
+
298
+ node = self .namehash (normal_name )
299
+
300
+ if _resolver_supports_interface (resolver , EXTENDED_RESOLVER_INTERFACE_ID ):
301
+ # update the resolver abi to the extended resolver abi
302
+ extended_resolver = self .w3 .eth .contract (abi = abis .EXTENDED_RESOLVER )(resolver .address )
303
+ contract_func_with_args = (fn_name , [node ])
304
+
305
+ calldata = extended_resolver .encodeABI (* contract_func_with_args )
306
+ contract_call_result = extended_resolver .caller .resolve (
307
+ ens_encode_name (normal_name ), calldata
308
+ )
309
+ result = self ._decode_ensip10_resolve_data (
310
+ contract_call_result , extended_resolver , fn_name
311
+ )
312
+ return to_checksum_address (result ) if is_address (result ) else result
313
+ elif normal_name == current_name :
314
+ lookup_function = getattr (resolver .functions , fn_name )
315
+ result = lookup_function (node ).call ()
316
+ if is_none_or_zero_address (result ):
317
+ return None
318
+ return to_checksum_address (result ) if is_address (result ) else result
319
+ return None
273
320
274
321
def reverser (self , target_address : ChecksumAddress ) -> Optional ['Contract' ]:
275
322
reversed_domain = address_to_reverse_domain (target_address )
@@ -295,38 +342,51 @@ def get_text(self, name: str, key: str) -> str:
295
342
296
343
:param str name: ENS name to look up
297
344
:param str key: ENS name's text record key
298
- :return: ENS name's text record value
345
+ :return: ENS name's text record value
299
346
:rtype: str
300
- :raises UnownedName: if no one owns `name`
347
+ :raises UnsupportedFunction: If the resolver does not support the "0x59d1d43c" interface id
348
+ :raises ResolverNotFound: If no resolver is found for the provided name
301
349
"""
302
350
node = raw_name_to_hash (name )
303
351
normal_name = normalize_name (name )
304
352
305
353
r = self .resolver (normal_name )
306
354
if r :
307
- return r .caller .text (node , key )
355
+ if _resolver_supports_interface (r , GET_TEXT_INTERFACE_ID ):
356
+ return r .caller .text (node , key )
357
+ else :
358
+ raise UnsupportedFunction (
359
+ f"Resolver for name { name } does not support `text` function."
360
+ )
308
361
else :
309
- raise UnownedName ("claim domain using setup_address() first" )
362
+ raise ResolverNotFound (
363
+ f"No resolver found for name `{ name } `. It is likely the name contains an "
364
+ "unsupported top level domain (tld)."
365
+ )
310
366
311
367
def set_text (
312
368
self ,
313
369
name : str ,
314
370
key : str ,
315
371
value : str ,
316
- transact : "TxParams" = {}
372
+ transact : "TxParams" = None
317
373
) -> HexBytes :
318
374
"""
319
375
Set the value of a text record of an ENS name.
320
376
321
377
:param str name: ENS name
322
378
:param str key: Name of the attribute to set
323
379
:param str value: Value to set the attribute to
324
- :param dict transact: the transaction configuration, like in
380
+ :param dict transact: The transaction configuration, like in
325
381
:meth:`~web3.eth.Eth.send_transaction`
326
382
:return: Transaction hash
327
383
:rtype: HexBytes
328
- :raises UnownedName: if no one owns `name`
384
+ :raises UnsupportedFunction: If the resolver does not support the "0x59d1d43c" interface id
385
+ :raises ResolverNotFound: If no resolver is found for the provided name
329
386
"""
387
+ if not transact :
388
+ transact = {}
389
+
330
390
owner = self .owner (name )
331
391
node = raw_name_to_hash (name )
332
392
normal_name = normalize_name (name )
@@ -335,16 +395,24 @@ def set_text(
335
395
336
396
r = self .resolver (normal_name )
337
397
if r :
338
- return r .functions .setText (node , key , value ).transact (transaction_dict )
398
+ if _resolver_supports_interface (r , GET_TEXT_INTERFACE_ID ):
399
+ return r .functions .setText (node , key , value ).transact (transaction_dict )
400
+ else :
401
+ raise UnsupportedFunction (
402
+ f"Resolver for name `{ name } ` does not support `text` function"
403
+ )
339
404
else :
340
- raise UnownedName ("claim domain using setup_address() first" )
405
+ raise ResolverNotFound (
406
+ f"No resolver found for name `{ name } `. It is likely the name contains an "
407
+ "unsupported top level domain (tld)."
408
+ )
341
409
342
410
def setup_owner (
343
411
self ,
344
412
name : str ,
345
413
new_owner : ChecksumAddress = cast (ChecksumAddress , default ),
346
414
transact : Optional ["TxParams" ] = None
347
- ) -> ChecksumAddress :
415
+ ) -> Optional [ ChecksumAddress ] :
348
416
"""
349
417
Set the owner of the supplied name to `new_owner`.
350
418
@@ -449,21 +517,61 @@ def _set_resolver(
449
517
namehash ,
450
518
resolver_addr
451
519
).transact (transact )
452
- return self ._resolverContract (address = resolver_addr )
520
+ return self ._resolver_contract (address = resolver_addr )
521
+
522
+ def _get_resolver (
523
+ self ,
524
+ normal_name : str ,
525
+ func : str = 'addr'
526
+ ) -> Tuple [Optional ['Contract' ], str ]:
527
+ current_name = normal_name
528
+
529
+ # look for a resolver, starting at the full name and taking the parent each time that no
530
+ # resolver is found
531
+ while True :
532
+ if is_empty_name (current_name ):
533
+ # if no resolver found across all iterations, current_name will eventually be the
534
+ # empty string '' which returns here
535
+ return None , current_name
536
+
537
+ resolver_addr = self .ens .caller .resolver (normal_name_to_hash (current_name ))
538
+ if not is_none_or_zero_address (resolver_addr ):
539
+ # if resolver found, return it
540
+ return self ._type_aware_resolver (resolver_addr , func ), current_name
541
+
542
+ # set current_name to parent and try again
543
+ current_name = self .parent (current_name )
544
+
545
+ def _decode_ensip10_resolve_data (
546
+ self , contract_call_result : bytes , extended_resolver : 'Contract' , fn_name : str ,
547
+ ) -> Any :
548
+ func = extended_resolver .get_function_by_name (fn_name )
549
+ output_types = get_abi_output_types (func .abi )
550
+ decoded = self .w3 .codec .decode_abi (output_types , contract_call_result )
551
+ return decoded [0 ] if len (decoded ) == 1 else decoded
453
552
454
553
def _setup_reverse (
455
554
self , name : str , address : ChecksumAddress , transact : Optional ["TxParams" ] = None
456
555
) -> HexBytes :
556
+ name = normalize_name (name ) if name else ''
457
557
if not transact :
458
558
transact = {}
459
559
transact = deepcopy (transact )
460
- if name :
461
- name = normalize_name (name )
462
- else :
463
- name = ''
464
560
transact ['from' ] = address
465
561
return self ._reverse_registrar ().functions .setName (name ).transact (transact )
466
562
563
+ def _type_aware_resolver (self , address : ChecksumAddress , func : str ) -> 'Contract' :
564
+ return (
565
+ self ._reverse_resolver_contract (address = address ) if func == 'name' else
566
+ self ._resolver_contract (address = address )
567
+ )
568
+
467
569
def _reverse_registrar (self ) -> 'Contract' :
468
570
addr = self .ens .caller .owner (normal_name_to_hash (REVERSE_REGISTRAR_DOMAIN ))
469
571
return self .w3 .eth .contract (address = addr , abi = abis .REVERSE_REGISTRAR )
572
+
573
+
574
+ def _resolver_supports_interface (resolver : 'Contract' , interface_id : HexStr ) -> bool :
575
+ if not any ('supportsInterface' in repr (func ) for func in resolver .all_functions ()):
576
+ return False
577
+ return resolver .caller .supportsInterface (interface_id )
0 commit comments