#!/usr/bin/env python3 """ app.py — Autonomous Deployer (with Oversight, Logged) ----------------------------------------------------- Single-file portable deployer system. • Auto-creates local wallet (never uploads keys) • Accepts Solidity project ZIPs • Compiles using Foundry (forge) • Displays bytecode/ABI preview • Requires manual confirmation before deployment • Works on any EVM RPC (Infura, Alchemy, etc.) • Adds structured logging for every step """ import os import json import base64 import zipfile import subprocess import logging import uuid from pathlib import Path from flask import Flask, render_template_string, request, abort from web3 import Web3 from eth_account import Account # === ENV / CONFIG ============================================================= RPC_URL = os.getenv("RPC_URL", "https://sepolia.infura.io/v3/YOUR_KEY") UPLOAD_DIR = Path("uploads"); UPLOAD_DIR.mkdir(exist_ok=True) BUILD_DIR = Path("build"); BUILD_DIR.mkdir(exist_ok=True) WALLET_FILE = Path("wallet.json") LOG_FILE = Path("app.log") # === LOGGING ================================================================== logger = logging.getLogger("autodeployer") logger.setLevel(logging.INFO) if not logger.handlers: fh = logging.FileHandler(LOG_FILE) fh.setLevel(logging.INFO) fmt = logging.Formatter( fmt="%(asctime)s | %(levelname)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) fh.setFormatter(fmt) logger.addHandler(fh) def log_event(event: str, **fields): parts = [f"event={event}"] for k, v in fields.items(): parts.append(f"{k}={v}") logger.info(" ".join(parts)) # === AUTO WALLET SETUP ======================================================== def load_or_create_wallet(): if WALLET_FILE.exists(): data = json.load(open(WALLET_FILE)) key = base64.b64decode(data["private_key"]) acct = Account.from_key(key) print(f"[Wallet] Loaded existing wallet: {acct.address}") log_event("wallet_loaded", address=acct.address) return acct acct = Account.create() key_data = { "address": acct.address, "private_key": base64.b64encode(acct.key).decode() } WALLET_FILE.write_text(json.dumps(key_data)) print(f"[Wallet] Created new wallet: {acct.address}") log_event("wallet_created", address=acct.address) return acct wallet = load_or_create_wallet() # === WEB3 INIT ================================================================ w3 = Web3(Web3.HTTPProvider(RPC_URL)) if not w3.is_connected(): log_event("rpc_connect_failed", rpc_url=RPC_URL) raise SystemExit(f"[RPC] Cannot connect to {RPC_URL}") try: chain_id = w3.eth.chain_id print(f"[RPC] Connected to chain_id={chain_id}") log_event("rpc_connected", rpc_url=RPC_URL, chain_id=chain_id) except Exception as e: log_event("rpc_chainid_error", error=str(e)) chain_id = None # === BUILD / DEPLOY LOGIC ===================================================== def extract_project(zip_path: Path, build_id: str) -> Path: target_dir = BUILD_DIR / build_id target_dir.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(zip_path, "r") as z: z.extractall(target_dir) log_event("project_extracted", zip=str(zip_path), build_id=build_id, path=str(target_dir)) return target_dir def compile_project(path: Path) -> Path: """ Uses `forge build` and expects out/Factory.sol/Factory.json. You can generalize this to support other entrypoints. """ log_event("compile_start", path=str(path)) print("[Compile] Building project with Foundry...") try: subprocess.run( ["forge", "build", "--force"], cwd=path, check=True ) except subprocess.CalledProcessError as e: log_event("compile_error", path=str(path), error=str(e)) raise factory_json = path / "out" / "Factory.sol" / "Factory.json" if not factory_json.exists(): log_event("compile_manifest_missing", path=str(factory_json)) raise FileNotFoundError("Factory.json not found after build.") log_event("compile_success", manifest=str(factory_json)) return factory_json def deploy_with_oversight(manifest_path: Path) -> str: log_event("deploy_start", manifest=str(manifest_path), wallet=wallet.address) manifest = json.load(open(manifest_path)) abi = manifest["abi"] bytecode = manifest["bytecode"]["object"] contract = w3.eth.contract(abi=abi, bytecode=bytecode) nonce = w3.eth.get_transaction_count(wallet.address) gas_price = w3.to_wei("15", "gwei") tx = contract.constructor().build_transaction({ "from": wallet.address, "nonce": nonce, "gas": 5_000_000, "gasPrice": gas_price, # chainId may be added here if desired: # "chainId": chain_id, }) log_event( "tx_built", wallet=wallet.address, nonce=nonce, gas=tx.get("gas"), gas_price=gas_price ) signed = wallet.sign_transaction(tx) print("\n[Oversight] Manual confirmation required to broadcast.") print(f"From: {wallet.address}") print(f"RPC: {RPC_URL}") print(f"Gas: {tx['gas']} @ {gas_price} wei") print(f"Nonce: {nonce}") input(">>> Press ENTER to confirm deployment (Ctrl+C to abort): ") try: tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction) log_event("tx_sent", tx_hash=tx_hash.hex()) receipt = w3.eth.wait_for_transaction_receipt(tx_hash) addr = receipt.contractAddress print(f"[Deployed] Contract at {addr}") log_event( "deploy_success", contract_address=addr, tx_hash=tx_hash.hex(), status=receipt.status ) return addr except Exception as e: log_event("deploy_error", error=str(e)) raise # === FLASK APP ================================================================ app = Flask(__name__) # build_id -> manifest_path (server-side mapping; we don't trust raw paths from client) MANIFEST_INDEX: dict[str, str] = {} INDEX_HTML = """

