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

feat: custom networks work with explorer #2325

Merged
merged 6 commits into from
Oct 21, 2024
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
16 changes: 16 additions & 0 deletions src/ape/api/explorers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 13 additions & 7 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,8 +975,8 @@ def explorer(self) -> Optional["ExplorerAPI"]:
Returns:
:class:`ape.api.explorers.ExplorerAPI`, optional
"""

for plugin_name, plugin_tuple in self.plugin_manager.explorers:
chain_id = None if self.network_manager.active_provider is None else self.provider.chain_id
for plugin_name, plugin_tuple in self._plugin_explorers:
ecosystem_name, network_name, explorer_class = plugin_tuple

# Check for explicitly configured custom networks
Expand All @@ -987,17 +987,23 @@ def explorer(self) -> Optional["ExplorerAPI"]:
and self.name in plugin_config[self.ecosystem.name]
)

# Return the first registered explorer (skipping any others)
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

@property
def _plugin_explorers(self) -> list[tuple]:
# Abstracted for testing purposes.
return self.plugin_manager.explorers

@property
def is_mainnet(self) -> bool:
"""
Expand Down
30 changes: 30 additions & 0 deletions tests/functional/test_explorer.py
Original file line number Diff line number Diff line change
@@ -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)
63 changes: 63 additions & 0 deletions tests/functional/test_network_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy
from pathlib import Path
from unittest import mock

import pytest

Expand Down Expand Up @@ -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
Loading