Skip to content

Commit

Permalink
feat: custom networks work with explorer (#2325)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 21, 2024
1 parent ca8786a commit 6d2944e
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 7 deletions.
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

0 comments on commit 6d2944e

Please sign in to comment.