merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
129
app/routers/criminal_clusters.py
Normal file
129
app/routers/criminal_clusters.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
#!/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}")
|
||||
|
||||
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)}
|
||||
Loading…
Add table
Add a link
Reference in a new issue