| """ |
| RMI Community Forensics Worker |
| Background processing for: |
| - Report verification pipeline |
| - Graph generation for forensic reports |
| - Cross-chain correlation batch jobs |
| - Community reputation updates |
| """ |
|
|
| import asyncio |
| import os |
| from datetime import UTC, datetime |
|
|
| import httpx |
|
|
| from app.core.logging import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
| |
| REDIS_HOST = os.getenv("REDIS_HOST", "rmi-redis") |
| REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) |
| REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "") |
| REDIS_DB = int(os.getenv("REDIS_DB", 0)) |
| BLOCKSCOUT_URL = os.getenv("BLOCKSCOUT_URL", "http://rmi-blockscout-backend:4000") |
| SUPABASE_URL = os.getenv("SUPABASE_URL", "") |
| SUPABASE_KEY = os.getenv("SUPABASE_KEY", "") |
|
|
|
|
| class CommunityForensicsWorker: |
| """Background worker for community forensics tasks.""" |
|
|
| def __init__(self): |
| self.blockscout = httpx.AsyncClient(base_url=BLOCKSCOUT_URL, timeout=30.0) |
| self.running = True |
|
|
| async def verify_report_graph(self, report_id: str, graph_data: dict) -> dict: |
| """Verify and enrich a community report's graph data.""" |
| |
| nodes = graph_data.get("nodes", []) |
| edges = graph_data.get("edges", []) |
|
|
| |
| enriched_nodes = [] |
| for node in nodes: |
| addr = node.get("id", "") |
| if len(addr) >= 20: |
| try: |
| resp = await self.blockscout.get(f"/api/v2/addresses/{addr}") |
| if resp.status_code == 200: |
| data = resp.json() |
| node["verified_name"] = data.get("name", "") |
| node["tx_count"] = data.get("transactions_count", 0) |
| except Exception: |
| pass |
| enriched_nodes.append(node) |
|
|
| return { |
| "report_id": report_id, |
| "nodes_verified": len(enriched_nodes), |
| "edges_count": len(edges), |
| "enriched": True, |
| "timestamp": datetime.now(UTC).isoformat(), |
| } |
|
|
| async def batch_cross_chain_correlate(self, addresses: list[str]) -> list[dict]: |
| """Batch cross-chain correlation for community investigations.""" |
| results = [] |
|
|
| for addr in addresses: |
| |
| chains = ["eth", "base", "bsc", "arbitrum"] |
| chain_hits = [] |
|
|
| for chain in chains: |
| try: |
| |
| |
| resp = await self.blockscout.get(f"/api/v2/addresses/{addr}") |
| if resp.status_code == 200: |
| chain_hits.append(chain) |
| except Exception: |
| pass |
|
|
| results.append( |
| { |
| "address": addr, |
| "chains_found": chain_hits, |
| "multi_chain": len(chain_hits) > 1, |
| } |
| ) |
|
|
| return results |
|
|
| async def generate_leaderboard_snapshot(self) -> dict: |
| """Generate daily leaderboard snapshot.""" |
| |
| return { |
| "timestamp": datetime.now(UTC).isoformat(), |
| "period": "daily", |
| "top_sleuths": [], |
| } |
|
|
| async def run(self): |
| """Main worker loop.""" |
| logger.info("[OK] Community Forensics Worker started") |
| while self.running: |
| try: |
| |
| |
| await asyncio.sleep(60) |
|
|
| |
| try: |
| resp = await self.blockscout.get("/api/v2/main-page/indexing-status") |
| if resp.status_code == 200: |
| logger.info(f"[OK] Blockscout healthy: {resp.json().get('finished_indexing_blocks', False)}") |
| except Exception as e: |
| logger.warning(f"[WARN] Blockscout health check failed: {e}") |
| except Exception as e: |
| logger.warning(f"[ERROR] Worker loop: {e}") |
| await asyncio.sleep(5) |
|
|
| async def shutdown(self): |
| """Graceful shutdown.""" |
| self.running = False |
| await self.blockscout.aclose() |
| logger.info("[OK] Community Forensics Worker stopped") |
| async def main(): |
| worker = CommunityForensicsWorker() |
|
|
| try: |
| await worker.run() |
| except KeyboardInterrupt: |
| await worker.shutdown() |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|