rmi-backend/app/routers/criminal_clusters.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

129 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""#8 - Real-CATS Criminal Cluster Explorer. Interactive graph of 153K labeled tokens.
Shows relationships: same deployer, funding source, code patterns. Rug adjacency graph."""
import os
from pathlib import Path
import httpx
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
router = APIRouter(prefix="/api/v1/criminal-clusters", tags=["criminal-clusters"])
BACKEND = os.environ.get("BACKEND_URL", "http://localhost:8000")
REAL_CATS_DB = os.environ.get("REAL_CATS_DB", str(Path.home() / "rmi/backend/data/real_cats.db"))
class ClusterNode(BaseModel):
id: str
label: str
type: str = "token" # token, deployer, funder
chain: str = ""
rug_count: int = 0
risk_score: int = 0
class ClusterEdge(BaseModel):
source: str
target: str
relationship: str # same_deployer, same_funder, code_similar, fund_flow
strength: float = 1.0
@router.get("/deployer/{deployer_address}")
async def get_deployer_cluster(deployer_address: str, chain: str = Query("ethereum")):
"""Get all tokens deployed by this address. Shows full rug adjacency."""
tokens: list[dict] = []
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{BACKEND}/api/v1/eth-labels/search", params={"address": deployer_address, "type": "deployer"}
)
if resp.status_code == 200:
data = resp.json()
for entry in data.get("labels", []):
tokens.append(
{
"address": entry.get("contract_address", ""),
"name": entry.get("name", "?"),
"chain": entry.get("chain", chain),
"label": entry.get("label", ""),
"risk": entry.get("risk_score", 0),
}
)
except Exception:
pass
return {
"deployer": deployer_address,
"chain": chain,
"tokens_deployed": len(tokens),
"rug_count": sum(1 for t in tokens if "rug" in t.get("label", "").lower()),
"tokens": tokens,
"risk_assessment": "HIGH" if any(t.get("risk", 0) > 50 for t in tokens) else "MEDIUM",
}
@router.get("/token/{token_address}")
async def get_token_cluster(token_address: str, chain: str = Query("ethereum")):
"""Get all related tokens for a given token - same deployer, same funder, code clones."""
nodes: list[dict] = []
edges: list[dict] = []
# Fetch token info from scanner
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.post(
f"{BACKEND}/api/v1/token/scan",
json={"token_address": token_address, "chain": chain},
headers={"X-RMI-Key": "rmi-internal-2026"},
)
if resp.status_code == 200:
scan = resp.json()
# Root node
nodes.append(
{
"id": token_address[:16],
"label": scan.get("symbol", token_address[:8]),
"type": "token",
"chain": chain,
}
)
# Get deployer info
deployer = scan.get("pro", {}).get("deployer_address", "")
if deployer:
nodes.append({"id": deployer[:16], "label": f"Deployer {deployer[:8]}", "type": "deployer"})
edges.append({"source": deployer[:16], "target": token_address[:16], "relationship": "deployed"})
# Get siblings (same deployer)
async with httpx.AsyncClient(timeout=10) as client:
resp2 = await client.get(f"{BACKEND}/api/v1/eth-labels/search", params={"address": deployer})
if resp2.status_code == 200:
for sibling in resp2.json().get("labels", [])[:10]:
sib_addr = sibling.get("contract_address", "")[:16]
if sib_addr and sib_addr != token_address[:16]:
nodes.append({"id": sib_addr, "label": sibling.get("name", sib_addr), "type": "token"})
edges.append({"source": deployer[:16], "target": sib_addr, "relationship": "deployed"})
edges.append({"source": sib_addr, "target": token_address[:16], "relationship": "sibling"})
except Exception as e:
raise HTTPException(502, f"Scanner unavailable: {e}") from e
return {"nodes": nodes, "edges": edges, "total_nodes": len(nodes), "total_edges": len(edges)}
@router.get("/search")
async def search_clusters(q: str = Query(..., min_length=2), limit: int = Query(20, le=50)):
"""Search for deployers/tokens in the cluster database."""
results: list[dict] = []
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{BACKEND}/api/v1/eth-labels/search", params={"q": q, "limit": limit})
if resp.status_code == 200:
results = resp.json().get("labels", [])
except Exception:
pass
return {"query": q, "results": results, "count": len(results)}