Skip to content

Commit

Permalink
feat: lookup network in evmchains; plugin-less networks, adhoc networ…
Browse files Browse the repository at this point in the history
…ks w/ correct name (#2328)
  • Loading branch information
antazoey authored Nov 4, 2024
1 parent 75e0d8e commit f833e46
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 19 deletions.
21 changes: 18 additions & 3 deletions docs/userguides/networks.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Networks

When interacting with a blockchain, you will have to select an ecosystem (e.g. Ethereum, Arbitrum, or Fantom), a network (e.g. Mainnet or Sepolia) and a provider (e.g. Eth-Tester, Node (Geth), or Alchemy).
Networks are part of ecosystems and typically defined in plugins.
For example, the `ape-ethereum` plugin comes with Ape and can be used for handling EVM-like behavior.
The `ape-ethereum` ecosystem and network(s) plugin comes with Ape and can be used for handling EVM-like behavior.
Networks are part of ecosystems and typically defined in plugins or custom-network configurations.
However, Ape works out-of-the-box (in a limited way) with any network defined in the [evmchains](https://github.com/ApeWorX/evmchains) library.

## Selecting a Network

Expand All @@ -25,7 +26,7 @@ ape test --network ethereum:local:foundry
ape console --network arbitrum:testnet:alchemy # NOTICE: All networks, even from other ecosystems, use this.
```

To see all possible values for `--network`, run the command:
To see all networks that work with the `--network` flag (besides those _only_ defined in `evmchains`), run the command:

```shell
ape networks list
Expand Down Expand Up @@ -100,6 +101,20 @@ ape networks list

In the remainder of this guide, any example below using Ethereum, you can replace with an L2 ecosystem's name and network combination.

## evmchains Networks

If a network is in the [evmchains](https://github.com/ApeWorX/evmchains) library, it will work in Ape automatically, even without a plugin or any custom configuration for that network.

```shell
ape console --network moonbeam
```

This works because the `moonbeam` network data is available in the `evmchains` library, and Ape is able to look it up.

```{warning}
Support for networks from evm-chains alone may be limited and require additional configuration to work in production use-cases.
```

## Custom Network Connection

You can add custom networks to Ape without creating a plugin.
Expand Down
34 changes: 27 additions & 7 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from eth_pydantic_types import HexBytes
from eth_utils import keccak, to_int
from evmchains import PUBLIC_CHAIN_META
from pydantic import model_validator

from ape.exceptions import (
Expand Down Expand Up @@ -109,7 +110,7 @@ def data_folder(self) -> Path:
"""
return self.config_manager.DATA_FOLDER / self.name

@cached_property
@property
def custom_network(self) -> "NetworkAPI":
"""
A :class:`~ape.api.networks.NetworkAPI` for custom networks where the
Expand All @@ -125,13 +126,11 @@ def custom_network(self) -> "NetworkAPI":
if ethereum_class is None:
raise NetworkError("Core Ethereum plugin missing.")

request_header = self.config_manager.REQUEST_HEADER
init_kwargs = {"name": "ethereum", "request_header": request_header}
ethereum = ethereum_class(**init_kwargs) # type: ignore
init_kwargs = {"name": "ethereum"}
evm_ecosystem = ethereum_class(**init_kwargs) # type: ignore
return NetworkAPI(
name="custom",
ecosystem=ethereum,
request_header=request_header,
ecosystem=evm_ecosystem,
_default_provider="node",
_is_custom=True,
)
Expand Down Expand Up @@ -301,6 +300,11 @@ def networks(self) -> dict[str, "NetworkAPI"]:
network_api._is_custom = True
networks[net_name] = network_api

# Add any remaining networks from EVM chains here (but don't override).
# NOTE: Only applicable to EVM-based ecosystems, of course.
# Otherwise, this is a no-op.
networks = {**self._networks_from_evmchains, **networks}

return networks

@cached_property
Expand All @@ -311,6 +315,17 @@ def _networks_from_plugins(self) -> dict[str, "NetworkAPI"]:
if ecosystem_name == self.name
}

@cached_property
def _networks_from_evmchains(self) -> dict[str, "NetworkAPI"]:
# NOTE: Purposely exclude plugins here so we also prefer plugins.
return {
network_name: create_network_type(data["chainId"], data["chainId"])(
name=network_name, ecosystem=self
)
for network_name, data in PUBLIC_CHAIN_META.get(self.name, {}).items()
if network_name not in self._networks_from_plugins
}

def __post_init__(self):
if len(self.networks) == 0:
raise NetworkError("Must define at least one network in ecosystem")
Expand Down Expand Up @@ -1057,7 +1072,6 @@ def providers(self): # -> dict[str, Partial[ProviderAPI]]
Returns:
dict[str, partial[:class:`~ape.api.providers.ProviderAPI`]]
"""

from ape.plugins._utils import clean_plugin_name

providers = {}
Expand Down Expand Up @@ -1089,6 +1103,12 @@ def providers(self): # -> dict[str, Partial[ProviderAPI]]
network=self,
)

# Any EVM-chain works with node provider.
if "node" not in providers and self.name in self.ecosystem._networks_from_evmchains:
# NOTE: Arbitrarily using sepolia to access the Node class.
node_provider_cls = self.network_manager.ethereum.sepolia.get_provider("node").__class__
providers["node"] = partial(node_provider_cls, name="node", network=self)

return providers

def _get_plugin_providers(self):
Expand Down
33 changes: 26 additions & 7 deletions src/ape/managers/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from functools import cached_property
from typing import TYPE_CHECKING, Optional, Union

from evmchains import PUBLIC_CHAIN_META

from ape.api.networks import EcosystemAPI, NetworkAPI, ProviderContextManager
from ape.exceptions import EcosystemNotFoundError, NetworkError, NetworkNotFoundError
from ape.managers.base import BaseManager
Expand Down Expand Up @@ -53,7 +55,6 @@ def active_provider(self) -> Optional["ProviderAPI"]:
"""
The currently connected provider if one exists. Otherwise, returns ``None``.
"""

return self._active_provider

@active_provider.setter
Expand Down Expand Up @@ -164,7 +165,6 @@ def ecosystem_names(self) -> set[str]:
"""
The set of all ecosystem names in ``ape``.
"""

return set(self.ecosystems)

@property
Expand Down Expand Up @@ -236,7 +236,8 @@ def ecosystems(self) -> dict[str, EcosystemAPI]:

existing_cls = plugin_ecosystems[base_ecosystem_name]
ecosystem_cls = existing_cls.model_copy(
update={"name": ecosystem_name}, cache_clear=("_networks_from_plugins",)
update={"name": ecosystem_name},
cache_clear=("_networks_from_plugins", "_networks_from_evmchains"),
)
plugin_ecosystems[ecosystem_name] = ecosystem_cls

Expand Down Expand Up @@ -437,10 +438,29 @@ def get_ecosystem(self, ecosystem_name: str) -> EcosystemAPI:
:class:`~ape.api.networks.EcosystemAPI`
"""

if ecosystem_name not in self.ecosystem_names:
raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names)
if ecosystem_name in self.ecosystem_names:
return self.ecosystems[ecosystem_name]

return self.ecosystems[ecosystem_name]
elif ecosystem_name.lower().replace(" ", "-") in PUBLIC_CHAIN_META:
ecosystem_name = ecosystem_name.lower().replace(" ", "-")
symbol = None
for net in PUBLIC_CHAIN_META[ecosystem_name].values():
if not (native_currency := net.get("nativeCurrency")):
continue

if "symbol" not in native_currency:
continue

symbol = native_currency["symbol"]
break

symbol = symbol or "ETH"

# Is an EVM chain, can automatically make a class using evm-chains.
evm_class = self._plugin_ecosystems["ethereum"].__class__
return evm_class(name=ecosystem_name, fee_token_symbol=symbol)

raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names)

def get_provider_from_choice(
self,
Expand Down Expand Up @@ -548,7 +568,6 @@ def parse_network_choice(
Returns:
:class:`~api.api.networks.ProviderContextManager`
"""

provider = self.get_provider_from_choice(
network_choice=network_choice, provider_settings=provider_settings
)
Expand Down
1 change: 0 additions & 1 deletion src/ape/managers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1940,7 +1940,6 @@ def reconfigure(self, **overrides):

self._config_override = overrides
_ = self.config

self.account_manager.test_accounts.reset()

def extract_manifest(self) -> PackageManifest:
Expand Down
21 changes: 20 additions & 1 deletion src/ape_ethereum/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from eth_pydantic_types import HexBytes
from eth_typing import BlockNumber, HexStr
from eth_utils import add_0x_prefix, is_hex, to_hex
from evmchains import get_random_rpc
from evmchains import PUBLIC_CHAIN_META, get_random_rpc
from pydantic.dataclasses import dataclass
from requests import HTTPError
from web3 import HTTPProvider, IPCProvider, Web3
Expand Down Expand Up @@ -1524,9 +1524,16 @@ def _complete_connect(self):
for option in ("earliest", "latest"):
try:
block = self.web3.eth.get_block(option) # type: ignore[arg-type]

except ExtraDataLengthError:
is_likely_poa = True
break

except Exception:
# Some chains are "light" and we may not be able to detect
# if it need PoA middleware.
continue

else:
is_likely_poa = (
"proofOfAuthorityData" in block
Expand All @@ -1540,6 +1547,18 @@ def _complete_connect(self):

self.network.verify_chain_id(chain_id)

# Correct network name, if using custom-URL approach.
if self.network.name == "custom":
for ecosystem_name, network in PUBLIC_CHAIN_META.items():
for network_name, meta in network.items():
if "chainId" not in meta or meta["chainId"] != chain_id:
continue

# Network found.
self.network.name = network_name
self.network.ecosystem.name = ecosystem_name
break

def disconnect(self):
self._call_trace_approach = None
self._web3 = None
Expand Down
14 changes: 14 additions & 0 deletions tests/functional/geth/test_network_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,17 @@ def test_fork_upstream_provider(networks, mock_geth_sepolia, geth_provider, mock
geth_provider.provider_settings["uri"] = orig
else:
del geth_provider.provider_settings["uri"]


@geth_process_test
@pytest.mark.parametrize(
"connection_str", ("moonbeam:moonriver", "https://moonriver.api.onfinality.io/public")
)
def test_parse_network_choice_evmchains(networks, connection_str):
"""
Show we can (without having a plugin installed) connect to a network
that evm-chains knows about.
"""
with networks.parse_network_choice(connection_str) as moon_provider:
assert moon_provider.network.name == "moonriver"
assert moon_provider.network.ecosystem.name == "moonbeam"

0 comments on commit f833e46

Please sign in to comment.