Skip to content

Commit

Permalink
chore(review): rewrite the code, with the suggestions from the review…
Browse files Browse the repository at this point in the history
… and redo the test and add it to the integrations folder
  • Loading branch information
developerfred committed Sep 6, 2024
1 parent c7aea18 commit fab7d43
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 180 deletions.
162 changes: 85 additions & 77 deletions boa/contracts/vyper/vyper_contract.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -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__(
Expand Down
17 changes: 1 addition & 16 deletions boa/interpret.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)



Expand Down
51 changes: 4 additions & 47 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
<tmp/Foo.vy at 0xf2Db9344e9B01CB353fe7a2d076ae34A9A442513, compiled with ...>
Contract verified successfully on blockscout
Expand All @@ -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.

Expand Down Expand Up @@ -697,54 +695,13 @@ 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::

To use the contract verification functionality, you need to set the following environment variables:

- `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.
Expand Down
37 changes: 37 additions & 0 deletions tests/integration/test_verify_blockscout.py
Original file line number Diff line number Diff line change
@@ -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")
Loading

0 comments on commit fab7d43

Please sign in to comment.