Download app.py from luguog/F.art: direct link, hf CLI and curl.
- Browser
- Download file 9.2 kB
-
https://huggingface.co/spaces/luguog/F.art/resolve/ae8a413a48d71ffd68578ce92fb1158b8c2a067f/app.py
- Command line
-
hf download hf://spaces/luguog/F.art@ae8a413a48d71ffd68578ce92fb1158b8c2a067f/app.py
-
curl -L -o app.py https://huggingface.co/spaces/luguog/F.art/resolve/ae8a413a48d71ffd68578ce92fb1158b8c2a067f/app.py
9.2 kB
| #!/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 = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <body style="font-family:monospace;background:#0b0b0b;color:#ff8800"> | |
| <h2>🦾 Autodeployer (Oversight Mode)</h2> | |
| <p>Wallet: {{ wallet }}</p> | |
| <form action="/upload" method="post" enctype="multipart/form-data"> | |
| <input type="file" name="zipfile" required /> | |
| <button type="submit">Upload Solidity ZIP</button> | |
| </form> | |
| </body> | |
| </html> | |
| """ | |
| REVIEW_HTML = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <body style="font-family:monospace;background:#0b0b0b;color:#ff8800"> | |
| <h3>📦 Deployment Preview</h3> | |
| <pre>{{ preview | tojson(indent=2) }}</pre> | |
| <form action="/deploy" method="post"> | |
| <input type="hidden" name="build_id" value="{{ build_id }}"> | |
| <button type="submit">Deploy (Manual Approval Required)</button> | |
| </form> | |
| </body> | |
| </html> | |
| """ | |
| def index(): | |
| return render_template_string(INDEX_HTML, wallet=wallet.address) | |
| 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) | |
| 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"<h2 style='color:red'>❌ Deployment failed</h2>" | |
| f"<pre>{str(e)}</pre>" | |
| ) | |
| return ( | |
| f"<h2 style='color:lime'>✅ Deployed at: {address}</h2>" | |
| f"<p>Wallet: {wallet.address}</p>" | |
| ) | |
| 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) |