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: lookup network in evmchains; plugin-less networks, adhoc networks w/ correct name #2328

Merged
merged 5 commits into from
Nov 4, 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
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"
Loading