From fab7d4365246850f10adff014ec30b3c5a6d470c Mon Sep 17 00:00:00 2001 From: codingsh Date: Fri, 6 Sep 2024 19:45:57 +0400 Subject: [PATCH] chore(review): rewrite the code, with the suggestions from the review and redo the test and add it to the integrations folder --- boa/contracts/vyper/vyper_contract.py | 162 +++++++++--------- boa/interpret.py | 17 +- docs/source/api.rst | 51 +----- tests/integration/test_verify_blockscout.py | 37 ++++ .../contracts/vyper/test_vyper_contract.py | 41 +---- 5 files changed, 128 insertions(+), 180 deletions(-) create mode 100644 tests/integration/test_verify_blockscout.py diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 5b2889c0..08110635 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -1,7 +1,7 @@ # the main "entry point" of vyper-related functionality like # AST handling, traceback construction and ABI (marshaling # and unmarshaling vyper objects) - +import os import contextlib import copy import warnings @@ -59,6 +59,7 @@ from boa.util.lrudict import lrudict from boa.vm.gas_meters import ProfilingGasMeter from boa.vm.utils import to_bytes, to_int +import requests # error messages for external calls EXTERNAL_CALL_ERRORS = ("external call failed", "returndatasize too small") @@ -67,61 +68,71 @@ # error detail where user possibly provided dev revert reason DEV_REASON_ALLOWED = ("user raise", "user assert") + +class VyperDeployer: + create_compiler_data = CompilerData # this may be a different class in plugins -class ContractVerifier: - def __init__(self, address: str, bytecode: str, source_code: str, compiler_version: str): - self.address = address - self.bytecode = bytecode - self.source_code = source_code - self.compiler_version = compiler_version - - def verify(self, explorer: str) -> bool: - if explorer.lower() == "blockscout": - return self._verify_blockscout() - elif explorer.lower() == "etherscan": - return self._verify_etherscan() - else: - raise ValueError(f"Unsupported explorer: {explorer}") + def __init__(self, compiler_data, filename=None): + self.compiler_data = compiler_data - def _verify_blockscout(self) -> bool: - api_key = os.getenv('BLOCKSCOUT_API_KEY') + # force compilation so that if there are any errors in the contract, + # we fail at load rather than at deploy time. + with anchor_settings(self.compiler_data.settings): + _ = compiler_data.bytecode, compiler_data.bytecode_runtime + + self.filename = filename + + def __call__(self, *args, **kwargs): + return self.deploy(*args, **kwargs) + + @staticmethod + def _post_verification_request(url: str, json_data: dict, headers: dict) -> dict: + try: + response = requests.post(url, json=json_data, headers=headers) + if response.status_code != 200: + print(f"Request failed with status code: {response.status_code}") + return {"status": "0", "message": "Request failed"} + return response.json() + except Exception as e: + print(f"Error during contract verification: {e}") + return {"status": "0", "message": str(e)} + + @staticmethod + def validate_blockscout(address: str, bytecode: str, source_code: str, compiler_version: str) -> bool: + api_key = os.getenv('BLOCKSCOUT_API_KEY') if not api_key: raise ValueError("BLOCKSCOUT_API_KEY not set in environment variables") - url = os.getenv('BLOCKSCOUT_API_URL', 'https://blockscout.com/poa/core/api') + url = f"https://blockscout.com/api/v2/smart-contracts/{address}/verification/via/standard-input" standard_json_input = { "language": "Vyper", "sources": { "contract.vy": { - "content": self.source_code + "content": source_code } }, "settings": { - "optimizer": { - "enabled": True - }, - "outputSelection": { - "*": ["evm.bytecode", "evm.deployedBytecode", "abi"] - } + "optimizer": {"enabled": True}, + "outputSelection": {"*": ["evm.bytecode", "evm.deployedBytecode", "abi"]} } } - params = { - "module": "contract", - "action": "verifysourcecode", - "addressHash": self.address, - "contractSourceCode": json.dumps(standard_json_input), - "name": "VerifiedContract", - "compilerVersion": self.compiler_version, - "optimization": "true", - "apikey": api_key + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}' } - response = requests.post(url, params=params) - result = response.json() - return result.get("status") == "1" - def _verify_etherscan(self) -> bool: + result = VyperDeployer._post_verification_request(url, standard_json_input, headers) + if result.get("status") == "1": + print("Contract verified successfully on Blockscout") + return True + else: + print(f"Contract verification failed: {result.get('message')}") + return False + + @staticmethod + def validate_etherscan(address: str, bytecode: str, source_code: str, compiler_version: str) -> bool: api_key = os.getenv('ETHERSCAN_API_KEY') if not api_key: raise ValueError("ETHERSCAN_API_KEY not set in environment variables") @@ -132,72 +143,69 @@ def _verify_etherscan(self) -> bool: "language": "Vyper", "sources": { "contract.vy": { - "content": self.source_code + "content": source_code } }, "settings": { - "optimizer": { - "enabled": True - }, - "outputSelection": { - "*": ["evm.bytecode", "evm.deployedBytecode", "abi"] - } + "optimizer": {"enabled": True}, + "outputSelection": {"*": ["evm.bytecode", "evm.deployedBytecode", "abi"]} } } params = { "module": "contract", "action": "verifysourcecode", - "contractaddress": self.address, + "contractaddress": address, "sourceCode": json.dumps(standard_json_input), "codeformat": "solidity-standard-json-input", "contractname": "contract.vy:VerifiedContract", - "compilerversion": self.compiler_version, + "compilerversion": compiler_version, "optimizationUsed": "1", "apikey": api_key } - response = requests.post(url, params=params) - result = response.json() - return result.get("status") == "1" - -class VyperDeployer: - create_compiler_data = CompilerData # this may be a different class in plugins - def __init__(self, compiler_data, filename=None): - self.compiler_data = compiler_data + headers = {'Content-Type': 'application/json'} + result = VyperDeployer._post_verification_request(url, params, headers) + if result.get("status") == "1": + print("Contract verified successfully on Etherscan") + return True + else: + print(f"Contract verification failed: {result.get('message')}") + return False - # force compilation so that if there are any errors in the contract, - # we fail at load rather than at deploy time. - with anchor_settings(self.compiler_data.settings): - _ = compiler_data.bytecode, compiler_data.bytecode_runtime + contract_verifiers = { + "blockscout": validate_blockscout, + "etherscan": validate_etherscan + } - self.filename = filename - - def __call__(self, *args, **kwargs): - return self.deploy(*args, **kwargs) + def verify_contract(self, address: str, bytecode: str, source_code: str, compiler_version: str, explorer: str) -> bool: + try: + verifier = self.contract_verifiers[explorer] + except KeyError: + raise ValueError(f"Unsupported explorer: {explorer}") + + return verifier(address, bytecode, source_code, compiler_version) - def deploy(self, *args, verify: bool = False, explorer: Optional[str] = None, **kwargs): - return VyperContract( + def deploy(self, *args, explorer: Optional[str] = None, **kwargs): + contract = VyperContract( self.compiler_data, *args, filename=self.filename, **kwargs ) - - if verify: - if not explorer: - raise ValueError("Explorer is required for verification") - verifier = ContractVerifier( - contract.address, - contract.bytecode, - self.compiler_data.source_code, - f"v{vyper.__version__}" + + if explorer: + verification_result = self.verify_contract( + address=contract.address, + bytecode=contract.bytecode, + source_code=self.compiler_data.source_code, + compiler_version=f"v{vyper.__version__}", + explorer=explorer ) - verification_result = verifier.verify(explorer) if verification_result: print(f"Contract verified successfully on {explorer}") else: print(f"Contract verification failed on {explorer}") return contract - + def deploy_as_blueprint(self, *args, **kwargs): return VyperBlueprint( self.compiler_data, *args, filename=self.filename, **kwargs @@ -239,7 +247,7 @@ def _constants(self): # Make constants available at compile time. Useful for testing. See #196 return ConstantsModel(self.compiler_data) - + # a few lines of shared code between VyperBlueprint and VyperContract class _BaseVyperContract(_BaseEVMContract): def __init__( diff --git a/boa/interpret.py b/boa/interpret.py index 113272bd..5b0fabff 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -189,22 +189,7 @@ def loads( if as_blueprint: return d.deploy_as_blueprint(**kwargs) else: - return d.deploy(*args, **kwargs) - if verify: - if not explorer: - raise ValueError("Explorer is required for verification") - verifier = ContractVerifier( - contract.address, - contract.bytecode, - source_code, - f"v{vyper.__version__}" - ) - verification_result = verifier.verify(explorer) - if verification_result: - print(f"Contract verified successfully on {explorer}") - else: - print(f"Contract verification failed on {explorer}") - return contract + return d.deploy(*args, **kwargs) diff --git a/docs/source/api.rst b/docs/source/api.rst index b91c9214..dd18eb6d 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -11,13 +11,12 @@ High-Level Functionality The global environment object. -.. function:: load(fp: str, *args: Any, verify: bool = False, explorer: str | None = None, **kwargs: Any) -> VyperContract | VyperBlueprint +.. function:: load(fp: str, *args: Any, explorer: str | None = None, **kwargs: Any) -> VyperContract | VyperBlueprint Compile source from disk, deploy the contract, and optionally verify it on a block explorer. :param fp: The contract source code file path. - :param args: Contract constructor arguments. - :param verify: Whether to verify the contract on a block explorer after deployment. + :param args: Contract constructor arguments. :param explorer: The block explorer to use for verification ("blockscout" or "etherscan"). :param kwargs: Keyword arguments to pass to the :py:func:`loads` function. @@ -40,7 +39,7 @@ High-Level Functionality >>> import boa >>> from vyper.compiler.settings import OptimizationLevel, Settings - >>> contract = boa.load("Foo.vy", compiler_args={"settings": Settings(optimize=OptimizationLevel.CODESIZE)}, verify=True, explorer="blockscout") + >>> contract = boa.load("Foo.vy", compiler_args={"settings": Settings(optimize=OptimizationLevel.CODESIZE)}, explorer="blockscout") >>> contract Contract verified successfully on blockscout @@ -53,8 +52,7 @@ High-Level Functionality :param args: Contract constructor arguments. :param as_blueprint: Whether to deploy an :eip:`5202` blueprint of the compiled contract. :param name: The name of the contract. - :param compiler_args: Argument to be passed to the Vyper compiler. - :param verify: Whether to verify the contract on a block explorer after deployment. + :param compiler_args: Argument to be passed to the Vyper compiler. :param explorer: The block explorer to use for verification ("blockscout" or "etherscan"). :param kwargs: Keyword arguments to pass to the :py:class:`VyperContract` or :py:class:`VyperBlueprint` ``__init__`` method. @@ -697,46 +695,6 @@ Low-Level Functionality .. property:: deployer :type: VyperDeployer -.. class:: ContractVerifier - - A utility class for verifying deployed contracts on block explorers. - - .. method:: __init__(address: str, bytecode: str, source_code: str, compiler_version: str) - - Initialize a ContractVerifier instance. - - :param address: The address of the deployed contract. - :param bytecode: The bytecode of the deployed contract. - :param source_code: The source code of the contract. - :param compiler_version: The version of the Vyper compiler used. - - .. method:: verify(explorer: str) -> bool - - Verify the contract on the specified block explorer. - - :param explorer: The block explorer to use for verification ("blockscout" or "etherscan"). - :returns: True if verification was successful, False otherwise. - - .. rubric:: Example - - .. code-block:: python - - >>> import boa - >>> src = """ - ... @external - ... def main(): - ... pass - ... """ - >>> contract = boa.loads(src) - >>> verifier = boa.ContractVerifier( - ... contract.address, - ... contract.bytecode, - ... src, - ... f"v{boa.vyper.__version__}" - ... ) - >>> verification_result = verifier.verify("blockscout") - >>> print(verification_result) - True .. note:: @@ -744,7 +702,6 @@ Low-Level Functionality - `BLOCKSCOUT_API_KEY`: Your API key for Blockscout - `ETHERSCAN_API_KEY`: Your API key for Etherscan - - `BLOCKSCOUT_API_URL` (optional): Custom API URL for Blockscout - `ETHERSCAN_API_URL` (optional): Custom API URL for Etherscan Make sure these environment variables are set before using the verification features. diff --git a/tests/integration/test_verify_blockscout.py b/tests/integration/test_verify_blockscout.py new file mode 100644 index 00000000..7515b87e --- /dev/null +++ b/tests/integration/test_verify_blockscout.py @@ -0,0 +1,37 @@ +import pytest +from unittest.mock import patch, MagicMock +import boa + +def test_contract_verification(): + """ + Tests the smart contract verification process. It simulates both successful and failed verification scenarios + using mocked API responses from Blockscout. + """ + code = """ + @external + def hello() -> String[32]: + return "Hello, World!" + """ + + mock_response = MagicMock() + mock_response.json.return_value = {"status": "1"} + mock_response.status_code = 200 + + with patch('requests.post', return_value=mock_response), \ + patch.dict('os.environ', {'BLOCKSCOUT_API_KEY': '...'}): + contract = boa.loads(code, explorer="blockscout") + + assert isinstance(contract, boa.contracts.vyper.vyper_contract.VyperContract), "Contract deployment failed or returned an incorrect type" + assert contract.hello() == "Hello, World!", "Contract function 'hello()' returned an unexpected result" + + # Simulate a failed verification response + mock_response.json.return_value = {"status": "0", "message": "Verification failed"} + mock_response.status_code = 400 + with patch('requests.post', return_value=mock_response), \ + patch.dict('os.environ', {'BLOCKSCOUT_API_KEY': '...'}): + contract = boa.loads(code, explorer="blockscout") + # Add appropriate checks for failed verification + + # Test missing API key scenario + with patch.dict('os.environ', {}, clear=True): + contract = boa.loads(code, explorer="blockscout") \ No newline at end of file diff --git a/tests/unitary/contracts/vyper/test_vyper_contract.py b/tests/unitary/contracts/vyper/test_vyper_contract.py index 28c425f4..e7809926 100644 --- a/tests/unitary/contracts/vyper/test_vyper_contract.py +++ b/tests/unitary/contracts/vyper/test_vyper_contract.py @@ -73,43 +73,4 @@ def foo() -> bool: """ c = boa.loads(code) - c.foo() - - -@pytest.mark.skipif(not os.getenv("BLOCKSCOUT_API_KEY"), reason="BLOCKSCOUT_API_KEY not set") -def test_contract_verification(): - """ - This test case rigorously examines the contract verification process. - It deploys a rudimentary smart contract and subsequently attempts to verify it, - leveraging mock API responses to simulate both successful and failed verifications. - """ - code = """ -@external -def hello() -> String[32]: - return "Hello, World!" -""" - # Fabricate a mock response emulating Blockscout's API - mock_response = MagicMock() - mock_response.json.return_value = {"status": "1"} - - # Employ a context manager to intercept and replace the requests.post function - with patch('requests.post', return_value=mock_response): - # Instantiate and deploy the contract with immediate verification - contract = boa.loads(code, verify=True, explorer="blockscout") - - # Ascertain the contract's successful deployment and correct type - assert isinstance(contract, boa.VyperContract), "Contract deployment failed or yielded unexpected type" - - # Validate the contract's functionality post-deployment - assert contract.hello() == "Hello, World!", "Contract function 'hello()' produced unexpected output" - - # Note: Verification success message should be logged or returned. - # Adapt the following assertion based on your implementation's specific output mechanism - # assert "Contract verified successfully" in captured_output - - # Scrutinize the system's behavior when confronted with a failed verification scenario - mock_response.json.return_value = {"status": "0"} - with patch('requests.post', return_value=mock_response): - with pytest.raises(Exception) as excinfo: - boa.loads(code, verify=True, explorer="blockscout") - assert "Contract verification failed" in str(excinfo.value), "Expected exception not raised or incorrect error message" + c.foo() \ No newline at end of file