From c884b675d280337d94f65622ea33143de08154bd Mon Sep 17 00:00:00 2001 From: antazoey Date: Mon, 18 Dec 2023 23:02:50 -0600 Subject: [PATCH] feat: upgrade to ape 0.7 (#110) --- .mdformat.toml | 1 - ape_etherscan/verify.py | 126 ++++++++++-------- pyproject.toml | 3 + setup.py | 13 +- tests/conftest.py | 108 +++++++-------- .../get_account_transactions.json | 4 +- ...t_account_transactions_with_ctor_args.json | 4 +- 7 files changed, 142 insertions(+), 117 deletions(-) delete mode 100644 .mdformat.toml diff --git a/.mdformat.toml b/.mdformat.toml deleted file mode 100644 index 01b2fb0..0000000 --- a/.mdformat.toml +++ /dev/null @@ -1 +0,0 @@ -number = true diff --git a/ape_etherscan/verify.py b/ape_etherscan/verify.py index 4c7412c..3e262fe 100644 --- a/ape_etherscan/verify.py +++ b/ape_etherscan/verify.py @@ -4,12 +4,13 @@ from pathlib import Path from typing import Dict, Optional +from ape.api import CompilerAPI from ape.contracts import ContractInstance from ape.logging import LogLevel, logger from ape.types import AddressType from ape.utils import ManagerAccessMixin, cached_property -from ethpm_types import ContractType -from semantic_version import Version # type: ignore +from ape_solidity.compiler import DEFAULT_OPTIMIZATION_RUNS +from ethpm_types import Compiler, ContractType from ape_etherscan.client import AccountClient, ClientFactory, ContractClient from ape_etherscan.exceptions import ContractVerificationError, EtherscanResponseError @@ -144,32 +145,36 @@ def __init__(self, address: AddressType, client_factory: ClientFactory): self.client_factory = client_factory @cached_property - def _account_client(self) -> AccountClient: + def account_client(self) -> AccountClient: return self.client_factory.get_account_client(str(self.address)) @cached_property - def _contract_client(self) -> ContractClient: + def contract_client(self) -> ContractClient: return self.client_factory.get_contract_client(str(self.address)) @cached_property - def _contract(self) -> ContractInstance: + def contract(self) -> ContractInstance: return self.chain_manager.contracts.instance_at(self.address) @property - def _contract_type(self) -> ContractType: - return self._contract.contract_type + def contract_type(self) -> ContractType: + return self.contract.contract_type + + @property + def contract_name(self) -> str: + return self.contract.contract_type.name or "" @property def _base_path(self) -> Path: return self.project_manager.contracts_folder @property - def _source_path(self) -> Path: - return self._base_path / (self._contract_type.source_id or "") + def source_path(self) -> Path: + return self._base_path / (self.contract_type.source_id or "") @property - def _ext(self) -> str: - return self._source_path.suffix + def ext(self) -> str: + return self.source_path.suffix @cached_property def constructor_arguments(self) -> str: @@ -182,7 +187,7 @@ def constructor_arguments(self) -> str: deploy_receipt = None while checks_done <= timeout: # If was just deployed, it takes a few seconds to show up in API response - if deploy_receipt := next(self._account_client.get_all_normal_transactions(), None): + if deploy_receipt := next(self.account_client.get_all_normal_transactions(), None): break else: @@ -195,7 +200,7 @@ def constructor_arguments(self) -> str: f"Failed to find to deploy receipt for '{self.address}'" ) - if code := self._contract_type.runtime_bytecode: + if code := self.contract_type.runtime_bytecode: runtime_code = code.bytecode or "" deployment_code = deploy_receipt["input"] ctor_args = extract_constructor_arguments(deployment_code, runtime_code) @@ -209,9 +214,37 @@ def license_code(self) -> LicenseType: The license type used in the code. """ - spdx_id = self._source_path.read_text().split("\n")[0] + spdx_id = self.source_path.read_text().split("\n")[0] return LicenseType.from_spdx_id(spdx_id) + @property + def compiler_api(self) -> CompilerAPI: + if compiler := self.compiler_manager.registered_compilers.get(self.ext): + return compiler + + raise ContractVerificationError( + f"Missing required compiler plugin for '{self.ext}' to verify." + ) + + @cached_property + def compiler_name(self) -> str: + return self.compiler_api.name + + @property + def compiler(self) -> Compiler: + # Check the cached manifest for the compiler artifacts. + if manifest := self.project_manager.local_project.cached_manifest: + if compiler := manifest.get_contract_compiler(self.contract_name): + return compiler + + # Look in the publishable manifest, as Ape includes these there. + manifest = self.project_manager.extract_manifest() + if compiler := manifest.get_contract_compiler(self.contract_name): + return compiler + + # Build a default one and hope for the best. + return Compiler(name=self.compiler_name, contractType=[self.contract_name], version=None) + def attempt_verification(self): """ Attempt to verify the source code. @@ -223,45 +256,14 @@ def attempt_verification(self): to validate the contract. """ - manifest = self.project_manager.extract_manifest() - compilers_used = [ - c for c in manifest.compilers if self._contract_type.name in c.contractTypes - ] - - if not compilers_used: - raise ContractVerificationError("Compiler data missing from project manifest.") - - versions = [Version(c.version) for c in compilers_used] - if not versions: - # Might be impossible to get here. - raise ContractVerificationError("Unable to find compiler version used.") - - elif len(versions) > 1: - # Might be impossible to get here. - logger.warning("Source was compiled by multiple versions. Using max.") - version = max(versions) - - else: - version = versions[0] - - compiler_plugin = self.compiler_manager.registered_compilers[self._ext] - all_settings = compiler_plugin.get_compiler_settings( - [self._source_path], base_path=self._base_path - ) - - # Hack to allow any Version object work. - # TODO: Replace with all_settings[version] on 0.7 upgrade - settings = {str(v): s for v, s in all_settings.items() if str(v) == str(version)}[ - str(version) - ] - + version = str(self.compiler.version) + settings = self.compiler.settings or self._get_new_settings(version) optimizer = settings.get("optimizer", {}) optimized = optimizer.get("enabled", False) - runs = optimizer.get("runs", 200) - source_id = self._contract_type.source_id + runs = optimizer.get("runs", DEFAULT_OPTIMIZATION_RUNS) + source_id = self.contract_type.source_id base_folder = self.project_manager.contracts_folder standard_input_json = self._get_standard_input_json(source_id, base_folder, **settings) - evm_version = settings.get("evmVersion") license_code = self.license_code license_code_value = license_code.value if license_code else None @@ -274,14 +276,14 @@ def attempt_verification(self): # NOTE: Etherscan does not allow directory prefixes on the source ID. if self.provider.network.ecosystem.name in ECOSYSTEMS_VERIFY_USING_JSON: request_source_id = Path(source_id).name - contract_name = f"{request_source_id}:{self._contract_type.name}" + contract_name = f"{request_source_id}:{self.contract_type.name or ''}" else: # When we have a flattened contract, we don't need to specify the file name # only the contract name - contract_name = f"{self._contract_type.name}" + contract_name = self.contract_type.name or "" try: - guid = self._contract_client.verify_source_code( + guid = self.contract_client.verify_source_code( standard_input_json, str(version), contract_name=contract_name, @@ -301,13 +303,27 @@ def attempt_verification(self): self._wait_for_verification(guid) + def _get_new_settings(self, version: str) -> Dict: + logger.warning( + "Settings missing from cached manifest. " "Attempting to re-calculate find settings." + ) + + # Attempt to re-calculate settings. + compiler_plugin = self.compiler_manager.registered_compilers[self.ext] + all_settings = compiler_plugin.get_compiler_settings( + [self.source_path], base_path=self._base_path + ) + + # Hack to allow any Version object work. + return {str(v): s for v, s in all_settings.items() if str(v) == version}[version] + def _get_standard_input_json( self, source_id: str, base_folder: Optional[Path] = None, **settings ) -> Dict: base_dir = base_folder or self.project_manager.contracts_folder source_path = base_dir / source_id compiler = self.compiler_manager.registered_compilers[source_path.suffix] - sources = {self._source_path.name: {"content": source_path.read_text()}} + sources = {self.source_path.name: {"content": source_path.read_text()}} def build_map(_source_id: str): _source_path = base_dir / _source_id @@ -375,7 +391,7 @@ def _wait_for_verification(self, guid: str): for iteration in range(100): try: - verification_update = self._contract_client.check_verify_status(guid) + verification_update = self.contract_client.check_verify_status(guid) guid_did_exist = True except EtherscanResponseError as err: if "Resource not found" in str(err) and guid_did_exist: @@ -426,7 +442,7 @@ def extract_constructor_arguments(deployment_bytecode: str, runtime_bytecode: st # If the runtime bytecode is not found within the deployment bytecode, # return an error message. if start_index == -1: - raise ContractVerificationError("Runtime bytecode not found within deployment bytecode") + raise ContractVerificationError("Runtime bytecode not found within deployment bytecode.") # Cut the deployment bytecode at the start of the runtime bytecode # The remaining part is the constructor arguments diff --git a/pyproject.toml b/pyproject.toml index a1682d0..aedffe4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,6 @@ force_grid_wrap = 0 include_trailing_comma = true multi_line_output = 3 use_parentheses = true + +[tool.mdformat] +number = true diff --git a/setup.py b/setup.py index 895dd90..a1f978b 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ "ape-infura", # Needed for live network tests "ape-solidity", # Needed for contract verification tests "pytest>=6.0", # Core testing package - "pytest-xdist", # multi-process runner + "pytest-xdist", # Multi-process runner "pytest-cov", # Coverage analyzer plugin "hypothesis>=6.2.0,<7", # Strategy-based fuzzer "pytest-mock", # Test mocker @@ -25,15 +25,17 @@ "types-requests>=2.28.7", # Needed due to mypy typeshed "types-setuptools", # Needed due to mypy typeshed "flake8>=6.1.0,<7", # Style linter + "flake8-breakpoint>=1.1.0,<2", # Detect breakpoints left in code + "flake8-print>=5.0.0,<6", # Detect print statements left in code "isort>=5.10.1,<6", # Import sorting linter "mdformat>=0.7.17", # Auto-formatter for markdown "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown "mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates - "pydantic<2", # Needed for successful type check. + "mdformat-pyproject>=0.0.1", # Allows configuring in pyproject.toml ], "doc": [ - "Sphinx>=3.4.3,<4", # Documentation generator - "sphinx_rtd_theme>=0.1.9,<1", # Readthedocs.org theme + "Sphinx>=6.1.3,<7", # Documentation generator + "sphinx_rtd_theme>=1.2.0,<2", # Readthedocs.org theme "towncrier>=19.2.0,<20", # Generate release notes ], "release": [ # `release` GitHub Action job uses this @@ -76,7 +78,8 @@ url="https://github.com/ApeWorX/ape-etherscan", include_package_data=True, install_requires=[ - "eth-ape>=0.6.16,<0.7", + "eth-ape>=0.7.0,<0.8", + "ethpm_types", # Use same version as eth-ape "requests", # Use same version as eth-ape "yarl", # Use same version as eth-ape ], diff --git a/tests/conftest.py b/tests/conftest.py index eeed689..ddc555a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,22 +26,6 @@ ape.config.PROJECT_FOLDER = Path(mkdtemp()).resolve() MOCK_RESPONSES_PATH = Path(__file__).parent / "mock_responses" -CONTRACT_ADDRESS = "0xFe80e7afB7041c1592a2A5d8f617518c1591Aad4" -CONTRACT_ADDRESS_MAP = { - "get_contract_response": CONTRACT_ADDRESS, - "get_proxy_contract_response": "0x55A8a39bc9694714E2874c1ce77aa1E599461E18", - "get_vyper_contract_response": "0xdA816459F1AB5631232FE5e97a05BBBb94970c95", -} -EXPECTED_ACCOUNT_TXNS_PARAMS = { - "module": "account", - "action": "txlist", - "address": "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", - "endblock": None, - "startblock": None, - "offset": 100, - "page": 1, - "sort": "asc", -} FOO_SOURCE_CODE = """ // SPDX-License-Identifier: AGPL-3.0 pragma solidity ^0.8.20; @@ -100,7 +84,6 @@ def standard_input_json(library): "subcontracts/foo.sol": {"": ["ast"], "*": OUTPUT_SELECTION}, }, "remappings": ["@bar=.cache/bar/local"], - "viaIR": False, }, "libraryname1": "MyLib", "libraryaddress1": library.address, @@ -140,8 +123,17 @@ def project(): @pytest.fixture(scope="session") -def address(): - return CONTRACT_ADDRESS +def address(contract_to_verify): + return contract_to_verify.address + + +@pytest.fixture(scope="session") +def contract_address_map(address): + return { + "get_contract_response": address, + "get_proxy_contract_response": "0x55A8a39bc9694714E2874c1ce77aa1E599461E18", + "get_vyper_contract_response": "0xdA816459F1AB5631232FE5e97a05BBBb94970c95", + } @pytest.fixture(scope="session") @@ -149,6 +141,23 @@ def account(): return ape.accounts.test_accounts[0] +@pytest.fixture +def get_expected_account_txns_params(): + def fn(addr): + return { + "module": "account", + "action": "txlist", + "address": addr, + "endblock": None, + "startblock": None, + "offset": 100, + "page": 1, + "sort": "asc", + } + + return fn + + @pytest.fixture(scope="session") def fake_connection(): with ape.networks.ethereum.local.use_provider("test"): @@ -187,7 +196,7 @@ def fn( network.name = network_name network.ecosystem = ecosystem etherscan = ape.networks.get_ecosystem("ethereum").get_network("mainnet") - explorer = Etherscan.construct(name=etherscan.name, network=network) + explorer = Etherscan.model_construct(name=etherscan.name, network=network) network.explorer = explorer explorer.network = network else: @@ -207,19 +216,23 @@ def response(mocker): @pytest.fixture -def mock_backend(mocker): +def mock_backend(mocker, get_expected_account_txns_params, contract_address_map): session = mocker.MagicMock() - backend = MockEtherscanBackend(mocker, session) + backend = MockEtherscanBackend( + mocker, session, get_expected_account_txns_params, contract_address_map + ) _APIClient.session = session return backend class MockEtherscanBackend: - def __init__(self, mocker, session): - self._mocker = mocker - self._session = session - self._expected_base_uri = "https://api.etherscan.io/api" # Default - self._handlers = {"get": {}, "post": {}} + 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.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( @@ -278,7 +291,7 @@ def get_url_f(testnet: bool = False, tld: str = "io"): } def set_network(self, ecosystem: str, network: str): - self._expected_base_uri = self.expected_uri_map[ecosystem][network.replace("-fork", "")] + self.expected_base_uri = self.expected_uri_map[ecosystem][network.replace("-fork", "")] def add_handler( self, @@ -330,8 +343,8 @@ def handler(self, method, base_uri, params=None, data=None, headers=None): result = side_effect() return result if isinstance(result, Response) else self.get_mock_response(result) - self._handlers[method.lower()][module] = handler - self._session.request.side_effect = self.handle_request + self.handlers[method.lower()][module] = handler + self.session.request.side_effect = self.handle_request def handle_request(self, method, base_uri, timeout, headers=None, params=None, data=None): if params and "apikey" in params: @@ -339,7 +352,7 @@ def handle_request(self, method, base_uri, timeout, headers=None, params=None, d if data and "apiKey" in data: del data["apiKey"] - assert base_uri == self._expected_base_uri + assert base_uri == self.expected_base_uri if params: module = params.get("module") @@ -348,12 +361,12 @@ def handle_request(self, method, base_uri, timeout, headers=None, params=None, d else: raise AssertionError("Expected either 'params' or 'data'.") - handler = self._handlers[method.lower()][module] + handler = self.handlers[method.lower()][module] return handler(self, method, base_uri, headers=headers, params=params, data=data) def setup_mock_get_contract_type_response(self, file_name: str): response = self._get_contract_type_response(file_name) - address = CONTRACT_ADDRESS_MAP[file_name] + address = self.contract_address_map[file_name] expected_params = self._expected_get_ct_params(address) self.add_handler("GET", "contract", expected_params, return_value=response) response.expected_address = address @@ -363,9 +376,9 @@ def setup_mock_get_contract_type_response_with_throttling( self, file_name: str, retries: int = 2 ): response = self._get_contract_type_response(file_name) - address = CONTRACT_ADDRESS_MAP[file_name] + address = self.contract_address_map[file_name] expected_params = self._expected_get_ct_params(address) - throttled = self._mocker.MagicMock(spec=Response) + throttled = self.mocker.MagicMock(spec=Response) throttled.status_code = 429 class ThrottleMock: @@ -391,16 +404,11 @@ def _get_contract_type_response(self, file_name: str) -> Any: def _expected_get_ct_params(self, address: str) -> Dict: return {"module": "contract", "action": "getsourcecode", "address": address} - def setup_mock_account_transactions_response( - self, address: Optional[AddressType] = None, **overrides - ): + def setup_mock_account_transactions_response(self, address: AddressType, **overrides): file_name = "get_account_transactions.json" test_data_path = MOCK_RESPONSES_PATH / file_name - if address: - params = EXPECTED_ACCOUNT_TXNS_PARAMS.copy() - params["address"] = address - else: - params = EXPECTED_ACCOUNT_TXNS_PARAMS + params = self.get_expected_account_txns_params(address) + params["address"] = address with open(test_data_path) as response_data_file: response = self.get_mock_response( @@ -410,16 +418,12 @@ def setup_mock_account_transactions_response( return self._setup_account_response(params, response) def setup_mock_account_transactions_with_ctor_args_response( - self, address: Optional[AddressType] = None, **overrides + self, address: AddressType, **overrides ): file_name = "get_account_transactions_with_ctor_args.json" test_data_path = MOCK_RESPONSES_PATH / file_name - - if address: - params = EXPECTED_ACCOUNT_TXNS_PARAMS.copy() - params["address"] = address - else: - params = EXPECTED_ACCOUNT_TXNS_PARAMS + params = self.get_expected_account_txns_params(address) + params["address"] = address with open(test_data_path) as response_data_file: response = self.get_mock_response( @@ -446,7 +450,7 @@ def get_mock_response( # Mock wasn't set. response_data = {} - response = self._mocker.MagicMock(spec=Response) + response = self.mocker.MagicMock(spec=Response) assert isinstance(response_data, dict) # For mypy overrides: Dict = kwargs.get("response_overrides", {}) response.json.return_value = {**response_data, **overrides} @@ -490,7 +494,7 @@ def verification_params_with_ctor_args( address_to_verify_with_ctor_args, library, standard_input_json, constructor_arguments ): json_data = standard_input_json.copy() - json_data["libraryaddress1"] = "0xF2Df0b975c0C9eFa2f8CA0491C2d1685104d2488" + json_data["libraryaddress1"] = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" return { "action": "verifysourcecode", diff --git a/tests/mock_responses/get_account_transactions.json b/tests/mock_responses/get_account_transactions.json index 97f5d93..1a0a6bf 100644 --- a/tests/mock_responses/get_account_transactions.json +++ b/tests/mock_responses/get_account_transactions.json @@ -9,8 +9,8 @@ "nonce": "0", "blockHash": "0x3175f953c1da4bf3d15b853dae4a150ae44e2e71380936463e89142c12961968", "transactionIndex": "31", - "from": "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", - "to": "0xFe80e7afB7041c1592a2A5d8f617518c1591Aad4", + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "to": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "value": "0", "gas": "4712388", "gasPrice": "1499999989", diff --git a/tests/mock_responses/get_account_transactions_with_ctor_args.json b/tests/mock_responses/get_account_transactions_with_ctor_args.json index 107c67d..9a169ce 100644 --- a/tests/mock_responses/get_account_transactions_with_ctor_args.json +++ b/tests/mock_responses/get_account_transactions_with_ctor_args.json @@ -9,8 +9,8 @@ "nonce": "0", "blockHash": "0x3175f953c1da4bf3d15b853dae4a150ae44e2e71380936463e89142c12961968", "transactionIndex": "31", - "from": "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", - "to": "0xFe80e7afB7041c1592a2A5d8f617518c1591Aad4", + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "to": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", "value": "0", "gas": "4712388", "gasPrice": "1499999989",