diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e0b64e..dccf3e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.12.0 hooks: - id: black name: black @@ -21,7 +21,7 @@ repos: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.1 hooks: - id: mypy additional_dependencies: [types-PyYAML, types-requests, types-setuptools, pydantic] diff --git a/ape_etherscan/client.py b/ape_etherscan/client.py index 8b4c1b2..e774d4c 100644 --- a/ape_etherscan/client.py +++ b/ape_etherscan/client.py @@ -44,7 +44,7 @@ def get_etherscan_uri(ecosystem_name: str, network_name: str): return ( "https://optimistic.etherscan.io" if network_name == "mainnet" - else "https://goerli-optimism.etherscan.io" + else f"https://{network_name}-optimism.etherscan.io" ) elif ecosystem_name == "polygon-zkevm": return ( @@ -105,7 +105,7 @@ def get_etherscan_api_uri(ecosystem_name: str, network_name: str): return ( "https://api-optimistic.etherscan.io/api" if network_name == "mainnet" - else "https://api-goerli-optimistic.etherscan.io/api" + else f"https://api-{network_name}-optimistic.etherscan.io/api" ) elif ecosystem_name == "polygon-zkevm": return ( diff --git a/ape_etherscan/utils.py b/ape_etherscan/utils.py index 954b7a9..5d518d7 100644 --- a/ape_etherscan/utils.py +++ b/ape_etherscan/utils.py @@ -11,42 +11,43 @@ "gnosis": "GNOSISSCAN_API_KEY", } NETWORKS = { - "ethereum": [ + "arbitrum": [ "mainnet", "goerli", - "sepolia", ], - "arbitrum": [ + "avalanche": [ + "mainnet", + "fuji", + ], + "base": [ "mainnet", "goerli", ], - "fantom": [ - "opera", + "bsc": [ + "mainnet", "testnet", ], - "optimism": [ + "ethereum": [ "mainnet", "goerli", + "sepolia", ], - "base": [ - "mainnet", - "goerli", + "fantom": [ + "opera", + "testnet", ], - "polygon-zkevm": [ + "gnosis": ["mainnet"], + "optimism": [ "mainnet", "goerli", + "sepolia", ], "polygon": [ "mainnet", "mumbai", ], - "avalanche": [ - "mainnet", - "fuji", - ], - "bsc": [ + "polygon-zkevm": [ "mainnet", - "testnet", + "goerli", ], - "gnosis": ["mainnet"], } diff --git a/ape_etherscan/verify.py b/ape_etherscan/verify.py index e92178d..4c7412c 100644 --- a/ape_etherscan/verify.py +++ b/ape_etherscan/verify.py @@ -182,26 +182,24 @@ 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): + break - try: - deploy_receipt = next(self._account_client.get_all_normal_transactions()) - except StopIteration: - continue - - logger.debug("Waiting for deploy receipt in Etherscan...") - checks_done += 1 - time.sleep(2.5) + else: + logger.debug("Waiting for deploy receipt in Etherscan...") + checks_done += 1 + time.sleep(2.5) if not deploy_receipt: raise ContractVerificationError( f"Failed to find to deploy receipt for '{self.address}'" ) - runtime_bytecode = self._contract_type.runtime_bytecode - if runtime_bytecode: - return extract_constructor_arguments( - deploy_receipt["input"], runtime_bytecode.bytecode or "" - ) + 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) + return ctor_args else: raise ContractVerificationError("Failed to find runtime bytecode.") @@ -250,7 +248,13 @@ def attempt_verification(self): all_settings = compiler_plugin.get_compiler_settings( [self._source_path], base_path=self._base_path ) - settings = all_settings[version] + + # 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) + ] + optimizer = settings.get("optimizer", {}) optimized = optimizer.get("enabled", False) runs = optimizer.get("runs", 200) @@ -411,7 +415,6 @@ def extract_constructor_arguments(deployment_bytecode: str, runtime_bytecode: st runtime_bytecode = ( runtime_bytecode[2:] if runtime_bytecode.startswith("0x") else runtime_bytecode ) - if deployment_bytecode.endswith(runtime_bytecode): # If the runtime bytecode is at the end of the deployment bytecode, # there are no constructor arguments @@ -421,7 +424,7 @@ def extract_constructor_arguments(deployment_bytecode: str, runtime_bytecode: st start_index = deployment_bytecode.find(runtime_bytecode) # If the runtime bytecode is not found within the deployment bytecode, - # return an error message + # return an error message. if start_index == -1: raise ContractVerificationError("Runtime bytecode not found within deployment bytecode") diff --git a/setup.py b/setup.py index e12d53c..895dd90 100644 --- a/setup.py +++ b/setup.py @@ -20,8 +20,8 @@ "pytest-mock", # Test mocker ], "lint": [ - "black>=23.10.0,<24", # auto-formatter and linter - "mypy>=1.6.1,<2", # Static type analyzer + "black>=23.12.0,<24", # auto-formatter and linter + "mypy>=1.7.1,<2", # Static type analyzer "types-requests>=2.28.7", # Needed due to mypy typeshed "types-setuptools", # Needed due to mypy typeshed "flake8>=6.1.0,<7", # Style linter @@ -78,6 +78,7 @@ install_requires=[ "eth-ape>=0.6.16,<0.7", "requests", # Use same version as eth-ape + "yarl", # Use same version as eth-ape ], python_requires=">=3.8,<4", extras_require=extras_require, diff --git a/tests/conftest.py b/tests/conftest.py index 54bb977..eeed689 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -253,6 +253,7 @@ def get_url_f(testnet: bool = False, tld: str = "io"): "optimism": { "mainnet": testnet_url("optimistic", "etherscan"), "goerli": testnet_url("goerli-optimistic", "etherscan"), + "sepolia": testnet_url("sepolia-optimistic", "etherscan"), }, "polygon": { "mainnet": com_url("polygonscan"), @@ -390,10 +391,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): + def setup_mock_account_transactions_response( + self, address: Optional[AddressType] = None, **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 @@ -401,13 +403,14 @@ def setup_mock_account_transactions_response(self, address: Optional[AddressType params = EXPECTED_ACCOUNT_TXNS_PARAMS with open(test_data_path) as response_data_file: - response = self.get_mock_response(response_data_file, file_name=file_name) - self.add_handler("GET", "account", params, return_value=response) - self.set_network("ethereum", "mainnet") - return response + response = self.get_mock_response( + response_data_file, file_name=file_name, response_overrides=overrides + ) + + return self._setup_account_response(params, response) def setup_mock_account_transactions_with_ctor_args_response( - self, address: Optional[AddressType] = None + self, address: Optional[AddressType] = None, **overrides ): file_name = "get_account_transactions_with_ctor_args.json" test_data_path = MOCK_RESPONSES_PATH / file_name @@ -419,10 +422,16 @@ def setup_mock_account_transactions_with_ctor_args_response( params = EXPECTED_ACCOUNT_TXNS_PARAMS with open(test_data_path) as response_data_file: - response = self.get_mock_response(response_data_file, file_name=file_name) - self.add_handler("GET", "account", params, return_value=response) - self.set_network("ethereum", "mainnet") - return response + response = self.get_mock_response( + response_data_file, file_name=file_name, response_overrides=overrides + ) + + return self._setup_account_response(params, response) + + def _setup_account_response(self, params, response): + self.add_handler("GET", "account", params, return_value=response) + self.set_network("ethereum", "mainnet") + return response def get_mock_response( self, response_data: Optional[Union[IO, Dict, str, MagicMock]] = None, **kwargs @@ -438,7 +447,9 @@ def get_mock_response( response_data = {} response = self._mocker.MagicMock(spec=Response) - response.json.return_value = response_data + assert isinstance(response_data, dict) # For mypy + overrides: Dict = kwargs.get("response_overrides", {}) + response.json.return_value = {**response_data, **overrides} response.text = json.dumps(response_data or {}) response.status_code = 200 @@ -469,19 +480,22 @@ def verification_params(address_to_verify, standard_input_json): @pytest.fixture(scope="session") -def verification_params_with_ctor_args( - address_to_verify_with_ctor_args, library, standard_input_json -): +def constructor_arguments(): # abi-encoded representation of uint256 value 42 - ctor_args = "000000000000000000000000000000000000000000000000000000000000002a" # noqa: E501 + return "000000000000000000000000000000000000000000000000000000000000002a" # noqa: E501 + +@pytest.fixture(scope="session") +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" return { "action": "verifysourcecode", "codeformat": "solidity-standard-json-input", - "constructorArguements": ctor_args, + "constructorArguements": constructor_arguments, "contractaddress": address_to_verify_with_ctor_args, "contractname": "foo.sol:fooWithConstructor", "evmversion": None, @@ -512,15 +526,18 @@ def library(account, project, chain, solidity): @pytest.fixture(scope="session") -def address_to_verify(fake_connection, library, project, account): +def contract_to_verify(fake_connection, library, project, account): _ = library # Ensure library is deployed first. - foo = project.foo.deploy(sender=account) - ape.chain.contracts._local_contract_types[address] = foo.contract_type - return foo.address + return project.foo.deploy(sender=account) @pytest.fixture(scope="session") -def address_to_verify_with_ctor_args(fake_connection, project, account): +def address_to_verify(contract_to_verify): + return contract_to_verify + + +@pytest.fixture(scope="session") +def contract_to_verify_with_ctor_args(fake_connection, project, account): # Deploy the library first. library = account.deploy(project.MyLib) ape.chain.contracts._local_contract_types[library.address] = library.contract_type @@ -531,7 +548,12 @@ def address_to_verify_with_ctor_args(fake_connection, project, account): foo = project.fooWithConstructor.deploy(42, sender=account) ape.chain.contracts._local_contract_types[address] = foo.contract_type - return foo.address + return foo + + +@pytest.fixture(scope="session") +def address_to_verify_with_ctor_args(contract_to_verify_with_ctor_args): + return contract_to_verify_with_ctor_args.address @pytest.fixture(scope="session") diff --git a/tests/test_etherscan.py b/tests/test_etherscan.py index 2ca38c3..015efb8 100644 --- a/tests/test_etherscan.py +++ b/tests/test_etherscan.py @@ -43,6 +43,8 @@ ("optimism", "mainnet-fork", "optimistic.etherscan.io"), ("optimism", "goerli", "goerli-optimism.etherscan.io"), ("optimism", "goerli-fork", "goerli-optimism.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", "mumbai", "mumbai.polygonscan.com"), @@ -85,10 +87,13 @@ def sim(self): @pytest.fixture def setup_verification_test( - mock_backend, verification_params, verification_tester_cls, address_to_verify + mock_backend, verification_params, verification_tester_cls, contract_to_verify ): def setup(found_handler: Callable, threshold: int = 2): - mock_backend.setup_mock_account_transactions_response(address=address_to_verify) + overrides = _acct_tx_overrides(contract_to_verify) + mock_backend.setup_mock_account_transactions_response( + address=contract_to_verify.address, **overrides + ) mock_backend.add_handler("POST", "contract", verification_params, return_value=PUBLISH_GUID) verification_tester = verification_tester_cls(found_handler, threshold=threshold) mock_backend.add_handler( @@ -107,11 +112,15 @@ def setup_verification_test_with_ctor_args( mock_backend, verification_params_with_ctor_args, verification_tester_cls, - address_to_verify_with_ctor_args, + contract_to_verify_with_ctor_args, + constructor_arguments, ): def setup(found_handler: Callable, threshold: int = 2): + overrides = _acct_tx_overrides( + contract_to_verify_with_ctor_args, args=constructor_arguments + ) mock_backend.setup_mock_account_transactions_with_ctor_args_response( - address=address_to_verify_with_ctor_args + address=contract_to_verify_with_ctor_args.address, **overrides ) mock_backend.add_handler( "POST", "contract", verification_params_with_ctor_args, return_value=PUBLISH_GUID @@ -251,3 +260,15 @@ def test_publish_contract_with_ctor_args( setup_verification_test_with_ctor_args(lambda: "Pass - You made it!") explorer.publish_contract(address_to_verify_with_ctor_args) assert caplog.records[-1].message == expected_verification_log_with_ctor_args + + +def _acct_tx_overrides(contract, args=None): + suffix = args or "" + if suffix.startswith("0x"): + suffix = suffix[2:] + + # Include construcor aguments! + ct = contract.contract_type + prefix = ct.deployment_bytecode.bytecode + code = f"{prefix}{suffix}" + return {"result": [{"input": code}]}