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