diff --git a/account_abstraction.py b/account_abstraction.py new file mode 100644 index 0000000..a06c846 --- /dev/null +++ b/account_abstraction.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 + +import os +import shutil + +import libroll as lib +import deps +from processes import PROCESS_MGR +from config import Config + + +#################################################################################################### + +def is_setup(): + """ + Checks if the account abstraction setup has been run. + """ + return os.path.isdir("account-abstraction") \ + and shutil.which("stackup-bundler") is not None + + # TODO check contracts have been built + # TODO check paymaster dependencies have been built + + +# -------------------------------------------------------------------------------------------------- + +def setup(): + """ + Sets up all components necessary to run the account abstraction setup (cloning directories, + building & installing sfotware). + """ + + # === clone account-abstraction repo === + + github_url = "https://github.com/0xFableOrg/account-abstraction.git" + + if os.path.isfile("account-abstraction"): + raise Exception("Error: 'account-abstraction' exists as a file and not a directory.") + elif not os.path.exists("account-abstraction"): + print("Cloning the account-abstraction repository. This may take a while...") + lib.clone_repo(github_url, "clone the account-abstraction repository") + + # === build account abstraction contracts === + + deps.check_or_install_node() + + log_file = "logs/build_aa_contracts.log" + print(f"Building account abstraction contracts. Logging to {log_file}") + + lib.run_roll_log( + "build account abstraction contracts", + command=deps.cmd_with_node("yarn install"), + cwd="account-abstraction", + log_file=log_file) + + # === install stackup bundler === + + github_url = "github.com/stackup-wallet/stackup-bundler" + version = "v0.6.21" + + log_file = "logs/install_bundler.log" + print(f"Installing stackup bundler. Logging to {log_file}") + + env = {**os.environ, "GOBIN": os.path.abspath("bin")} + + lib.run_roll_log( + "install stackup bundler", + f"go install {github_url}@{version}", + env=env, + log_file=log_file) + + # === build paymaster dependencies === + + log_file = "logs/build_paymaster.log" + print(f"Building paymaster dependencies. {log_file}") + lib.run_roll_log( + "build paymaster dependencies", + command=deps.cmd_with_node("pnpm install"), + cwd="paymaster", + log_file=log_file) + + +#################################################################################################### + +def deploy(config: Config): + """ + Deploy the account abstraction contracts. + """ + + env = {**os.environ, + "PRIVATE_KEY": config.aa_deployer_key, + "PAYMASTER_PRIVATE_KEY": config.paymaster_key, + "RPC_URL": config.l2_engine_rpc} + + log_file = f"logs/{config.deploy_aa_log_file_name}" + print(f"Deploying account abstraction contracts. Logging to {log_file}") + + lib.run_roll_log( + "deploy contracts", + command=deps.cmd_with_node("yarn deploy --network opstack"), + cwd="account-abstraction", + env=env, + log_file=log_file) + + print("Account abstraction contracts successfully deployed.") + + # NOTE: Deployment state can be checked at "account-abstraction/deployments/opstack" + + +#################################################################################################### + +def start(config: Config): + start_bundler(config) + start_paymaster(config) + + +# -------------------------------------------------------------------------------------------------- + +def start_bundler(config: Config): + """ + Start the stackup bundler. + """ + + bundler_key = config.bundler_key + if bundler_key.startswith("0x"): + bundler_key = bundler_key[2:] + + env = {**os.environ, + "ERC4337_BUNDLER_ETH_CLIENT_URL": config.l2_engine_rpc, + "ERC4337_BUNDLER_PRIVATE_KEY": bundler_key} + + log_file_path = "logs/stackup_bundler.log" + log_file = open(log_file_path, "w") + print(f"Starting the stackup bundler. Logging to {log_file_path}.") + + PROCESS_MGR.start( + "start bundler", + "stackup-bundler start --mode private", + env=env, + forward="fd", + stdout=log_file) + + +# -------------------------------------------------------------------------------------------------- + + +def start_paymaster(config: Config): + """ + Starts the paymaster signer service. + """ + + entrypoint_address = lib.read_json_file( + "account-abstraction/deployments/opstack/EntryPoint.json" + )["address"] + + simple_account_factory_address = lib.read_json_file( + "account-abstraction/deployments/opstack/SimpleAccountFactory.json" + )["address"] + + paymaster_address = lib.run( + "parsing paymaster address", + f"grep '==VerifyingPaymaster addr=' logs/{config.deploy_aa_log_file_name}" + ).strip().split(' ')[-1] + + env = {**os.environ, + "RPC_URL": config.l2_engine_rpc, + "PAYMASTER_RPC_URL": "http://localhost:3000", + "ENTRYPOINT_ADDRESS": entrypoint_address, + "SIMPLE_ACCOUNT_FACTORY_ADDRESS": simple_account_factory_address, + "PAYMASTER_ADDRESS": paymaster_address, + "TIME_VALIDITY": str(config.paymaster_validity), + "PRIVATE_KEY": config.paymaster_key} + + # start paymaster signer service + log_file_path = "logs/paymaster_signer.log" + log_file = open(log_file_path, "w") + print(f"Starting paymaster signer service. Logging to {log_file_path}") + + PROCESS_MGR.start( + "start paymaster signer service", + "pnpm run start", + cwd="paymaster", + env=env, + forward="fd", + stdout=log_file) + + +#################################################################################################### + +def clean(): + """ + Deletes the account abstraction deployment artifacts. + """ + shutil.rmtree("account-abstraction/deployments/opstack", ignore_errors=True) + + +#################################################################################################### diff --git a/bundler.py b/bundler.py deleted file mode 100644 index ebb6bea..0000000 --- a/bundler.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env python3 - -import subprocess -import argparse -import os -import libroll as lib -import deps -from processes import PROCESS_MGR -from config import Config - -#################################################################################################### -# ARGUMENT PARSING - -# Store the parsed arguments here. -global args - -parser = argparse.ArgumentParser( - description="Helps you spin up an op-stack rollup.") - -subparsers = parser.add_subparsers( - title="commands", - dest="command", - metavar="") - -subparsers.add_parser( - "start", - help="start an ERC4337 bundler") - -subparsers.add_parser( - "clean", - help="cleanup bundler processes") - -parser.add_argument( - "--no-ansi-esc", - help="disable ANSI escape codes for terminal manipulation", - default=True, - dest="use_ansi_esc", - action="store_false") - - -#################################################################################################### -# SETUP - -def start(): - config = Config() - setup_4337_contracts(config) - setup_stackup_bundler(config) - setup_paymaster(config) - - -# -------------------------------------------------------------------------------------------------- - -def setup_4337_contracts(config: Config): - github_url = "https://github.com/0xFableOrg/account-abstraction.git" - - if os.path.isfile("account-abstraction"): - raise Exception("Error: 'account-abstraction' exists as a file and not a directory.") - elif not os.path.exists("account-abstraction"): - print("Cloning the account-abstraction repository. This may take a while...") - lib.clone_repo(github_url, "clone the account-abstraction repository") - - # If contracts have not been previously deployed - if not os.path.exists("account-abstraction/deployments/opstack"): - log_file = "logs/build_4337_contracts.log" - lib.run_roll_log( - "install account abstraction dependencies", - command=deps.cmd_with_node("yarn install"), - cwd="account-abstraction", - log_file=log_file - ) - # set private key for deployment - if config.deployer_key is None: - config.deployer_key = input( - "Enter private key that you would like to deploy contracts with: ") - lib.run("set deployment key", - f"echo PRIVATE_KEY={config.deployer_key} > account-abstraction/.env") - # set private key for paymaster - if config.paymaster_key is None: - config.paymaster_key = input("Enter private key for paymaster signer: ") - lib.run( - "set paymaster key", - f"echo PAYMASTER_PRIVATE_KEY={config.paymaster_key} >> account-abstraction/.env" - ) - # set rpc url for deployment - lib.run("set rpc url", f"echo RPC_URL={config.l2_engine_rpc} >> account-abstraction/.env") - log_file = "logs/deploy_4337_contracts.log" - lib.run_roll_log( - "deploy contracts", - command=deps.cmd_with_node("yarn deploy --network opstack"), - cwd="account-abstraction", - log_file=log_file - ) - print("Account abstraction contracts successfully deployed.") - else: - print("Account abstraction contracts already deployed.") - - -def setup_stackup_bundler(config: Config): - github_url = "github.com/stackup-wallet/stackup-bundler" - version = "latest" - - lib.run("installing stackup bundler", f"go install {github_url}@{version}") - print("Installation successful") - - # set environment variables for bundler - lib.run( - "set full node RPC", - f"echo ERC4337_BUNDLER_ETH_CLIENT_URL={config.l2_engine_rpc} > .env" - ) - if config.bundler_key is None: - config.bundler_key = input("Enter private key for bundler: ") - lib.run("set private key", f"echo ERC4337_BUNDLER_PRIVATE_KEY={config.bundler_key} >> .env") - - # start bundler as a persistent process - print("Starting bundler...") - log_file_path = "logs/stackup_bundler.log" - log_file = open(log_file_path, "w") - PROCESS_MGR.start( - "start bundler", - "stackup-bundler start --mode private", - forward="fd", - stdout=log_file, - stderr=subprocess.STDOUT - ) - print("Bundler is running!") - - -def setup_paymaster(config: Config): - # install paymaster dependencies - lib.run_roll_log( - "install paymaster dependencies", - command=deps.cmd_with_node("pnpm install"), - cwd="paymaster", - log_file="logs/install_paymaster_dependencies.log" - ) - - # set environment variables for paymaster (deterministic deployments can be hardcoded) - lib.run( - "set node RPC", - f"echo RPC_URL={config.l2_engine_rpc} > paymaster/.env" - ) - lib.run("set paymaster RPC", "echo PAYMASTER_RPC_URL=http://localhost:3000 >> paymaster/.env") - entrypointAddress = lib.read_json_file( - "account-abstraction/deployments/opstack/EntryPoint.json" - )["address"] - lib.run( - "set entrypoint", - f"echo ENTRYPOINT_ADDRESS={entrypointAddress} >> paymaster/.env" - ) - simpleAccountFactoryAddress = lib.read_json_file( - "account-abstraction/deployments/opstack/SimpleAccountFactory.json" - )["address"] - lib.run( - "set factory", - f"echo SIMPLE_ACCOUNT_FACTORY_ADDRESS={simpleAccountFactoryAddress} >> paymaster/.env" - ) - paymaster_address = subprocess.check_output( - ["grep", '==VerifyingPaymaster addr=', "logs/deploy_4337_contracts.log"] - ).decode().strip().split(' ')[-1] - lib.run("set paymaster", f"echo PAYMASTER_ADDRESS={paymaster_address} >> paymaster/.env") - lib.run( - "set sponsored transactions time validity", - f"echo TIME_VALIDITY={config.paymaster_validity} >> paymaster/.env" - ) - # set private key for paymaster - lib.run("set private key", f"echo PRIVATE_KEY={config.paymaster_key} >> paymaster/.env") - - # start paymaster signer service - print("Starting paymaster signer service...") - log_file_path = "logs/paymaster_signer.log" - log_file = open(log_file_path, "w") - PROCESS_MGR.start( - "start paymaster", - "pnpm run start", - cwd="paymaster", - forward="fd", - stdout=log_file, - stderr=subprocess.STDOUT - ) - print("Paymaster service is running!") - - -#################################################################################################### -# CLEANUP - -def clean(): - lib.run("cleanup account abstraction directory", - "rm -rf account-abstraction/deployments/opstack") - lib.run("cleanup environment variable", "rm .env") - print("Cleanup successful!") - - -#################################################################################################### - -if __name__ == "__main__": - args = parser.parse_args() - try: - if args.command is None: - parser.print_help() - exit() - - deps.check_basic_prerequisites() - deps.check_go() - if args.command == "start": - start() - # allow background processes to continue running - PROCESS_MGR.wait_all() - elif args.command == "clean": - clean() - - print("Done.") - except Exception as e: - print(f"Aborted with error: {e}") - -#################################################################################################### diff --git a/config.py b/config.py index 14945dc..b750987 100644 --- a/config.py +++ b/config.py @@ -672,24 +672,22 @@ def __init__(self, paths: OPPaths = None): # ========================================================================================== # Account Abstraction Configuration - # === Private Key === - - self.deployer_key = None + self.aa_deployer_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" """ - Private key to use for deploying 4337 contracts (None by default). - Will be used if set, otherwise will prompt users to enter private key. + Private key to use for deploying 4337 contracts. + Uses the 0th "test junk" mnemonnic key by default. """ - self.bundler_key = None + self.bundler_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" """ - Private key to use for submitting bundled transactions (None by default). - Will be used if set, otherwise will prompt users to enter private key. + Private key to use for submitting bundled transactions. + Uses the 0th "test junk" mnemonnic key by default. """ - self.paymaster_key = None + self.paymaster_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" """ - Private key as paymaster signer (None by default). - Will be used if set, otherwise will prompt users to enter private key. + Private key to use as paymaster signer. + Uses the 0th "test junk" mnemonnic key by default. """ self.paymaster_validity = 300 @@ -697,6 +695,14 @@ def __init__(self, paths: OPPaths = None): Time validity (in seconds) for the sponsored transaction that is signed by paymaster. """ + self.deploy_aa_log_file_name = "deploy_aa_contracts.log" + """ + File name for the log file that will be created when deploying the AA contracts. + + It's recommended not to mess with this, it's only in the config because it's written and read + in different locations, so it helps to maintain a single source of truth. + """ + # ============================================================================================== @property diff --git a/deps.py b/deps.py index c836310..ba95885 100644 --- a/deps.py +++ b/deps.py @@ -215,10 +215,6 @@ def go_path_setup(): if os.path.isfile("bin/go"): os.environ.pop("GOROOT", None) # works if GOROOT isn't set - # make sure that GOPATH is set in PATH (this is needed for 4337 bundler) - gopath = lib.run("get GOPATH", "go env GOPATH") - lib.prepend_to_path(gopath) - #################################################################################################### diff --git a/roll.py b/roll.py index 06e27ac..f4e41ce 100755 --- a/roll.py +++ b/roll.py @@ -10,6 +10,7 @@ import argparsext import block_explorer +import account_abstraction import deps import l1 import l2 @@ -68,6 +69,10 @@ def delimiter(*args, **kwargs): "l2", help="deploys and starts a local L2 blockchain") +command( + "aa", + help="starts an ERC-4337 bundler and a paymaster signer service") + # -------------------------------------------------------------------------------------------------- delimiter("GRANULAR COMMANDS") @@ -113,6 +118,11 @@ def delimiter(*args, **kwargs): "as well as anything that was downloaded. " "Mostly used to get the Optimism monorepo to rebuild.") +command( + "clean-aa", + help="cleans up deployment outputs for account abstraction", +) + command( "clean-l1", help="cleans up deployment outputs & databases for L1") @@ -178,11 +188,18 @@ def delimiter(*args, **kwargs): # NOTE: When functional, might want to add to other commands (e.g. `start-l2`). cmd.add_argument( "--explorer", - help="deploy a blockscout explorer for the L2 chain (NOT FUNCTIONAL)", + help="deploys a blockscout explorer for the L2 chain (NOT FUNCTIONAL)", default=False, dest="explorer", action="store_true") + cmd.add_argument( + "--aa", + help="starts an ERC4337 bundler and a paymaster signer service", + default=False, + dest="aa", + action="store_true") + #################################################################################################### @@ -270,6 +287,20 @@ def load_config() -> Config: return config +#################################################################################################### + +def start_addons(config: Config): + """ + Starts a block explorer and/or an ERC4337 bundler and paymaster, if configured to do so. + """ + if hasattr(lib.args, "explorer") and lib.args.explorer: + block_explorer.launch_blockscout() + if hasattr(lib.args, "aa") and lib.args.aa: + account_abstraction.setup() + account_abstraction.deploy(config) + account_abstraction.start(config) + + #################################################################################################### def clean(config: Config): @@ -308,8 +339,7 @@ def main(): if config.deploy_devnet_l1: l1.deploy_devnet_l1(config) l2.deploy_and_start(config) - if lib.args.explorer: - block_explorer.launch_blockscout() + start_addons(config) PROCESS_MGR.wait_all() elif lib.args.command == "clean": @@ -319,8 +349,13 @@ def main(): deps.check_or_install_foundry() l2.deploy_and_start(config) - if lib.args.explorer: - block_explorer.launch_blockscout() + start_addons(config) + PROCESS_MGR.wait_all() + + elif lib.args.command == "aa": + account_abstraction.setup() + account_abstraction.deploy(config) + account_abstraction.start(config) PROCESS_MGR.wait_all() elif lib.args.command == "l1": @@ -370,6 +405,9 @@ def main(): elif lib.args.command == "clean-l2": l2.clean(config) + elif lib.args.command == "clean-aa": + account_abstraction.clean() + print("Done.") except KeyboardInterrupt: # Usually not triggered because we will exit via the exit hook handler.