F.art / app.py
luguog's picture
Update app.py
c0bd405 verified
Raw History Blame
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>
"""
@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"<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)