Skip to content

Commit 4fee040

Browse files
authored
feat: allow changing the registry address (#45)
1 parent a8094a2 commit 4fee040

8 files changed

Lines changed: 164 additions & 24 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,25 @@ Configuring entries is useful for:
165165
1. Testing in the `local` network.
166166
2. Attaining faster performance (no Ethereum call).
167167
3. Avoiding connecting to Ethereum mainnet.
168+
169+
### Change Registry
170+
171+
Change the registry contract address by configuring it in your `pyproject.toml`:
172+
173+
```toml
174+
[tool.ape.ens]
175+
registry_address = "0x123..."
176+
```
177+
178+
or using `ape-config.yaml`:
179+
180+
```yaml
181+
ens:
182+
registry_address: "0x123..."
183+
```
184+
185+
You can also switch the registry adhoc during CLI commands:
186+
187+
```shell
188+
ape ens resolve vitalik.eth --registry-address 0x123...311
189+
```

ape_ens/_cli.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import click
44
from ape.cli import ApeCliContextObject, ape_cli_context, network_option
5+
from ape.exceptions import ConversionError
6+
from ape.types.address import AddressType
57
from eth_utils import to_hex
68

79
from ape_ens.ens import ENS
@@ -25,15 +27,40 @@ def cli():
2527
"""
2628

2729

30+
def registry_address_option(**kwargs):
31+
if "callback" not in kwargs:
32+
33+
def validate_address(ctx, param, value):
34+
if value is None:
35+
return value
36+
37+
from ape import convert
38+
39+
try:
40+
return convert(value, AddressType)
41+
except ConversionError:
42+
raise click.BadOptionUsage(
43+
"--registry-address", f"Invalid `--registry-address` {value}."
44+
)
45+
46+
kwargs["callback"] = validate_address
47+
48+
if "help" not in kwargs:
49+
kwargs["help"] = "ENS registry address"
50+
51+
return click.option("--registry-address", **kwargs)
52+
53+
2854
@cli.command()
2955
@ape_cli_context(obj_type=ENSContext)
3056
@click.argument("name")
3157
@network_option(default=None)
32-
def resolve(cli_ctx, name):
58+
@registry_address_option()
59+
def resolve(cli_ctx, name, registry_address):
3360
"""
3461
Resolve an ENS address.
3562
"""
36-
if address := cli_ctx.ens.resolve(name):
63+
if address := cli_ctx.ens.resolve(name, registry_address=registry_address):
3764
click.echo(address)
3865
else:
3966
click.echo(f"Could not resolve ENS '{name}'.", err=True)
@@ -43,11 +70,12 @@ def resolve(cli_ctx, name):
4370
@ape_cli_context(obj_type=ENSContext)
4471
@click.argument("address")
4572
@network_option(default=None)
46-
def name_cmd(cli_ctx, address):
73+
@registry_address_option()
74+
def name_cmd(cli_ctx, address, registry_address):
4775
"""
4876
Get the ENS of an address.
4977
"""
50-
if name := cli_ctx.ens.name(address):
78+
if name := cli_ctx.ens.name(address, registry_address=registry_address):
5179
click.echo(name)
5280
else:
5381
click.echo(f"No ENS name found for '{address}'.", err=True)
@@ -57,11 +85,12 @@ def name_cmd(cli_ctx, address):
5785
@ape_cli_context(obj_type=ENSContext)
5886
@click.argument("name")
5987
@network_option(default=None)
60-
def owner(cli_ctx, name):
88+
@registry_address_option()
89+
def owner(cli_ctx, name, registry_address):
6190
"""
6291
Get the owner of an ENS domain.
6392
"""
64-
if owner_address := cli_ctx.ens.owner(name):
93+
if owner_address := cli_ctx.ens.owner(name, registry_address=registry_address):
6594
click.echo(owner_address)
6695
else:
6796
click.echo(f"No owner found for '{name}'.", err=True)

ape_ens/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from typing import Optional
2+
13
from ape.api import PluginConfig
4+
from ape.types import AddressType
25

36

47
class ENSConfig(PluginConfig):
@@ -19,3 +22,9 @@ class ENSConfig(PluginConfig):
1922
performance and in testing environments without access
2023
to Ethereum mainnet.
2124
"""
25+
26+
registry_address: Optional[AddressType] = None
27+
"""
28+
Configure the registry address if it different than the default
29+
Ethereum mainnet address.
30+
"""

ape_ens/converter.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import TYPE_CHECKING, Any
22

33
from ape.api import ConverterAPI
4+
from ape.exceptions import ConversionError
45

56
from ape_ens.ens import ENS
67

@@ -40,7 +41,13 @@ def is_convertible(self, value: Any) -> bool:
4041
if not isinstance(value, str):
4142
return False
4243

43-
return self.ens.can_resolve(value)
44+
try:
45+
return self.ens.can_resolve(value)
46+
except Exception:
47+
return False
4448

4549
def convert(self, value: str) -> "AddressType":
46-
return self.ens.resolve(value)
50+
try:
51+
return self.ens.resolve(value)
52+
except Exception as err:
53+
raise ConversionError(str(err)) from err

ape_ens/ens.py

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from ape.exceptions import ProviderError
55
from ape.logging import logger
66
from ape.utils.basemodel import ManagerAccessMixin
7-
from web3.exceptions import CannotHandleRequest
7+
from web3.exceptions import BadFunctionCallOutput, CannotHandleRequest, Web3RPCError
88
from web3.main import ENS as Web3ENS
99

10+
from ape_ens.exceptions import MissingRegistryError
1011
from ape_ens.utils.namehash import namehash
1112

1213
if TYPE_CHECKING:
@@ -37,7 +38,7 @@ class ENS(ManagerAccessMixin):
3738
"""
3839

3940
def __init__(self, backend: Optional["Web3ENS"] = None) -> None:
40-
self._ens = backend
41+
self.__initialized_ens = backend
4142
self.local_registry: dict[str, AddressType] = {}
4243

4344
@classmethod
@@ -112,16 +113,28 @@ def _get_mainnet_provider(self) -> "Web3Provider":
112113

113114
@cached_property
114115
def _web3_ens(self) -> "Web3ENS":
115-
if ens := self._ens:
116+
if ens := self.__initialized_ens:
116117
# Initialized with ENS (testing?)
117118
return ens
118119

119-
return self._mainnet_provider.web3.ens
120+
return self._create_web3_ens()
120121

121122
@property
122123
def config(self) -> "ENSConfig":
123124
return self.config_manager.ens
124125

126+
def _create_web3_ens(self, registry_address: Optional["AddressType"] = None) -> "Web3ENS":
127+
if registry_address:
128+
return Web3ENS.from_web3(self._mainnet_provider.web3, registry_address)
129+
130+
else:
131+
# Check config.
132+
if address := self.config.registry_address:
133+
return Web3ENS.from_web3(self._mainnet_provider.web3, address)
134+
135+
# Use default (most common).
136+
return self._mainnet_provider.web3.ens
137+
125138
def can_resolve(self, name: str) -> bool:
126139
"""
127140
Returns ``True`` when ENS can resolve the name.
@@ -144,17 +157,29 @@ def can_resolve(self, name: str) -> bool:
144157

145158
return address is not None
146159

147-
def resolve(self, name: str, use_cache: Optional[bool] = None) -> Optional["AddressType"]:
160+
def resolve(
161+
self,
162+
name: str,
163+
use_cache: Optional[bool] = None,
164+
registry_address: Optional["AddressType"] = None,
165+
) -> Optional["AddressType"]:
148166
"""
149167
Resolve an ENS name.
150168
151169
Args:
152170
name (str): The name to resolve.
153171
use_cache (bool): Set to ``False`` to not use the in-memory cache.
172+
registry_address (Optional[AddressType]): Optionally, change the registry
173+
address.
154174
155175
Returns:
156176
AddressType | None
157177
"""
178+
ens = (
179+
self._create_web3_ens(registry_address=registry_address)
180+
if registry_address
181+
else self._web3_ens
182+
)
158183
if use_cache is None:
159184
# Use default from config.
160185
use_cache = self.config.use_cache
@@ -168,37 +193,55 @@ def resolve(self, name: str, use_cache: Optional[bool] = None) -> Optional["Addr
168193
self.local_registry[name] = address
169194
return address
170195

171-
if address := self._web3_ens.address(name):
172-
if use_cache:
173-
self.local_registry[name] = address
196+
try:
197+
address = ens.address(name)
198+
except (Web3RPCError, BadFunctionCallOutput) as err:
199+
raise MissingRegistryError(str(err))
174200

175-
return address
201+
if use_cache and address is not None:
202+
self.local_registry[name] = address
176203

177-
return None
204+
return address
178205

179-
def name(self, address: "AddressType") -> Optional[str]:
206+
def name(
207+
self, address: "AddressType", registry_address: Optional["AddressType"] = None
208+
) -> Optional[str]:
180209
"""
181210
Reverse look-up an address to get the ENS name.
182211
183212
Args:
184213
address (AddressType): The address to resolve.
214+
registry_address (Optional[AddressType]): Optionally, change the registry.
185215
186216
Returns:
187217
str | None: The ENS name.
188218
"""
189-
return self._web3_ens.name(address)
190-
191-
def owner(self, name: str) -> Optional["AddressType"]:
219+
ens = (
220+
self._create_web3_ens(registry_address=registry_address)
221+
if registry_address
222+
else self._web3_ens
223+
)
224+
return ens.name(address)
225+
226+
def owner(
227+
self, name: str, registry_address: Optional["AddressType"] = None
228+
) -> Optional["AddressType"]:
192229
"""
193230
Get the owner of an ENS domain.
194231
195232
Args:
196233
name (str): The ENS name to check.
234+
registry_address (Optional[AddressType]): Optionally, change the registry.
197235
198236
Returns:
199237
AddressType | None
200238
"""
201-
return self._web3_ens.owner(name)
239+
ens = (
240+
self._create_web3_ens(registry_address=registry_address)
241+
if registry_address
242+
else self._web3_ens
243+
)
244+
return ens.owner(name)
202245

203246
def namehash(self, name: str) -> "HexBytes":
204247
"""

ape_ens/exceptions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from ape.exceptions import ApeException
2+
3+
4+
class ApeENSException(ApeException):
5+
"""
6+
Base class for exceptions in the `ape-ens` package.
7+
"""
8+
9+
10+
class MissingRegistryError(ApeENSException):
11+
"""
12+
Raised when ape-ens detect the registry is missing.
13+
"""

tests/test_cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ def runner():
1515
return CliRunner()
1616

1717

18-
def test_resolve(networks, runner):
18+
def test_resolve(runner):
1919
result = runner.invoke(cli, ["resolve", "vitalik.eth"])
2020
assert "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" in result.output, result.output
2121

2222

23+
def test_resolve_invalid_registry_address(runner):
24+
result = runner.invoke(cli, ["resolve", "vitalik.eth", "--registry-address", "asdf"])
25+
assert result.exit_code != 0
26+
27+
2328
def test_name(runner):
2429
result = runner.invoke(cli, ["name", "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"])
2530
assert "vitalik.eth" in result.output, result.output

tests/test_converter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
from ape.exceptions import ConversionError
23
from web3.exceptions import CannotHandleRequest
34

45
from tests.conftest import negative_tests
@@ -63,6 +64,17 @@ def test_convert_after_adding_to_local_registry(converter, vitalik, accounts):
6364
assert actual == dev_account.address
6465

6566

67+
def test_convert_change_registry_address(project, converter, vitalik, accounts):
68+
fake_registry = accounts[0].address
69+
ens = converter.ens
70+
converter._ens = None # Pretend this is the start of the session.
71+
with project.temp_config(ens={"registry_address": fake_registry}):
72+
with pytest.raises(ConversionError):
73+
_ = converter.convert("apepython.eth")
74+
75+
converter._ens = ens
76+
77+
6678
def test_address_cache(converter, address):
6779
converter.address_cache["test.eth"] = address
6880
assert converter.convert("test.eth") == address

0 commit comments

Comments
 (0)