Skip to content

Commit 39c4c3a

Browse files
committed
Add ENSIP-10 support:
- Adds support for the extended resolver concept as defined by ENSIP-10. - Adds the method ``parent()`` to the ENS class whose function is to extract the proper parent from an ENS name. - Adds the ``ens_encode_name()`` method that uses DNS name-encoding standards with a few tweaks, such as skipping the fully-encoded length validation for the domain (limit of 255 for DNS) and encoding the empty names as defined by ENSIP-10 as a single zero byte ``b'\x00'``. - Adds an abstracted logic for decoding the raw result of a contract method as a ``decodeABI()`` method on the ``Contract`` class to facilitate implementation of ENSIP-10. Unrelated: - Minor cleanup for some existing ens tests
1 parent 2cd75fd commit 39c4c3a

12 files changed

+1900
-1546
lines changed

ens/abis.py

Lines changed: 1561 additions & 1448 deletions
Large diffs are not rendered by default.

ens/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@
1818
REVERSE_REGISTRAR_DOMAIN = 'addr.reverse'
1919

2020
ENS_MAINNET_ADDR = ChecksumAddress(HexAddress(HexStr('0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e')))
21+
22+
23+
# --- interface ids --- #
24+
25+
GET_TEXT_INTERFACE_ID = HexStr("0x59d1d43c")
26+
EXTENDED_RESOLVER_INTERFACE_ID = HexStr('0x9061b923') # ENSIP-10

ens/contract_data.py

Lines changed: 33 additions & 10 deletions
Large diffs are not rendered by default.

ens/exceptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ class UnownedName(Exception):
4040
pass
4141

4242

43+
class ResolverNotFound(Exception):
44+
"""
45+
Raised if no resolver was found for the name you are trying to resolve.
46+
"""
47+
pass
48+
49+
50+
class UnsupportedFunction(Exception):
51+
"""
52+
Raised if a resolver does not support a particular method.
53+
"""
54+
pass
55+
56+
4357
class BidTooLow(ValueError):
4458
"""
4559
Raised if you bid less than the minimum amount

ens/main.py

Lines changed: 131 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
Address,
1818
ChecksumAddress,
1919
HexAddress,
20+
HexStr,
2021
)
2122
from eth_utils import (
23+
is_address,
2224
is_binary_address,
2325
is_checksum_address,
2426
to_checksum_address,
@@ -34,18 +36,24 @@
3436
from ens.constants import (
3537
EMPTY_ADDR_HEX,
3638
ENS_MAINNET_ADDR,
39+
EXTENDED_RESOLVER_INTERFACE_ID,
40+
GET_TEXT_INTERFACE_ID,
3741
REVERSE_REGISTRAR_DOMAIN,
3842
)
3943
from ens.exceptions import (
4044
AddressMismatch,
45+
ResolverNotFound,
4146
UnauthorizedError,
4247
UnownedName,
48+
UnsupportedFunction,
4349
)
4450
from ens.utils import (
4551
address_in,
4652
address_to_reverse_domain,
4753
default,
54+
ens_encode_name,
4855
init_web3,
56+
is_empty_name,
4957
is_none_or_zero_address,
5058
is_valid_name,
5159
label_to_hash,
@@ -119,7 +127,8 @@ def __init__(
119127

120128
ens_addr = addr if addr else ENS_MAINNET_ADDR
121129
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)
123132

124133
@classmethod
125134
def fromWeb3(cls, w3: 'Web3', addr: ChecksumAddress = None) -> 'ENS':
@@ -152,18 +161,40 @@ def name(self, address: ChecksumAddress) -> Optional[str]:
152161
:type address: hex-string
153162
"""
154163
reversed_domain = address_to_reverse_domain(address)
155-
name = self.resolve(reversed_domain, get='name')
164+
name = self.resolve(reversed_domain, func='name')
156165

157166
# To be absolutely certain of the name, via reverse resolution, the address must match in
158167
# the forward resolution
159168
return name if to_checksum_address(address) == self.address(name) else None
160169

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+
161192
def setup_address(
162193
self,
163194
name: str,
164195
address: Union[Address, ChecksumAddress, HexAddress] = cast(ChecksumAddress, default),
165196
transact: Optional["TxParams"] = None
166-
) -> HexBytes:
197+
) -> Optional[HexBytes]:
167198
"""
168199
Set up the name to point to the supplied address.
169200
The sender of the transaction must own the name, or
@@ -252,24 +283,38 @@ def setup_name(
252283
self.setup_address(name, address, transact=transact)
253284
return self._setup_reverse(name, address, transact=transact)
254285

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]]:
256290
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
267291

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:
271294
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
273318

274319
def reverser(self, target_address: ChecksumAddress) -> Optional['Contract']:
275320
reversed_domain = address_to_reverse_domain(target_address)
@@ -295,38 +340,51 @@ def get_text(self, name: str, key: str) -> str:
295340
296341
:param str name: ENS name to look up
297342
: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
299344
: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
301347
"""
302348
node = raw_name_to_hash(name)
303349
normal_name = normalize_name(name)
304350

305351
r = self.resolver(normal_name)
306352
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+
)
308359
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+
)
310364

311365
def set_text(
312366
self,
313367
name: str,
314368
key: str,
315369
value: str,
316-
transact: "TxParams" = {}
370+
transact: "TxParams" = None
317371
) -> HexBytes:
318372
"""
319373
Set the value of a text record of an ENS name.
320374
321375
:param str name: ENS name
322376
:param str key: Name of the attribute to set
323377
: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
325379
:meth:`~web3.eth.Eth.send_transaction`
326380
:return: Transaction hash
327381
: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
329384
"""
385+
if not transact:
386+
transact = {}
387+
330388
owner = self.owner(name)
331389
node = raw_name_to_hash(name)
332390
normal_name = normalize_name(name)
@@ -335,16 +393,24 @@ def set_text(
335393

336394
r = self.resolver(normal_name)
337395
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+
)
339402
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+
)
341407

342408
def setup_owner(
343409
self,
344410
name: str,
345411
new_owner: ChecksumAddress = cast(ChecksumAddress, default),
346412
transact: Optional["TxParams"] = None
347-
) -> ChecksumAddress:
413+
) -> Optional[ChecksumAddress]:
348414
"""
349415
Set the owner of the supplied name to `new_owner`.
350416
@@ -449,21 +515,53 @@ def _set_resolver(
449515
namehash,
450516
resolver_addr
451517
).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)
453542

454543
def _setup_reverse(
455544
self, name: str, address: ChecksumAddress, transact: Optional["TxParams"] = None
456545
) -> HexBytes:
546+
name = normalize_name(name) if name else ''
457547
if not transact:
458548
transact = {}
459549
transact = deepcopy(transact)
460-
if name:
461-
name = normalize_name(name)
462-
else:
463-
name = ''
464550
transact['from'] = address
465551
return self._reverse_registrar().functions.setName(name).transact(transact)
466552

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+
467559
def _reverse_registrar(self) -> 'Contract':
468560
addr = self.ens.caller.owner(normal_name_to_hash(REVERSE_REGISTRAR_DOMAIN))
469561
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

Comments
 (0)