#!/usr/bin/env python3
"""
SWARM Worker-Bee — mempool sensor module  (EXPERIMENTAL, opt-in)

WHAT IT DOES (and only this):
  • Subscribes to the PUBLIC Ethereum mempool over a websocket RPC
    (eth_subscribe newPendingTransactions).
  • Sends SWARM anonymized SIGHTINGS: tx hash + timestamp + a 4-char node tag.
  • Powers the live Swarm Radar (jointheswarm.co.uk/radar) and honeypot early-warning.

WHAT IT NEVER DOES:
  • No trades. No funds. No wallet. No private keys. It cannot move anything.
  • It only ever reports that a public transaction exists — data already public.

It is GATED server-side: SWARM only accepts sightings if YOU have switched the
"mempool sensor" module ON for your node (bot → Your Node → Modules, or the website
→ Manage your Node). Turn it off and this stops instantly.

Deps:  pip install websockets httpx
Env:   SWARM_NODE_KEY=wb_...   ETH_WSS=wss://<your-eth-node>/ws   (a public ws RPC works)
"""
import asyncio
import json
import os
import time

import httpx
import websockets

CONFIG_URL = "https://jointheswarm.co.uk/api/node/config"
MEMPOOL_URL = "https://jointheswarm.co.uk/api/node/mempool"
DEFAULT_WSS = os.environ.get("ETH_WSS", "wss://ethereum-rpc.publicnode.com")
BATCH_MAX = 50
FLUSH_SECS = 2.0


async def module_enabled(key: str) -> bool:
    """Respect the transparent control channel: only run if SWARM has the module ON."""
    try:
        async with httpx.AsyncClient(timeout=10) as c:
            r = await c.get(CONFIG_URL, params={"key": key})
            data = r.json()
        for m in data.get("modules", []):
            if m.get("id") == "mempool_sensor":
                return bool(m.get("enabled"))
    except Exception:
        pass
    return False


async def run(key: str, wss: str) -> None:
    node_tag = key[-4:]
    print(f"[mempool] sensor starting · node ·{node_tag} · {wss}")
    async with websockets.connect(wss, ping_interval=20, max_size=2**20) as ws:
        await ws.send(json.dumps({"id": 1, "jsonrpc": "2.0",
                                  "method": "eth_subscribe",
                                  "params": ["newPendingTransactions"]}))
        await ws.recv()  # subscription ack

        batch: list[dict] = []
        last_flush = time.time()
        last_gate_check = 0.0

        async def flush() -> None:
            nonlocal batch
            if not batch:
                return
            payload = {"token": key, "sightings": batch}
            batch = []
            try:
                async with httpx.AsyncClient(timeout=10) as c:
                    await c.post(MEMPOOL_URL, json=payload)
            except Exception as e:
                print(f"[mempool] flush failed: {e}")

        while True:
            # Re-check the gate every ~60s so an operator/user toggle takes effect live.
            if time.time() - last_gate_check > 60:
                if not await module_enabled(key):
                    print("[mempool] module disabled — stopping sensor.")
                    await flush()
                    return
                last_gate_check = time.time()

            try:
                raw = await asyncio.wait_for(ws.recv(), timeout=FLUSH_SECS)
                msg = json.loads(raw)
                tx_hash = msg.get("params", {}).get("result")
                if isinstance(tx_hash, str) and tx_hash.startswith("0x"):
                    batch.append({"h": tx_hash, "t": int(time.time() * 1000), "c": "eth"})
            except asyncio.TimeoutError:
                pass

            if len(batch) >= BATCH_MAX or (time.time() - last_flush) >= FLUSH_SECS:
                await flush()
                last_flush = time.time()


async def main() -> None:
    key = os.environ.get("SWARM_NODE_KEY", "").strip()
    if not key.startswith("wb_"):
        print("Set SWARM_NODE_KEY=wb_... (get it in @jointheswarmbot -> Run a node).")
        return
    if not await module_enabled(key):
        print("Mempool sensor is OFF for this node. Enable it in the bot -> Your Node -> Modules.")
        return
    while True:
        try:
            await run(key, DEFAULT_WSS)
        except Exception as e:
            print(f"[mempool] reconnect in 5s after: {e}")
            await asyncio.sleep(5)


if __name__ == "__main__":
    asyncio.run(main())
