- 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>
682 lines
23 KiB
Python
682 lines
23 KiB
Python
"""
|
|
Security Intelligence Router v2 - Complete Crypto Security Stack
|
|
===================================================================
|
|
|
|
All crypto security modules exposed via REST:
|
|
• Contract: bytecode scan + deep scan (Slither + Mythril)
|
|
• Wallet: reputation + threat intel + anomaly detection
|
|
• Graph: clustering + fund flow + cross-chain correlation
|
|
• Mempool: real-time attack detection + sentinel control
|
|
• Threat: OFAC + local blocklist + batch lookup
|
|
• Dashboard: alert aggregation + monitoring + scoring
|
|
• ML: anomaly detection on wallets + token metrics
|
|
• CrossChain: wallet linking across chains
|
|
|
|
Updated 2026-05-08 for world-class crypto intelligence.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# ═══ LAZY IMPORTS (deferred to first use - saves ~2.2s cold start) ═══
|
|
import importlib as _il
|
|
import os
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
_lazy_cache = {}
|
|
|
|
|
|
def _L(module_path):
|
|
if module_path not in _lazy_cache:
|
|
_lazy_cache[module_path] = _il.import_module(module_path)
|
|
return _lazy_cache[module_path]
|
|
|
|
|
|
# Lightweight imports (eager - these are fast)
|
|
from fastapi import APIRouter # noqa: E402
|
|
from pydantic import BaseModel, Field # noqa: E402
|
|
|
|
from app.cross_chain_correlator import ChainFingerprint, CrossChainCorrelator # noqa: E402
|
|
|
|
router = APIRouter(prefix="/api/v1/security", tags=["security"])
|
|
|
|
# ─── SINGLETONS ────────────────────────────────────────────
|
|
|
|
_sentinel_manager = None
|
|
_wallet_detector = None
|
|
_token_detector = None
|
|
|
|
|
|
async def get_sentinel() -> SentinelManager: # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
|
|
global _sentinel_manager
|
|
if _sentinel_manager is None:
|
|
_sentinel_manager = _L("app.mempool_sentinel").SentinelManager()
|
|
return _sentinel_manager
|
|
|
|
|
|
async def get_wallet_detector():
|
|
global _wallet_detector
|
|
if _wallet_detector is None:
|
|
_ml = _L("app.ml_anomaly")
|
|
_wallet_detector = _ml.WalletAnomalyDetector(contamination=0.05)
|
|
return _wallet_detector
|
|
|
|
|
|
async def get_token_detector() -> TokenMetricAnomalyDetector: # noqa: F821 -- pre-existing bug, see fix(f821) tracking issue
|
|
global _token_detector
|
|
if _token_detector is None:
|
|
_ml = _L("app.ml_anomaly")
|
|
_token_detector = _ml.TokenMetricAnomalyDetector()
|
|
return _token_detector
|
|
|
|
|
|
# ─── MODELS ────────────────────────────────────────────────
|
|
|
|
|
|
class ContractScanRequest(BaseModel):
|
|
address: str = Field(..., min_length=32, max_length=44)
|
|
chain: str = Field(default="ethereum")
|
|
deep: bool = Field(default=False, description="Run Slither + Mythril deep scan")
|
|
|
|
|
|
class BatchContractRequest(BaseModel):
|
|
addresses: list[str] = Field(..., min_length=1, max_length=50)
|
|
chain: str = Field(default="ethereum")
|
|
deep: bool = Field(default=False)
|
|
|
|
|
|
class WalletCheckRequest(BaseModel):
|
|
address: str = Field(..., min_length=32, max_length=44)
|
|
chain: str = Field(default="auto")
|
|
|
|
|
|
class WalletAnalyzeRequest(BaseModel):
|
|
address: str = Field(..., min_length=32, max_length=44)
|
|
transactions: list[dict[str, Any]] = Field(default_factory=list, description="Raw tx list for ML analysis")
|
|
chain: str = Field(default="auto")
|
|
|
|
|
|
class GraphAnalyzeRequest(BaseModel):
|
|
transactions: list[dict[str, Any]] = Field(..., min_length=2, max_length=1000)
|
|
chain: str = Field(default="ethereum")
|
|
analysis_types: list[str] = Field(default=["clusters", "flows", "anomalies"])
|
|
|
|
|
|
class ThreatBatchRequest(BaseModel):
|
|
addresses: list[str] = Field(..., min_length=1, max_length=100)
|
|
chain: str = Field(default="evm")
|
|
|
|
|
|
class CrossChainRequest(BaseModel):
|
|
fingerprints: list[dict[str, Any]] = Field(..., min_length=2, description="Chain fingerprints")
|
|
chains: list[str] = Field(default=["ethereum", "base", "solana"])
|
|
|
|
|
|
class TokenMetricRequest(BaseModel):
|
|
token: str = Field(...)
|
|
metrics: dict[str, float] = Field(..., description="Current metric values")
|
|
|
|
|
|
class AlertAckRequest(BaseModel):
|
|
alert_id: str = Field(...)
|
|
|
|
|
|
# ─── CONTRACT ANALYSIS ─────────────────────────────────────
|
|
|
|
|
|
@router.get("/contract/{address}")
|
|
async def contract_analysis(address: str, chain: str = "ethereum", deep: bool = False):
|
|
"""Analyze contract - bytecode scan + optional Slither/Mythril deep scan."""
|
|
|
|
# Fast bytecode scan
|
|
_oca = _L("app.onchain_analyzer")
|
|
result = await _oca.analyze_contract(address, chain)
|
|
response = {
|
|
"address": result.address,
|
|
"chain": result.chain,
|
|
"is_contract": result.is_contract,
|
|
"is_proxy": result.is_proxy,
|
|
"proxy_type": result.proxy_type,
|
|
"proxy_implementation": result.proxy_implementation,
|
|
"risk_score": result.risk_score,
|
|
"risk_flags": result.risk_flags,
|
|
"detected_interfaces": result.detected_interfaces,
|
|
"has_selfdestruct": result.has_selfdestruct,
|
|
"has_delegatecall": result.has_delegatecall,
|
|
"has_mint": result.has_mint,
|
|
"has_burn": result.has_burn,
|
|
"has_renounce_ownership": result.has_renounce_ownership,
|
|
"has_reentrancy_guard": result.has_reentrancy_guard,
|
|
"uses_tx_origin": result.uses_tx_origin,
|
|
"uses_timestamp": result.uses_timestamp,
|
|
"honeypot_indicators": result.honeypot_indicators,
|
|
"anti_bot_detected": result.anti_bot_detected,
|
|
"uniswap_pair": result.uniswap_pair,
|
|
"analysis_time": result.analysis_time,
|
|
}
|
|
|
|
# Deep scan (if requested)
|
|
if deep:
|
|
try:
|
|
_deep = _L("app.contract_deepscan")
|
|
deep_result = await _deep.deep_scan_contract(address, chain, use_cache=True)
|
|
response["deep_scan"] = deep_result.to_dict()
|
|
# Merge scores
|
|
response["risk_score"] = max(response["risk_score"], deep_result.risk_score)
|
|
response["deep_vulnerabilities"] = len(deep_result.vulnerabilities)
|
|
except Exception as e:
|
|
response["deep_scan_error"] = str(e)
|
|
|
|
return response
|
|
|
|
|
|
@router.post("/scan/batch")
|
|
async def batch_contract_scan(req: BatchContractRequest):
|
|
"""Batch contract scan - fast or deep."""
|
|
if req.deep:
|
|
_deep = _L("app.contract_deepscan")
|
|
results = await _deep.batch_deep_scan(req.addresses, req.chain)
|
|
return {
|
|
"scanned": len(results),
|
|
"chain": req.chain,
|
|
"high_risk": [r.address for r in results if r.risk_score >= 70],
|
|
"critical": [r.address for r in results if r.risk_score >= 90],
|
|
"results": [r.to_dict() for r in results],
|
|
}
|
|
else:
|
|
_oca = _L("app.onchain_analyzer")
|
|
results = await _oca.batch_analyze_contracts(req.addresses, req.chain)
|
|
return {
|
|
"scanned": len(results),
|
|
"chain": req.chain,
|
|
"high_risk": [r.address for r in results if r.risk_score >= 70],
|
|
"proxies": [r.address for r in results if r.is_proxy],
|
|
"honeypots": [r.address for r in results if r.honeypot_indicators],
|
|
"results": [
|
|
{
|
|
"address": r.address,
|
|
"risk_score": r.risk_score,
|
|
"risk_flags": r.risk_flags,
|
|
"is_proxy": r.is_proxy,
|
|
"honeypot": r.honeypot_indicators,
|
|
}
|
|
for r in results
|
|
],
|
|
}
|
|
|
|
|
|
# ─── WALLET / THREAT INTEL ──────────────────────────────────
|
|
|
|
|
|
@router.get("/wallet/{address}")
|
|
async def wallet_security_check(address: str, chain: str = "auto"):
|
|
"""Full wallet security check: reputation + threat intel."""
|
|
_cg = _L("app.crypto_guard")
|
|
reputation = await _cg.check_wallet_reputation(address, chain)
|
|
_ti = _L("app.threat_intel")
|
|
agg = await _ti.get_aggregator()
|
|
threat_rep = await agg.check_address(address, chain)
|
|
|
|
final_score = max(reputation.get("score", 0), threat_rep.risk_score)
|
|
|
|
return {
|
|
"address": address,
|
|
"chain": chain,
|
|
"risk_score": final_score,
|
|
"blocked": reputation.get("blocked", False) or threat_rep.is_blocked,
|
|
"sanctioned": threat_rep.is_sanctioned,
|
|
"scam": threat_rep.is_scam,
|
|
"mixer": threat_rep.is_mixer,
|
|
"categories": list(threat_rep.categories),
|
|
"tags": list(threat_rep.tags),
|
|
"threat_reports": len(threat_rep.threat_reports),
|
|
"reputation_flags": reputation.get("flags", []),
|
|
"first_flagged": threat_rep.first_flagged,
|
|
"last_flagged": threat_rep.last_flagged,
|
|
}
|
|
|
|
|
|
@router.post("/wallet/analyze")
|
|
async def wallet_ml_analysis(req: WalletAnalyzeRequest):
|
|
"""ML behavioral analysis of wallet transactions."""
|
|
from app.ml_anomaly import extract_wallet_features
|
|
|
|
features = extract_wallet_features(req.transactions, req.address)
|
|
detector = await get_wallet_detector()
|
|
|
|
# Fit detector if we have comparison data
|
|
if req.transactions and len(req.transactions) > 5:
|
|
detector.fit([features])
|
|
result = detector.predict(features)
|
|
else:
|
|
result = {"error": "Insufficient transactions for ML analysis (need >5)"}
|
|
|
|
return {
|
|
"address": req.address,
|
|
"features": {
|
|
"tx_count": features.tx_count,
|
|
"avg_tx_value": features.avg_tx_value,
|
|
"unique_counterparties": features.unique_counterparties,
|
|
"dex_swaps": features.dex_swaps,
|
|
"burst_ratio": features.burst_ratio,
|
|
"night_ratio": features.night_tx_ratio,
|
|
},
|
|
"ml_result": result,
|
|
}
|
|
|
|
|
|
@router.post("/threat/check")
|
|
async def batch_threat_check(req: ThreatBatchRequest):
|
|
"""Batch threat intel lookup."""
|
|
_ti = _L("app.threat_intel")
|
|
agg = await _ti.get_aggregator()
|
|
results = await agg.batch_check(req.addresses, req.chain)
|
|
|
|
return {
|
|
"checked": len(results),
|
|
"blocked": [a for a, r in results.items() if r.is_blocked],
|
|
"sanctioned": [a for a, r in results.items() if r.is_sanctioned],
|
|
"scams": [a for a, r in results.items() if r.is_scam],
|
|
"mixers": [a for a, r in results.items() if r.is_mixer],
|
|
"results": {a: r.to_dict() for a, r in results.items()},
|
|
}
|
|
|
|
|
|
# ─── GRAPH ANALYSIS ─────────────────────────────────────────
|
|
|
|
|
|
@router.post("/graph/analyze")
|
|
async def graph_analyze(req: GraphAnalyzeRequest):
|
|
"""Transaction graph: clusters, flows, anomalies."""
|
|
_tga = _L("app.tx_graph_analyzer")
|
|
graph = await _tga.build_graph_from_transactions(req.transactions, req.chain)
|
|
|
|
response = {
|
|
"nodes": len(graph.nodes),
|
|
"edges": len(graph.edges),
|
|
"wallets": list(graph.nodes.keys())[:50],
|
|
}
|
|
|
|
if "clusters" in req.analysis_types:
|
|
clusters = graph.cluster_by_louvain()
|
|
response["clusters"] = [
|
|
{
|
|
"id": c.id,
|
|
"wallet_count": len(c.wallets),
|
|
"total_volume": c.total_volume,
|
|
}
|
|
for c in clusters[:10]
|
|
]
|
|
|
|
if "shared_funding" in req.analysis_types:
|
|
shared = graph.cluster_by_shared_funding(min_common=2)
|
|
response["shared_funding_clusters"] = [
|
|
{
|
|
"id": c.id,
|
|
"wallet_count": len(c.wallets),
|
|
"shared_funders": c.shared_funders[:5],
|
|
}
|
|
for c in shared[:10]
|
|
]
|
|
|
|
if "flows" in req.analysis_types:
|
|
cycles = graph.detect_circular_flows(max_cycle_len=4)
|
|
response["circular_flows"] = len(cycles)
|
|
response["wash_trading_suspected"] = len(cycles) > 2
|
|
|
|
if "anomalies" in req.analysis_types:
|
|
bursts = graph.detect_burst_transfers(min_count=15)
|
|
sudden = graph.detect_sudden_wealth(multiplier=20.0)
|
|
response["burst_activity"] = bursts[:5]
|
|
response["sudden_wealth"] = sudden[:5]
|
|
|
|
if "export" in req.analysis_types:
|
|
import tempfile
|
|
|
|
path = tempfile.mktemp(suffix=".gexf")
|
|
graph.export_gexf(path)
|
|
response["gexf_export_path"] = path
|
|
|
|
return response
|
|
|
|
|
|
# ─── CROSS-CHAIN ────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/crosschain/correlate")
|
|
async def crosschain_correlate(req: CrossChainRequest):
|
|
"""Find cross-chain wallet correlations."""
|
|
correlator = CrossChainCorrelator(confidence_threshold=0.6)
|
|
|
|
# Add fingerprints
|
|
for fp_data in req.fingerprints:
|
|
import numpy as np
|
|
|
|
fp = ChainFingerprint(
|
|
address=fp_data["address"],
|
|
chain=fp_data["chain"],
|
|
feature_vector=np.array(fp_data.get("features", []), dtype=np.float64),
|
|
first_active=fp_data.get("first_active"),
|
|
last_active=fp_data.get("last_active"),
|
|
total_txs=fp_data.get("total_txs", 0),
|
|
total_volume=fp_data.get("total_volume", 0.0),
|
|
known_tags=fp_data.get("tags", []),
|
|
)
|
|
correlator.add_fingerprint(fp)
|
|
|
|
# Find links
|
|
links = correlator.find_all_links(req.chains)
|
|
|
|
return {
|
|
"fingerprints_analyzed": len(req.fingerprints),
|
|
"links_found": len(links),
|
|
"high_confidence_links": [line.to_dict() for line in links if line.confidence >= 0.8],
|
|
"all_links": [line.to_dict() for line in links[:20]],
|
|
}
|
|
|
|
|
|
# ─── ML / ANOMALY ───────────────────────────────────────────
|
|
|
|
|
|
@router.post("/ml/token/anomaly")
|
|
async def token_metric_anomaly(req: TokenMetricRequest):
|
|
"""Detect token metric anomalies vs baseline."""
|
|
detector = await get_token_detector()
|
|
detector.update_baseline(req.token, req.metrics)
|
|
result = detector.detect_anomaly(req.token, req.metrics)
|
|
return result
|
|
|
|
|
|
# ─── MEMPOOL ────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/mempool/status")
|
|
async def mempool_status():
|
|
"""Mempool sentinel status + recent detections."""
|
|
sentinel = await get_sentinel()
|
|
stats = sentinel.get_stats()
|
|
|
|
patterns = sentinel.sentinel.get_recent_patterns(20) if sentinel.sentinel else []
|
|
|
|
return {
|
|
**stats,
|
|
"recent_patterns": patterns,
|
|
}
|
|
|
|
|
|
@router.post("/mempool/start")
|
|
async def mempool_start(chain: str = "ethereum"):
|
|
sentinel = await get_sentinel()
|
|
await sentinel.start(chain)
|
|
return {"status": "started", "chain": chain}
|
|
|
|
|
|
@router.post("/mempool/stop")
|
|
async def mempool_stop():
|
|
sentinel = await get_sentinel()
|
|
await sentinel.stop()
|
|
return {"status": "stopped"}
|
|
|
|
|
|
# ─── DASHBOARD ──────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/dashboard/stats")
|
|
async def dashboard_stats():
|
|
"""Security dashboard statistics."""
|
|
_sd = _L("app.security_dashboard")
|
|
dash = _sd.get_dashboard()
|
|
return dash.get_stats()
|
|
|
|
|
|
@router.get("/dashboard/alerts")
|
|
async def dashboard_alerts(
|
|
severity: str | None = None,
|
|
source: str | None = None,
|
|
limit: int = 100,
|
|
unacknowledged: bool = False,
|
|
):
|
|
"""Query security alerts."""
|
|
_sd = _L("app.security_dashboard")
|
|
dash = _sd.get_dashboard()
|
|
return dash.get_alerts(severity, source, limit, unacknowledged)
|
|
|
|
|
|
@router.post("/dashboard/acknowledge")
|
|
async def dashboard_acknowledge(req: AlertAckRequest):
|
|
"""Acknowledge an alert."""
|
|
_sd = _L("app.security_dashboard")
|
|
dash = _sd.get_dashboard()
|
|
success = await dash.acknowledge(req.alert_id)
|
|
return {"acknowledged": success}
|
|
|
|
|
|
@router.get("/dashboard/score")
|
|
async def dashboard_score(hours: int = 24):
|
|
"""Get rolling security score."""
|
|
_sd = _L("app.security_dashboard")
|
|
dash = _sd.get_dashboard()
|
|
return {
|
|
"security_score": dash.get_security_score(hours),
|
|
"window_hours": hours,
|
|
"max": 100,
|
|
}
|
|
|
|
|
|
@router.get("/dashboard/timeline/{target}")
|
|
async def dashboard_timeline(target: str):
|
|
"""Get incident timeline for a target."""
|
|
_sd = _L("app.security_dashboard")
|
|
dash = _sd.get_dashboard()
|
|
return {"timeline": dash.get_incident_timeline(target)}
|
|
|
|
|
|
# ─── ENTITY LABELING ─────────────────────────────────────────
|
|
|
|
|
|
@router.get("/entity/label/{address}")
|
|
async def entity_label(address: str):
|
|
"""Lookup entity label for an address."""
|
|
_el = _L("app.entity_labeler")
|
|
entity = await _el.label_address(address)
|
|
if not entity:
|
|
return {"address": address, "entity": None, "known": False}
|
|
return {"address": address, "entity": entity.to_dict(), "known": True}
|
|
|
|
|
|
@router.post("/entity/batch")
|
|
async def entity_batch_label(addresses: list[str]):
|
|
"""Batch entity label lookup."""
|
|
_el = _L("app.entity_labeler")
|
|
return await _el.batch_label_addresses(addresses)
|
|
|
|
|
|
@router.get("/entity/search")
|
|
async def entity_search(q: str = "", category: str | None = None, limit: int = 50):
|
|
"""Search entity database."""
|
|
_el = _L("app.entity_labeler")
|
|
db = await _el.get_entity_labeler()
|
|
results = await db.search_entities(query=q, category=category, limit=limit)
|
|
return {"results": [r.to_dict() for r in results], "count": len(results)}
|
|
|
|
|
|
@router.get("/entity/stats")
|
|
async def entity_stats():
|
|
"""Entity database statistics."""
|
|
_el = _L("app.entity_labeler")
|
|
db = await _el.get_entity_labeler()
|
|
return await db.get_stats()
|
|
|
|
|
|
@router.get("/entity/{entity_id}")
|
|
async def entity_detail(entity_id: str):
|
|
"""Full entity graph with addresses and relationships."""
|
|
_el = _L("app.entity_labeler")
|
|
db = await _el.get_entity_labeler()
|
|
return await db.get_entity_graph(entity_id)
|
|
|
|
|
|
# ─── PORTFOLIO TRACKER ──────────────────────────────────────
|
|
|
|
|
|
@router.get("/portfolio/{wallet}")
|
|
async def portfolio_single(wallet: str, chain: str = "ethereum"):
|
|
"""Get portfolio for a single wallet."""
|
|
_pt = _L("app.portfolio_tracker")
|
|
portfolio = await _pt.get_portfolio(wallet, chain)
|
|
return portfolio.to_dict()
|
|
|
|
|
|
@router.post("/portfolio/multi")
|
|
async def portfolio_multi(wallets: list[dict[str, str]]):
|
|
"""Aggregate portfolio across multiple wallets/chains."""
|
|
_pt = _L("app.portfolio_tracker")
|
|
result = await _pt.get_multi_wallet_portfolio(wallets)
|
|
return result.to_dict()
|
|
|
|
|
|
@router.get("/portfolio/history/{wallet}")
|
|
async def portfolio_history(wallet: str, chain: str = "ethereum", hours: int = 24):
|
|
"""Historical net worth chart data."""
|
|
_pt = _L("app.portfolio_tracker")
|
|
history = await _pt.get_portfolio_history(wallet, chain, hours)
|
|
return {
|
|
"wallet": wallet,
|
|
"chain": chain,
|
|
"hours": hours,
|
|
"data_points": len(history),
|
|
"history": history,
|
|
}
|
|
|
|
|
|
# ─── WALLET MONITOR ─────────────────────────────────────────
|
|
|
|
|
|
@router.post("/monitor/watch")
|
|
async def monitor_watch(address: str, chain: str = "ethereum", tags: str = ""):
|
|
"""Add a wallet to the watchlist."""
|
|
_wm = _L("app.wallet_monitor")
|
|
manager = _wm.WalletMonitorManager()
|
|
await manager.add_watch(address, chain, tags.split(",") if tags else [])
|
|
return {"watched": True, "address": address, "chain": chain}
|
|
|
|
|
|
@router.post("/monitor/unwatch")
|
|
async def monitor_unwatch(address: str, chain: str = "ethereum"):
|
|
"""Remove a wallet from the watchlist."""
|
|
_wm = _L("app.wallet_monitor")
|
|
manager = _wm.WalletMonitorManager()
|
|
await manager.remove_watch(address, chain)
|
|
return {"unwatched": True, "address": address, "chain": chain}
|
|
|
|
|
|
@router.get("/monitor/watches")
|
|
async def monitor_list():
|
|
"""List all watched wallets."""
|
|
_wm = _L("app.wallet_monitor")
|
|
manager = _wm.WalletMonitorManager()
|
|
watches = await manager.list_watches()
|
|
return {"watches": watches, "count": len(watches)}
|
|
|
|
|
|
# ─── EXCHANGE FLOW ANALYZER ─────────────────────────────────
|
|
|
|
|
|
@router.post("/flows/analyze")
|
|
async def flows_analyze(
|
|
entity_addresses: list[str],
|
|
entity_name: str,
|
|
entity_id: str,
|
|
chain: str,
|
|
transactions: list[dict[str, Any]],
|
|
hours: int = 24,
|
|
):
|
|
"""Analyze fund flows for an entity."""
|
|
_efa = _L("app.exchange_flow_analyzer")
|
|
return await _efa.analyze_entity_flows(entity_addresses, entity_name, entity_id, chain, transactions, hours)
|
|
|
|
|
|
@router.post("/flows/reserves")
|
|
async def flows_reserves(
|
|
entity_addresses: list[str],
|
|
entity_name: str,
|
|
entity_id: str,
|
|
chain: str,
|
|
):
|
|
"""Get reserve snapshot for entity addresses."""
|
|
_efa = _L("app.exchange_flow_analyzer")
|
|
return await _efa.get_reserves(entity_addresses, entity_name, entity_id, chain)
|
|
|
|
|
|
@router.post("/flows/compare")
|
|
async def flows_compare(
|
|
entity_a: dict[str, Any],
|
|
entity_b: dict[str, Any],
|
|
transactions: list[dict[str, Any]],
|
|
):
|
|
"""Compare fund flows between two entities."""
|
|
_efa = _L("app.exchange_flow_analyzer")
|
|
return await _efa.compare_flows(entity_a, entity_b, transactions)
|
|
|
|
|
|
# ─── HEALTH ────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/health")
|
|
async def security_health():
|
|
"""Full subsystem health check."""
|
|
|
|
# Check all modules
|
|
modules = {}
|
|
|
|
try:
|
|
from app.onchain_analyzer import get_w3
|
|
|
|
w3 = get_w3("ethereum")
|
|
modules["onchain_analyzer"] = "connected" if w3 and w3.is_connected() else "disconnected"
|
|
except Exception as e:
|
|
modules["onchain_analyzer"] = f"error: {str(e)[:30]}"
|
|
|
|
try:
|
|
import sklearn
|
|
|
|
modules["ml_anomaly"] = f"sklearn {sklearn.__version__}"
|
|
except ImportError:
|
|
modules["ml_anomaly"] = "sklearn not installed"
|
|
|
|
try:
|
|
import slither_analyzer
|
|
|
|
modules["contract_deepscan"] = f"slither {slither_analyzer.__version__}"
|
|
except ImportError:
|
|
modules["contract_deepscan"] = "slither not installed"
|
|
|
|
# Redis
|
|
try:
|
|
r = await _get_redis()
|
|
modules["redis"] = "connected" if r and await r.ping() else "disconnected"
|
|
except Exception:
|
|
modules["redis"] = "disconnected"
|
|
|
|
return {
|
|
"status": "operational",
|
|
"modules": modules,
|
|
"version": "2.0.0",
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
|
|
|
|
# Redis helper for health check
|
|
async def _get_redis():
|
|
try:
|
|
import redis.asyncio as redis_async
|
|
|
|
return redis_async.Redis(
|
|
host=os.getenv("REDIS_HOST", "127.0.0.1"),
|
|
port=int(os.getenv("REDIS_PORT", "6379")),
|
|
password=os.getenv("REDIS_PASSWORD") or None,
|
|
db=int(os.getenv("REDIS_DB", "0")),
|
|
decode_responses=True,
|
|
)
|
|
except Exception:
|
|
return None
|