From ff7e671a127451cb125f3536250b3240840288bb Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 12 Sep 2024 13:45:18 -0400 Subject: [PATCH 01/18] feat(draft): add deployment logs to network mode --- boa/deployments.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++ boa/network.py | 26 +++++++++++++-- 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 boa/deployments.py diff --git a/boa/deployments.py b/boa/deployments.py new file mode 100644 index 00000000..a2ccbd39 --- /dev/null +++ b/boa/deployments.py @@ -0,0 +1,81 @@ +from dataclasses import dataclass, asdict +from boa.util.open_ctx import Open +from pathlib import Path +from typing import Optional,Any +import json +import sqlite3 +from boa.util.abi import Address + +@dataclass(frozen=True) +class Deployment: + contract_address: Address + name: str + rpc: str + from_: Address + tx_hash: str + broadcast_ts: float + tx_dict: dict # raw tx fields + receipt_dict: dict # raw receipt fields + source_code: Optional[Any] # optional source code or bundle + + def sql_values(self): + ret = asdict(self) + ret["contract_address"] = str(ret["contract_address"]) + ret["from_"] = str(ret["from_"]) + ret["tx_hash"] = ret["tx_hash"] + ret["tx_dict"] = json.dumps(ret["tx_dict"]) + ret["receipt_dict"] = json.dumps(ret["receipt_dict"]) + if ret["source_code"] is not None: + ret["source_code"] = json.dumps(ret["source_code"]) + return ret + + +_CREATE_CMD = """ +CREATE TABLE IF NOT EXISTS + deployments( + contract_address text, + name text, + rpc text, + tx_hash text, + from_ text, + tx_dict text, + broadcast_ts real, + receipt_dict text, + source_code text + ); +""" + +class DeploymentsDB: + def __init__(self, path="./.boa/deployments.db"): + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + + # once 3.12 is min version, use autocommit=True + self.db = sqlite3.connect(path) + + self.db.execute(_CREATE_CMD) + + def __del__(self): + self.db.close() + + def insert_deployment(self, deployment: Deployment): + values = deployment.sql_values() + + values_placeholder = ",".join(["?"] * len(values)) + + insert_cmd = f"INSERT INTO deployments VALUES({values_placeholder});" + + self.db.execute(insert_cmd, tuple(values.values())) + self.db.commit() + +_db: Optional[DeploymentsDB] = None + +def set_deployments_db(db: Optional[DeploymentsDB]): + def set_(db): + global _db + _db = db + return Open(get_deployments_db, set_, db) + +def get_deployments_db(): + global _db + return _db diff --git a/boa/network.py b/boa/network.py index 9deb0860..10bedcb2 100644 --- a/boa/network.py +++ b/boa/network.py @@ -1,5 +1,6 @@ # an Environment which interacts with a real (prod or test) chain import contextlib +import time import warnings from dataclasses import dataclass from functools import cached_property @@ -8,6 +9,7 @@ from eth_account import Account from requests.exceptions import HTTPError +from boa.deployments import Deployment, get_deployments_db from boa.environment import Env, _AddressType from boa.rpc import ( RPC, @@ -152,6 +154,7 @@ def __init__( nickname: str = None, accounts: dict[str, Account] = None, fork_try_prefetch_state=True, + deployments_db=None, **kwargs, ): super().__init__(fork_try_prefetch_state, **kwargs) @@ -300,7 +303,7 @@ def execute_code( if is_modifying: try: - receipt, trace = self._send_txn( + txdata, receipt, trace = self._send_txn( from_=sender, to=to_address, value=value, gas=gas, data=hexdata ) except _EstimateGasFailed: @@ -375,7 +378,9 @@ def deploy( bytecode = to_hex(bytecode) sender = self._check_sender(self._get_sender(sender)) - receipt, trace = self._send_txn( + broadcast_ts = time.time() + + txdata, receipt, trace = self._send_txn( from_=sender, value=value, gas=gas, data=bytecode ) @@ -397,6 +402,21 @@ def deploy( # TODO get contract info in here print(f"contract deployed at {create_address}") + if (deployments_db := get_deployments_db()) is not None: + contract_name = getattr(contract, "contract_name", None) + deployment_data = Deployment( + create_address, + contract_name, + self._rpc.identifier, + sender, + receipt["transactionHash"], + broadcast_ts, + txdata, + receipt, + None, + ) + deployments_db.insert_deployment(deployment_data) + return create_address, computation @cached_property @@ -538,7 +558,7 @@ def _send_txn(self, from_, to=None, gas=None, value=None, data=None): self._reset_fork(block_identifier=receipt["blockNumber"]) t_obj = TraceObject(trace) if trace is not None else None - return receipt, t_obj + return tx_data, receipt, t_obj def get_chain_id(self) -> int: """Get the current chain ID of the network as an integer.""" From 08e2ec69c4137f5965d819269b0b5072f2da8453 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 13 Sep 2024 22:47:33 -0400 Subject: [PATCH 02/18] remove dead kwarg --- boa/network.py | 1 - 1 file changed, 1 deletion(-) diff --git a/boa/network.py b/boa/network.py index 10bedcb2..cfb3edf6 100644 --- a/boa/network.py +++ b/boa/network.py @@ -154,7 +154,6 @@ def __init__( nickname: str = None, accounts: dict[str, Account] = None, fork_try_prefetch_state=True, - deployments_db=None, **kwargs, ): super().__init__(fork_try_prefetch_state, **kwargs) From fd43f05a3d17fe5e03b17bf473e3ff3008b20d93 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 18 Sep 2024 09:06:15 -0400 Subject: [PATCH 03/18] set default db path to memory --- boa/deployments.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index a2ccbd39..b6ffc94e 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -46,9 +46,10 @@ def sql_values(self): """ class DeploymentsDB: - def __init__(self, path="./.boa/deployments.db"): - path = Path(path) - path.parent.mkdir(parents=True, exist_ok=True) + def __init__(self, path=":memory:"): + if path != ":memory:": + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) # once 3.12 is min version, use autocommit=True self.db = sqlite3.connect(path) From fd4b5bfe05a79992788ade8e7d7448af418d81ec Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 18 Sep 2024 21:47:20 -0400 Subject: [PATCH 04/18] add get_deployments, get_deployments_from_sql --- boa/deployments.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index b6ffc94e..c11bbc13 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, asdict +from dataclasses import dataclass, asdict, fields from boa.util.open_ctx import Open from pathlib import Path from typing import Optional,Any @@ -22,13 +22,25 @@ def sql_values(self): ret = asdict(self) ret["contract_address"] = str(ret["contract_address"]) ret["from_"] = str(ret["from_"]) - ret["tx_hash"] = ret["tx_hash"] ret["tx_dict"] = json.dumps(ret["tx_dict"]) ret["receipt_dict"] = json.dumps(ret["receipt_dict"]) if ret["source_code"] is not None: ret["source_code"] = json.dumps(ret["source_code"]) return ret + @classmethod + def from_sql_tuple(cls, values): + assert len(values) == len(fields(cls)) + ret = dict(zip([field.name for field in fields(cls)], values)) + ret["contract_address"] = Address(ret["contract_address"]) + ret["from_"] = Address(ret["from_"]) + ret["tx_dict"] = json.loads(ret["tx_dict"]) + ret["receipt_dict"] = json.loads(ret["receipt_dict"]) + if ret["source_code"] is not None: + ret["source_code"] = json.loads(ret["source_code"]) + return ret + + _CREATE_CMD = """ CREATE TABLE IF NOT EXISTS @@ -64,11 +76,20 @@ def insert_deployment(self, deployment: Deployment): values_placeholder = ",".join(["?"] * len(values)) - insert_cmd = f"INSERT INTO deployments VALUES({values_placeholder});" + insert_cmd = f"INSERT INTO deployments VALUES({values_placeholder})" self.db.execute(insert_cmd, tuple(values.values())) self.db.commit() + def get_deployments_from_sql(self, sql_query: str, parameters=(), /): + cur = self.db.execute(sql_query, parameters=parameters) + ret = [Deployment.from_sql_tuple(item) for item in cur.fetchall()] + return ret + + + def get_deployments(self) -> list[Deployment]: + return self.get_deployments_from_sql("SELECT * FROM deployments") + _db: Optional[DeploymentsDB] = None def set_deployments_db(db: Optional[DeploymentsDB]): From d581d33cc1cbc2f845286d08a8d82d9dbaa8776f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 18 Sep 2024 21:49:07 -0400 Subject: [PATCH 05/18] fix ctor --- boa/deployments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index c11bbc13..056b29b8 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -38,7 +38,7 @@ def from_sql_tuple(cls, values): ret["receipt_dict"] = json.loads(ret["receipt_dict"]) if ret["source_code"] is not None: ret["source_code"] = json.loads(ret["source_code"]) - return ret + return cls(**ret) @@ -82,7 +82,7 @@ def insert_deployment(self, deployment: Deployment): self.db.commit() def get_deployments_from_sql(self, sql_query: str, parameters=(), /): - cur = self.db.execute(sql_query, parameters=parameters) + cur = self.db.execute(sql_query, parameters) ret = [Deployment.from_sql_tuple(item) for item in cur.fetchall()] return ret From 361228d57264c7850cad34c51b1662735b8d8007 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 18 Sep 2024 21:54:18 -0400 Subject: [PATCH 06/18] fix lint --- boa/deployments.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index 056b29b8..93810e24 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -1,10 +1,12 @@ -from dataclasses import dataclass, asdict, fields -from boa.util.open_ctx import Open -from pathlib import Path -from typing import Optional,Any import json import sqlite3 +from dataclasses import asdict, dataclass, fields +from pathlib import Path +from typing import Any, Optional + from boa.util.abi import Address +from boa.util.open_ctx import Open + @dataclass(frozen=True) class Deployment: @@ -41,7 +43,6 @@ def from_sql_tuple(cls, values): return cls(**ret) - _CREATE_CMD = """ CREATE TABLE IF NOT EXISTS deployments( @@ -57,6 +58,7 @@ def from_sql_tuple(cls, values): ); """ + class DeploymentsDB: def __init__(self, path=":memory:"): if path != ":memory:": @@ -86,18 +88,21 @@ def get_deployments_from_sql(self, sql_query: str, parameters=(), /): ret = [Deployment.from_sql_tuple(item) for item in cur.fetchall()] return ret - def get_deployments(self) -> list[Deployment]: return self.get_deployments_from_sql("SELECT * FROM deployments") + _db: Optional[DeploymentsDB] = None + def set_deployments_db(db: Optional[DeploymentsDB]): def set_(db): global _db _db = db + return Open(get_deployments_db, set_, db) + def get_deployments_db(): global _db return _db From cff75aa888071a7f5ddc3a4e5cd60b9829ea65a1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 18 Sep 2024 21:55:14 -0400 Subject: [PATCH 07/18] make _get_deployments_from_sql private --- boa/deployments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/deployments.py b/boa/deployments.py index 93810e24..67368f11 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -83,7 +83,7 @@ def insert_deployment(self, deployment: Deployment): self.db.execute(insert_cmd, tuple(values.values())) self.db.commit() - def get_deployments_from_sql(self, sql_query: str, parameters=(), /): + def _get_deployments_from_sql(self, sql_query: str, parameters=(), /): cur = self.db.execute(sql_query, parameters) ret = [Deployment.from_sql_tuple(item) for item in cur.fetchall()] return ret From 61208f219e4fe1b5a78d8a266e7a971300382d7c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 22 Sep 2024 14:10:29 -0400 Subject: [PATCH 08/18] add json to Deployment class --- boa/deployments.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/boa/deployments.py b/boa/deployments.py index 67368f11..394fe235 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -30,6 +30,11 @@ def sql_values(self): ret["source_code"] = json.dumps(ret["source_code"]) return ret + def to_json(self): + ret = self.sql_values() + ret["from"] = ret.pop("from_") + return ret + @classmethod def from_sql_tuple(cls, values): assert len(values) == len(fields(cls)) From 4087502ce62caefb7d9db7c66a7b3042f405dc93 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 22 Sep 2024 14:16:48 -0400 Subject: [PATCH 09/18] rename standard-json to solc_json --- boa/contracts/vyper/vyper_contract.py | 4 ++-- boa/verifiers.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index c506944e..e2e7acec 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -126,9 +126,9 @@ def at(self, address: Any) -> "VyperContract": return ret @cached_property - def standard_json(self): + def solc_json(self): """ - Generates a standard JSON representation of the Vyper contract. + Generates a solc "standard json" representation of the Vyper contract. """ return build_solc_json(self.compiler_data) diff --git a/boa/verifiers.py b/boa/verifiers.py index bf0db3f0..88755275 100644 --- a/boa/verifiers.py +++ b/boa/verifiers.py @@ -36,7 +36,7 @@ def verify( self, address: Address, contract_name: str, - standard_json: dict, + solc_json: dict, license_type: str = None, wait: bool = False, ) -> Optional["VerificationResult"]: @@ -44,7 +44,7 @@ def verify( Verify the Vyper contract on Blockscout. :param address: The address of the contract. :param contract_name: The name of the contract. - :param standard_json: The standard JSON output of the Vyper compiler. + :param solc_json: The solc_json output of the Vyper compiler. :param license_type: The license to use for the contract. Defaults to "none". :param wait: Whether to return a VerificationResult immediately or wait for verification to complete. Defaults to False @@ -57,13 +57,13 @@ def verify( url = f"{self.uri}/api/v2/smart-contracts/{address}/" url += f"verification/via/vyper-standard-input?apikey={api_key}" data = { - "compiler_version": standard_json["compiler_version"], + "compiler_version": solc_json["compiler_version"], "license_type": license_type, } files = { "files[0]": ( contract_name, - json.dumps(standard_json).encode("utf-8"), + json.dumps(solc_json).encode("utf-8"), "application/json", ) } @@ -148,15 +148,13 @@ def verify(contract, verifier=None, license_type: str = None) -> VerificationRes if verifier is None: verifier = get_verifier() - if not hasattr(contract, "deployer") or not hasattr( - contract.deployer, "standard_json" - ): + if not hasattr(contract, "deployer") or not hasattr(contract.deployer, "solc_json"): raise ValueError(f"Not a contract! {contract}") address = contract.address return verifier.verify( address=address, - standard_json=contract.deployer.standard_json, + solc_json=contract.deployer.solc_json, contract_name=contract.contract_name, license_type=license_type, ) From b81fb4b3581cfd3fd3210dee079dea68aceb9c38 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 22 Sep 2024 14:18:04 -0400 Subject: [PATCH 10/18] add source bundle to deployments db --- boa/network.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/boa/network.py b/boa/network.py index cfb3edf6..49dd9406 100644 --- a/boa/network.py +++ b/boa/network.py @@ -403,6 +403,7 @@ def deploy( if (deployments_db := get_deployments_db()) is not None: contract_name = getattr(contract, "contract_name", None) + source_bundle = getattr(contract, "solc_json", None) deployment_data = Deployment( create_address, contract_name, @@ -412,7 +413,7 @@ def deploy( broadcast_ts, txdata, receipt, - None, + source_bundle, ) deployments_db.insert_deployment(deployment_data) From 512962a9803520a20c1ac023f759cc2040d245fe Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 22 Sep 2024 23:38:10 -0400 Subject: [PATCH 11/18] fix up code, add tests --- boa/deployments.py | 34 ++++++++++++------- boa/network.py | 6 ++-- boa/util/open_ctx.py | 4 +-- boa/verifiers.py | 21 +++++++++--- .../network/anvil/test_network_env.py | 28 +++++++++++++++ 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index 394fe235..301e624b 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -13,7 +13,7 @@ class Deployment: contract_address: Address name: str rpc: str - from_: Address + deployer: Address tx_hash: str broadcast_ts: float tx_dict: dict # raw tx fields @@ -22,25 +22,33 @@ class Deployment: def sql_values(self): ret = asdict(self) - ret["contract_address"] = str(ret["contract_address"]) - ret["from_"] = str(ret["from_"]) + # sqlite doesn't have json, just dump to string ret["tx_dict"] = json.dumps(ret["tx_dict"]) ret["receipt_dict"] = json.dumps(ret["receipt_dict"]) if ret["source_code"] is not None: ret["source_code"] = json.dumps(ret["source_code"]) return ret - def to_json(self): - ret = self.sql_values() - ret["from"] = ret.pop("from_") - return ret + def to_dict(self): + """ + Convert Deployment object to a dict, which is prepared to be + dumped to json. + """ + return asdict(self) + + def to_json(self, *args, **kwargs): + """ + Convert a Deployment object to a json object. *args and **kwargs + are forwarded to the `json.dumps()` call. + """ + return json.dumps(self.to_dict(), *args, **kwargs) @classmethod def from_sql_tuple(cls, values): assert len(values) == len(fields(cls)) ret = dict(zip([field.name for field in fields(cls)], values)) ret["contract_address"] = Address(ret["contract_address"]) - ret["from_"] = Address(ret["from_"]) + ret["deployer"] = Address(ret["deployer"]) ret["tx_dict"] = json.loads(ret["tx_dict"]) ret["receipt_dict"] = json.loads(ret["receipt_dict"]) if ret["source_code"] is not None: @@ -54,10 +62,10 @@ def from_sql_tuple(cls, values): contract_address text, name text, rpc text, + deployer text, tx_hash text, - from_ text, - tx_dict text, broadcast_ts real, + tx_dict text, receipt_dict text, source_code text ); @@ -82,8 +90,9 @@ def insert_deployment(self, deployment: Deployment): values = deployment.sql_values() values_placeholder = ",".join(["?"] * len(values)) + colnames = ",".join(values.keys()) - insert_cmd = f"INSERT INTO deployments VALUES({values_placeholder})" + insert_cmd = f"INSERT INTO deployments({colnames}) VALUES({values_placeholder})" self.db.execute(insert_cmd, tuple(values.values())) self.db.commit() @@ -94,7 +103,8 @@ def _get_deployments_from_sql(self, sql_query: str, parameters=(), /): return ret def get_deployments(self) -> list[Deployment]: - return self.get_deployments_from_sql("SELECT * FROM deployments") + fieldnames = ",".join(field.name for field in fields(Deployment)) + return self._get_deployments_from_sql(f"SELECT {fieldnames} FROM deployments") _db: Optional[DeploymentsDB] = None diff --git a/boa/network.py b/boa/network.py index 49dd9406..7f758378 100644 --- a/boa/network.py +++ b/boa/network.py @@ -22,6 +22,7 @@ trim_dict, ) from boa.util.abi import Address +from boa.verifiers import get_verification_bundle class TraceObject: @@ -398,16 +399,15 @@ def deploy( if local_address != create_address: raise RuntimeError(f"uh oh! {local_address} != {create_address}") - # TODO get contract info in here print(f"contract deployed at {create_address}") if (deployments_db := get_deployments_db()) is not None: contract_name = getattr(contract, "contract_name", None) - source_bundle = getattr(contract, "solc_json", None) + source_bundle = get_verification_bundle(contract) deployment_data = Deployment( create_address, contract_name, - self._rpc.identifier, + self._rpc.name, sender, receipt["transactionHash"], broadcast_ts, diff --git a/boa/util/open_ctx.py b/boa/util/open_ctx.py index 1df97272..53c4d346 100644 --- a/boa/util/open_ctx.py +++ b/boa/util/open_ctx.py @@ -6,10 +6,10 @@ def __init__(self, get, set_, item): self.anchor = get() self._set = set_ self._set(item) + self._item = item def __enter__(self): - # dummy implementation, no-op - pass + return self._item def __exit__(self, *args): self._set(self.anchor) diff --git a/boa/verifiers.py b/boa/verifiers.py index 88755275..533b18ac 100644 --- a/boa/verifiers.py +++ b/boa/verifiers.py @@ -137,7 +137,18 @@ def set_verifier(verifier): return Open(get_verifier, _set_verifier, verifier) -def verify(contract, verifier=None, license_type: str = None) -> VerificationResult: +def get_verification_bundle(contract_like): + if not hasattr(contract_like, "deployer"): + return None + if not hasattr(contract_like.deployer, "solc_json"): + return None + return contract_like.deployer.solc_json + + +# should we also add a `verify_deployment` function? +def verify( + contract, verifier=None, license_type: str = None, wait=False +) -> VerificationResult: """ Verifies the contract on a block explorer. :param contract: The contract to verify. @@ -148,13 +159,13 @@ def verify(contract, verifier=None, license_type: str = None) -> VerificationRes if verifier is None: verifier = get_verifier() - if not hasattr(contract, "deployer") or not hasattr(contract.deployer, "solc_json"): + if (bundle := get_verification_bundle(contract)) is None: raise ValueError(f"Not a contract! {contract}") - address = contract.address return verifier.verify( - address=address, - solc_json=contract.deployer.solc_json, + address=contract.address, + solc_json=bundle, contract_name=contract.contract_name, license_type=license_type, + wait=wait, ) diff --git a/tests/integration/network/anvil/test_network_env.py b/tests/integration/network/anvil/test_network_env.py index 1303af9e..1118e271 100644 --- a/tests/integration/network/anvil/test_network_env.py +++ b/tests/integration/network/anvil/test_network_env.py @@ -3,7 +3,10 @@ import boa import boa.test.strategies as vy +from boa.deployments import DeploymentsDB, set_deployments_db from boa.network import NetworkEnv +from boa.rpc import to_bytes +from boa.util.abi import Address code = """ totalSupply: public(uint256) @@ -68,3 +71,28 @@ def test_failed_transaction(): # XXX: probably want to test deployment revert behavior + + +def test_deployment_db(): + with set_deployments_db(DeploymentsDB(":memory:")) as db: + arg = 5 + + # contract is written to deployments db + contract = boa.loads(code, arg) + + # test get_deployments() + deployment = db.get_deployments()[0] + + initcode = contract.compiler_data.bytecode + arg.to_bytes(32, "big") + + # sanity check all the fields + assert deployment.contract_address == contract.address + assert deployment.name == contract.contract_name + assert deployment.deployer == boa.env.eoa + assert deployment.rpc == boa.env._rpc.identifier + assert deployment.source_code == contract.deployer.solc_json + + # some sanity checks on tx_dict and rx_dict fields + assert to_bytes(deployment.tx_dict["data"]) == initcode + assert deployment.tx_dict["chainId"] == hex(boa.env.get_chain_id()) + assert Address(deployment.receipt_dict["contractAddress"]) == contract.address From 787d894cdcdfd4b33fb73cf966d55200ad101a45 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 22 Sep 2024 23:45:58 -0400 Subject: [PATCH 12/18] add sepolia test for deployments --- tests/integration/network/anvil/conftest.py | 3 +- .../network/anvil/test_network_env.py | 4 +-- tests/integration/network/sepolia/conftest.py | 5 +++- .../network/sepolia/test_sepolia_env.py | 28 +++++++++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/integration/network/anvil/conftest.py b/tests/integration/network/anvil/conftest.py index daa386ba..905c03e8 100644 --- a/tests/integration/network/anvil/conftest.py +++ b/tests/integration/network/anvil/conftest.py @@ -9,6 +9,7 @@ from eth_account import Account import boa +from boa.deployments import DeploymentsDB, set_deployments_db from boa.network import NetworkEnv ANVIL_FORK_PKEYS = [ @@ -76,7 +77,7 @@ def anvil_env(free_port): # max coverage across VM implementations? @pytest.fixture(scope="module", autouse=True) def networked_env(accounts, anvil_env): - with boa.swap_env(anvil_env): + with boa.swap_env(anvil_env), set_deployments_db(DeploymentsDB(":memory:")): for account in accounts: boa.env.add_account(account) yield diff --git a/tests/integration/network/anvil/test_network_env.py b/tests/integration/network/anvil/test_network_env.py index 1118e271..e18eda03 100644 --- a/tests/integration/network/anvil/test_network_env.py +++ b/tests/integration/network/anvil/test_network_env.py @@ -81,7 +81,7 @@ def test_deployment_db(): contract = boa.loads(code, arg) # test get_deployments() - deployment = db.get_deployments()[0] + deployment = db.get_deployments()[-1] initcode = contract.compiler_data.bytecode + arg.to_bytes(32, "big") @@ -89,7 +89,7 @@ def test_deployment_db(): assert deployment.contract_address == contract.address assert deployment.name == contract.contract_name assert deployment.deployer == boa.env.eoa - assert deployment.rpc == boa.env._rpc.identifier + assert deployment.rpc == boa.env._rpc.name assert deployment.source_code == contract.deployer.solc_json # some sanity checks on tx_dict and rx_dict fields diff --git a/tests/integration/network/sepolia/conftest.py b/tests/integration/network/sepolia/conftest.py index c9016d26..7db642b7 100644 --- a/tests/integration/network/sepolia/conftest.py +++ b/tests/integration/network/sepolia/conftest.py @@ -6,6 +6,7 @@ from eth_account import Account import boa +from boa.deployments import DeploymentsDB, set_deployments_db PKEY = os.environ["SEPOLIA_PKEY"] SEPOLIA_URI = os.environ["SEPOLIA_ENDPOINT"] @@ -14,6 +15,8 @@ # run all tests with testnet @pytest.fixture(scope="module", autouse=True) def sepolia_env(): - with boa.set_network_env(SEPOLIA_URI): + with boa.set_network_env(SEPOLIA_URI), set_deployments_db( + DeploymentsDB(":memory:") + ): boa.env.add_account(Account.from_key(PKEY)) yield diff --git a/tests/integration/network/sepolia/test_sepolia_env.py b/tests/integration/network/sepolia/test_sepolia_env.py index 4d0d11d2..1d841946 100644 --- a/tests/integration/network/sepolia/test_sepolia_env.py +++ b/tests/integration/network/sepolia/test_sepolia_env.py @@ -3,7 +3,10 @@ import pytest import boa +from boa.deployments import DeploymentsDB, set_deployments_db from boa.network import NetworkEnv +from boa.rpc import to_bytes +from boa.util.abi import Address from boa.verifiers import Blockscout # boa.env.anchor() does not work in prod environment @@ -67,3 +70,28 @@ def test_raise_exception(simple_contract, amount): # XXX: probably want to test deployment revert behavior + + +def test_deployment_db(): + with set_deployments_db(DeploymentsDB(":memory:")) as db: + arg = 5 + + # contract is written to deployments db + contract = boa.loads(code, arg) + + # test get_deployments() + deployment = db.get_deployments()[-1] + + initcode = contract.compiler_data.bytecode + arg.to_bytes(32, "big") + + # sanity check all the fields + assert deployment.contract_address == contract.address + assert deployment.name == contract.contract_name + assert deployment.deployer == boa.env.eoa + assert deployment.rpc == boa.env._rpc.name + assert deployment.source_code == contract.deployer.solc_json + + # some sanity checks on tx_dict and rx_dict fields + assert to_bytes(deployment.tx_dict["data"]) == initcode + assert deployment.tx_dict["chainId"] == hex(boa.env.get_chain_id()) + assert Address(deployment.receipt_dict["contractAddress"]) == contract.address From 496df54082fe2295a2175437e19e34d9eecf1f0f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 23 Sep 2024 18:33:09 -0400 Subject: [PATCH 13/18] add some comments --- boa/deployments.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index 301e624b..ac7b7d65 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -10,10 +10,10 @@ @dataclass(frozen=True) class Deployment: - contract_address: Address - name: str + contract_address: Address # receipt_dict["createAddress"] + name: str # contract_name rpc: str - deployer: Address + deployer: Address # ostensibly equal to tx_dict["from"] tx_hash: str broadcast_ts: float tx_dict: dict # raw tx fields From 37f63c76b6a5b6453dbc59d1d94a76de6deb885c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 26 Sep 2024 12:08:59 -0400 Subject: [PATCH 14/18] update some column names, add unique id and session id --- boa/deployments.py | 29 +++++++++++++++---- .../network/anvil/test_network_env.py | 2 +- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index ac7b7d65..8ca63144 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -1,24 +1,36 @@ import json import sqlite3 -from dataclasses import asdict, dataclass, fields +import uuid +from dataclasses import asdict, dataclass, field, fields from pathlib import Path from typing import Any, Optional from boa.util.abi import Address from boa.util.open_ctx import Open +_session_id: str = None # type: ignore + + +def get_session_id(): + global _session_id + if _session_id is None: + _session_id = str(uuid.uuid4()) + return _session_id + @dataclass(frozen=True) class Deployment: contract_address: Address # receipt_dict["createAddress"] - name: str # contract_name + contract_name: str rpc: str deployer: Address # ostensibly equal to tx_dict["from"] tx_hash: str - broadcast_ts: float + broadcast_ts: float # time the tx was broadcast tx_dict: dict # raw tx fields receipt_dict: dict # raw receipt fields source_code: Optional[Any] # optional source code or bundle + session_id: str = field(default_factory=get_session_id) + deployment_id: Optional[int] = None def sql_values(self): ret = asdict(self) @@ -59,8 +71,10 @@ def from_sql_tuple(cls, values): _CREATE_CMD = """ CREATE TABLE IF NOT EXISTS deployments( + deployment_id integer primary key autoincrement, + session_id text, contract_address text, - name text, + contract_name text, rpc text, deployer text, tx_hash text, @@ -74,7 +88,7 @@ def from_sql_tuple(cls, values): class DeploymentsDB: def __init__(self, path=":memory:"): - if path != ":memory:": + if path != ":memory:": # sqlite magic path path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) @@ -102,8 +116,11 @@ def _get_deployments_from_sql(self, sql_query: str, parameters=(), /): ret = [Deployment.from_sql_tuple(item) for item in cur.fetchall()] return ret + def _get_fieldnames_str(self) -> str: + return ",".join(field.name for field in fields(Deployment)) + def get_deployments(self) -> list[Deployment]: - fieldnames = ",".join(field.name for field in fields(Deployment)) + fieldnames = self._get_fieldnames_str() return self._get_deployments_from_sql(f"SELECT {fieldnames} FROM deployments") diff --git a/tests/integration/network/anvil/test_network_env.py b/tests/integration/network/anvil/test_network_env.py index e18eda03..c21ed885 100644 --- a/tests/integration/network/anvil/test_network_env.py +++ b/tests/integration/network/anvil/test_network_env.py @@ -87,7 +87,7 @@ def test_deployment_db(): # sanity check all the fields assert deployment.contract_address == contract.address - assert deployment.name == contract.contract_name + assert deployment.contract_name == contract.contract_name assert deployment.deployer == boa.env.eoa assert deployment.rpc == boa.env._rpc.name assert deployment.source_code == contract.deployer.solc_json From a40ee195536cf690e62b51fa2cb3f0206692f702 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 30 Sep 2024 15:16:40 -0400 Subject: [PATCH 15/18] fix a test --- tests/integration/network/sepolia/test_sepolia_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/network/sepolia/test_sepolia_env.py b/tests/integration/network/sepolia/test_sepolia_env.py index 1d841946..d37ba0bc 100644 --- a/tests/integration/network/sepolia/test_sepolia_env.py +++ b/tests/integration/network/sepolia/test_sepolia_env.py @@ -86,7 +86,7 @@ def test_deployment_db(): # sanity check all the fields assert deployment.contract_address == contract.address - assert deployment.name == contract.contract_name + assert deployment.contract_name == contract.contract_name assert deployment.deployer == boa.env.eoa assert deployment.rpc == boa.env._rpc.name assert deployment.source_code == contract.deployer.solc_json From d265af8358b0391a92f39180d5c23ff794df75c1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 30 Sep 2024 20:39:00 -0400 Subject: [PATCH 16/18] add comments --- boa/deployments.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/boa/deployments.py b/boa/deployments.py index 8ca63144..d743f451 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -8,6 +8,13 @@ from boa.util.abi import Address from boa.util.open_ctx import Open +""" +Module to handle deployment objects. When a contract is deployed, we enter +it into the deployments database so that it can be queried/verified later. +""" +# maybe this shouldn't be handled in boa proper, but more like as a plugin, +# or leave the functionality to higher level frameworks? + _session_id: str = None # type: ignore From 2fe6980447268588d6789f5b61d4389f6bc317eb Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 30 Sep 2024 20:43:27 -0400 Subject: [PATCH 17/18] update comments --- boa/deployments.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/boa/deployments.py b/boa/deployments.py index d743f451..3eb5c5a0 100644 --- a/boa/deployments.py +++ b/boa/deployments.py @@ -9,15 +9,18 @@ from boa.util.open_ctx import Open """ -Module to handle deployment objects. When a contract is deployed, we enter -it into the deployments database so that it can be queried/verified later. +Module to handle deployment objects. When a contract is deployed in network +mode, we enter it into the deployments database so that it can be +queried/verified later. + +This module could potentially be handled as plugin functionality / or left +as functionality for higher-level frameworks. """ -# maybe this shouldn't be handled in boa proper, but more like as a plugin, -# or leave the functionality to higher level frameworks? _session_id: str = None # type: ignore +# generate a unique session id, so that deployments can be queried by session def get_session_id(): global _session_id if _session_id is None: @@ -37,7 +40,7 @@ class Deployment: receipt_dict: dict # raw receipt fields source_code: Optional[Any] # optional source code or bundle session_id: str = field(default_factory=get_session_id) - deployment_id: Optional[int] = None + deployment_id: Optional[int] = None # the db-assigned id - primary key def sql_values(self): ret = asdict(self) From e57f65c67905017acad19080cc35df78f7ca354d Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Mon, 30 Sep 2024 21:02:54 -0400 Subject: [PATCH 18/18] handle verification bundle failure gracefully --- boa/network.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/boa/network.py b/boa/network.py index 7f758378..9b9ad5de 100644 --- a/boa/network.py +++ b/boa/network.py @@ -403,7 +403,18 @@ def deploy( if (deployments_db := get_deployments_db()) is not None: contract_name = getattr(contract, "contract_name", None) - source_bundle = get_verification_bundle(contract) + try: + source_bundle = get_verification_bundle(contract) + except Exception as e: + # there was a problem constructing the verification bundle. + # assume the user cares more about continuing, than getting + # the bundle into the db + msg = "While saving deployment data, couldn't construct" + msg += f" verification bundle for {contract_name}! Full stack" + msg += f" trace:\n```\n{e}\n```\nContinuing.\n" + warnings.warn(msg, stacklevel=2) + source_bundle = None + deployment_data = Deployment( create_address, contract_name,