From c81eac1a96796bb1b3cf9f6f56541d6dbb2927e4 Mon Sep 17 00:00:00 2001 From: Patrick Collins <54278053+PatrickAlphaC@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:10:27 -0400 Subject: [PATCH] Zksync test network (#76) * wip * feat: zksync support --- .gitignore | 1 + CONTRIBUTING.md | 9 + docs/source/all_moccasin_toml_parameters.rst | 11 + docs/source/script.rst | 2 +- justfile | 8 +- moccasin/__main__.py | 7 + moccasin/_sys_path_and_config_setup.py | 84 ++++--- moccasin/commands/compile.py | 66 ++++- moccasin/commands/console.py | 4 +- moccasin/commands/deploy.py | 4 +- moccasin/commands/install.py | 6 +- moccasin/commands/run.py | 4 +- moccasin/commands/test.py | 4 +- moccasin/config.py | 234 +++++++++++------- moccasin/constants/vars.py | 16 +- moccasin/named_contract.py | 8 +- pyproject.toml | 2 +- tests/conftest.py | 1 + .../script/mock_deployer/deploy_feed.py | 3 +- tests/data/zksync_project/moccasin.toml | 11 + tests/data/zksync_project/script/deploy.py | 10 +- tests/data/zksync_project/src/Counter.vy | 14 -- tests/data/zksync_project/src/Difficulty.vy | 8 + tests/data/zksync_project/src/SelfDestruct.vy | 6 + tests/zksync/conftest.py | 49 ++++ tests/zksync/test_zksync.py | 70 ++++++ uv.lock | 12 +- 27 files changed, 470 insertions(+), 184 deletions(-) delete mode 100644 tests/data/zksync_project/src/Counter.vy create mode 100644 tests/data/zksync_project/src/Difficulty.vy create mode 100644 tests/data/zksync_project/src/SelfDestruct.vy create mode 100644 tests/zksync/conftest.py create mode 100644 tests/zksync/test_zksync.py diff --git a/.gitignore b/.gitignore index 135fd8a..ba0c58c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ default secrets/ secret/ .DS_Store +era_test_node.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19634c6..c4b4f99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,7 @@ Issues, feedback, and sharing that you're using Titanoboa and Vyper on social me - [Table of Contents](#table-of-contents) - [Setup](#setup) - [Requirements](#requirements) + - [ZKSync requirements](#zksync-requirements) - [Installing for local development](#installing-for-local-development) - [Running Tests](#running-tests) - [Code Style Guide](#code-style-guide) @@ -36,6 +37,14 @@ You must have the following installed to proceed with contributing to this proje - [just](https://github.com/casey/just) - You'll know you did it right if you can run `just --version` and you see a response like `just 1.35.0` +### ZKSync requirements +If you wish to run the ZKSync tests, you'll need these as well (ran with `just test-z`) + +- [era_test_node](https://github.com/matter-labs/era-test-node) + - You'll know you did it right if you can run `era_test_node --version` and you see a response like `era_test_node 0.1.0 (a178051e8 2024-09-07)` +- [era-compiler-vyper](https://github.com/matter-labs/era-compiler-vyper) + - You'll know you did it right if you can run `zkvyper --version` and you see a response like `Vyper compiler for ZKsync v1.5.4 (LLVM build f9f732c8ebdb88fb8cd4528482a00e4f65bcb8b7)` + ## Installing for local development Follow the steps to clone the repo for you to make changes to this project. diff --git a/docs/source/all_moccasin_toml_parameters.rst b/docs/source/all_moccasin_toml_parameters.rst index e8f024e..ba4d8a4 100644 --- a/docs/source/all_moccasin_toml_parameters.rst +++ b/docs/source/all_moccasin_toml_parameters.rst @@ -45,6 +45,17 @@ All possible options save_abi_path = "abis" # location to save ABIs from the explorer cov_config = ".coveragerc" # coverage configuration file dot_env = ".env" # environment variables file + default_network = "pyevm" # default network to use. `pyevm` is the local network. "eravm" is the local ZKSync network + + [networks.pyevm] + # The basic EVM local network + # cannot set URL, chain_id, is_fork, is_zksync, prompt_live, explorer_uri, explorer_api_key + default_account_name = "anvil" + + [networks.eravm] + # The special ZKSync Era local network + # cannot set URL, chain_id, is_fork, is_zksync, prompt_live, explorer_uri, explorer_api_key + default_account_name = "anvil" [networks.contracts] # Default named contract parameters diff --git a/docs/source/script.rst b/docs/source/script.rst index b7ab265..1334c55 100644 --- a/docs/source/script.rst +++ b/docs/source/script.rst @@ -141,6 +141,6 @@ Working with dependencies There are two kinds of dependencies you can work with in your moccasin project: - :doc:`Smart Contract dependencies `: For contracts that you want to use packages from. -- :doc: `Python dependencies `: For python packages that you want to use in your scripts. +- :doc:`Python dependencies `: For python packages that you want to use in your scripts. Each have their own respective documentation. \ No newline at end of file diff --git a/justfile b/justfile index dff4076..f5fa457 100644 --- a/justfile +++ b/justfile @@ -18,16 +18,20 @@ format-check: # Run unit and CLI tests, fail on first test failure test: - uv run pytest -x -s --ignore=tests/data/ --ignore=tests/integration/ + uv run pytest -x -s --ignore=tests/data/ --ignore=tests/integration/ --ignore=tests/zksync/ # Run integration tests, read the README.md in the tests/integration directory for more information test-i: - uv run pytest tests/integration -x -s --ignore=tests/data/ + uv run pytest tests/integration -x -s --ignore=tests/data/ --ignore=tests/zksync/ + +test-z: + uv run pytest tests/zksync -x -s --ignore=tests/data/ --ignore=tests/integration/ # Run both unit and integration tests test-all: @just test @just test-i + @just test-z # Run tests, fail on first test failure, enter debugger on failure test-pdb: diff --git a/moccasin/__main__.py b/moccasin/__main__.py index f73d2be..d20f6fc 100644 --- a/moccasin/__main__.py +++ b/moccasin/__main__.py @@ -136,6 +136,13 @@ def generate_main_parser_and_sub_parsers() -> ( help="Optional argument to compile a specific contract.", ) + zksync_ground = compile_parser.add_mutually_exclusive_group() + zksync_ground.add_argument( + "--network", help=f"Alias of the network (from the {CONFIG_NAME})." + ) + + zksync_ground.add_argument("--is_zksync", nargs="?", const=True, default=None) + # ------------------------------------------------------------------ # TEST COMMAND # ------------------------------------------------------------------ diff --git a/moccasin/_sys_path_and_config_setup.py b/moccasin/_sys_path_and_config_setup.py index 056aff6..653145e 100644 --- a/moccasin/_sys_path_and_config_setup.py +++ b/moccasin/_sys_path_and_config_setup.py @@ -5,7 +5,8 @@ import boa -from moccasin.config import get_config +from moccasin.config import Network, get_config +from moccasin.constants.vars import ERA_DEFAULT_PRIVATE_KEY, ERAVM from moccasin.logging import logger from moccasin.moccasin_account import MoccasinAccount @@ -22,7 +23,7 @@ def _patch_sys_path(paths: List[str | Path]) -> Iterator[None]: sys.path = anchor -def _setup_network_and_account_from_args( +def _setup_network_and_account_from_args_and_cli( network: str = None, url: str = None, fork: bool | None = None, @@ -31,36 +32,48 @@ def _setup_network_and_account_from_args( password: str | None = None, password_file_path: Path | None = None, prompt_live: bool | None = None, + explorer_uri: str | None = None, + explorer_api_key: str | None = None, ): + """All the network and account logic in the function parameters are from the CLI. + We will use the order of operations to setup the network: + + 1. scripts (which, we don't touch here) + 2. CLI + 3. Config + 4. Default Values + + All the values passed into this function come from the CLI. + """ + if account is not None and private_key is not None: + raise ValueError("Cannot set both account and private key in the CLI!") + mox_account: MoccasinAccount | None = None config = get_config() - # Specifically a CLI check - if fork and account: - raise ValueError("Cannot use --fork and --account at the same time") + if network is None: + network = config.default_network - # Setup Network - is_fork_from_cli = fork if fork is not None else None - if network and not url: - config.networks.set_active_network(network, is_fork=is_fork_from_cli) - if url: - config.networks.set_active_network(url, is_fork=is_fork_from_cli) + # 1. Update the network with the CLI values + config.set_active_network( + network, + is_fork=fork, + url=url, + default_account_name=account, + # private_key=private_key, # No private key in networks + # password=password, # No password in networks + password_file_path=password_file_path, + prompt_live=prompt_live, + explorer_uri=explorer_uri, + explorer_api_key=explorer_api_key, + ) - active_network = config.get_active_network() + active_network: Network = config.get_active_network() if active_network is None: raise ValueError("No active network set. Please set a network.") - # Update parameters if not provided in the CLI - if fork is None: - fork = active_network.is_fork - if password_file_path is None: - password_file_path = active_network.unsafe_password_file - if account is None: - account = active_network.default_account_name - if prompt_live is None: - prompt_live = active_network.prompt_live - - if prompt_live: + # 2. Update and set account + if active_network.prompt_live: if not fork: response = input( "The transactions run on this will actually be broadcast/transmitted, spending gas associated with your account. Are you sure you wish to continue?\nType 'y' or 'Y' and hit 'ENTER' or 'RETURN' to continue:\n" @@ -69,27 +82,34 @@ def _setup_network_and_account_from_args( logger.info("Operation cancelled.") sys.exit(0) - if account: + if active_network.default_account_name and private_key is None: # This will also attempt to unlock the account with a prompt # If no password or password file is passed mox_account = MoccasinAccount( - keystore_path_or_account_name=account, + keystore_path_or_account_name=active_network.default_account_name, password=password, - password_file_path=password_file_path, + password_file_path=active_network.unsafe_password_file, ) + # Private key overrides the default account if private_key: mox_account = MoccasinAccount( private_key=private_key, password=password, - password_file_path=password_file_path, + password_file_path=active_network.unsafe_password_file, ) if mox_account: - if fork: + if active_network.is_fork: boa.env.eoa = mox_account.address else: boa.env.add_account(mox_account, force_eoa=True) - if boa.env.eoa is None: - logger.warning( - "No default EOA account found. Please add an account to the environment before attempting a transaction." - ) + + if not mox_account and active_network.name is ERAVM: + boa.env.add_account(MoccasinAccount(private_key=ERA_DEFAULT_PRIVATE_KEY)) + + # Check if it's a fork, pyevm, or eravm + if not active_network.is_testing_network(): + if boa.env.eoa is None: + logger.warning( + "No default EOA account found. Please add an account to the environment before attempting a transaction." + ) diff --git a/moccasin/commands/compile.py b/moccasin/commands/compile.py index 0f9ab38..4a80740 100644 --- a/moccasin/commands/compile.py +++ b/moccasin/commands/compile.py @@ -14,8 +14,13 @@ from vyper.compiler.phases import CompilerData from vyper.exceptions import VersionException, _BaseVyperException -from moccasin.config import get_config, initialize_global_config -from moccasin.constants.vars import BUILD_FOLDER, CONTRACTS_FOLDER, MOCCASIN_GITHUB +from moccasin.config import Config, get_config, initialize_global_config +from moccasin.constants.vars import ( + BUILD_FOLDER, + CONTRACTS_FOLDER, + ERAVM, + MOCCASIN_GITHUB, +) from moccasin.logging import logger @@ -24,10 +29,16 @@ def main(args: Namespace) -> int: config = get_config() project_path: Path = config.get_root() + is_zksync: bool = _set_zksync_test_env_if_applicable(args, config) + if args.contract_or_contract_path: contract_path = config._find_contract(args.contract_or_contract_path) + compile_( - contract_path, project_path.joinpath(config.out_folder), write_data=True + contract_path, + project_path.joinpath(config.out_folder), + is_zksync=is_zksync, + write_data=True, ) logger.info(f"Done compiling {contract_path.stem}") else: @@ -35,11 +46,33 @@ def main(args: Namespace) -> int: project_path, project_path.joinpath(config.out_folder), project_path.joinpath(config.contracts_folder), + is_zksync=is_zksync, write_data=True, ) return 0 +def _set_zksync_test_env_if_applicable(args: Namespace, config: Config) -> bool: + is_zksync = args.is_zksync if args.is_zksync is not None else None + + if is_zksync: + config.set_active_network(ERAVM) + return True + + if args.network is not None and is_zksync is None: + config.set_active_network(args.network) + is_zksync = config.get_active_network().is_zksync + + if config.default_network is not None and is_zksync is None: + config.set_active_network(config.default_network) + is_zksync = config.get_active_network().is_zksync + + if is_zksync is None: + is_zksync = False + + return is_zksync + + def _get_cpu_count(): if hasattr(os, "process_cpu_count"): # python 3.13+ @@ -51,6 +84,7 @@ def compile_project( project_path: Path | None = None, build_folder: Path | None = None, contracts_folder: Path | None = None, + is_zksync: bool = False, write_data: bool = False, ): if project_path is None: @@ -75,7 +109,9 @@ def compile_project( with multiprocessing.Pool(n_cpus) as pool: for contract_path in contracts_to_compile: res = pool.apply_async( - compile_, (contract_path, build_folder), dict(write_data=write_data) + compile_, + (contract_path, build_folder), + dict(is_zksync=is_zksync, write_data=write_data), ) jobs.append(res) @@ -106,6 +142,7 @@ def compile_( contract_path: Path, build_folder: Path, compiler_args: dict | None = None, + is_zksync: bool = False, write_data: bool = False, ) -> VyperDeployer | None: logger.debug(f"Compiling contract {contract_path}") @@ -132,21 +169,28 @@ def compile_( abi: list bytecode: bytes - if isinstance(deployer, VVMDeployer): - abi = deployer.abi - bytecode = deployer.bytecode + vm = "evm" + + if is_zksync: + abi = deployer._abi + bytecode = deployer.zkvyper_data.bytecode + vm = "eravm" else: - compiler_data: CompilerData = deployer.compiler_data - bytecode = compiler_data.bytecode - abi = vyper.compiler.output.build_abi_output(compiler_data) + if isinstance(deployer, VVMDeployer): + abi = deployer.abi + bytecode = deployer.bytecode + else: + compiler_data: CompilerData = deployer.compiler_data + bytecode = compiler_data.bytecode + abi = vyper.compiler.output.build_abi_output(compiler_data) # Save Compilation Data contract_name = Path(contract_path).stem - build_data = { "contract_name": contract_name, "bytecode": bytecode.hex(), "abi": abi, + "vm": vm, } if write_data: diff --git a/moccasin/commands/console.py b/moccasin/commands/console.py index 1c319c5..578d0d3 100644 --- a/moccasin/commands/console.py +++ b/moccasin/commands/console.py @@ -6,7 +6,7 @@ from moccasin._sys_path_and_config_setup import ( _patch_sys_path, - _setup_network_and_account_from_args, + _setup_network_and_account_from_args_and_cli, ) from moccasin.config import get_config, initialize_global_config from moccasin.constants.vars import CONSOLE_HISTORY_FILE, DEFAULT_MOCCASIN_FOLDER @@ -20,7 +20,7 @@ def main(args: Namespace) -> int: # Set up the environment (add necessary paths to sys.path, etc.) with _patch_sys_path([config_root, config_root / config.contracts_folder]): - _setup_network_and_account_from_args( + _setup_network_and_account_from_args_and_cli( network=args.network, url=args.url, fork=args.fork, diff --git a/moccasin/commands/deploy.py b/moccasin/commands/deploy.py index df119ea..3460122 100644 --- a/moccasin/commands/deploy.py +++ b/moccasin/commands/deploy.py @@ -2,7 +2,7 @@ from moccasin._sys_path_and_config_setup import ( _patch_sys_path, - _setup_network_and_account_from_args, + _setup_network_and_account_from_args_and_cli, ) from moccasin.config import get_config, initialize_global_config from moccasin.logging import logger @@ -15,7 +15,7 @@ def main(args: Namespace) -> int: # Set up the environment (add necessary paths to sys.path, etc.) with _patch_sys_path([config_root, config_root / config_contracts]): - _setup_network_and_account_from_args( + _setup_network_and_account_from_args_and_cli( network=args.network, url=args.url, fork=args.fork, diff --git a/moccasin/commands/install.py b/moccasin/commands/install.py index c99db79..d7ba3e6 100644 --- a/moccasin/commands/install.py +++ b/moccasin/commands/install.py @@ -19,7 +19,7 @@ from packaging.requirements import InvalidRequirement, Requirement from tqdm import tqdm -from moccasin.config import get_config +from moccasin.config import get_or_initialize_config from moccasin.constants.vars import PACKAGE_VERSION_FILE, REQUEST_HEADERS from moccasin.logging import logger @@ -31,7 +31,7 @@ class DependencyType(Enum): def main(args: Namespace): requirements = args.requirements - config = get_config() + config = get_or_initialize_config() if len(requirements) == 0: requirements = config.get_dependencies() if len(requirements) == 0: @@ -255,7 +255,7 @@ def _pip_installs(package_ids: list[str], base_install_path: Path, quiet: bool = def _write_dependencies(new_package_ids: list[str], dependency_type: DependencyType): - config = get_config() + config = get_or_initialize_config() dependencies = config.get_dependencies() typed_dependencies = [ preprocess_requirement(dep) diff --git a/moccasin/commands/run.py b/moccasin/commands/run.py index ec1e5f8..810dff4 100644 --- a/moccasin/commands/run.py +++ b/moccasin/commands/run.py @@ -4,7 +4,7 @@ from moccasin._sys_path_and_config_setup import ( _patch_sys_path, - _setup_network_and_account_from_args, + _setup_network_and_account_from_args_and_cli, ) from moccasin.config import get_config, initialize_global_config from moccasin.logging import logger @@ -43,7 +43,7 @@ def run_script( # Set up the environment (add necessary paths to sys.path, etc.) with _patch_sys_path([config_root, config_root / config.contracts_folder]): - _setup_network_and_account_from_args( + _setup_network_and_account_from_args_and_cli( network=network, url=url, fork=fork, diff --git a/moccasin/commands/test.py b/moccasin/commands/test.py index 368c270..dd0d862 100644 --- a/moccasin/commands/test.py +++ b/moccasin/commands/test.py @@ -6,7 +6,7 @@ from moccasin._sys_path_and_config_setup import ( _patch_sys_path, - _setup_network_and_account_from_args, + _setup_network_and_account_from_args_and_cli, ) from moccasin.config import get_config, initialize_global_config from moccasin.constants.vars import TESTS_FOLDER @@ -87,7 +87,7 @@ def _run_project_tests( pytest_args.extend(["--cov-config", str(config.cov_config)]) with _patch_sys_path([config_root, config_root / test_path]): - _setup_network_and_account_from_args( + _setup_network_and_account_from_args_and_cli( network=network, url=None, fork=fork, diff --git a/moccasin/config.py b/moccasin/config.py index f50eb14..e3d89a8 100644 --- a/moccasin/config.py +++ b/moccasin/config.py @@ -11,17 +11,20 @@ from boa.contracts.abi.abi_contract import ABIContract, ABIContractFactory from boa.contracts.vyper.vyper_contract import VyperContract, VyperDeployer from boa.environment import Env +from boa_zksync import set_zksync_fork, set_zksync_test_env from dotenv import load_dotenv from moccasin.constants.vars import ( BUILD_FOLDER, CONFIG_NAME, CONTRACTS_FOLDER, - DEFAULT_INSTALLER, + DEFAULT_NETWORK, DEPENDENCIES_FOLDER, DOT_ENV_FILE, DOT_ENV_KEY, - INSTALLER, + ERAVM, + PYEVM, + RESTRICTED_VALUES_FOR_LOCAL_NETWORK, SAVE_ABI_PATH, SCRIPT_FOLDER, TESTS_FOLDER, @@ -33,7 +36,6 @@ from boa.network import NetworkEnv from boa_zksync import ZksyncEnv - _AnyEnv = Union["NetworkEnv", "Env", "ZksyncEnv"] @@ -54,43 +56,50 @@ class Network: extra_data: dict[str, Any] = field(default_factory=dict) _network_env: _AnyEnv | None = None - def _create_env(self) -> _AnyEnv: + def _set_boa_env(self) -> _AnyEnv: # perf: save time on imports in the (common) case where # we just import config for its utils but don't actually need # to switch networks from boa.network import EthereumRPC, NetworkEnv from boa_zksync import ZksyncEnv + # 1. Check for forking, and set (You cannot fork from a NetworkEnv, only a "new" Env!) if self.is_fork: - self._set_fork() - else: if self.is_zksync: - self._network_env = ZksyncEnv(EthereumRPC(self.url), nickname=self.name) + set_zksync_fork(url=self.url) else: - self._network_env = NetworkEnv( - EthereumRPC(self.url), nickname=self.name - ) + boa.fork(self.url) + + # 2. Non-forked local networks + elif self.name == PYEVM: + env = Env() + boa.set_env(env) + elif self.name == ERAVM: + set_zksync_test_env() + + # 3. Finally, "true" networks + elif self.is_zksync: + env = ZksyncEnv(EthereumRPC(self.url), nickname=self.name) + boa.set_env(env) + else: + env = NetworkEnv(EthereumRPC(self.url), nickname=self.name) + boa.set_env(env) + + boa.env.nickname = self.name + return boa.env + + def create_and_set_or_set_boa_env(self, **kwargs) -> _AnyEnv: + for key, value in kwargs.items(): + if value is not None: + setattr(self, key, value) + + # REVIEW: Performance improvement. We don't need to create a new env if the kwargs are the same. + # Potentially unnecessary + if self.kwargs_are_different(**kwargs) or self._network_env is None: + self._set_boa_env() + self._network_env = boa.env return self._network_env - def _set_fork(self): - self._network_env = Env() - self._network_env = cast(_AnyEnv, self._network_env) - self._network_env.fork(url=self.url, deprecated=False) - self._network_env.nickname = self.name - self.is_fork = True - - def get_or_create_env(self, is_fork: bool | None) -> _AnyEnv: - if is_fork is None: - is_fork = self.is_fork - if is_fork: - self._set_fork() - if self._network_env: - boa.set_env(self._network_env) - return self._network_env - new_env: _AnyEnv = self._create_env() - boa.set_env(new_env) - return new_env - def manifest_contract( self, contract_name: str, force_deploy: bool = False, address: str | None = None ) -> VyperContract | ABIContract: @@ -215,6 +224,14 @@ def get_or_deploy_contract( return self._deploy_named_contract(named_contract, deployer_script) + def is_testing_network(self) -> bool: + """Returns True if network is: + 1. pyevm + 2. eravm + 3. A fork + """ + return self.name in [PYEVM, ERAVM] or self.is_fork + def _deploy_named_contract( self, named_contract: NamedContract, deployer_script: str | Path ) -> VyperContract: @@ -292,22 +309,31 @@ def alias(self) -> str: def identifier(self) -> str: return self.name + def kwargs_are_different(self, **kwargs) -> bool: + for key, value in kwargs.items(): + if getattr(self, key, None) != value: + return True + return False + class _Networks: _networks: dict[str, Network] _default_named_contracts: dict[str, NamedContract] + default_network_name: str def __init__(self, toml_data: dict): self._networks = {} self._default_named_contracts = {} self.custom_networks_counter = 0 + project_data = toml_data.get("project", {}) - default_explorer_api_key = toml_data.get("project", {}).get( - "explorer_api_key", None - ) - default_explorer_uri = toml_data.get("project", {}).get("explorer_uri", None) - default_save_abi_path = toml_data.get("project", {}).get("save_abi_path", None) + default_explorer_api_key = project_data.get("explorer_api_key", None) + default_explorer_uri = project_data.get("explorer_uri", None) + default_save_abi_path = project_data.get("save_abi_path", None) default_contracts = toml_data.get("networks", {}).get("contracts", {}) + self.default_network_name = project_data.get( + "default_network_name", DEFAULT_NETWORK + ) self._validate_network_contracts_dict(default_contracts) for contract_name, contract_data in default_contracts.items(): @@ -320,11 +346,26 @@ def __init__(self, toml_data: dict): address=contract_data.get("address", None), ) + # add pyevm and eravm + if PYEVM not in toml_data["networks"]: + toml_data["networks"][PYEVM] = {} + if ERAVM not in self._networks: + toml_data["networks"][ERAVM] = {"is_zksync": True} + for network_name, network_data in toml_data["networks"].items(): - if network_name == "pyevm": - raise ValueError( - "pyevm is a reserved network name, at this time, overriding defaults is not supported. Please remove it from your moccasin.toml." - ) + # Check for restricted items for pyevm or eravm + if network_name in [PYEVM, ERAVM]: + for key in network_data.keys(): + if key in RESTRICTED_VALUES_FOR_LOCAL_NETWORK: + raise ValueError( + f"Cannot set {key} for network {network_name}." + ) + if network_name is PYEVM: + if "is_zksync" in network_data.keys(): + raise ValueError( + f"Cannot set is_zksync for network {network_name}." + ) + if network_name == "contracts": continue else: @@ -332,25 +373,12 @@ def __init__(self, toml_data: dict): self._validate_network_contracts_dict( starting_network_contracts_dict, network_name=network_name ) - final_network_contracts = self._default_named_contracts.copy() - for ( - contract_name, - contract_data, - ) in starting_network_contracts_dict.items(): - named_contract = NamedContract( - contract_name, - force_deploy=contract_data.get("force_deploy", None), - abi=contract_data.get("abi", None), - abi_from_explorer=contract_data.get("abi_from_explorer", None), - deployer_script=contract_data.get("deployer_script", None), - address=contract_data.get("address", None), + final_network_contracts = ( + self._generate_network_contracts_from_defaults( + self._default_named_contracts.copy(), + starting_network_contracts_dict, ) - if self._default_named_contracts.get(contract_name, None): - named_contract.set_defaults( - self._default_named_contracts[contract_name] - ) - final_network_contracts[contract_name] = named_contract - + ) network = Network( name=network_name, is_fork=network_data.get("fork", False), @@ -376,6 +404,25 @@ def __init__(self, toml_data: dict): def __len__(self): return len(self._networks) + def _generate_network_contracts_from_defaults( + self, starting_default_contracts: dict, starting_network_contracts_dict: dict + ) -> dict: + for contract_name, contract_data in starting_network_contracts_dict.items(): + named_contract = NamedContract( + contract_name, + force_deploy=contract_data.get("force_deploy", None), + abi=contract_data.get("abi", None), + abi_from_explorer=contract_data.get("abi_from_explorer", None), + deployer_script=contract_data.get("deployer_script", None), + address=contract_data.get("address", None), + ) + if self._default_named_contracts.get(contract_name, None): + named_contract.set_defaults( + self._default_named_contracts[contract_name] + ) + starting_default_contracts[contract_name] = named_contract + return starting_default_contracts + def get_active_network(self) -> Network: if boa.env.nickname in self._networks: return self._networks[boa.env.nickname] @@ -408,33 +455,18 @@ def get_network_by_name(self, alias: str) -> Network: def get_or_deploy_contract(self, *args, **kwargs) -> VyperContract | ABIContract: return self.get_active_network().get_or_deploy_contract(*args, **kwargs) - # REVIEW: i think it might be better to delegate to `boa.set_env` - # so the usage would be like: - # ``` - # boa.set_env_from_network(moccasin.networks.zksync) - # ``` - # otherwise it is too confusing where moccasin ends and boa starts. - def set_active_network( - self, name_url_or_id: str | Network, is_fork: bool | None = None - ): - env_to_set: _AnyEnv - if isinstance(name_url_or_id, Network): - env_to_set = name_url_or_id.get_or_create_env(is_fork) - self._networks[name_url_or_id.name] = env_to_set - else: - if name_url_or_id.startswith("http"): - new_network = self._create_custom_network( - name_url_or_id, is_fork=is_fork - ) - env_to_set = new_network.get_or_create_env(is_fork) - else: - network = self.get_network(name_url_or_id) - if network: - network.get_or_create_env(is_fork) - else: - raise ValueError( - f"Network {name_url_or_id} not found. Please pass a valid URL/RPC or valid network name." - ) + def set_active_network(self, name_of_network_or_network: str | Network, **kwargs): + if not isinstance(name_of_network_or_network, str) and not isinstance( + name_of_network_or_network, Network + ): + raise ValueError("The first argument must be a string or a Network object.") + + if isinstance(name_of_network_or_network, str): + name_of_network_or_network = self.get_network(name_of_network_or_network) + name_of_network_or_network = cast(Network, name_of_network_or_network) + + name_of_network_or_network.create_and_set_or_set_boa_env(**kwargs) + self._networks[name_of_network_or_network.name] = name_of_network_or_network def _create_custom_network(self, url: str, is_fork: bool | None = False) -> Network: if is_fork is None: @@ -528,7 +560,7 @@ def expand_env_vars(self, value): return [self.expand_env_vars(item) for item in value] return value - def get_active_network(self): + def get_active_network(self) -> Network: return self.networks.get_active_network() def get_or_deploy_contract(self, *args, **kwargs) -> VyperContract | ABIContract: @@ -609,14 +641,8 @@ def _find_contract(self, contract_or_contract_path: str) -> Path: # Return the single found contract return contract_paths[0] - def set_active_network( - self, name_url_or_id: str | Network, is_fork: bool | None = None - ): - self.networks.set_active_network(name_url_or_id, is_fork=is_fork) - - @property - def installer(self) -> str: - return self.project.get(INSTALLER, DEFAULT_INSTALLER) + def set_active_network(self, name_url_or_id: str | Network, **kwargs): + self.networks.set_active_network(name_url_or_id, **kwargs) @property def project_root(self) -> Path: @@ -659,6 +685,14 @@ def script_folder(self) -> str: def lib_folder(self) -> str: return self.project.get(DEPENDENCIES_FOLDER, DEPENDENCIES_FOLDER) + @property + def default_network(self) -> str: + return self.networks.default_network_name + + @property + def default_network_name(self) -> str: + return self.default_network + @staticmethod def load_config_from_path(config_path: Path | None = None) -> "Config": if config_path is None: @@ -691,11 +725,21 @@ def find_project_root(start_path: Path | str = Path.cwd()) -> Path: _config: Config | None = None +def get_or_initialize_config() -> Config: + global _config + if _config is None: + _config = initialize_global_config() + return _config + + def get_config() -> Config: + """Get the global Config object.""" global _config - if _config is not None: - return _config - return initialize_global_config() + if _config is None: + raise ValueError( + "Global Config object not initialized, initialize with initialize_global_config" + ) + return _config def initialize_global_config(config_path: Path | None = None) -> Config: diff --git a/moccasin/constants/vars.py b/moccasin/constants/vars.py index f622505..1ae12ae 100644 --- a/moccasin/constants/vars.py +++ b/moccasin/constants/vars.py @@ -10,8 +10,9 @@ CONTRACTS_FOLDER = "src" SCRIPT_FOLDER = "script" DEPENDENCIES_FOLDER = "lib" -INSTALLER = "installer" -DEFAULT_INSTALLER = "uv" +PYEVM = "pyevm" +ERAVM = "eravm" +DEFAULT_NETWORK = PYEVM # Project Config Keys SAVE_ABI_PATH = "save_abi_path" @@ -28,8 +29,19 @@ DOT_ENV_KEY = "dot_env" CONSOLE_HISTORY_FILE = "moccasin_history" DEFAULT_API_KEY_ENV_VAR = "EXPLORER_API_KEY" +RESTRICTED_VALUES_FOR_LOCAL_NETWORK = [ + "url", + "chain_id", + "is_fork", + "prompt_live", + "explorer_uri", + "exploer_api_key", +] # Testing Vars +ERA_DEFAULT_PRIVATE_KEY = ( + "0x3d3cbc973389cb26f657686445bcc75662b415b656078503592ac8c1abb8810e" +) DEFAULT_ANVIL_PRIVATE_KEY = ( "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" ) diff --git a/moccasin/named_contract.py b/moccasin/named_contract.py index 04bbe6b..6d3324e 100644 --- a/moccasin/named_contract.py +++ b/moccasin/named_contract.py @@ -3,6 +3,7 @@ from typing import Any from boa.contracts.vyper.vyper_contract import VyperContract, VyperDeployer +from boa_zksync.contract import ZksyncContract from moccasin.logging import logger @@ -76,9 +77,12 @@ def _deploy( vyper_contract: VyperContract = importlib.import_module( f"{deployer_module_path}" ).moccasin_main() - if not isinstance(vyper_contract, VyperContract): + + if not isinstance(vyper_contract, VyperContract) and not isinstance( + vyper_contract, ZksyncContract + ): raise ValueError( - f"Your {deployer_module_path} script for {self.contract_name} set in deployer path must return a VyperContract object" + f"Your {deployer_module_path} script for {self.contract_name} set in deployer path must return a VyperContract or ZksyncContract object" ) if update_from_deploy: self.update_from_deployment(vyper_contract) diff --git a/pyproject.toml b/pyproject.toml index 41e8b65..61c6263 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ dependencies = [ "titanoboa @ git+https://github.com/vyperlang/titanoboa.git@master", "python-dotenv>=1.0.1", - "titanoboa-zksync>=v0.2.2", + "titanoboa-zksync>=v0.2.3", "tqdm>=4.66.5", "tomlkit>=0.13.2", # For preserving comments when writing to toml "tomli-w>=1.0.0", diff --git a/tests/conftest.py b/tests/conftest.py index 5441bce..2674fe5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ COMPLEX_PROJECT_PATH = Path(__file__).parent.joinpath("data/complex_project/") INSTALL_PROJECT_PATH = Path(__file__).parent.joinpath("data/installation_project/") +ZKSYNC_PROJECT_PATH = Path(__file__).parent.joinpath("data/zksync_project/") ANVIL1_PRIVATE_KEY = ( "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" ) diff --git a/tests/data/complex_project/script/mock_deployer/deploy_feed.py b/tests/data/complex_project/script/mock_deployer/deploy_feed.py index 45b85c7..36bf4a2 100644 --- a/tests/data/complex_project/script/mock_deployer/deploy_feed.py +++ b/tests/data/complex_project/script/mock_deployer/deploy_feed.py @@ -7,7 +7,8 @@ def deploy_mock() -> VyperContract: - return MockV3Aggregator.deploy(DECIMALS, INITIAL_ANSWER) + aggregator = MockV3Aggregator.deploy(DECIMALS, INITIAL_ANSWER) + return aggregator def moccasin_main() -> VyperContract: diff --git a/tests/data/zksync_project/moccasin.toml b/tests/data/zksync_project/moccasin.toml index 27d6c0c..fc30bd5 100644 --- a/tests/data/zksync_project/moccasin.toml +++ b/tests/data/zksync_project/moccasin.toml @@ -1,3 +1,14 @@ +[project] +default_network_name = "eravm" +# default_network = "pyevm" + +# [networks.pyevm] +# # cannot set URL, chain_id, is_fork, is_zksync + +# eravm will always have is_zksync be true! +# [networks.eravm] +# is_zksync = true + [networks.zksync-local] url = "http://127.0.0.1:8011" chain_id = 260 diff --git a/tests/data/zksync_project/script/deploy.py b/tests/data/zksync_project/script/deploy.py index 389becd..54c66d6 100644 --- a/tests/data/zksync_project/script/deploy.py +++ b/tests/data/zksync_project/script/deploy.py @@ -1,4 +1,4 @@ -from src import Counter +from src import Difficulty from moccasin.boa_tools import ZksyncContract @@ -6,11 +6,9 @@ def deploy() -> ZksyncContract: - counter: ZksyncContract = Counter.deploy() - print("Starting count: ", counter.number()) - counter.increment() - print("Ending count: ", counter.number()) - return counter + difficulty: ZksyncContract = Difficulty.deploy() + print("Difficulty: ", difficulty.get_difficulty()) + return difficulty def moccasin_main() -> ZksyncContract: diff --git a/tests/data/zksync_project/src/Counter.vy b/tests/data/zksync_project/src/Counter.vy deleted file mode 100644 index a36bca2..0000000 --- a/tests/data/zksync_project/src/Counter.vy +++ /dev/null @@ -1,14 +0,0 @@ -# SPDX-License-Identifier: MIT -# pragma version 0.4.0 - -number: public(uint256) - - -@external -def set_number(new_number: uint256): - self.number = new_number - - -@external -def increment(): - self.number += 1 diff --git a/tests/data/zksync_project/src/Difficulty.vy b/tests/data/zksync_project/src/Difficulty.vy new file mode 100644 index 0000000..b821a50 --- /dev/null +++ b/tests/data/zksync_project/src/Difficulty.vy @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: MIT +# pragma version 0.4.0 + +# Should return 2500000000000000 +@external +@view +def get_difficulty() -> uint256: + return block.difficulty \ No newline at end of file diff --git a/tests/data/zksync_project/src/SelfDestruct.vy b/tests/data/zksync_project/src/SelfDestruct.vy new file mode 100644 index 0000000..35574c0 --- /dev/null +++ b/tests/data/zksync_project/src/SelfDestruct.vy @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MIT +# pragma version 0.4.0 + +@deploy +def __init__(): + selfdestruct(msg.sender) diff --git a/tests/zksync/conftest.py b/tests/zksync/conftest.py new file mode 100644 index 0000000..b57b0d6 --- /dev/null +++ b/tests/zksync/conftest.py @@ -0,0 +1,49 @@ +import os +import shutil + +import pytest +from boa_zksync import set_zksync_test_env + +from moccasin.config import Config, initialize_global_config +from moccasin.constants.vars import DEPENDENCIES_FOLDER +from tests.conftest import ZKSYNC_PROJECT_PATH + + +## ZKSync Fixtures +@pytest.fixture(scope="session") +def zksync_project_config() -> Config: + return initialize_global_config(ZKSYNC_PROJECT_PATH) + + +@pytest.fixture(scope="session") +def zksync_out_folder(zksync_project_config) -> Config: + return zksync_project_config.out_folder + + +@pytest.fixture +def zksync_cleanup_coverage(): + yield + coverage_file = ZKSYNC_PROJECT_PATH.joinpath(".coverage") + if os.path.exists(coverage_file): + os.remove(coverage_file) + + +@pytest.fixture +def zksync_cleanup_out_folder(zksync_out_folder): + yield + created_folder_path = ZKSYNC_PROJECT_PATH.joinpath(zksync_out_folder) + if os.path.exists(created_folder_path): + shutil.rmtree(created_folder_path) + + +@pytest.fixture +def zksync_cleanup_dependencies_folder(): + yield + created_folder_path = ZKSYNC_PROJECT_PATH.joinpath(DEPENDENCIES_FOLDER) + if os.path.exists(created_folder_path): + shutil.rmtree(created_folder_path) + + +@pytest.fixture(scope="module") +def zksync_test_env(): + set_zksync_test_env() diff --git a/tests/zksync/test_zksync.py b/tests/zksync/test_zksync.py new file mode 100644 index 0000000..abd74e5 --- /dev/null +++ b/tests/zksync/test_zksync.py @@ -0,0 +1,70 @@ +import json +import os +import subprocess +from pathlib import Path + +import pytest + +from moccasin.commands.compile import compile_ +from moccasin.commands.run import run_script +from tests.conftest import ZKSYNC_PROJECT_PATH + + +def test_compile_zksync_pyevm(zksync_cleanup_out_folder, zksync_out_folder, mox_path): + current_dir = Path.cwd() + try: + os.chdir(current_dir.joinpath(ZKSYNC_PROJECT_PATH)) + result = subprocess.run( + [mox_path, "build", "Difficulty.vy", "--network", "pyevm"], + check=True, + capture_output=True, + text=True, + ) + finally: + os.chdir(current_dir) + assert "Done compiling Difficulty" in result.stderr + assert result.returncode == 0 + # read the Difficulty.json in zksync_out_folder + with open( + ZKSYNC_PROJECT_PATH.joinpath(zksync_out_folder).joinpath("Difficulty.json"), "r" + ) as f: + data = json.load(f) + assert data["vm"] == "evm" + + +def test_compile_zksync_one( + zksync_cleanup_out_folder, zksync_out_folder, zksync_test_env +): + compile_( + ZKSYNC_PROJECT_PATH.joinpath("src/Difficulty.vy"), + ZKSYNC_PROJECT_PATH.joinpath(zksync_out_folder), + is_zksync=True, + write_data=True, + ) + with open( + ZKSYNC_PROJECT_PATH.joinpath(zksync_out_folder).joinpath("Difficulty.json"), "r" + ) as f: + data = json.load(f) + assert data["vm"] == "eravm" + + +def test_compile_zksync_bad( + zksync_cleanup_out_folder, zksync_out_folder, zksync_test_env +): + with pytest.raises(AssertionError) as excinfo: + compile_( + ZKSYNC_PROJECT_PATH.joinpath("src/SelfDestruct.vy"), + ZKSYNC_PROJECT_PATH.joinpath(zksync_out_folder), + is_zksync=True, + write_data=True, + ) + error_message = str(excinfo.value) + assert "subprocess compiling" in error_message + assert "failed with exit code Some(1)" in error_message + assert "The `SELFDESTRUCT` instruction is not supported" in error_message + + +def test_run_zksync_good(zksync_cleanup_out_folder, zksync_test_env): + difficulty_contract = run_script(ZKSYNC_PROJECT_PATH.joinpath("script/deploy.py")) + difficulty = difficulty_contract.get_difficulty() + assert difficulty == 2500000000000000 diff --git a/uv.lock b/uv.lock index be855a2..30aee3b 100644 --- a/uv.lock +++ b/uv.lock @@ -660,7 +660,7 @@ requires-dist = [ { name = "sphinx-multiversion", marker = "extra == 'docs'", specifier = ">=0.2.4" }, { name = "sphinx-tabs", marker = "extra == 'docs'", specifier = ">=3.4.5" }, { name = "titanoboa", git = "https://github.com/vyperlang/titanoboa.git?rev=master" }, - { name = "titanoboa-zksync", specifier = ">=0.2.2" }, + { name = "titanoboa-zksync", specifier = ">=0.2.3" }, { name = "tomli-w", specifier = ">=1.0.0" }, { name = "tomlkit", specifier = ">=0.13.2" }, { name = "tqdm", specifier = ">=4.66.5" }, @@ -1187,8 +1187,8 @@ wheels = [ [[package]] name = "titanoboa" -version = "0.2.2" -source = { git = "https://github.com/vyperlang/titanoboa.git?rev=master#4768207288ec8fb23a4817ae193bd707ab9d1e8f" } +version = "0.2.3" +source = { git = "https://github.com/vyperlang/titanoboa.git?rev=master#f00e12bd0743c567a76885bcfd7a6151fcfbd951" } dependencies = [ { name = "eth-abi" }, { name = "eth-account" }, @@ -1207,14 +1207,14 @@ dependencies = [ [[package]] name = "titanoboa-zksync" -version = "0.2.2" +version = "0.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "titanoboa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/c9/051a16354c2b8347c5aeb803ad88965d4f31b7454d5c05c672d047f65521/titanoboa_zksync-0.2.2.tar.gz", hash = "sha256:d7150635040b4fff25d9866c4531dcf8061be49885ae1d8d19a8b2161b09bf40", size = 21754 } +sdist = { url = "https://files.pythonhosted.org/packages/c1/93/44d1a5103ca3861b2dfefbeb325ce05aaa5ea13023bb8496d58a73536dc1/titanoboa_zksync-0.2.3.tar.gz", hash = "sha256:658608df0aed48f37d0b0a91a16a010fc94f99083c0568b922749a357a1c861a", size = 22404 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/b0/45e66ba7144693f7f70a162834e6696072142249a492e36bec692ed9223c/titanoboa_zksync-0.2.2-py3-none-any.whl", hash = "sha256:e6633fd5cf60dfcb935c8bbb2595e9e3bd83e4ef9ed8a6e394c3a4b2911f7ab9", size = 20950 }, + { url = "https://files.pythonhosted.org/packages/ab/52/dece80b7a7e7ccea8433ea7c828bbd0366bf70ece14a4b8af4035a02c732/titanoboa_zksync-0.2.3-py3-none-any.whl", hash = "sha256:e0faba82915ed36482282d6bdf36e920b92a98649e1977906ecde6d1c216e9fb", size = 21335 }, ] [[package]]