🦾 Autodeployer (Oversight Mode)

Wallet: {{ wallet }}

""" REVIEW_HTML = """

📦 Deployment Preview

{{ preview | tojson(indent=2) }}
""" @app.route("/") def index(): return render_template_string(INDEX_HTML, wallet=wallet.address) @app.route("/upload", methods=["POST"]) def upload(): if "zipfile" not in request.files: abort(400, "missing file") f = request.files["zipfile"] if not f.filename.lower().endswith(".zip"): abort(400, "only .zip supported") build_id = uuid.uuid4().hex zip_path = UPLOAD_DIR / f"{build_id}.zip" f.save(zip_path) log_event("upload_received", filename=f.filename, saved_as=str(zip_path), build_id=build_id) path = extract_project(zip_path, build_id=build_id) manifest_path = compile_project(path) MANIFEST_INDEX[build_id] = str(manifest_path) manifest = json.load(open(manifest_path)) bytecode_obj = manifest.get("bytecode", {}).get("object", "") or "" preview = { "contract": manifest.get("contractName"), "bytecodePrefix": bytecode_obj[:20], "abiLength": len(manifest.get("abi", [])), } log_event( "preview_ready", build_id=build_id, contract=preview["contract"], abi_len=preview["abiLength"], ) return render_template_string(REVIEW_HTML, preview=preview, build_id=build_id) @app.route("/deploy", methods=["POST"]) def deploy(): build_id = request.form.get("build_id") if not build_id or build_id not in MANIFEST_INDEX: abort(400, "Invalid build id") manifest_path = Path(MANIFEST_INDEX[build_id]) if not manifest_path.exists(): log_event("deploy_manifest_missing", build_id=build_id, path=str(manifest_path)) abort(500, "Manifest not found on server") try: address = deploy_with_oversight(manifest_path) except Exception as e: return ( f"

❌ Deployment failed

" f"
{str(e)}
" ) return ( f"

✅ Deployed at: {address}

" f"

Wallet: {wallet.address}

" ) if __name__ == "__main__": print("\n=== Autodeployer Ready ===") print(f"Wallet: {wallet.address}") print(f"RPC: {RPC_URL}") print("Upload ZIP via http://localhost:7860") app.run(host="0.0.0.0", port=7860)