Download app.py from luguog/F.art: direct link, hf CLI and curl.
- Browser
- Download file 18.4 kB
-
https://huggingface.co/spaces/luguog/F.art/resolve/main/app.py
- Command line
-
hf download hf://spaces/luguog/F.art/app.py
-
curl -L -o app.py https://huggingface.co/spaces/luguog/F.art/resolve/main/app.py
18.4 kB
| #!/usr/bin/env python3 | |
| """ | |
| app.py — Strategy Factory: Profitability-Gated EVM Deployer | |
| ----------------------------------------------------------- | |
| - Runs on Hugging Face / any container | |
| - Uses RPC + keys only from env/secrets (no user input) | |
| - Auto-creates a local wallet (EOA), private key never leaves disk | |
| - Accepts Solidity project ZIPs | |
| - Compiles with Foundry (forge) | |
| - Discovers deployable artifacts | |
| - Runs a profitability check on the project | |
| - Only allows deployment if profitability gate passes | |
| - No hardcoded RPC URL or key (HF/Secrets-safe) | |
| """ | |
| import os | |
| import json | |
| import base64 | |
| import zipfile | |
| import subprocess | |
| import logging | |
| import uuid | |
| from pathlib import Path | |
| from typing import Dict, Any, List, Tuple | |
| from flask import Flask, render_template_string, request, abort | |
| from web3 import Web3 | |
| from eth_account import Account | |
| # ============================================================================== | |
| # 0. CONFIG / ENV | |
| # ============================================================================== | |
| BASE_DIR = Path(__file__).resolve().parent | |
| UPLOAD_DIR = BASE_DIR / "uploads" | |
| BUILD_ROOT = BASE_DIR / "builds" | |
| WALLET_FILE = BASE_DIR / "wallet.json" | |
| LOG_FILE = BASE_DIR / "app.log" | |
| UPLOAD_DIR.mkdir(exist_ok=True) | |
| BUILD_ROOT.mkdir(exist_ok=True) | |
| # RPC resolution: | |
| # 1) Prefer explicit RPC_URL secret | |
| # 2) Else build from YOUR_KEY + INFURA_PROJECT (also secrets) | |
| RPC_URL = os.getenv("RPC_URL") | |
| if not RPC_URL: | |
| key = os.getenv("YOUR_KEY") # HF secret: Infura key, for example | |
| project = os.getenv("INFURA_PROJECT") # HF secret: e.g. "base-mainnet" | |
| if not key or not project: | |
| raise SystemExit( | |
| "[Config] Set either RPC_URL, or YOUR_KEY + INFURA_PROJECT as secrets." | |
| ) | |
| # NOTE: no literal full URL or key in code; built only from env | |
| RPC_URL = f"https://{project}.infura.io/v3/{key}" | |
| DEFAULT_GAS_LIMIT = int(os.getenv("DEPLOY_GAS_LIMIT", "5000000")) | |
| DEFAULT_GAS_PRICE_GWEI = os.getenv("DEPLOY_GAS_PRICE_GWEI", "15") | |
| PREFERRED_CONTRACT_NAME = os.getenv("DEPLOY_CONTRACT_NAME", "").strip() # optional | |
| # Profitability gate config | |
| MIN_EXPECTED_PROFIT = float(os.getenv("MIN_EXPECTED_PROFIT", "0.0")) # in your units | |
| # ============================================================================== | |
| # 1. LOGGING | |
| # ============================================================================== | |
| logger = logging.getLogger("strategy_factory") | |
| 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: Any) -> None: | |
| parts = [f"event={event}"] | |
| for k, v in fields.items(): | |
| parts.append(f"{k}={v}") | |
| logger.info(" ".join(parts)) | |
| # ============================================================================== | |
| # 2. WALLET MANAGEMENT | |
| # ============================================================================== | |
| def load_or_create_wallet() -> Account: | |
| if WALLET_FILE.exists(): | |
| data = json.loads(WALLET_FILE.read_text()) | |
| key = base64.b64decode(data["private_key"]) | |
| acct = Account.from_key(key) | |
| print(f"[Wallet] Loaded: {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: {acct.address}") | |
| log_event("wallet_created", address=acct.address) | |
| return acct | |
| wallet = load_or_create_wallet() | |
| # ============================================================================== | |
| # 3. RPC / WEB3 | |
| # ============================================================================== | |
| 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: chain_id={CHAIN_ID}") | |
| log_event("rpc_connected", rpc_url=RPC_URL, chain_id=CHAIN_ID) | |
| except Exception as e: | |
| CHAIN_ID = None | |
| log_event("rpc_chainid_error", error=str(e)) | |
| # ============================================================================== | |
| # 4. TOOLING CHECKS | |
| # ============================================================================== | |
| def check_forge() -> None: | |
| try: | |
| subprocess.run( | |
| ["forge", "--version"], | |
| check=True, | |
| stdout=subprocess.DEVNULL, | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| log_event("forge_available") | |
| except Exception as e: | |
| log_event("forge_missing", error=str(e)) | |
| raise SystemExit("[Setup] Foundry 'forge' binary not found in PATH.") | |
| check_forge() | |
| # ============================================================================== | |
| # 5. BUILD PIPELINE | |
| # ============================================================================== | |
| def extract_project(zip_path: Path, build_id: str) -> Path: | |
| target_dir = BUILD_ROOT / build_id | |
| target_dir.mkdir(parents=True, exist_ok=True) | |
| with zipfile.ZipFile(zip_path, "r") as zf: | |
| zf.extractall(target_dir) | |
| log_event("project_extracted", build_id=build_id, zip=str(zip_path), dest=str(target_dir)) | |
| return target_dir | |
| def compile_project(project_path: Path) -> None: | |
| log_event("compile_start", path=str(project_path)) | |
| print("[Compile] forge build ...") | |
| try: | |
| subprocess.run( | |
| ["forge", "build", "--force"], | |
| cwd=project_path, | |
| check=True, | |
| ) | |
| except subprocess.CalledProcessError as e: | |
| log_event("compile_error", path=str(project_path), error=str(e)) | |
| raise RuntimeError(f"forge build failed: {e}") | |
| log_event("compile_success", path=str(project_path)) | |
| def discover_artifacts(project_path: Path) -> List[Dict[str, Any]]: | |
| """ | |
| Deployable artifacts: JSON with non-empty 'bytecode.object' and non-empty 'abi'. | |
| """ | |
| out_dir = project_path / "out" | |
| results: List[Dict[str, Any]] = [] | |
| if not out_dir.exists(): | |
| log_event("artifact_out_missing", out=str(out_dir)) | |
| return results | |
| for json_path in out_dir.rglob("*.json"): | |
| try: | |
| manifest = json.loads(json_path.read_text()) | |
| except Exception: | |
| continue | |
| bytecode_obj = (manifest.get("bytecode") or {}).get("object") or "" | |
| abi = manifest.get("abi") or [] | |
| contract_name = manifest.get("contractName") | |
| if bytecode_obj and abi and contract_name: | |
| relpath = json_path.relative_to(project_path) | |
| results.append( | |
| { | |
| "path": json_path, | |
| "relpath": relpath, | |
| "contractName": contract_name, | |
| "bytecodePrefix": bytecode_obj[:32], | |
| "abiLength": len(abi), | |
| } | |
| ) | |
| log_event("artifact_scan_done", path=str(project_path), count=len(results)) | |
| return results | |
| def select_artifact(artifacts: List[Dict[str, Any]]) -> Dict[str, Any]: | |
| """ | |
| Selection strategy: | |
| - If PREFERRED_CONTRACT_NAME is set and present, use it. | |
| - If only one artifact, use it. | |
| - Else pick lexicographically smallest relpath. | |
| """ | |
| if not artifacts: | |
| raise RuntimeError("No deployable artifacts found.") | |
| if PREFERRED_CONTRACT_NAME: | |
| for art in artifacts: | |
| if art["contractName"] == PREFERRED_CONTRACT_NAME: | |
| log_event("artifact_selected_preferred", contract=art["contractName"], relpath=str(art["relpath"])) | |
| return art | |
| if len(artifacts) == 1: | |
| art = artifacts[0] | |
| log_event("artifact_selected_single", contract=art["contractName"], relpath=str(art["relpath"])) | |
| return art | |
| artifacts_sorted = sorted(artifacts, key=lambda a: str(a["relpath"])) | |
| art = artifacts_sorted[0] | |
| log_event( | |
| "artifact_selected_default", | |
| contract=art["contractName"], | |
| relpath=str(art["relpath"]), | |
| candidates=len(artifacts), | |
| ) | |
| return art | |
| def write_build_metadata(build_id: str, artifact_relpath: str) -> None: | |
| meta = { | |
| "build_id": build_id, | |
| "artifact_relpath": artifact_relpath, | |
| } | |
| meta_path = BUILD_ROOT / build_id / "deploy_meta.json" | |
| meta_path.write_text(json.dumps(meta)) | |
| log_event("build_meta_written", build_id=build_id, meta=str(meta_path)) | |
| def read_build_metadata(build_id: str) -> Dict[str, Any]: | |
| meta_path = BUILD_ROOT / build_id / "deploy_meta.json" | |
| if not meta_path.exists(): | |
| log_event("build_meta_missing", build_id=build_id, path=str(meta_path)) | |
| raise RuntimeError("Build metadata not found.") | |
| return json.loads(meta_path.read_text()) | |
| # ============================================================================== | |
| # 6. PROFITABILITY VERIFIER (FACTORY GATE) | |
| # ============================================================================== | |
| def verify_profitability(project_path: Path) -> Tuple[bool, str]: | |
| """ | |
| Concrete, deterministic gate: | |
| - Looks for project_path / profitability.json | |
| - Expects: { "expectedProfit": <float>, ... } | |
| - Accepts only if expectedProfit >= MIN_EXPECTED_PROFIT | |
| This gives you a real, pluggable contract: | |
| - Your MEV backtester / ZK verifier / simulator can emit this file. | |
| - The deployer refuses to deploy projects that don't meet the threshold. | |
| """ | |
| metrics_path = project_path / "profitability.json" | |
| if not metrics_path.exists(): | |
| msg = "profitability.json not found in project root" | |
| log_event("profitability_missing", path=str(metrics_path)) | |
| return False, msg | |
| try: | |
| data = json.loads(metrics_path.read_text()) | |
| except Exception as e: | |
| msg = f"invalid profitability.json: {e}" | |
| log_event("profitability_parse_error", path=str(metrics_path), error=str(e)) | |
| return False, msg | |
| expected_profit = float(data.get("expectedProfit", 0.0)) | |
| log_event("profitability_read", expected_profit=expected_profit, min_required=MIN_EXPECTED_PROFIT) | |
| if expected_profit < MIN_EXPECTED_PROFIT: | |
| msg = f"expectedProfit {expected_profit} < required {MIN_EXPECTED_PROFIT}" | |
| log_event("profitability_rejected", expected_profit=expected_profit) | |
| return False, msg | |
| msg = f"expectedProfit {expected_profit} >= required {MIN_EXPECTED_PROFIT}" | |
| log_event("profitability_accepted", expected_profit=expected_profit) | |
| return True, msg | |
| # ============================================================================== | |
| # 7. DEPLOYMENT | |
| # ============================================================================== | |
| def load_manifest(project_path: Path, artifact_relpath: str) -> Dict[str, Any]: | |
| manifest_path = project_path / artifact_relpath | |
| if not manifest_path.exists(): | |
| log_event("manifest_missing", path=str(manifest_path)) | |
| raise RuntimeError("Manifest file missing on disk.") | |
| return json.loads(manifest_path.read_text()) | |
| def build_deploy_tx(manifest: Dict[str, Any]) -> Dict[str, Any]: | |
| 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(DEFAULT_GAS_PRICE_GWEI, "gwei") | |
| tx = contract.constructor().build_transaction({ | |
| "from": wallet.address, | |
| "nonce": nonce, | |
| "gas": DEFAULT_GAS_LIMIT, | |
| "gasPrice": gas_price, | |
| "chainId": CHAIN_ID, | |
| }) | |
| log_event( | |
| "tx_built", | |
| wallet=wallet.address, | |
| nonce=nonce, | |
| gas=tx.get("gas"), | |
| gas_price=gas_price, | |
| ) | |
| return tx | |
| def deploy_manifest(project_path: Path, artifact_relpath: str) -> str: | |
| log_event( | |
| "deploy_start", | |
| project=str(project_path), | |
| artifact_relpath=artifact_relpath, | |
| wallet=wallet.address, | |
| ) | |
| manifest = load_manifest(project_path, artifact_relpath) | |
| tx = build_deploy_tx(manifest) | |
| signed = wallet.sign_transaction(tx) | |
| 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 | |
| status = receipt.status | |
| log_event( | |
| "deploy_success", | |
| contract_address=addr, | |
| tx_hash=tx_hash.hex(), | |
| status=status, | |
| ) | |
| if status != 1: | |
| raise RuntimeError(f"Deployment transaction reverted, status={status}") | |
| return addr | |
| except Exception as e: | |
| log_event("deploy_error", error=str(e)) | |
| raise | |
| # ============================================================================== | |
| # 8. FLASK APP (FACTORY UI) | |
| # ============================================================================== | |
| app = Flask(__name__) | |
| INDEX_HTML = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <body style="font-family:monospace;background:#0b0b0b;color:#ff8800"> | |
| <h2>🧱 Strategy Factory: Profitability-Gated Deployer</h2> | |
| <p>Wallet: {{ wallet }}</p> | |
| <p>RPC: {{ rpc }}</p> | |
| <p>Chain ID: {{ chain_id }}</p> | |
| <p>Min expectedProfit: {{ min_profit }}</p> | |
| <form action="/upload" method="post" enctype="multipart/form-data"> | |
| <input type="file" name="zipfile" required /> | |
| <button type="submit">Upload Strategy ZIP</button> | |
| </form> | |
| </body> | |
| </html> | |
| """ | |
| REVIEW_HTML = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <body style="font-family:monospace;background:#0b0b0b;color:#ff8800"> | |
| <h3>📦 Strategy Preview</h3> | |
| <p>Build ID: {{ build_id }}</p> | |
| <p>Artifact: {{ artifact_relpath }}</p> | |
| <p>Profitability Gate: {{ profit_msg }}</p> | |
| {% if can_deploy %} | |
| <pre>{{ preview | tojson(indent=2) }}</pre> | |
| <form action="/deploy" method="post"> | |
| <input type="hidden" name="build_id" value="{{ build_id }}"> | |
| <button type="submit">Deploy (Profitability Verified)</button> | |
| </form> | |
| {% else %} | |
| <h3 style="color:#ff4444">Deployment blocked by profitability gate.</h3> | |
| <pre>{{ preview | tojson(indent=2) }}</pre> | |
| <a href="/">Back</a> | |
| {% endif %} | |
| </body> | |
| </html> | |
| """ | |
| ERROR_HTML = """ | |
| <!DOCTYPE html> | |
| <html> | |
| <body style="font-family:monospace;background:#0b0b0b;color:#ff4444"> | |
| <h2>❌ Error</h2> | |
| <pre>{{ message }}</pre> | |
| <a href="/">Back</a> | |
| </body> | |
| </html> | |
| """ | |
| def index(): | |
| return render_template_string( | |
| INDEX_HTML, | |
| wallet=wallet.address, | |
| rpc=RPC_URL, | |
| chain_id=CHAIN_ID, | |
| min_profit=MIN_EXPECTED_PROFIT, | |
| ) | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "chainId": CHAIN_ID, | |
| "wallet": wallet.address, | |
| "rpcConfigured": bool(RPC_URL), | |
| } | |
| 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) | |
| try: | |
| project_path = extract_project(zip_path, build_id) | |
| compile_project(project_path) | |
| artifacts = discover_artifacts(project_path) | |
| artifact = select_artifact(artifacts) | |
| # profitability gate | |
| ok, profit_msg = verify_profitability(project_path) | |
| write_build_metadata(build_id, str(artifact["relpath"])) | |
| preview = { | |
| "contract": artifact["contractName"], | |
| "artifactRelpath": str(artifact["relpath"]), | |
| "bytecodePrefix": artifact["bytecodePrefix"], | |
| "abiLength": artifact["abiLength"], | |
| } | |
| log_event( | |
| "preview_ready", | |
| build_id=build_id, | |
| contract=preview["contract"], | |
| artifact_relpath=str(artifact["artifactRelpath"]) | |
| if "artifactRelpath" in preview | |
| else preview["artifactRelpath"] if "artifactRelpath" in preview else "", | |
| abi_len=preview["abiLength"], | |
| profitability_ok=ok, | |
| ) | |
| return render_template_string( | |
| REVIEW_HTML, | |
| preview=preview, | |
| build_id=build_id, | |
| artifact_relpath=str(artifact["relpath"]), | |
| profit_msg=profit_msg, | |
| can_deploy=ok, | |
| ) | |
| except Exception as e: | |
| log_event("upload_processing_error", build_id=build_id, error=str(e)) | |
| return render_template_string(ERROR_HTML, message=str(e)), 500 | |
| def deploy(): | |
| build_id = request.form.get("build_id", "").strip() | |
| if not build_id: | |
| abort(400, "missing build id") | |
| try: | |
| meta = read_build_metadata(build_id) | |
| artifact_relpath = meta["artifact_relpath"] | |
| project_path = BUILD_ROOT / build_id | |
| if not project_path.exists(): | |
| raise RuntimeError("Build directory not found on server.") | |
| # Optional: re-run profitability gate here if you want to be strict | |
| ok, _ = verify_profitability(project_path) | |
| if not ok: | |
| raise RuntimeError("Profitability gate failed at deploy time.") | |
| address = deploy_manifest(project_path, artifact_relpath) | |
| except Exception as e: | |
| msg = f"Deployment failed: {e}" | |
| log_event("deploy_exception", build_id=build_id, error=str(e)) | |
| return render_template_string(ERROR_HTML, message=msg), 500 | |
| return ( | |
| f"<h2 style='color:lime'>✅ Deployed at: {address}</h2>" | |
| f"<p>Wallet: {wallet.address}</p>" | |
| f"<p><a href='/'>Back</a></p>" | |
| ) | |
| if __name__ == "__main__": | |
| print("\n=== Strategy Factory Ready ===") | |
| print(f"Wallet: {wallet.address}") | |
| print(f"RPC: {RPC_URL}") | |
| print(f"Chain ID: {CHAIN_ID}") | |
| print(f"Min expectedProfit: {MIN_EXPECTED_PROFIT}") | |
| print("Upload strategy ZIP via http://localhost:7860") | |
| app.run(host="0.0.0.0", port=7860) |