- 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>
129 lines
5.1 KiB
Python
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)}
|