Skip to content

Commit 260cec2

Browse files
committed
Add ENSIP-10 support:
- Adds support for extended resolvers for wildcard resolution 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'``. Unrelated: - Minor cleanup for some existing ens tests
1 parent 2cd75fd commit 260cec2

File tree

11 files changed

+1901
-1541
lines changed

11 files changed

+1901
-1541
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: 141 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
)
77
from typing import (
88
TYPE_CHECKING,
9+
Any,
910
Optional,
1011
Sequence,
1112
Tuple,
@@ -17,8 +18,10 @@
1718
Address,
1819
ChecksumAddress,
1920
HexAddress,
21+
HexStr,
2022
)
2123
from eth_utils import (
24+
is_address,
2225
is_binary_address,
2326
is_checksum_address,
2427
to_checksum_address,
@@ -34,18 +37,25 @@
3437
from ens.constants import (
3538
EMPTY_ADDR_HEX,
3639
ENS_MAINNET_ADDR,
40+
EXTENDED_RESOLVER_INTERFACE_ID,
41+
GET_TEXT_INTERFACE_ID,
3742
REVERSE_REGISTRAR_DOMAIN,
3843
)
3944
from ens.exceptions import (
4045
AddressMismatch,
46+
ResolverNotFound,
4147
UnauthorizedError,
4248
UnownedName,
49+
UnsupportedFunction,
4350
)
4451
from ens.utils import (
4552
address_in,
4653
address_to_reverse_domain,
4754
default,
55+
ens_encode_name,
56+
get_abi_output_types,
4857
init_web3,
58+
is_empty_name,
4959
is_none_or_zero_address,
5060
is_valid_name,
5161
label_to_hash,
@@ -119,7 +129,8 @@ def __init__(
119129

120130
ens_addr = addr if addr else ENS_MAINNET_ADDR
121131
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)
123134

124135
@classmethod
125136
def fromWeb3(cls, w3: 'Web3', addr: ChecksumAddress = None) -> 'ENS':
@@ -152,18 +163,40 @@ def name(self, address: ChecksumAddress) -> Optional[str]:
152163
:type address: hex-string
153164
"""
154165
reversed_domain = address_to_reverse_domain(address)
155-
name = self.resolve(reversed_domain, get='name')
166+
name = self.resolve(reversed_domain, fn_name='name')
156167

157168
# To be absolutely certain of the name, via reverse resolution, the address must match in
158169
# the forward resolution
159170
return name if to_checksum_address(address) == self.address(name) else None
160171

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+
161194
def setup_address(
162195
self,
163196
name: str,
164197
address: Union[Address, ChecksumAddress, HexAddress] = cast(ChecksumAddress, default),
165198
transact: Optional["TxParams"] = None
166-
) -> HexBytes:
199+
) -> Optional[HexBytes]:
167200
"""
168201
Set up the name to point to the supplied address.
169202
The sender of the transaction must own the name, or
@@ -252,24 +285,38 @@ def setup_name(
252285
self.setup_address(name, address, transact=transact)
253286
return self._setup_reverse(name, address, transact=transact)
254287

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]]:
256292
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
267293

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:
271296
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
273320

274321
def reverser(self, target_address: ChecksumAddress) -> Optional['Contract']:
275322
reversed_domain = address_to_reverse_domain(target_address)
@@ -295,38 +342,51 @@ def get_text(self, name: str, key: str) -> str:
295342
296343
:param str name: ENS name to look up
297344
: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
299346
: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
301349
"""
302350
node = raw_name_to_hash(name)
303351
normal_name = normalize_name(name)
304352

305353
r = self.resolver(normal_name)
306354
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+
)
308361
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+
)
310366

311367
def set_text(
312368
self,
313369
name: str,
314370
key: str,
315371
value: str,
316-
transact: "TxParams" = {}
372+
transact: "TxParams" = None
317373
) -> HexBytes:
318374
"""
319375
Set the value of a text record of an ENS name.
320376
321377
:param str name: ENS name
322378
:param str key: Name of the attribute to set
323379
: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
325381
:meth:`~web3.eth.Eth.send_transaction`
326382
:return: Transaction hash
327383
: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
329386
"""
387+
if not transact:
388+
transact = {}
389+
330390
owner = self.owner(name)
331391
node = raw_name_to_hash(name)
332392
normal_name = normalize_name(name)
@@ -335,16 +395,24 @@ def set_text(
335395

336396
r = self.resolver(normal_name)
337397
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+
)
339404
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+
)
341409

342410
def setup_owner(
343411
self,
344412
name: str,
345413
new_owner: ChecksumAddress = cast(ChecksumAddress, default),
346414
transact: Optional["TxParams"] = None
347-
) -> ChecksumAddress:
415+
) -> Optional[ChecksumAddress]:
348416
"""
349417
Set the owner of the supplied name to `new_owner`.
350418
@@ -449,21 +517,61 @@ def _set_resolver(
449517
namehash,
450518
resolver_addr
451519
).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
453552

454553
def _setup_reverse(
455554
self, name: str, address: ChecksumAddress, transact: Optional["TxParams"] = None
456555
) -> HexBytes:
556+
name = normalize_name(name) if name else ''
457557
if not transact:
458558
transact = {}
459559
transact = deepcopy(transact)
460-
if name:
461-
name = normalize_name(name)
462-
else:
463-
name = ''
464560
transact['from'] = address
465561
return self._reverse_registrar().functions.setName(name).transact(transact)
466562

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

Comments
 (0)