-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add contract verification with standard json input #304
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,9 @@ | |
|
||
import contextlib | ||
import copy | ||
import json | ||
import os | ||
import requests | ||
import warnings | ||
from dataclasses import dataclass | ||
from functools import cached_property | ||
|
@@ -85,10 +88,14 @@ def __init__(self, compiler_data, filename=None): | |
def __call__(self, *args, **kwargs): | ||
return self.deploy(*args, **kwargs) | ||
|
||
def deploy(self, *args, **kwargs): | ||
return VyperContract( | ||
def deploy(self, *args, verify=False, **kwargs): | ||
contract = VyperContract( | ||
self.compiler_data, *args, filename=self.filename, **kwargs | ||
) | ||
if verify: | ||
print("Verifying vyper contract with default args") | ||
contract.verify() | ||
return contract | ||
|
||
def deploy_as_blueprint(self, *args, **kwargs): | ||
return VyperBlueprint( | ||
|
@@ -924,6 +931,128 @@ def eval( | |
|
||
return self.marshal_to_python(c, typ) | ||
|
||
def verify( | ||
self, | ||
explorer: Optional[str] = None, | ||
api_key: Optional[str] = None, | ||
) -> bool: | ||
"""verify vyper contract code in given explorer with given api_key""" | ||
|
||
# prioritize on given args, if not we will load the following in orders | ||
# - load etherscan api_key from ENV if present | ||
# - load blockscout url from ENV if present | ||
# - throw an error if none of those was provided | ||
ETHERSCAN_API_KEY = os.getenv("ETHERSCAN_API_KEY") | ||
BLOCKSCOUT_API_URL = os.getenv("BLOCKSCOUT_API_URL") | ||
api_key = api_key if api_key is not None else ETHERSCAN_API_KEY | ||
has_etherscan = bool(api_key) | ||
has_blockscout = bool(BLOCKSCOUT_API_URL) | ||
|
||
if explorer == 'etherscan': | ||
assert has_etherscan, "API key was not provided!" | ||
return self._verify_etherscan(api_key=api_key) | ||
elif explorer == 'blockscout': | ||
assert has_blockscout, "API URL was not provided!" | ||
return self._verify_blockscout() | ||
else: | ||
assert has_etherscan or has_blockscout, "None of those ENV were provided!" | ||
is_etherscan = self._verify_etherscan(api_key=api_key) if has_etherscan else False | ||
is_blockscout = self._verify_blockscout() if has_blockscout else False | ||
return is_etherscan or is_blockscout | ||
|
||
def _verify_etherscan(self, api_key: str) -> bool: | ||
# get API endpoint of etherscan-liked block explorer, e.g. BSC blockchain explorer | ||
api_endpoint = os.getenv("ETHERSCAN_API_URL", "https://api.etherscan.io/api") | ||
|
||
# constructing contract source code in JSON format | ||
source_code = json.dumps({ | ||
"language": "Vyper", | ||
"sources": { | ||
self.filename: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this only supports a single file There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As in a single contract |
||
"content": self.compiler_data.source_code | ||
} | ||
}, | ||
"settings": self.compiler_data.settings | ||
}) | ||
|
||
# constructing the request body for verification | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. commented out code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I commented it out for now since it's not working yet, I'm not sure whether you read the description or not |
||
# body = { | ||
# "module": "contract", | ||
# "action": "verifysourcecode", | ||
# "apikey": api_key | ||
# "chainId": "0x01", | ||
# "codeformat": "solidity-standard-json-input", | ||
# "sourceCode": source_code, | ||
# "constructorArguements": self._ctor.bytecode.decode("utf-8") if self._ctor is not None else "", | ||
# "contractaddress": str(self.address), | ||
# "contractname": f"{self.filename}:{self.contract_name}", | ||
# "compilerversion": self.compiler_data.settings.compiler_version, | ||
# } | ||
body = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. really? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeahhh! |
||
|
||
response = requests.post( | ||
api_endpoint, | ||
headers={"Content-Type":"multipart/form-data"}, | ||
json=body | ||
) | ||
|
||
if response.status_code == 200: | ||
print(f"Successfully verified contract: {self.contract_name} at addresss: {self.address}") | ||
return True | ||
else: | ||
print(f"Failed to verify contract: {self.contract_name} at addresss: {self.address}") | ||
return False | ||
|
||
|
||
def _verify_blockscout(self) -> bool: | ||
# get API endpoint for blockscout explorer | ||
api_url = os.getenv("BLOCKSCOUT_API_URL", "https://eth.blockscout.com") | ||
api_endpoint = f"{api_url}/api/v2/smart-contracts/{str(self.address).lower()}/verification/via/vyper-standard-input" | ||
# print(f"API endpoint: {api_endpoint}") | ||
|
||
# constructing contract source code in JSON format | ||
filename = f"{self.contract_name}.vy" if self.filename is None or self.filename == "<unknown>" else self.filename | ||
standard_input_str = json.dumps({ | ||
"language": "Vyper", | ||
"sources": { | ||
filename: { | ||
"content": self.compiler_data.source_code | ||
} | ||
}, | ||
}, indent=4) | ||
files = { | ||
"files[0]": (filename, standard_input_str.encode("utf-8"), "application/json") | ||
} | ||
# print(f"Standard json input: {files}") | ||
|
||
# constructing the request body for verification | ||
settings = copy.copy(self.compiler_data.settings) | ||
data = { | ||
"compiler_version": settings.compiler_version if settings.compiler_version is not None else "v0.4.0+commit.e9db8d9f", | ||
"license_type": "none", | ||
"evm_version": settings.evm_version if settings.evm_version is not None else "cancun", | ||
} | ||
# print(f"Data: {data}") | ||
|
||
try: | ||
response = requests.post( | ||
api_endpoint, | ||
data=data, | ||
files=files, | ||
) | ||
# print(f"The whole response object: {response.json()}") | ||
|
||
if response.status_code == 200: | ||
print(f"Successfully verified contract: {self.contract_name} at addresss: {self.address}") | ||
return True | ||
else: | ||
print(f"Failed to verify contract: {self.contract_name} at addresss: {self.address}") | ||
return False | ||
except Exception as error: | ||
hangleang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
print(f"Error during verification: {error}") | ||
return False | ||
|
||
|
||
# inject a function into this VyperContract without affecting the | ||
# contract's source code. useful for testing private functionality | ||
def inject_function(self, fn_source_code, force=False): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,10 @@ def simple_contract(): | |
return boa.loads(code, STARTING_SUPPLY) | ||
|
||
|
||
def test_blockscout_verify(simple_contract): | ||
assert simple_contract.verify(explorer='blockscout') | ||
|
||
|
||
def test_env_type(): | ||
# sanity check | ||
assert isinstance(boa.env, NetworkEnv) | ||
|
@@ -40,18 +44,17 @@ def test_env_type(): | |
def test_total_supply(simple_contract): | ||
assert simple_contract.totalSupply() == STARTING_SUPPLY | ||
|
||
# NOTE: comment these fuzz tests for now to test verifying contract | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what? besides not adding tests, this is disabling existing ones? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for comment is to reduce cost on fuzz test txn, as I try to make sure the verification really works, will enable it back soon if you want. You might prefer LLM code than the hands-written one |
||
# @pytest.mark.parametrize("amount", [0, 1, 100]) | ||
# def test_update_total_supply(simple_contract, amount): | ||
# orig_supply = simple_contract.totalSupply() | ||
# simple_contract.update_total_supply(amount) | ||
# assert simple_contract.totalSupply() == orig_supply + amount | ||
|
||
@pytest.mark.parametrize("amount", [0, 1, 100]) | ||
def test_update_total_supply(simple_contract, amount): | ||
orig_supply = simple_contract.totalSupply() | ||
simple_contract.update_total_supply(amount) | ||
assert simple_contract.totalSupply() == orig_supply + amount | ||
|
||
|
||
@pytest.mark.parametrize("amount", [0, 1, 100]) | ||
def test_raise_exception(simple_contract, amount): | ||
with boa.reverts("oh no!"): | ||
simple_contract.raise_exception(amount) | ||
|
||
# @pytest.mark.parametrize("amount", [0, 1, 100]) | ||
# def test_raise_exception(simple_contract, amount): | ||
# with boa.reverts("oh no!"): | ||
# simple_contract.raise_exception(amount) | ||
|
||
# XXX: probably want to test deployment revert behavior |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
verify could be a list of explorer names
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I follow the reference issue linked in the description