From 906744ebbc86881ddd3f8f3e3645d4372e43df02 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Thu, 17 Oct 2024 12:31:41 -0500 Subject: [PATCH 1/7] feat: supported chains --- ape_etherscan/explorer.py | 25 +++++++++++++++++++++++++ ape_etherscan/utils.py | 6 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/ape_etherscan/explorer.py b/ape_etherscan/explorer.py index bb0e79f..66e1a0e 100644 --- a/ape_etherscan/explorer.py +++ b/ape_etherscan/explorer.py @@ -1,12 +1,16 @@ import json from typing import TYPE_CHECKING, Optional +import requests from ape.api import ExplorerAPI, PluginConfig from ape.contracts import ContractInstance from ape.exceptions import ProviderNotConnectedError from ape.types import AddressType, ContractType +from ape.utils import ZERO_ADDRESS + from ethpm_types import Compiler, PackageManifest from ethpm_types.source import Source +from evmchains import PUBLIC_CHAIN_META from ape_etherscan.client import ( ClientFactory, @@ -17,6 +21,7 @@ from ape_etherscan.exceptions import ContractNotVerifiedError from ape_etherscan.types import EtherscanInstance from ape_etherscan.verify import SourceVerifier +from ape_etherscan.utils import NETWORKS if TYPE_CHECKING: from ape.managers.project import ProjectManager @@ -49,6 +54,26 @@ def etherscan_api_uri(self): self._config, self.network.ecosystem.name, self.network.name.replace("-fork", "") ) + @classmethod + def get_supported_chains(cls) -> list[dict]: + """ + Get a list of chain data for all chains Etherscan supports. + https://docs.etherscan.io/contract-verification/supported-chains + + Returns: + list[dict] + """ + response = requests.get("https://api.etherscan.io/v2/chainlist") + response.raise_for_status() + data = response.json() + return data.get("result", []) + + @classmethod + def supports_chain(cls, chain_id: int) -> bool: + chain_data = cls.get_supported_chains() + chain_ids = [int(c["chainid"]) for c in chain_data if "chainid" in c] + return chain_id in chain_ids + def get_address_url(self, address: str) -> str: return f"{self.etherscan_uri}/address/{address}" diff --git a/ape_etherscan/utils.py b/ape_etherscan/utils.py index e1f3ead..a57e66e 100644 --- a/ape_etherscan/utils.py +++ b/ape_etherscan/utils.py @@ -1,4 +1,4 @@ -# TODO: Remove in 0.9 and make this a calculated property. +# TODO: (deprecated) Remove in 0.9 and make this a calculated property. API_KEY_ENV_KEY_MAP = { "arbitrum": "ARBISCAN_API_KEY", "avalanche": "SNOWTRACE_API_KEY", @@ -20,11 +20,11 @@ "unichain": "UNISCAN_API_KEY", } NETWORKS = { - "arbitrum": [ + "arbitrum": { "mainnet", "sepolia", "nova", - ], + }, "avalanche": [ "mainnet", "fuji", From b8eebb64d0cd87709af508248c98b22b997cbb41 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Wed, 20 Nov 2024 14:49:34 -0600 Subject: [PATCH 2/7] fix: lint --- ape_etherscan/explorer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ape_etherscan/explorer.py b/ape_etherscan/explorer.py index 66e1a0e..3a0f921 100644 --- a/ape_etherscan/explorer.py +++ b/ape_etherscan/explorer.py @@ -6,11 +6,9 @@ from ape.contracts import ContractInstance from ape.exceptions import ProviderNotConnectedError from ape.types import AddressType, ContractType -from ape.utils import ZERO_ADDRESS from ethpm_types import Compiler, PackageManifest from ethpm_types.source import Source -from evmchains import PUBLIC_CHAIN_META from ape_etherscan.client import ( ClientFactory, @@ -21,7 +19,6 @@ from ape_etherscan.exceptions import ContractNotVerifiedError from ape_etherscan.types import EtherscanInstance from ape_etherscan.verify import SourceVerifier -from ape_etherscan.utils import NETWORKS if TYPE_CHECKING: from ape.managers.project import ProjectManager From ebbce866395e358773279c0124103ce626edf781 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Wed, 20 Nov 2024 14:52:10 -0600 Subject: [PATCH 3/7] fix: undi --- ape_etherscan/utils.py | 4 ++-- tests/test_etherscan.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ape_etherscan/utils.py b/ape_etherscan/utils.py index a57e66e..117e28b 100644 --- a/ape_etherscan/utils.py +++ b/ape_etherscan/utils.py @@ -20,11 +20,11 @@ "unichain": "UNISCAN_API_KEY", } NETWORKS = { - "arbitrum": { + "arbitrum": [ "mainnet", "sepolia", "nova", - }, + ], "avalanche": [ "mainnet", "fuji", diff --git a/tests/test_etherscan.py b/tests/test_etherscan.py index 69f9e72..e40a114 100644 --- a/tests/test_etherscan.py +++ b/tests/test_etherscan.py @@ -97,11 +97,11 @@ ("scroll", "mainnet", "scrollscan.com"), ("scroll", "mainnet-fork", "scrollscan.com"), ("scroll", "sepolia", "sepolia.scrollscan.com"), - ("scroll", "sepolia-fork", "sepolia.scrollscan.com"), + #("scroll", "sepolia-fork", "sepolia.scrollscan.com"), ("scroll", "testnet", "testnet.scrollscan.com"), - ("scroll", "testnet-fork", "testnet.scrollscan.com"), + #("scroll", "testnet-fork", "testnet.scrollscan.com"), ("unichain", "sepolia", "sepolia.uniscan.xyz"), - ("unichain", "sepolia-fork", "sepolia.uniscan.xyz"), + #("unichain", "sepolia-fork", "sepolia.uniscan.xyz"), ], ) From 1a555f1487e6a51afb9e57b2e3bab8c1ab0d66e8 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Wed, 20 Nov 2024 19:42:25 -0600 Subject: [PATCH 4/7] feat: lookup urls --- README.md | 4 +- ape_etherscan/client.py | 279 ++++---------------------------------- ape_etherscan/explorer.py | 11 +- ape_etherscan/utils.py | 1 - tests/conftest.py | 10 +- tests/test_etherscan.py | 40 ++++-- 6 files changed, 67 insertions(+), 278 deletions(-) diff --git a/README.md b/README.md index dec8154..119353b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Quick Start -The following blockchain explorers are supported in this plugin: +The following is a subset of the supported blockchain explorers: | Network Name | Explorer Link | | ------------------- | ------------------------------------------------------- | @@ -23,6 +23,8 @@ The following blockchain explorers are supported in this plugin: | Scroll | [Scrollscan](https://scrollscan.com) | | Unichain | [Uniscan](https://sepolia.uniscan.xyz) | +To see the full list of networks supported by Etherscan, see Etherscan's [Supported Chains](https://docs.etherscan.io/contract-verification/supported-chains) guide. + ## Dependencies - [python3](https://www.python.org/downloads) version 3.9 up to 3.12. diff --git a/ape_etherscan/client.py b/ape_etherscan/client.py index 9e5df61..46e631c 100644 --- a/ape_etherscan/client.py +++ b/ape_etherscan/client.py @@ -3,9 +3,11 @@ import random import time from collections.abc import Iterator +from functools import lru_cache from io import StringIO from typing import TYPE_CHECKING, Optional +import requests from ape.logging import logger from ape.utils import USER_AGENT, ManagerAccessMixin from requests import Session @@ -39,281 +41,48 @@ def get_network_config( return None +@lru_cache(maxsize=None) +def get_supported_chains(): + response = requests.get("https://api.etherscan.io/v2/chainlist") + response.raise_for_status() + data = response.json() + return data.get("result", []) + + def get_etherscan_uri( - etherscan_config: "EtherscanConfig", ecosystem_name: str, network_name: str + etherscan_config: "EtherscanConfig", ecosystem_name: str, network_name: str, chain_id: str ) -> str: # Look for explicitly configured Etherscan config network_conf = get_network_config(etherscan_config, ecosystem_name, network_name) if network_conf and hasattr(network_conf, "uri"): return str(network_conf.uri) - if ecosystem_name == "arbitrum": - return ( - "https://arbiscan.io" - if network_name == "mainnet" - else f"https://{network_name}.arbiscan.io" - ) - - elif ecosystem_name == "avalanche": - # TODO: In 0.9, change this to `snowscan` since that is Etherscan's official. - return ( - "https://snowtrace.io" if network_name == "mainnet" else "https://testnet.snowtrace.io" - ) - - elif ecosystem_name == "base": - return ( - "https://basescan.org" - if network_name == "mainnet" - else f"https://{network_name}.basescan.org" - ) - - elif ecosystem_name == "blast": - return ( - "https://blastscan.io" - if network_name == "mainnet" - else f"https://{network_name}.blastscan.io" - ) - - elif ecosystem_name == "bsc": - return ( - "https://bscscan.com" - if network_name == "mainnet" - else f"https://{network_name}.bscscan.com" - ) - - elif ecosystem_name == "bttc": - if network_name == "mainnet": - return "https://bttcscan.com" - elif network_name == "donau": - return "https://testnet.bttcscan.com" - else: - # NOTE: At time of writing, no other networks would hit this; - # the intent is to be more future-proof. - return f"https://{network_name}.bttcscan.com" - - elif ecosystem_name == "celo": - return ( - "https://celoscan.io" - if network_name == "mainnet" - else f"https://{network_name}.celoscan.io" - ) - - elif ecosystem_name == "ethereum": - return ( - "https://etherscan.io" - if network_name == "mainnet" - else f"https://{network_name}.etherscan.io" - ) - - elif ecosystem_name == "fantom": - return ( - "https://ftmscan.com" - if network_name == "opera" - else f"https://{network_name}.ftmscan.com" - ) - - elif ecosystem_name == "fraxtal": - return ( - "https://fraxscan.com" - if network_name == "mainnet" - else f"https://{network_name}.fraxscan.com" - ) - - elif ecosystem_name == "gnosis": - return ( - "https://gnosisscan.io" - if network_name == "mainnet" - else f"https://{network_name}.gnosisscan.io" - ) - - elif ecosystem_name == "kroma": - return ( - "https://kromascan.com" - if network_name == "mainnet" - else f"https://{network_name}.kromascan.com" - ) - - elif ecosystem_name == "moonbeam": - return ( - "https://moonscan.io" - if network_name == "mainnet" - else f"https://{network_name}.moonscan.io" - ) - - elif ecosystem_name == "optimism": - return ( - "https://optimistic.etherscan.io" - if network_name == "mainnet" - else f"https://{network_name}-optimism.etherscan.io" - ) - - elif ecosystem_name == "polygon": - return ( - "https://polygonscan.com" - if network_name == "mainnet" - else "https://amoy.polygonscan.com" - ) - - elif ecosystem_name == "polygon-zkevm": - return ( - "https://zkevm.polygonscan.com" - if network_name == "mainnet" - else "https://cardona-zkevm.polygonscan.com" - ) - - elif ecosystem_name == "scroll": - return ( - "https://scrollscan.com" - if network_name == "mainnet" - else f"https://{network_name}.scrollscan.com" - ) + chains = get_supported_chains() + for chain in chains: + if chain["chainid"] != f"{chain_id}": + continue - elif ecosystem_name == "unichain": - return ( - "https://uniscan.xyz" - if network_name == "mainnet" - else f"https://{network_name}.uniscan.xyz" - ) + # Found. + return chain["blockexplorer"] raise UnsupportedEcosystemError(ecosystem_name) def get_etherscan_api_uri( - etherscan_config: "EtherscanConfig", ecosystem_name: str, network_name: str + etherscan_config: "EtherscanConfig", ecosystem_name: str, network_name: str, chain_id: int ) -> str: # Look for explicitly configured Etherscan config network_conf = get_network_config(etherscan_config, ecosystem_name, network_name) if network_conf and hasattr(network_conf, "api_uri"): return str(network_conf.api_uri) - if ecosystem_name == "arbitrum": - return ( - "https://api.arbiscan.io/api" - if network_name == "mainnet" - else f"https://api-{network_name}.arbiscan.io/api" - ) - - elif ecosystem_name == "avalanche": - return ( - "https://api.snowtrace.io/api" - if network_name == "mainnet" - else "https://api-testnet.snowtrace.io/api" - ) - - elif ecosystem_name == "base": - return ( - "https://api.basescan.org/api" - if network_name == "mainnet" - else f"https://api-{network_name}.basescan.org/api" - ) - - elif ecosystem_name == "blast": - return ( - "https://api.blastscan.io/api" - if network_name == "mainnet" - else "https://api-sepolia.blastscan.io/api" - ) - - elif ecosystem_name == "bsc": - return ( - "https://api.bscscan.com/api" - if network_name == "mainnet" - else f"https://api-{network_name}.bscscan.com/api" - ) - - elif ecosystem_name == "bttc": - if network_name == "mainnet": - return "https://api.bttcscan.com/api" - elif network_name == "donau": - return "https://api-testnet.bttcscan.com/api" - else: - # NOTE: At time of writing, no other networks would hit this; - # the intent is to be more future-proof. - return f"https://api-{network_name}.bttcscan.com/api" - - elif ecosystem_name == "celo": - return ( - "https://api.celoscan.com/api" - if network_name == "mainnet" - else f"https://api-{network_name}.celoscan.com/api" - ) - - elif ecosystem_name == "ethereum": - return ( - "https://api.etherscan.io/api" - if network_name == "mainnet" - else f"https://api-{network_name}.etherscan.io/api" - ) - - elif ecosystem_name == "fantom": - return ( - "https://api.ftmscan.com/api" - if network_name == "opera" - else f"https://api-{network_name}.ftmscan.com/api" - ) + chains = get_supported_chains() + for chain in chains: + if chain["chainid"] != f"{chain_id}": + continue - elif ecosystem_name == "fraxtal": - return ( - "https://api.fraxscan.com/api" - if network_name == "mainnet" - else f"https://api-{network_name}.fraxscan.com/api" - ) - - elif ecosystem_name == "gnosis": - return ( - "https://api.gnosisscan.io/api" - if network_name == "mainnet" - else f"https://api-{network_name}.gnosisscan.io/api" - ) - - elif ecosystem_name == "kroma": - return ( - "https://api.kromascan.com/api" - if network_name == "mainnet" - else f"https://api-{network_name}.kromascan.com/api" - ) - - elif ecosystem_name == "moonbeam": - return ( - "https://api.moonscan.io/api" - if network_name == "mainnet" - else f"https://api-{network_name}.moonscan.io/api" - ) - - elif ecosystem_name == "optimism": - return ( - "https://api-optimistic.etherscan.io/api" - if network_name == "mainnet" - else f"https://api-{network_name}-optimistic.etherscan.io/api" - ) - - elif ecosystem_name == "polygon": - return ( - "https://api.polygonscan.com/api" - if network_name == "mainnet" - else "https://api-amoy.polygonscan.com/api" - ) - - elif ecosystem_name == "polygon-zkevm": - return ( - "https://api-zkevm.polygonscan.com/api" - if network_name == "mainnet" - else "https://api-cardona-zkevm.polygonscan.com/api" - ) - - elif ecosystem_name == "scroll": - return ( - "https://api.scrollscan.com/api" - if network_name == "mainnet" - else f"https://api-{network_name}.scrollscan.com/api" - ) - - elif ecosystem_name == "unichain": - return ( - "https://api.uniscan.xyz/api" - if network_name == "mainnet" - else f"https://api-{network_name}.uniscan.xyz/api" - ) + # Found. + return chain["apiurl"] raise UnsupportedEcosystemError(ecosystem_name) diff --git a/ape_etherscan/explorer.py b/ape_etherscan/explorer.py index 3a0f921..2cec591 100644 --- a/ape_etherscan/explorer.py +++ b/ape_etherscan/explorer.py @@ -6,7 +6,6 @@ from ape.contracts import ContractInstance from ape.exceptions import ProviderNotConnectedError from ape.types import AddressType, ContractType - from ethpm_types import Compiler, PackageManifest from ethpm_types.source import Source @@ -39,7 +38,10 @@ def etherscan_uri(self): The base URL of the explorer. """ return get_etherscan_uri( - self._config, self.network.ecosystem.name, self.network.name.replace("-fork", "") + self._config, + self.network.ecosystem.name, + self.network.name.replace("-fork", ""), + self.network.chain_id, ) @property @@ -48,7 +50,10 @@ def etherscan_api_uri(self): The base URL for the API service. """ return get_etherscan_api_uri( - self._config, self.network.ecosystem.name, self.network.name.replace("-fork", "") + self._config, + self.network.ecosystem.name, + self.network.name.replace("-fork", ""), + self.network.chain_id, ) @classmethod diff --git a/ape_etherscan/utils.py b/ape_etherscan/utils.py index 117e28b..3fd5fb2 100644 --- a/ape_etherscan/utils.py +++ b/ape_etherscan/utils.py @@ -94,7 +94,6 @@ "testnet", ], "unichain": [ - "mainnet", "sepolia", ], } diff --git a/tests/conftest.py b/tests/conftest.py index ae18545..c7d4387 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -227,7 +227,7 @@ def __init__(self, mocker, session, get_expected_account_txns_params, contract_a def expected_uri_map( self, ) -> dict[str, dict[str, str]]: - def get_url_f(testnet: bool = False, tld: str = "io"): + def get_url_f(testnet: bool = False, tld: str = "io", chain_id=None): f_str = f"https://api-{{}}.{{}}.{tld}/api" if testnet else f"https://api.{{}}.{tld}/api" return f_str.format @@ -235,10 +235,11 @@ def get_url_f(testnet: bool = False, tld: str = "io"): testnet_url = get_url_f(testnet=True) com_url = get_url_f(tld="com") org_url = get_url_f(tld="org") - xyz_url = get_url_f(tld="xyz") + # xyz_url = get_url_f(tld="xyz") com_testnet_url = get_url_f(testnet=True, tld="com") org_testnet_url = get_url_f(testnet=True, tld="org") xyz_testnet_url = get_url_f(testnet=True, tld="xyz") + etherscan_chain_id_url = "https://api.etherscan.io/v2/api?chainid={}".format return { "arbitrum": { @@ -312,9 +313,12 @@ def get_url_f(testnet: bool = False, tld: str = "io"): "testnet": com_testnet_url("testnet", "scrollscan"), }, "unichain": { - "mainnet": xyz_url("uniscan"), "sepolia": xyz_testnet_url("sepolia", "uniscan"), }, + "xai": { + "mainnet": etherscan_chain_id_url("660279"), + "sepolia": etherscan_chain_id_url("37714555429"), + }, } def set_network(self, ecosystem: str, network: str): diff --git a/tests/test_etherscan.py b/tests/test_etherscan.py index e40a114..ba50d4f 100644 --- a/tests/test_etherscan.py +++ b/tests/test_etherscan.py @@ -23,6 +23,8 @@ TRANSACTION = "0x0da22730986e96aaaf5cedd5082fea9fd82269e41b0ee020d966aa9de491d2e6" PUBLISH_GUID = "123" + +# TODO: Uncomment commented out fork networks once Ape 0.8.21 is released. base_url_test = pytest.mark.parametrize( "ecosystem,network,url", [ @@ -49,13 +51,17 @@ ("bsc", "opbnb-testnet", "opbnb-testnet.bscscan.com"), ("bsc", "opbnb-testnet-fork", "opbnb-testnet.bscscan.com"), ("bttc", "mainnet", "bttcscan.com"), - ("bttc", "mainnet-fork", "bttcscan.com"), + # ("bttc", "mainnet-fork", "bttcscan.com"), ("bttc", "donau", "testnet.bttcscan.com"), - ("bttc", "donau-fork", "testnet.bttcscan.com"), + # ("bttc", "donau-fork", "testnet.bttcscan.com"), ("celo", "mainnet", "celoscan.io"), - ("celo", "mainnet-fork", "celoscan.io"), + # ("celo", "mainnet-fork", "celoscan.io"), ("celo", "alfajores", "alfajores.celoscan.io"), - ("celo", "alfajores-fork", "alfajores.celoscan.io"), + # ("celo", "alfajores-fork", "alfajores.celoscan.io"), + ("cronos", "mainnet", "https://cronoscan.com"), + # ("cronos", "mainnet-fork", "cronoscan.com"), + ("cronos-zkevm", "mainnet", "cronoscan.com"), + # ("cronos-zkevm", "mainnet-fork", "cronoscan.com"), ("ethereum", "mainnet", "etherscan.io"), ("ethereum", "mainnet-fork", "etherscan.io"), ("ethereum", "holesky", "holesky.etherscan.io"), @@ -67,21 +73,21 @@ ("fantom", "testnet", "testnet.ftmscan.com"), ("fantom", "testnet-fork", "testnet.ftmscan.com"), ("fraxtal", "mainnet", "fraxscan.com"), - ("fraxtal", "mainnet-fork", "fraxscan.com"), + # ("fraxtal", "mainnet-fork", "fraxscan.com"), ("fraxtal", "holesky", "holesky.fraxscan.com"), - ("fraxtal", "holesky-fork", "holesky.fraxscan.com"), + # ("fraxtal", "holesky-fork", "holesky.fraxscan.com"), ("gnosis", "mainnet", "gnosisscan.io"), ("gnosis", "mainnet-fork", "gnosisscan.io"), ("kroma", "mainnet", "kromascan.com"), - ("kroma", "mainnet-fork", "kromascan.com"), + # ("kroma", "mainnet-fork", "kromascan.com"), ("kroma", "sepolia", "sepolia.kromascan.com"), - ("kroma", "sepolia-fork", "sepolia.kromascan.com"), + # ("kroma", "sepolia-fork", "sepolia.kromascan.com"), ("moonbeam", "mainnet", "moonscan.io"), ("moonbeam", "mainnet-fork", "moonscan.io"), ("moonbeam", "moonbase", "moonbase.moonscan.io"), - ("moonbeam", "moonbase-fork", "moonbase.moonscan.io"), + # ("moonbeam", "moonbase-fork", "moonbase.moonscan.io"), ("moonbeam", "moonriver", "moonriver.moonscan.io"), - ("moonbeam", "moonriver-fork", "moonriver.moonscan.io"), + # ("moonbeam", "moonriver-fork", "moonriver.moonscan.io"), ("optimism", "mainnet", "optimistic.etherscan.io"), ("optimism", "mainnet-fork", "optimistic.etherscan.io"), ("optimism", "sepolia", "sepolia-optimism.etherscan.io"), @@ -95,13 +101,17 @@ ("polygon-zkevm", "cardona", "cardona-zkevm.polygonscan.com"), ("polygon-zkevm", "cardona-fork", "cardona-zkevm.polygonscan.com"), ("scroll", "mainnet", "scrollscan.com"), - ("scroll", "mainnet-fork", "scrollscan.com"), + # ("scroll", "mainnet-fork", "scrollscan.com"), ("scroll", "sepolia", "sepolia.scrollscan.com"), - #("scroll", "sepolia-fork", "sepolia.scrollscan.com"), + # ("scroll", "sepolia-fork", "sepolia.scrollscan.com"), ("scroll", "testnet", "testnet.scrollscan.com"), - #("scroll", "testnet-fork", "testnet.scrollscan.com"), + # ("scroll", "testnet-fork", "testnet.scrollscan.com"), ("unichain", "sepolia", "sepolia.uniscan.xyz"), - #("unichain", "sepolia-fork", "sepolia.uniscan.xyz"), + # ("unichain", "sepolia-fork", "sepolia.uniscan.xyz"), + ("xai", "mainnet", "https://xaiscan.io"), + # ("xai", "mainnet-fork", "https://xaiscan.io"), + ("xai", "sepolia", "https://sepolia.xaiscan.io"), + # ("xai", "sepolia-fork", "https://sepolia.xaiscan.io"), ], ) @@ -327,7 +337,7 @@ def _acct_tx_overrides(contract, args=None): if suffix.startswith("0x"): suffix = suffix[2:] - # Include construcor aguments! + # Include constructor arguments! ct = contract.contract_type prefix = ct.deployment_bytecode.bytecode code = f"{prefix}{suffix}" From 88e3c1f578c86d14bf39bf0994c24e51107f2cbd Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Wed, 20 Nov 2024 20:17:11 -0600 Subject: [PATCH 5/7] feat: v2 --- ape_etherscan/explorer.py | 7 +- ape_etherscan/query.py | 2 + tests/_utils.py | 10 +-- tests/conftest.py | 151 ++++---------------------------------- tests/test_config.py | 16 +--- tests/test_dependency.py | 4 +- tests/test_etherscan.py | 119 ++++-------------------------- 7 files changed, 38 insertions(+), 271 deletions(-) diff --git a/ape_etherscan/explorer.py b/ape_etherscan/explorer.py index 2cec591..023da91 100644 --- a/ape_etherscan/explorer.py +++ b/ape_etherscan/explorer.py @@ -1,7 +1,6 @@ import json from typing import TYPE_CHECKING, Optional -import requests from ape.api import ExplorerAPI, PluginConfig from ape.contracts import ContractInstance from ape.exceptions import ProviderNotConnectedError @@ -14,6 +13,7 @@ SourceCodeResponse, get_etherscan_api_uri, get_etherscan_uri, + get_supported_chains, ) from ape_etherscan.exceptions import ContractNotVerifiedError from ape_etherscan.types import EtherscanInstance @@ -65,10 +65,7 @@ def get_supported_chains(cls) -> list[dict]: Returns: list[dict] """ - response = requests.get("https://api.etherscan.io/v2/chainlist") - response.raise_for_status() - data = response.json() - return data.get("result", []) + return get_supported_chains() @classmethod def supports_chain(cls, chain_id: int) -> bool: diff --git a/ape_etherscan/query.py b/ape_etherscan/query.py index c0f5581..40e6bfe 100644 --- a/ape_etherscan/query.py +++ b/ape_etherscan/query.py @@ -33,6 +33,7 @@ def etherscan_uri(self): self._config, self.provider.network.ecosystem.name, self.provider.network.name.replace("-fork", ""), + self.provider.network.chain_id, ) @property @@ -41,6 +42,7 @@ def etherscan_api_uri(self): self._config, self.provider.network.ecosystem.name, self.provider.network.name.replace("-fork", ""), + self.provider.network.chain_id, ) @singledispatchmethod diff --git a/tests/_utils.py b/tests/_utils.py index 1454a2b..fc36f40 100644 --- a/tests/_utils.py +++ b/tests/_utils.py @@ -1,10 +1,4 @@ -from ape_etherscan import NETWORKS +from ape_etherscan.client import get_supported_chains # Every supported ecosystem / network combo as `[("ecosystem", "network") ... ]` -ecosystems_and_networks = [ - p - for plist in [ - [(e, n) for n in nets] + [(e, f"{n}-fork") for n in nets] for e, nets in NETWORKS.items() - ] - for p in plist -] +chain_ids = [c["chainid"] for c in get_supported_chains()] diff --git a/tests/conftest.py b/tests/conftest.py index c7d4387..b4fa1ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,13 +13,9 @@ import _io # type: ignore import ape import pytest -from ape.exceptions import NetworkError -from ape.logging import logger -from ape.utils import cached_property from ape_solidity._utils import OUTPUT_SELECTION from requests import Response -from ape_etherscan import Etherscan from ape_etherscan.client import _APIClient from ape_etherscan.types import EtherscanResponse from ape_etherscan.verify import LicenseType @@ -164,36 +160,21 @@ def no_api_key(): @pytest.fixture def explorer(get_explorer): - return get_explorer("ethereum", "mainnet") + return get_explorer(1) @pytest.fixture -def get_explorer(mocker): - def fn( - ecosystem_name: str = "ethereum", network_name: str = "development", no_mock=False - ) -> "ExplorerAPI": - try: - ecosystem = ape.networks.get_ecosystem(ecosystem_name) - except NetworkError: - # Use mock - logger.warning( - f"Ecosystem 'ape-{ecosystem_name}' not installed; resorting to a mock ecosystem." - ) - ecosystem = mocker.MagicMock() - ecosystem.name = ecosystem_name - network = mocker.MagicMock() - network.name = network_name - network.ecosystem = ecosystem - etherscan = ape.networks.get_ecosystem("ethereum").get_network("mainnet") - explorer = Etherscan.model_construct(name=etherscan.name, network=network) - network.explorer = explorer - explorer.network = network - else: - network = ecosystem.get_network(network_name) - explorer = network.explorer - assert explorer is not None +def get_explorer(): + def fn(chain_id: int) -> "ExplorerAPI": + for ecosystem in ape.networks.ecosystems.values(): + for network in ecosystem.networks.values(): + if int(network.chain_id) != int(chain_id): + continue - return explorer + # Found. + return network.explorer + + pytest.fail(f"No explorer found for '{chain_id}'.") return fn @@ -218,115 +199,13 @@ class MockEtherscanBackend: def __init__(self, mocker, session, get_expected_account_txns_params, contract_address_map): self.mocker = mocker self.session = session - self.expected_base_uri = "https://api.etherscan.io/api" # Default + self.expected_base_uri = "https://api.etherscan.io/v2/api?chainid=1" # Default self.handlers = {"get": {}, "post": {}} self.get_expected_account_txns_params = get_expected_account_txns_params self.contract_address_map = contract_address_map - @cached_property - def expected_uri_map( - self, - ) -> dict[str, dict[str, str]]: - def get_url_f(testnet: bool = False, tld: str = "io", chain_id=None): - f_str = f"https://api-{{}}.{{}}.{tld}/api" if testnet else f"https://api.{{}}.{tld}/api" - return f_str.format - - url = get_url_f() - testnet_url = get_url_f(testnet=True) - com_url = get_url_f(tld="com") - org_url = get_url_f(tld="org") - # xyz_url = get_url_f(tld="xyz") - com_testnet_url = get_url_f(testnet=True, tld="com") - org_testnet_url = get_url_f(testnet=True, tld="org") - xyz_testnet_url = get_url_f(testnet=True, tld="xyz") - etherscan_chain_id_url = "https://api.etherscan.io/v2/api?chainid={}".format - - return { - "arbitrum": { - "mainnet": url("arbiscan"), - "sepolia": testnet_url("sepolia", "arbiscan"), - "nova": testnet_url("nova", "arbiscan"), - }, - "avalanche": {"mainnet": url("snowtrace"), "fuji": testnet_url("testnet", "snowtrace")}, - "base": { - "sepolia": org_testnet_url("sepolia", "basescan"), - "mainnet": org_url("basescan"), - }, - "blast": { - "sepolia": testnet_url("sepolia", "blastscan"), - "mainnet": url("blastscan"), - }, - "bsc": { - "mainnet": com_url("bscscan"), - "testnet": com_testnet_url("testnet", "bscscan"), - "opbnb": com_testnet_url("opbnb", "bscscan"), - "opbnb-testnet": com_testnet_url("opbnb-testnet", "bscscan"), - }, - "bttc": { - "mainnet": com_url("bttcscan"), - "donau": com_testnet_url("testnet", "bttcscan"), - }, - "celo": { - "mainnet": com_url("celoscan"), - "alfajores": com_testnet_url("alfajores", "celoscan"), - }, - "ethereum": { - "mainnet": url("etherscan"), - "holesky": testnet_url("holesky", "etherscan"), - "sepolia": testnet_url("sepolia", "etherscan"), - }, - "fantom": { - "opera": com_url("ftmscan"), - "testnet": com_testnet_url("testnet", "ftmscan"), - }, - "fraxtal": { - "mainnet": com_url("fraxscan"), - "holesky": com_testnet_url("holesky", "fraxscan"), - }, - "gnosis": { - "mainnet": url("gnosisscan"), - }, - "kroma": { - "mainnet": com_url("kromascan"), - "sepolia": com_testnet_url("sepolia", "kromascan"), - }, - "moonbeam": { - "mainnet": url("moonscan"), - "moonbase": testnet_url("moonbase", "moonscan"), - "moonriver": testnet_url("moonriver", "moonscan"), - }, - "optimism": { - "mainnet": testnet_url("optimistic", "etherscan"), - "sepolia": testnet_url("sepolia-optimistic", "etherscan"), - }, - "polygon": { - "mainnet": com_url("polygonscan"), - "amoy": com_testnet_url("amoy", "polygonscan"), - }, - "polygon-zkevm": { - "mainnet": com_testnet_url("zkevm", "polygonscan"), - "cardona": com_testnet_url("cardona-zkevm", "polygonscan"), - }, - "scroll": { - "mainnet": com_url("scrollscan"), - "sepolia": com_testnet_url("sepolia", "scrollscan"), - "testnet": com_testnet_url("testnet", "scrollscan"), - }, - "unichain": { - "sepolia": xyz_testnet_url("sepolia", "uniscan"), - }, - "xai": { - "mainnet": etherscan_chain_id_url("660279"), - "sepolia": etherscan_chain_id_url("37714555429"), - }, - } - - def set_network(self, ecosystem: str, network: str): - key = network.replace("-fork", "") - if ecosystem not in self.expected_uri_map or key not in self.expected_uri_map[ecosystem]: - pytest.fail(f"Add {ecosystem}:{key} to the MockbBackend API map (check conftest.py)!") - - self.expected_base_uri = self.expected_uri_map[ecosystem][key] + def set_network(self, chain_id: int): + self.expected_base_uri = f"https://api.etherscan.io/v2/api?chainid={chain_id}" def add_handler( self, @@ -483,7 +362,7 @@ def setup_mock_account_transactions_with_ctor_args_response( def _setup_account_response(self, params, response): self.add_handler("GET", "account", params, return_value=response) - self.set_network("ethereum", "mainnet") + self.set_network(1) return response def get_mock_response( diff --git a/tests/test_config.py b/tests/test_config.py index 2f0c6d5..96d192b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,20 +1,6 @@ -import pytest - -from ._utils import ecosystems_and_networks - -network_ecosystems = pytest.mark.parametrize( - "ecosystem,network", - ecosystems_and_networks, -) - - -@network_ecosystems -def test_no_config(account, ecosystem, network, get_explorer, project): +def test_no_config(account, get_explorer, project): """Test default behavior""" with project.temp_config(name="ape-etherscan-test"): - explorer = get_explorer(ecosystem, network) - assert explorer.network.name == network - assert explorer.network.ecosystem.name == ecosystem assert account.query_manager.engines["etherscan"].rate_limit == 5 engine = account.query_manager.engines["etherscan"] diff --git a/tests/test_dependency.py b/tests/test_dependency.py index cac3c27..aaf9391 100644 --- a/tests/test_dependency.py +++ b/tests/test_dependency.py @@ -15,7 +15,7 @@ def test_dependency(mock_backend, verification_type, expected_name, contract_address): ecosystem = "ethereum" network = "mainnet" - mock_backend.set_network(ecosystem, network) + mock_backend.set_network(1) mock_backend.setup_mock_get_contract_type_response(f"get_contract_response_{verification_type}") dependency = EtherscanDependency( @@ -33,7 +33,7 @@ def test_dependency(mock_backend, verification_type, expected_name, contract_add def test_dependency_not_verified(mock_backend): - mock_backend.set_network("ethereum", "mainnet") + mock_backend.set_network(1) mock_backend.setup_mock_get_contract_type_response("get_contract_response_not_verified") dependency = EtherscanDependency( name="Apes", diff --git a/tests/test_etherscan.py b/tests/test_etherscan.py index ba50d4f..3db49e3 100644 --- a/tests/test_etherscan.py +++ b/tests/test_etherscan.py @@ -3,6 +3,7 @@ import pytest from ape.api.query import AccountTransactionQuery +from ape_etherscan.client import get_supported_chains from ape_etherscan.exceptions import ( EtherscanResponseError, EtherscanTooManyRequestsError, @@ -10,7 +11,7 @@ ) from ape_etherscan.verify import SourceVerifier, VerificationApproach -from ._utils import ecosystems_and_networks +from ._utils import chain_ids # A map of each mock response to its contract name for testing `get_contract_type()`. EXPECTED_CONTRACT_NAME_MAP = { @@ -24,95 +25,8 @@ PUBLISH_GUID = "123" -# TODO: Uncomment commented out fork networks once Ape 0.8.21 is released. base_url_test = pytest.mark.parametrize( - "ecosystem,network,url", - [ - ("arbitrum", "mainnet", "arbiscan.io"), - ("arbitrum", "mainnet-fork", "arbiscan.io"), - ("arbitrum", "sepolia", "sepolia.arbiscan.io"), - ("arbitrum", "sepolia-fork", "sepolia.arbiscan.io"), - ("arbitrum", "nova", "nova.arbiscan.io"), - ("arbitrum", "nova-fork", "nova.arbiscan.io"), - ("avalanche", "mainnet", "snowtrace.io"), - ("avalanche", "fuji", "testnet.snowtrace.io"), - ("base", "mainnet", "basescan.org"), - ("base", "sepolia", "sepolia.basescan.org"), - ("blast", "mainnet", "blastscan.io"), - ("blast", "mainnet-fork", "blastscan.io"), - ("blast", "sepolia", "sepolia.blastscan.io"), - ("blast", "sepolia-fork", "sepolia.blastscan.io"), - ("bsc", "mainnet", "bscscan.com"), - ("bsc", "mainnet-fork", "bscscan.com"), - ("bsc", "testnet", "testnet.bscscan.com"), - ("bsc", "testnet-fork", "testnet.bscscan.com"), - ("bsc", "opbnb", "opbnb.bscscan.com"), - ("bsc", "opbnb-fork", "opbnb.bscscan.com"), - ("bsc", "opbnb-testnet", "opbnb-testnet.bscscan.com"), - ("bsc", "opbnb-testnet-fork", "opbnb-testnet.bscscan.com"), - ("bttc", "mainnet", "bttcscan.com"), - # ("bttc", "mainnet-fork", "bttcscan.com"), - ("bttc", "donau", "testnet.bttcscan.com"), - # ("bttc", "donau-fork", "testnet.bttcscan.com"), - ("celo", "mainnet", "celoscan.io"), - # ("celo", "mainnet-fork", "celoscan.io"), - ("celo", "alfajores", "alfajores.celoscan.io"), - # ("celo", "alfajores-fork", "alfajores.celoscan.io"), - ("cronos", "mainnet", "https://cronoscan.com"), - # ("cronos", "mainnet-fork", "cronoscan.com"), - ("cronos-zkevm", "mainnet", "cronoscan.com"), - # ("cronos-zkevm", "mainnet-fork", "cronoscan.com"), - ("ethereum", "mainnet", "etherscan.io"), - ("ethereum", "mainnet-fork", "etherscan.io"), - ("ethereum", "holesky", "holesky.etherscan.io"), - ("ethereum", "holesky-fork", "holesky.etherscan.io"), - ("ethereum", "sepolia", "sepolia.etherscan.io"), - ("ethereum", "sepolia-fork", "sepolia.etherscan.io"), - ("fantom", "opera", "ftmscan.com"), - ("fantom", "opera-fork", "ftmscan.com"), - ("fantom", "testnet", "testnet.ftmscan.com"), - ("fantom", "testnet-fork", "testnet.ftmscan.com"), - ("fraxtal", "mainnet", "fraxscan.com"), - # ("fraxtal", "mainnet-fork", "fraxscan.com"), - ("fraxtal", "holesky", "holesky.fraxscan.com"), - # ("fraxtal", "holesky-fork", "holesky.fraxscan.com"), - ("gnosis", "mainnet", "gnosisscan.io"), - ("gnosis", "mainnet-fork", "gnosisscan.io"), - ("kroma", "mainnet", "kromascan.com"), - # ("kroma", "mainnet-fork", "kromascan.com"), - ("kroma", "sepolia", "sepolia.kromascan.com"), - # ("kroma", "sepolia-fork", "sepolia.kromascan.com"), - ("moonbeam", "mainnet", "moonscan.io"), - ("moonbeam", "mainnet-fork", "moonscan.io"), - ("moonbeam", "moonbase", "moonbase.moonscan.io"), - # ("moonbeam", "moonbase-fork", "moonbase.moonscan.io"), - ("moonbeam", "moonriver", "moonriver.moonscan.io"), - # ("moonbeam", "moonriver-fork", "moonriver.moonscan.io"), - ("optimism", "mainnet", "optimistic.etherscan.io"), - ("optimism", "mainnet-fork", "optimistic.etherscan.io"), - ("optimism", "sepolia", "sepolia-optimism.etherscan.io"), - ("optimism", "sepolia-fork", "sepolia-optimism.etherscan.io"), - ("polygon", "mainnet", "polygonscan.com"), - ("polygon", "mainnet-fork", "polygonscan.com"), - ("polygon", "amoy", "amoy.polygonscan.com"), - ("polygon", "amoy-fork", "amoy.polygonscan.com"), - ("polygon-zkevm", "mainnet", "zkevm.polygonscan.com"), - ("polygon-zkevm", "mainnet-fork", "zkevm.polygonscan.com"), - ("polygon-zkevm", "cardona", "cardona-zkevm.polygonscan.com"), - ("polygon-zkevm", "cardona-fork", "cardona-zkevm.polygonscan.com"), - ("scroll", "mainnet", "scrollscan.com"), - # ("scroll", "mainnet-fork", "scrollscan.com"), - ("scroll", "sepolia", "sepolia.scrollscan.com"), - # ("scroll", "sepolia-fork", "sepolia.scrollscan.com"), - ("scroll", "testnet", "testnet.scrollscan.com"), - # ("scroll", "testnet-fork", "testnet.scrollscan.com"), - ("unichain", "sepolia", "sepolia.uniscan.xyz"), - # ("unichain", "sepolia-fork", "sepolia.uniscan.xyz"), - ("xai", "mainnet", "https://xaiscan.io"), - # ("xai", "mainnet-fork", "https://xaiscan.io"), - ("xai", "sepolia", "https://sepolia.xaiscan.io"), - # ("xai", "sepolia-fork", "https://sepolia.xaiscan.io"), - ], + "chain_id,url", [(c["chainid"], c["blockexplorer"]) for c in get_supported_chains()] ) @@ -190,32 +104,27 @@ def setup(found_handler: Callable, threshold: int = 2): @base_url_test -def test_get_address_url(ecosystem, network, url, address, get_explorer): - expected = f"https://{url}/address/{address}" - explorer = get_explorer(ecosystem, network) +def test_get_address_url(chain_id, url, address, get_explorer): + expected = f"{url}/address/{address}" + explorer = get_explorer(chain_id) actual = explorer.get_address_url(address) assert actual == expected @base_url_test -def test_get_transaction_url(ecosystem, network, url, get_explorer): - expected = f"https://{url}/tx/{TRANSACTION}" - explorer = get_explorer(ecosystem, network) +def test_get_transaction_url(chain_id, url, get_explorer): + expected = f"{url}/tx/{TRANSACTION}" + explorer = get_explorer(chain_id) actual = explorer.get_transaction_url(TRANSACTION) assert actual == expected -@pytest.mark.parametrize("ecosystem,network", ecosystems_and_networks) -def test_get_contract_type_ecosystems_and_networks( - mock_backend, - ecosystem, - network, - get_explorer, -): +@pytest.mark.parametrize("chain_id", chain_ids) +def test_get_contract_type_ecosystems_and_networks(mock_backend, chain_id, get_explorer): # This test parametrizes getting contract types across ecosystem / network combos - mock_backend.set_network(ecosystem, network) + mock_backend.set_network(chain_id) response = mock_backend.setup_mock_get_contract_type_response("get_contract_response_flattened") - explorer = get_explorer(ecosystem, network) + explorer = get_explorer(chain_id) actual = explorer.get_contract_type(response.expected_address) contract_type_from_lowered_address = explorer.get_contract_type( response.expected_address.lower() @@ -236,7 +145,7 @@ def test_get_contract_type_additional_types(mock_backend, file_name, explorer, c # NOTE: Purposely not merged with test above to avoid adding a new dimension # to the parametrization. _ = connection # Needed for symbol lookup - mock_backend.set_network("ethereum", "mainnet") + mock_backend.set_network(1) response = mock_backend.setup_mock_get_contract_type_response(file_name) actual = explorer.get_contract_type(response.expected_address).name expected = EXPECTED_CONTRACT_NAME_MAP[response.file_name] From c3b6926cac299aabf5f8520b7488b393432fdecc Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Fri, 22 Nov 2024 11:40:22 -0600 Subject: [PATCH 6/7] refactor: API key simplification --- ape_etherscan/client.py | 20 ++++++++++++-------- ape_etherscan/exceptions.py | 7 +++---- ape_etherscan/utils.py | 5 +++-- tests/conftest.py | 5 ++++- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ape_etherscan/client.py b/ape_etherscan/client.py index 46e631c..eb18de5 100644 --- a/ape_etherscan/client.py +++ b/ape_etherscan/client.py @@ -25,7 +25,7 @@ EtherscanResponse, SourceCodeResponse, ) -from ape_etherscan.utils import API_KEY_ENV_KEY_MAP +from ape_etherscan.utils import ETHERSCAN_API_KEY_NAME if TYPE_CHECKING: from ape.api import PluginConfig @@ -162,6 +162,10 @@ def _request( data: Optional[dict] = None, ) -> EtherscanResponse: headers = headers or self.DEFAULT_HEADERS + if not self._retries: + raise ValueError(f"Retries must be at least 1: {self._retries}") + + response = None for i in range(self._retries): logger.debug(f"Request sent to {self._clean_uri}.") response = self.session.request( @@ -178,7 +182,7 @@ def _request( time.sleep(time_to_sleep) continue - # Recieved a real response unrelated to rate limiting. + # Received a real response unrelated to rate limiting. if raise_on_exceptions: response.raise_for_status() elif not 200 <= response.status_code < 300: @@ -186,14 +190,14 @@ def _request( break - return EtherscanResponse(response, self._instance.ecosystem_name, raise_on_exceptions) + if response: + return EtherscanResponse(response, self._instance.ecosystem_name, raise_on_exceptions) + else: + # Not possible (I don't think); just for type-checking. + raise ValueError("No response.") def __authorize(self, params_or_data: Optional[dict] = None) -> Optional[dict]: - env_var_key = API_KEY_ENV_KEY_MAP.get(self._instance.ecosystem_name) - if not env_var_key: - return params_or_data - - api_key = os.environ.get(env_var_key) + api_key = os.environ.get(ETHERSCAN_API_KEY_NAME) if api_key and (not params_or_data or "apikey" not in params_or_data): params_or_data = params_or_data or {} api_key = random.choice(api_key.split(",")) diff --git a/ape_etherscan/exceptions.py b/ape_etherscan/exceptions.py index ac8f72b..fbac0a4 100644 --- a/ape_etherscan/exceptions.py +++ b/ape_etherscan/exceptions.py @@ -4,7 +4,7 @@ from ape.exceptions import ApeException from requests import Response -from ape_etherscan.utils import API_KEY_ENV_KEY_MAP +from ape_etherscan.utils import ETHERSCAN_API_KEY_NAME if TYPE_CHECKING: from ape_etherscan.types import EtherscanResponse, ResponseValue @@ -74,9 +74,8 @@ class EtherscanTooManyRequestsError(EtherscanResponseError): def __init__(self, response: Union[Response, "EtherscanResponse"], ecosystem: str): message = "Etherscan API server rate limit exceeded." - api_key_name = API_KEY_ENV_KEY_MAP[ecosystem] - if not os.environ.get(api_key_name): - message = f"{message}. Try setting {api_key_name}'." + if not os.environ.get(ETHERSCAN_API_KEY_NAME): + message = f"{message}. Try setting {ETHERSCAN_API_KEY_NAME}'." super().__init__(response, message) diff --git a/ape_etherscan/utils.py b/ape_etherscan/utils.py index 3fd5fb2..f3603a2 100644 --- a/ape_etherscan/utils.py +++ b/ape_etherscan/utils.py @@ -1,4 +1,5 @@ -# TODO: (deprecated) Remove in 0.9 and make this a calculated property. +ETHERSCAN_API_KEY_NAME = "ETHERSCAN_API_KEY" +# TODO: (deprecated) Remove in 0.9 - Only 1 key required for v2. API_KEY_ENV_KEY_MAP = { "arbitrum": "ARBISCAN_API_KEY", "avalanche": "SNOWTRACE_API_KEY", @@ -7,7 +8,7 @@ "bsc": "BSCSCAN_API_KEY", "bttc": "BTTCSCAN_API_KEY", "celo": "CELOSCAN_API_KEY", - "ethereum": "ETHERSCAN_API_KEY", + "ethereum": f"{ETHERSCAN_API_KEY_NAME}", "fantom": "FTMSCAN_API_KEY", "fraxtal": "FRAXSCAN_API_KEY", "gnosis": "GNOSISSCAN_API_KEY", diff --git a/tests/conftest.py b/tests/conftest.py index b4fa1ab..dec671c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -168,7 +168,10 @@ def get_explorer(): def fn(chain_id: int) -> "ExplorerAPI": for ecosystem in ape.networks.ecosystems.values(): for network in ecosystem.networks.values(): - if int(network.chain_id) != int(chain_id): + if network.is_dev: + continue + + elif int(network.chain_id) != int(chain_id): continue # Found. From 8309767673611f24d71045822c58a43548c7d843 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Tue, 26 Nov 2024 13:06:27 -0600 Subject: [PATCH 7/7] fix: release name --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 67d242c..dda1885 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=51.1.1", "wheel", "setuptools_scm[toml]>=5.0"] +requires = ["setuptools>=72.2.0", "wheel", "setuptools_scm[toml]>=5.0"] [tool.mypy] exclude = "build/" diff --git a/setup.py b/setup.py index 204dfe7..f4e9944 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ ], "doc": ["sphinx-ape"], "release": [ # `release` GitHub Action job uses this - "setuptools", # Installation tool + "setuptools>=72.2.0", # Installation tool "setuptools-scm", # Installation tool "wheel", # Packaging tool "twine", # Package upload tool