From e70280e95526d421dc3daaca73c4327b6fc76960 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Wed, 16 Oct 2024 14:57:57 -0500 Subject: [PATCH 1/5] feat: start --- src/ape/api/explorers.py | 18 +++++++++++++++++- src/ape/api/networks.py | 14 ++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/ape/api/explorers.py b/src/ape/api/explorers.py index df3c19e433..6eb58d5ceb 100644 --- a/src/ape/api/explorers.py +++ b/src/ape/api/explorers.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Union from ethpm_types import ContractType @@ -60,3 +60,19 @@ def publish_contract(self, address: AddressType): Args: address (:class:`~ape.types.address.AddressType`): The address of the deployed contract. """ + + @classmethod + def supports_chain(cls, chain_id: int) -> bool: + """ + Returns ``True`` when the given chain ID is claimed to be + supported by this explorer. Adhoc / custom networks rely on + this feature to have automatic-explorer support. Explorer + plugins should override this. + + Args: + chain_id (int): The chain ID to check. + + Returns: + bool + """ + return False diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 6a3f39f574..039ef7c8d4 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -975,7 +975,7 @@ def explorer(self) -> Optional["ExplorerAPI"]: Returns: :class:`ape.api.explorers.ExplorerAPI`, optional """ - + chain_id = None if self.network_manager.active_provider is None else self.provider.chain_id for plugin_name, plugin_tuple in self.plugin_manager.explorers: ecosystem_name, network_name, explorer_class = plugin_tuple @@ -987,14 +987,16 @@ def explorer(self) -> Optional["ExplorerAPI"]: and self.name in plugin_config[self.ecosystem.name] ) + # Return the first registered explorer (skipping any others) + # TODO: In 0.9, delete this and only use `supports_chain()` approach. if self.ecosystem.name == ecosystem_name and ( self.name == network_name or has_explorer_config ): - # Return the first registered explorer (skipping any others) - return explorer_class( - name=plugin_name, - network=self, - ) + return explorer_class(name=plugin_name, network=self) + + elif chain_id is not None and explorer_class.supports_chain(chain_id): + # NOTE: Adhoc networks will likely reach here. + return explorer_class(name=plugin_name, network=self) return None # May not have an block explorer From 6e16253a82d438cb001113e17db395da4385a399 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Thu, 17 Oct 2024 12:37:29 -0500 Subject: [PATCH 2/5] docs: del tot --- src/ape/api/networks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 039ef7c8d4..8101469295 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -988,7 +988,6 @@ def explorer(self) -> Optional["ExplorerAPI"]: ) # Return the first registered explorer (skipping any others) - # TODO: In 0.9, delete this and only use `supports_chain()` approach. if self.ecosystem.name == ecosystem_name and ( self.name == network_name or has_explorer_config ): From 85abb45e51bb8d33d50a8d56b937c986cecb254f Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Thu, 17 Oct 2024 12:58:32 -0500 Subject: [PATCH 3/5] chore: lint --- src/ape/api/explorers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ape/api/explorers.py b/src/ape/api/explorers.py index 6eb58d5ceb..f79fed6668 100644 --- a/src/ape/api/explorers.py +++ b/src/ape/api/explorers.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Optional from ethpm_types import ContractType From 26d5dec3a7c844813ddaeed580058428372fb22b Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Mon, 21 Oct 2024 09:29:17 -0500 Subject: [PATCH 4/5] test: add really dumb test --- tests/functional/test_explorer.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/functional/test_explorer.py diff --git a/tests/functional/test_explorer.py b/tests/functional/test_explorer.py new file mode 100644 index 0000000000..71a832110c --- /dev/null +++ b/tests/functional/test_explorer.py @@ -0,0 +1,30 @@ +from typing import Optional + +import pytest +from ethpm_types import ContractType + +from ape.api import ExplorerAPI +from ape.types import AddressType + + +class MyExplorer(ExplorerAPI): + def get_transaction_url(self, transaction_hash: str) -> str: + return "" + + def get_address_url(self, address: AddressType) -> str: + return "" + + def get_contract_type(self, address: AddressType) -> Optional[ContractType]: + return None + + def publish_contract(self, address: AddressType): + return + + +@pytest.fixture +def explorer(networks): + return MyExplorer(name="mine", network=networks.ethereum.local) + + +def test_supports_chain(explorer): + assert not explorer.supports_chain(1) From a8c221f4daff6ee5cafc8c41e6cf82cd19904cc3 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Mon, 21 Oct 2024 09:53:05 -0500 Subject: [PATCH 5/5] test: add smarter tests --- src/ape/api/networks.py | 7 +++- tests/functional/test_network_api.py | 63 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 8101469295..56d7644921 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -976,7 +976,7 @@ def explorer(self) -> Optional["ExplorerAPI"]: :class:`ape.api.explorers.ExplorerAPI`, optional """ chain_id = None if self.network_manager.active_provider is None else self.provider.chain_id - for plugin_name, plugin_tuple in self.plugin_manager.explorers: + for plugin_name, plugin_tuple in self._plugin_explorers: ecosystem_name, network_name, explorer_class = plugin_tuple # Check for explicitly configured custom networks @@ -999,6 +999,11 @@ def explorer(self) -> Optional["ExplorerAPI"]: return None # May not have an block explorer + @property + def _plugin_explorers(self) -> list[tuple]: + # Abstracted for testing purposes. + return self.plugin_manager.explorers + @property def is_mainnet(self) -> bool: """ diff --git a/tests/functional/test_network_api.py b/tests/functional/test_network_api.py index 7f8254fd08..7578a975fa 100644 --- a/tests/functional/test_network_api.py +++ b/tests/functional/test_network_api.py @@ -1,5 +1,6 @@ import copy from pathlib import Path +from unittest import mock import pytest @@ -286,3 +287,65 @@ def config(self): ecosystem = MyEcosystem() network = network_type(name=network_name, ecosystem=ecosystem) assert network.is_mainnet + + +def test_explorer(networks): + """ + Local network does not have an explorer, by default. + """ + network = networks.ethereum.local + network.__dict__.pop("explorer", None) # Ensure not cached yet. + assert network.explorer is None + + +def test_explorer_when_network_registered(networks, mocker): + """ + Tests the simple flow of having the Explorer plugin register + the networks it supports. + """ + network = networks.ethereum.local + network.__dict__.pop("explorer", None) # Ensure not cached yet. + name = "my-explorer" + + def explorer_cls(*args, **kwargs): + res = mocker.MagicMock() + res.name = name + return res + + mock_plugin_explorers = mocker.patch( + "ape.api.networks.NetworkAPI._plugin_explorers", new_callable=mock.PropertyMock + ) + mock_plugin_explorers.return_value = [("my-example", ("ethereum", "local", explorer_cls))] + assert network.explorer is not None + assert network.explorer.name == name + + +def test_explorer_when_adhoc_network_supported(networks, mocker): + """ + Tests the flow of when a chain is supported by an explorer + but not registered in the plugin (API-flow). + """ + network = networks.ethereum.local + network.__dict__.pop("explorer", None) # Ensure not cached yet. + NAME = "my-explorer" + + class MyExplorer: + name: str = NAME + + def __init__(self, *args, **kwargs): + pass + + @classmethod + def supports_chain(cls, chain_id): + return True + + mock_plugin_explorers = mocker.patch( + "ape.api.networks.NetworkAPI._plugin_explorers", new_callable=mock.PropertyMock + ) + + # NOTE: Ethereum is not registered at the plugin level, but is at the API level. + mock_plugin_explorers.return_value = [ + ("my-example", ("some-other-ecosystem", "local", MyExplorer)) + ] + assert network.explorer is not None + assert network.explorer.name == NAME