Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor ens and async_ens to call resolver only if coin_type is specified #3584

Merged
merged 2 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ens/async_ens.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,12 @@ async def address(
:param int coin_type: if provided, look up the address for this coin type
:raises InvalidName: if `name` has invalid syntax
"""
r = await self.resolver(name)
if coin_type is None:
# don't validate `addr(bytes32)` interface id since extended resolvers
# can implement a "resolve" function as of ENSIP-10
return cast(ChecksumAddress, await self._resolve(name, "addr"))
else:
r = await self.resolver(name)
await _async_validate_resolver_and_interface_id(
name, r, ENS_MULTICHAIN_ADDRESS_INTERFACE_ID, "addr(bytes32,uint256)"
)
Expand Down
2 changes: 1 addition & 1 deletion ens/ens.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ def address(
:raises UnsupportedFunction: if the resolver does not support the ``addr()``
function
"""
r = self.resolver(name)
if coin_type is None:
# don't validate `addr(bytes32)` interface id since extended resolvers
# can implement a "resolve" function as of ENSIP-10
return cast(ChecksumAddress, self._resolve(name, "addr"))
else:
r = self.resolver(name)
_validate_resolver_and_interface_id(
name, r, ENS_MULTICHAIN_ADDRESS_INTERFACE_ID, "addr(bytes32,uint256)"
)
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3584.performance.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid unnecessary extra call to resolver when resolving an ENS address with no ``coin_type`` specified (default).
101 changes: 101 additions & 0 deletions tests/ens/test_ens.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest
from unittest.mock import (
AsyncMock,
MagicMock,
patch,
)

Expand All @@ -10,6 +12,9 @@
from ens._normalization import (
normalize_name_ensip15,
)
from ens.utils import (
raw_name_to_hash,
)
from web3 import (
AsyncWeb3,
)
Expand Down Expand Up @@ -143,6 +148,51 @@ def test_ens_methods_normalize_name(
ens.setup_address("tester.eth", None)


def test_ens_address_lookup_when_no_coin_type(ens):
"""
Test that when coin_type is None, the method calls _resolve(name, "addr")
and returns the expected address.
"""
name = "tester.eth"
address = ens.w3.eth.accounts[0]

with patch("ens.ENS.resolver") as mock_resolver_for_coin_types:
with patch("ens.ENS._resolve") as mock_resolver:
mock_resolver.return_value = address

returned_address = ens.address(name)

mock_resolver.assert_called_once_with(name, "addr")
mock_resolver_for_coin_types.assert_not_called()
assert returned_address == address


def test_ens_address_lookup_with_coin_type(ens):
"""
Test that when coin_type is specified, it calls:
1) _validate_resolver_and_interface_id
2) resolver.caller.addr(node, coin_type)
3) returns the checksum address
"""
name = "tester.eth"
address = ens.w3.eth.accounts[0]
coin_type = 60
node = raw_name_to_hash(name)

mock_resolver = MagicMock()
mock_resolver.caller.addr.return_value = address

with patch("ens.ENS.resolver") as resolver:
resolver.return_value = mock_resolver

with patch("ens.ens._validate_resolver_and_interface_id") as mock_validate:
returned_address = ens.address(name, coin_type=coin_type)

mock_validate.assert_called_once()
mock_resolver.caller.addr.assert_called_once_with(node, coin_type)
assert returned_address == address


# -- async -- #


Expand Down Expand Up @@ -268,3 +318,54 @@ async def test_async_ens_methods_normalize_name_with_ensip15(

# cleanup
await async_ens.setup_address("tester.eth", None)


@pytest.mark.asyncio
async def test_async_ens_address_lookup_when_no_coin_type(async_ens):
"""
Test that when coin_type is None, the method calls _resolve(name, "addr")
and returns the expected address.
"""
name = "tester.eth"
accounts = await async_ens.w3.eth.accounts
address = accounts[2]

with patch("ens.AsyncENS.resolver") as mock_resolver_for_coin_types:
with patch("ens.AsyncENS._resolve") as mock_resolver:
mock_resolver.return_value = address

returned_address = await async_ens.address(name)

mock_resolver.assert_called_once_with(name, "addr")
mock_resolver_for_coin_types.assert_not_called()
assert returned_address == address


@pytest.mark.asyncio
async def test_async_ens_address_lookup_with_coin_type(async_ens):
"""
Test that when coin_type is specified, it calls:
1) _async_validate_resolver_and_interface_id
2) async resolver.caller.addr(node, coin_type)
3) returns the checksum address
"""
name = "tester.eth"
accounts = await async_ens.w3.eth.accounts
address = accounts[2]
coin_type = 60
node = raw_name_to_hash(name)

mock_resolver = AsyncMock()
mock_resolver.caller.addr.return_value = address

with patch("ens.AsyncENS.resolver") as resolver:
resolver.return_value = mock_resolver

with patch(
"ens.async_ens._async_validate_resolver_and_interface_id"
) as mock_validate:
returned_address = await async_ens.address(name, coin_type=coin_type)

mock_validate.assert_called_once()
mock_resolver.caller.addr.assert_called_once_with(node, coin_type)
assert returned_address == address