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: add new chains supported by Alchemy #91

Merged
merged 6 commits into from
Dec 5, 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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,39 @@
Use the [Alchemy](https://alchemy.com/?r=jk3NDM0MTIwODIzM) provider plugin to interact with blockchains via APIs.
The `ape-alchemy` plugin supports the following ecosystems:

- Ethereum
- Abstract
- ApeChain
- Arbitrum
- Astar
- Avalanche
- Base
- Berachain
- Blast
- BSC (including opBNB)
- CrossFi
- Ethereum
- Fantom
- Flow EVM
- Geist
- Gnosis
- Lens
- Linea
- Lumia
- Mantle
- Metis
- Optimism
- Polygon
- Polygon-ZkEVM
- Rootstock
- Scroll
- Shape
- Soneium
- Unichain
- XMTP
- World-Chain
- zetachain
- ZkSync
- Zora

## Dependencies

Expand Down
96 changes: 94 additions & 2 deletions ape_alchemy/_utils.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,75 @@
NETWORKS = {
"ethereum": [
"abstract": [
"testnet",
],
"apechain": [
"mainnet",
"sepolia",
"curtis",
],
"arbitrum": [
"mainnet",
"nova",
"sepolia",
],
"astar": [
"mainnet",
],
"avalanche": [
"fuji",
"mainnet",
],
"base": [
"mainnet",
"sepolia",
],
"berachain": ["bartio"],
"blast": [
"mainnet",
"sepolia",
],
"bsc": ["mainnet", "testnet", "opbnb", "opbnb-testnet"],
"crossfi": [
"mainnet",
"testnet",
],
"ethereum": [
"mainnet",
"sepolia",
],
"fantom": [
"opera",
"testnet",
],
"flow-evm": [
"mainnet",
"testnet",
],
"geist": [
"mainnet",
"polter",
],
"gnosis": [
"mainnet",
"chiado",
],
"lens": [
"sepolia",
],
"linea": [
"mainnet",
"sepolia",
],
"lumia": [
"prism",
"testnet",
],
"mantle": [
"mainnet",
"sepolia",
],
"metis": [
"mainnet",
],
"optimism": [
"mainnet",
"sepolia",
Expand All @@ -27,4 +82,41 @@
"mainnet",
"cardona",
],
"rootstock": [
"mainnet",
"testnet",
],
"scroll": [
"mainnet",
"sepolia",
],
"shape": [
"mainnet",
"sepolia",
],
"soneium": [
"minato",
],
"unichain": [
"sepolia",
],
"xmtp": [
"sepolia",
],
"world-chain": [
"mainnet",
"sepolia",
],
"zetachain": [
"mainnet",
"testnet",
],
"zksync": [
"mainnet",
"sepolia",
],
"zora": [
"mainnet",
"sepolia",
],
}
84 changes: 62 additions & 22 deletions ape_alchemy/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@
from typing import TYPE_CHECKING, Any, Optional

from ape.api import ReceiptAPI, TraceAPI, TransactionAPI, UpstreamProvider
from ape.exceptions import (
APINotImplementedError,
ContractLogicError,
ProviderError,
VirtualMachineError,
)
from ape.exceptions import APINotImplementedError, ContractLogicError, VirtualMachineError
from ape.logging import logger
from ape_ethereum.provider import Web3Provider
from eth_pydantic_types import HexBytes
Expand Down Expand Up @@ -38,7 +33,29 @@
# Alchemy will try to publish private transactions for 25 blocks.
PRIVATE_TX_BLOCK_WAIT = 25

NETWORKS_SUPPORTING_WEBSOCKETS = ("ethereum", "arbitrum", "base", "optimism", "polygon", "fantom")
# NOTE: "*" means "all networks".
NETWORKS_SUPPORTING_WEBSOCKETS = {
"arbitrum": "*",
"avalanche": "*",
"base": "*",
"bsc": ("mainnet", "testnet"),
"berachain": "*",
"blast": "*",
"ethereum": "*",
"fantom": "*",
"geist": ("polter",),
"gnosis": "*",
"lens": "*",
"linea": "*",
"optimism": "*",
"polygon": "*",
"scroll": "*",
"shape": "*",
"soneium": "*",
"unichain": "*",
"world-chain": "*",
"zksync": "*",
}


class Alchemy(Web3Provider, UpstreamProvider):
Expand Down Expand Up @@ -80,24 +97,41 @@ def uri(self):
if not key:
raise MissingProjectKeyError(options)

network_formats_by_ecosystem = {
"ethereum": "https://eth-{0}.g.alchemy.com/v2/{1}",
"arbitrum": "https://arb-{0}.g.alchemy.com/v2/{1}",
"base": "https://base-{0}.g.alchemy.com/v2/{1}",
"optimism": "https://opt-{0}.g.alchemy.com/v2/{1}",
"polygon": "https://polygon-{0}.g.alchemy.com/v2/{1}",
"polygon-zkevm": "https://polygonzkevm-{0}.g.alchemy.com/v2/{1}",
"fantom": "https://fantom-{0}.g.alchemy.com/v2/{1}",
}

network_format = network_formats_by_ecosystem[ecosystem_name]
network_name = self.network.name

# NOTE: Fantom's mainnet is named "opera", but the Alchemy URI expects "mainnet".
if self.network.ecosystem.name == "fantom" and self.network.name == "opera":
network_name = "mainnet"

uri = network_format.format(network_name, key)
default_format = "{0}-{1}.g.alchemy.com/v2/{2}"
network_formats_by_ecosystem = {
"arbitrum": "arb-{0}.g.alchemy.com/v2/{1}",
"avalanche": "avax-{0}.g.alchemy.com/v2/{1}",
"bsc": "bnb-{0}.g.alchemy.com/v2/{1}",
"ethereum": "eth-{0}.g.alchemy.com/v2/{1}",
"flow-evm": "flow-{0}.g.alchemy.com/v2/{1}",
"fraxtal": "frax-{0}.g.alchemy.com/v2/{1}",
"optimism": "opt-{0}.g.alchemy.com/v2/{1}",
"polygon-zkevm": "polygonzkevm-{0}.g.alchemy.com/v2/{1}",
"world-chain": "worldchain-{0}.g.alchemy.com/v2/{1}",
}

if ecosystem_name in network_formats_by_ecosystem:
# Special cases.
if network_name == "nova":
uri = "arbnova-mainnet.g.alchemy.com/v2/{}".format(key)
elif network_name.startswith("opbnb"):
sub_network = "mainnet" if network_name == "opbnb" else "testnet"
uri = "opbnb-{0}.g.alchemy.com/v2/{1}".format(sub_network, key)
else:
network_format = network_formats_by_ecosystem[ecosystem_name]
uri = network_format.format(network_name, key)
elif ecosystem_name == "xmtp" and network_name == "sepolia":
uri = "xmtp-testnet.g.alchemy.com/v2/{0}".format(key)
else:
uri = default_format.format(ecosystem_name, network_name, key)

uri = f"https://{uri}"
self.network_uris[(ecosystem_name, network_name)] = uri
return uri

Expand All @@ -107,7 +141,13 @@ def http_uri(self) -> str:
return self.uri

@property
def ws_uri(self) -> str:
def ws_uri(self) -> Optional[str]:
ecosystem_name = self.network.ecosystem.name
network_name = self.network.name
supported_networks = NETWORKS_SUPPORTING_WEBSOCKETS.get(ecosystem_name, [])
if supported_networks != "*" and network_name not in supported_networks:
return None

# NOTE: Overriding `Web3Provider.ws_uri` implementation
return "ws" + self.uri[4:] # Remove `http` in default URI w/ `ws`

Expand Down Expand Up @@ -138,8 +178,8 @@ def connect(self):
is_poa = True

self._web3.eth.set_gas_price_strategy(rpc_gas_price_strategy)
except Exception as err:
raise ProviderError(f"Failed to connect to Alchemy.\n{repr(err)}") from err
except Exception:
is_poa = None

if is_poa is None:
# Check if is PoA but just wasn't as such yet.
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@
url="https://github.com/ApeWorX/ape-alchemy",
include_package_data=True,
install_requires=[
"eth-ape>=0.8.1,<0.9",
"eth-ape>=0.8.21,<0.9",
"eth-pydantic-types", # Use same version as eth-ape
"ethpm-types", # Use same version as eth-ape
"evm-trace", # Use same version as eth-ape
"evmchains>=0.1.3,<0.2", # Dependent on networks (not imports)
"web3", # Use same version as eth-ape
"requests",
],
Expand Down
9 changes: 6 additions & 3 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ape.utils import ZERO_ADDRESS

from ape_alchemy._utils import NETWORKS
from ape_alchemy.provider import NETWORKS_SUPPORTING_WEBSOCKETS, Alchemy
from ape_alchemy.provider import Alchemy


@pytest.fixture(params=[(name, net) for name, values in NETWORKS.items() for net in values])
Expand All @@ -19,13 +19,16 @@ def provider(request):
def test_http(provider):
assert isinstance(provider, Alchemy)
assert provider.http_uri.startswith("https")
assert provider.get_balance(ZERO_ADDRESS) > 0

# NOTE: Sometimes the balance is 0, for some chains.
assert provider.get_balance(ZERO_ADDRESS) is not None
assert provider.get_block(0)
assert provider.get_block("latest")


def test_ws(provider):
if provider.network.ecosystem.name not in NETWORKS_SUPPORTING_WEBSOCKETS:
ws_uri = provider.ws_uri
if ws_uri is None:
# Test will fail. Network does not support ws clients.
return

Expand Down
Loading