- 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>
156 lines
5.9 KiB
Python
156 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""#14 - Honeypot Network Map. Maps all known honeypot contracts, clusters by deployer,
|
|
fund flows. Interactive graph showing the honeypot factory network."""
|
|
|
|
import os
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
router = APIRouter(prefix="/api/v1/honeypot-map", tags=["honeypot-map"])
|
|
|
|
BACKEND = os.environ.get("BACKEND_URL", "http://localhost:8000")
|
|
|
|
|
|
class HoneypotNode(BaseModel):
|
|
id: str
|
|
label: str
|
|
type: str = "contract" # contract, deployer, funder, victim
|
|
chain: str
|
|
total_victims: int = 0
|
|
total_stolen_usd: float = 0
|
|
risk_score: int = 0
|
|
|
|
|
|
class HoneypotEdge(BaseModel):
|
|
source: str
|
|
target: str
|
|
relationship: str # deployed, funded_by, stole_from
|
|
amount_usd: float = 0
|
|
|
|
|
|
@router.get("/deployer/{deployer_address}")
|
|
async def get_honeypot_deployer(deployer_address: str, chain: str = Query("ethereum")):
|
|
"""Map all honeypot contracts deployed by a single address."""
|
|
nodes: list[dict] = []
|
|
edges: list[dict] = []
|
|
|
|
# Deployer node
|
|
deployer_id = deployer_address[:16]
|
|
nodes.append(
|
|
{
|
|
"id": deployer_id,
|
|
"label": f"Deployer {deployer_address[:10]}",
|
|
"type": "deployer",
|
|
"chain": chain,
|
|
}
|
|
)
|
|
|
|
# Find all contracts deployed
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) 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", []):
|
|
is_honeypot = any(w in (entry.get("label", "") or "").lower() for w in ["honeypot", "scam", "rug"])
|
|
contract_id = entry.get("contract_address", "")[:16]
|
|
if contract_id:
|
|
nodes.append(
|
|
{
|
|
"id": contract_id,
|
|
"label": entry.get("name", contract_id),
|
|
"type": "honeypot" if is_honeypot else "contract",
|
|
"chain": entry.get("chain", chain),
|
|
"risk_score": 80 if is_honeypot else 30,
|
|
}
|
|
)
|
|
edges.append(
|
|
{
|
|
"source": deployer_id,
|
|
"target": contract_id,
|
|
"relationship": "deployed",
|
|
}
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
return {"nodes": nodes, "edges": edges, "total_contracts": len([n for n in nodes if n["type"] != "deployer"])}
|
|
|
|
|
|
@router.get("/network")
|
|
async def get_honeypot_network(
|
|
chain: str = Query("ethereum"),
|
|
limit: int = Query(50, le=100),
|
|
min_risk: int = Query(50, le=100),
|
|
):
|
|
"""Get the current honeypot network map for a chain. Clusters by deployer groups."""
|
|
nodes: list[dict] = []
|
|
edges: list[dict] = []
|
|
seen_deployers: set[str] = set()
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.get(
|
|
f"{BACKEND}/api/v1/eth-labels/search", params={"q": "honeypot", "type": "deployer", "limit": limit}
|
|
)
|
|
if resp.status_code == 200:
|
|
entries = resp.json().get("labels", [])
|
|
for entry in entries[:limit]:
|
|
deployer = entry.get("address", "")
|
|
if deployer and deployer not in seen_deployers:
|
|
seen_deployers.add(deployer)
|
|
dep_id = deployer[:16]
|
|
nodes.append(
|
|
{
|
|
"id": dep_id,
|
|
"label": f"Deployer {deployer[:10]}",
|
|
"type": "deployer",
|
|
"chain": entry.get("chain", chain),
|
|
"risk_score": entry.get("risk_score", 70),
|
|
}
|
|
)
|
|
|
|
# Get their contracts
|
|
for contract in entry.get("contracts", [])[:5]:
|
|
c_id = contract.get("address", "")[:16]
|
|
if c_id:
|
|
nodes.append(
|
|
{
|
|
"id": c_id,
|
|
"label": contract.get("name", c_id),
|
|
"type": "contract",
|
|
"chain": entry.get("chain", chain),
|
|
"risk_score": 75,
|
|
}
|
|
)
|
|
edges.append(
|
|
{
|
|
"source": dep_id,
|
|
"target": c_id,
|
|
"relationship": "deployed",
|
|
}
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(502, f"Labels service unavailable: {e}") from e
|
|
|
|
return {
|
|
"chain": chain,
|
|
"nodes": nodes,
|
|
"edges": edges,
|
|
"total_deployers": len(seen_deployers),
|
|
"total_contracts": len([n for n in nodes if n["type"] == "contract"]),
|
|
}
|
|
|
|
|
|
@router.get("/stats")
|
|
async def get_honeypot_stats():
|
|
"""Global honeypot statistics."""
|
|
return {
|
|
"description": "Honeypot contract network intelligence from eth-labels + SENTINEL + Real-CATS",
|
|
"data_sources": ["eth-labels (115K addresses)", "Real-CATS (153K tokens)", "SENTINEL scanner"],
|
|
"chains_supported": ["ethereum", "bsc", "polygon", "arbitrum", "base", "avalanche", "optimism"],
|
|
}
|