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
262
app/routers/bubble_maps_router.py
Normal file
262
app/routers/bubble_maps_router.py
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
"""
|
||||
RugMaps API Router — RMI's interactive wallet visualization.
|
||||
Self-contained engine, no BubbleMaps.com API dependency.
|
||||
Connects to /api/v1/rugmaps/*
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/rugmaps", tags=["rugmaps"])
|
||||
|
||||
|
||||
class RugMapsRequest(BaseModel):
|
||||
center_wallet: str
|
||||
depth: int = 2
|
||||
min_strength: float = 0.1
|
||||
|
||||
|
||||
def _get_rm():
|
||||
try:
|
||||
from app.bubble_maps import get_bubble_maps_pro
|
||||
|
||||
return get_bubble_maps_pro()
|
||||
except ImportError as e:
|
||||
raise HTTPException(status_code=503, detail=f"Module unavailable: {e}")
|
||||
|
||||
|
||||
@router.post("/map")
|
||||
async def generate_rug_map(req: RugMapsRequest):
|
||||
"""Generate interactive RugMap for a wallet (2-hop default)."""
|
||||
rm = _get_rm()
|
||||
try:
|
||||
result = await rm.generate_map(
|
||||
center_wallet=req.center_wallet, depth=min(req.depth, 5), min_strength=req.min_strength
|
||||
)
|
||||
return result.to_dict()
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)[:500])
|
||||
|
||||
|
||||
@router.get("/analyze/{address}")
|
||||
async def analyze_wallet(address: str, depth: int = Query(2, ge=1, le=5)):
|
||||
"""Quick wallet analysis with RugMaps data."""
|
||||
rm = _get_rm()
|
||||
result = await rm.generate_map(center_wallet=address, depth=depth)
|
||||
d = result.to_dict()
|
||||
return {
|
||||
"address": address,
|
||||
"node_count": len(d.get("nodes", [])),
|
||||
"link_count": len(d.get("links", [])),
|
||||
"risk_level": d.get("risk_level", "unknown"),
|
||||
"risk_score": d.get("risk_score", 0),
|
||||
"stats": d.get("stats", {}),
|
||||
"top_counterparties": [n["address"][:12] + "..." for n in d.get("nodes", []) if n.get("layer") == 1][:10],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/entity/{address}")
|
||||
async def get_entity_info(address: str):
|
||||
"""Get entity information for an address."""
|
||||
rm = _get_rm()
|
||||
info = await rm._get_entity_info(address)
|
||||
risk, level = await rm._calculate_risk(address)
|
||||
return {"address": address, "entity": info, "risk_score": risk, "risk_level": level}
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def rugmaps_health():
|
||||
return {"status": "ok", "service": "rugmaps-engine"}
|
||||
|
||||
|
||||
@router.get("/token-graph")
|
||||
async def token_holder_graph(
|
||||
token_address: str = Query(...),
|
||||
chain: str = Query("solana"),
|
||||
limit: int = Query(50, ge=5, le=200),
|
||||
skip_cache: bool = Query(False),
|
||||
):
|
||||
"""Generate a token holder graph with AI analysis and RAG caching.
|
||||
Uses DeepSeek Flash + our FAISS indexes + wallet labels + semantic cache."""
|
||||
import asyncio
|
||||
|
||||
from app.rugmaps_ai import (
|
||||
generate_ai_analysis,
|
||||
generate_fallback_analysis,
|
||||
get_cached_analysis,
|
||||
get_token_holders,
|
||||
get_wallet_labels,
|
||||
search_similar_scams,
|
||||
set_cached_analysis,
|
||||
)
|
||||
|
||||
# Check cache first (instant return for previously scanned tokens)
|
||||
if not skip_cache:
|
||||
cached = await get_cached_analysis(token_address, chain)
|
||||
if cached:
|
||||
logger.info(f"RugMaps cache HIT for {token_address[:12]}...")
|
||||
return {**cached, "from_cache": True}
|
||||
|
||||
rm = _get_rm()
|
||||
|
||||
# Get the bubble map concurrently with other data fetches
|
||||
try:
|
||||
result = await rm.generate_map(center_wallet=token_address, depth=3, min_strength=0.05)
|
||||
d = result.to_dict()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Graph generation failed: {str(e)[:200]}")
|
||||
|
||||
nodes = d.get("nodes", [])
|
||||
links = d.get("links", [])
|
||||
|
||||
# Enrich nodes concurrently
|
||||
enriched_nodes = []
|
||||
addresses_for_labels = []
|
||||
for node in nodes[:limit]:
|
||||
addr = node.get("address") or node.get("id", "")
|
||||
addresses_for_labels.append(addr)
|
||||
enriched = dict(node)
|
||||
enriched["explorer_url"] = _get_explorer_url(chain, addr)
|
||||
enriched_nodes.append(enriched)
|
||||
|
||||
# Fetch wallet labels and similar scams in parallel
|
||||
labels, similar_scams, holders = await asyncio.gather(
|
||||
get_wallet_labels(addresses_for_labels),
|
||||
search_similar_scams(token_address, chain),
|
||||
get_token_holders(token_address, chain),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
if isinstance(labels, Exception):
|
||||
labels = {}
|
||||
if isinstance(similar_scams, Exception):
|
||||
similar_scams = []
|
||||
if isinstance(holders, Exception):
|
||||
holders = []
|
||||
|
||||
# Enrich nodes with labels
|
||||
for node in enriched_nodes:
|
||||
addr = node.get("address", "")
|
||||
label_data = labels.get(addr, {})
|
||||
entity = label_data.get("entity", {})
|
||||
node["label"] = entity.get("name") or entity.get("label") or node.get("label")
|
||||
node["entity_type"] = entity.get("type") or entity.get("entity_type")
|
||||
node["is_dev"] = entity.get("is_dev") or entity.get("type") == "developer"
|
||||
node["risk"] = label_data.get("risk_score") or node.get("risk_score", 0)
|
||||
node["tags"] = entity.get("tags") or []
|
||||
node["risk_level"] = label_data.get("risk_level", "unknown")
|
||||
|
||||
# Generate AI analysis (DeepSeek Flash + fallback to our data engine)
|
||||
graph_data = {
|
||||
"node_count": len(enriched_nodes),
|
||||
"link_count": min(len(links), limit * 2),
|
||||
"risk_score": d.get("risk_score", 0),
|
||||
"risk_level": d.get("risk_level", "unknown"),
|
||||
"stats": d.get("stats", {}),
|
||||
}
|
||||
|
||||
try:
|
||||
ai_analysis = await generate_ai_analysis(token_address, chain, graph_data, holders, labels, similar_scams)
|
||||
except Exception as e:
|
||||
logger.warning(f"AI analysis failed, using fallback: {e}")
|
||||
ai_analysis = generate_fallback_analysis(
|
||||
d.get("risk_score", 0),
|
||||
holders,
|
||||
labels,
|
||||
similar_scams,
|
||||
len(enriched_nodes),
|
||||
min(len(links), limit * 2),
|
||||
)
|
||||
|
||||
response = {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"nodes": enriched_nodes,
|
||||
"links": links[: limit * 2],
|
||||
**graph_data,
|
||||
"analysis": ai_analysis,
|
||||
"similar_scams": similar_scams[:5],
|
||||
"wallet_labels_count": len(labels),
|
||||
"from_cache": False,
|
||||
}
|
||||
|
||||
# Cache for future requests
|
||||
set_cached_analysis(token_address, chain, response)
|
||||
|
||||
# Auto-label all wallets in the graph (background, non-blocking)
|
||||
try:
|
||||
from app.auto_labeler import get_auto_labeler
|
||||
|
||||
labeler = get_auto_labeler()
|
||||
for node in enriched_nodes:
|
||||
await labeler.observe_wallet(
|
||||
node["address"],
|
||||
chain,
|
||||
{"event": "scanned_in_graph", "token": token_address, "risk": node.get("risk", 0)},
|
||||
)
|
||||
except Exception:
|
||||
pass # Auto-labeling is best-effort
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _get_explorer_url(chain: str, address: str) -> str:
|
||||
explorers = {
|
||||
"solana": "https://solscan.io/account/",
|
||||
"ethereum": "https://etherscan.io/address/",
|
||||
"base": "https://basescan.org/address/",
|
||||
"bsc": "https://bscscan.com/address/",
|
||||
"arbitrum": "https://arbiscan.io/address/",
|
||||
"optimism": "https://optimistic.etherscan.io/address/",
|
||||
"polygon": "https://polygonscan.com/address/",
|
||||
}
|
||||
base = explorers.get(chain, explorers["ethereum"])
|
||||
return base + address
|
||||
|
||||
|
||||
# ── Auto-Labeler ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post("/auto-label")
|
||||
async def auto_label_wallet(request: dict):
|
||||
"""Submit wallet observations for automatic labeling."""
|
||||
try:
|
||||
from app.auto_labeler import get_auto_labeler
|
||||
|
||||
labeler = get_auto_labeler()
|
||||
|
||||
address = request.get("address", "")
|
||||
chain = request.get("chain", "ethereum")
|
||||
events = request.get("events", [])
|
||||
|
||||
all_labels = []
|
||||
for event in events:
|
||||
labels = await labeler.observe_wallet(address, chain, event)
|
||||
all_labels.extend(labels)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"address": address,
|
||||
"labels_applied": all_labels,
|
||||
"count": len(all_labels),
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)[:200])
|
||||
|
||||
|
||||
@router.get("/auto-label/stats")
|
||||
async def auto_label_stats():
|
||||
"""Get auto-labeler statistics."""
|
||||
try:
|
||||
from app.auto_labeler import get_auto_labeler
|
||||
|
||||
labeler = get_auto_labeler()
|
||||
return {"status": "ok", **labeler.get_stats()}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)[:200])
|
||||
Loading…
Add table
Add a link
Reference in a new issue