rmi-backend/app/intelligent_webhooks.py

407 lines
14 KiB
Python

"""
Intelligent Webhook Processors — Smart event analysis.
Processes Helius, Moralis, and custom webhook events with:
- Whale detection (large transfers)
- Cluster correlation (linked wallets)
- Scam pattern matching (known signatures)
- Cross-platform enrichment (RAG lookup)
- Alert prioritization (critical/high/medium/low)
"""
import hashlib
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import UTC, datetime
logger = logging.getLogger(__name__)
# ── Alert Severity Levels ───────────────────────────────────
CRITICAL = "critical" # Confirmed scam, large exploit
HIGH = "high" # Whale movement, suspicious pattern
MEDIUM = "medium" # Cluster detected, unusual activity
LOW = "low" # Routine monitoring
@dataclass
class SmartAlert:
"""Intelligent alert with enrichment and scoring."""
alert_id: str
severity: str
alert_type: str
source: str # helius, moralis, custom
timestamp: datetime
wallet: str
chain: str
description: str
amount_usd: float = 0.0
risk_score: float = 0.0
cluster_id: str | None = None
related_wallets: list[str] = field(default_factory=list)
enriched_data: dict = field(default_factory=dict)
actions: list[str] = field(default_factory=list) # Recommended actions
class IntelligentWebhookProcessor:
"""Smart webhook event processor with multi-signal analysis."""
# Thresholds
WHALE_THRESHOLD_SOL = 100.0 # SOL
WHALE_THRESHOLD_ETH = 50.0 # ETH
WHALE_THRESHOLD_USD = 100000.0 # USD
# Known scam patterns
SCAM_PATTERNS = {
"dust_attack": {
"min_amount": 0.000001,
"max_amount": 0.001,
"pattern": "unsolicited_token",
},
"approval_drain": {"method": "setApprovalForAll", "risk": "high"},
"permit2_phishing": {"method": "permit", "risk": "critical"},
}
def __init__(self):
self._recent_events: dict[str, list[dict]] = defaultdict(list) # wallet -> events
self._cluster_cache: dict[str, str] = {} # wallet -> cluster_id
self._scam_registry: set[str] = set() # known scam addresses
def _generate_alert_id(self, source: str, wallet: str, ts: float) -> str:
"""Generate unique alert ID."""
data = f"{source}:{wallet}:{int(ts)}"
return hashlib.sha256(data.encode()).hexdigest()[:16]
async def _enrich_wallet(self, wallet: str, chain: str) -> dict:
"""Enrich wallet with RAG lookup and cluster data."""
enriched = {
"labels": [],
"cluster_id": None,
"is_scammer": False,
"is_exchange": False,
"risk_score": 0.0,
}
try:
# Check cluster cache
if wallet in self._cluster_cache:
enriched["cluster_id"] = self._cluster_cache[wallet]
# RAG lookup (if available)
try:
from app.chain_cache import get_chain_cache
cache = get_chain_cache()
labels = await cache.get("wallet_labels", wallet)
if labels:
enriched["labels"] = labels if isinstance(labels, list) else [labels]
except Exception:
pass
# Entity registry check
try:
from app.entity_registry import get_entity_registry
registry = get_entity_registry()
match = registry.classify_address(wallet)
if match and match.entity_name:
enriched["is_exchange"] = match.entity_type in ("cex", "defi")
enriched["labels"].append(f"{match.entity_type}:{match.entity_name}")
except Exception:
pass
except Exception as e:
logger.debug(f"Wallet enrichment failed: {e}")
return enriched
async def _check_cluster(self, wallet: str, chain: str) -> str | None:
"""Check if wallet belongs to known cluster."""
try:
from app.cluster_detection import get_cluster_detector
detector = get_cluster_detector()
# Quick lookup (don't run full detection)
if hasattr(detector, "_wallet_cluster_map"):
cluster_id = detector._wallet_cluster_map.get(wallet)
if cluster_id:
self._cluster_cache[wallet] = cluster_id
return cluster_id
except Exception:
pass
return None
async def _check_scam_registry(self, wallet: str) -> bool:
"""Check if wallet is in scam database."""
try:
# Spam registry
from app.spam_registry import get_spam_registry
sr = get_spam_registry()
if wallet in sr._scam_addresses:
return True
except Exception:
pass
# Threat feeds
try:
from app.threat_feeds import get_threat_feeds
feeds = get_threat_feeds()
result = await feeds.check_cryptoscamdb(wallet)
if result and result.get("is_scam"):
return True
except Exception:
pass
return False
def _calculate_risk_score(self, event: dict, enriched: dict) -> float:
"""Calculate composite risk score (0-100)."""
score = 0.0
# Amount-based risk
amount_usd = event.get("amount_usd", 0)
if amount_usd > 1000000:
score += 40
elif amount_usd > 100000:
score += 25
elif amount_usd > 10000:
score += 10
# Scam registry
if enriched.get("is_scammer"):
score += 50
# Cluster membership
if enriched.get("cluster_id"):
score += 20
# Exchange wallet (lower risk)
if enriched.get("is_exchange"):
score *= 0.3
# Pattern matching
if event.get("is_dust_attack"):
score += 60
if event.get("is_approval"):
score += 30
return min(score, 100.0)
def _determine_severity(self, risk_score: float, amount_usd: float) -> str:
"""Determine alert severity."""
if risk_score >= 70 or amount_usd > 500000:
return CRITICAL
elif risk_score >= 50 or amount_usd > 100000:
return HIGH
elif risk_score >= 30 or amount_usd > 10000:
return MEDIUM
else:
return LOW
def _generate_actions(self, alert: SmartAlert) -> list[str]:
"""Generate recommended actions based on alert type."""
actions = []
if alert.severity == CRITICAL:
actions.extend(
[
"🚨 IMMEDIATE: Add to scam watchlist",
"🚨 Alert affected users if cluster detected",
"🚨 Freeze related accounts (if platform)",
]
)
elif alert.severity == HIGH:
actions.extend(
[
"⚠️ Monitor wallet for 24h",
"⚠️ Check for related cluster activity",
"⚠️ Log for pattern analysis",
]
)
elif alert.severity == MEDIUM:
actions.extend(
[
"📊 Add to monitoring queue",
"📊 Correlate with recent events",
]
)
else:
actions.append("📝 Log for historical analysis")
return actions
async def process_helius_event(self, event: dict) -> SmartAlert | None:
"""Process Helius webhook event with intelligent analysis."""
try:
# Extract key data
wallet = event.get("feePayer", "")
signature = event.get("signature", "")
description = event.get("description", "")
timestamp = event.get("timestamp", 0)
if not wallet:
return None
# Calculate amounts
native_transfers = event.get("nativeTransfers", [])
max_transfer = max(native_transfers, key=lambda t: t.get("amount", 0), default={})
amount_sol = max_transfer.get("amount", 0) / 1e9
amount_usd = amount_sol * 150.0 # Approx SOL price
token_transfers = event.get("tokenTransfers", [])
is_dust = any(float(tt.get("tokenAmount", {}).get("uiAmount", 0) or 0) < 0.001 for tt in token_transfers)
# Enrich
enriched = await self._enrich_wallet(wallet, "solana")
is_scam = await self._check_scam_registry(wallet)
enriched["is_scammer"] = is_scam
# Risk score
risk_score = self._calculate_risk_score(
{
"amount_usd": amount_usd,
"is_dust_attack": is_dust,
"is_approval": "approval" in description.lower(),
},
enriched,
)
severity = self._determine_severity(risk_score, amount_usd)
# Only create alert if meaningful
if severity == LOW and risk_score < 20 and amount_usd < 1000:
return None
alert = SmartAlert(
alert_id=self._generate_alert_id("helius", wallet, timestamp),
severity=severity,
alert_type="whale_transfer" if amount_sol > self.WHALE_THRESHOLD_SOL else "transfer",
source="helius",
timestamp=datetime.fromtimestamp(timestamp, tz=UTC) if timestamp else datetime.now(UTC),
wallet=wallet,
chain="solana",
description=description[:200],
amount_usd=amount_usd,
risk_score=risk_score,
enriched_data=enriched,
)
alert.actions = self._generate_actions(alert)
# Store for correlation
self._recent_events[wallet].append(
{
"type": "helius",
"timestamp": timestamp,
"amount": amount_sol,
"signature": signature,
}
)
# Keep only last 50 events per wallet
self._recent_events[wallet] = self._recent_events[wallet][-50:]
return alert
except Exception as e:
logger.warning(f"Helius event processing failed: {e}")
return None
async def process_moralis_event(self, event: dict) -> SmartAlert | None:
"""Process Moralis Stream event with intelligent analysis."""
try:
# Extract key data
wallet = event.get("fromAddress", "")
to_address = event.get("toAddress", "")
tx_hash = event.get("transactionHash", "")
chain_id = event.get("chainId", "0x1")
timestamp = event.get("blockTimestamp", "")
value = event.get("value", "0")
if not wallet:
return None
# Map chain ID to name
chain_map = {"0x1": "eth", "0x38": "bsc", "0x89": "polygon", "0xa86a": "avalanche"}
chain = chain_map.get(chain_id, "eth")
# Calculate amount
amount_eth = float(value) / 1e18 if value else 0
amount_usd = amount_eth * 2500.0 # Approx ETH price
# Check for token transfers
erc20_transfers = event.get("erc20Transfers", [])
is_approval = any(
t.get("value") == "0" and t.get("toAddress", "").lower() != wallet.lower() for t in erc20_transfers
)
# Enrich
enriched = await self._enrich_wallet(wallet, chain)
is_scam = await self._check_scam_registry(wallet)
enriched["is_scammer"] = is_scam
# Risk score
risk_score = self._calculate_risk_score(
{
"amount_usd": amount_usd,
"is_approval": is_approval,
},
enriched,
)
severity = self._determine_severity(risk_score, amount_usd)
if severity == LOW and risk_score < 20 and amount_usd < 1000:
return None
alert = SmartAlert(
alert_id=self._generate_alert_id("moralis", wallet, time.time()),
severity=severity,
alert_type="whale_transfer" if amount_eth > self.WHALE_THRESHOLD_ETH else "transfer",
source="moralis",
timestamp=datetime.fromisoformat(timestamp.replace("Z", "+00:00")) if timestamp else datetime.now(UTC),
wallet=wallet,
chain=chain,
description=f"Transfer to {to_address[:12]}... ({amount_eth:.4f} ETH)",
amount_usd=amount_usd,
risk_score=risk_score,
enriched_data=enriched,
related_wallets=[to_address] if to_address else [],
)
alert.actions = self._generate_actions(alert)
# Store for correlation
self._recent_events[wallet].append(
{
"type": "moralis",
"timestamp": time.time(),
"amount": amount_eth,
"tx_hash": tx_hash,
}
)
self._recent_events[wallet] = self._recent_events[wallet][-50:]
return alert
except Exception as e:
logger.warning(f"Moralis event processing failed: {e}")
return None
async def get_correlated_alerts(self, wallet: str, window_hours: int = 24) -> list[dict]:
"""Get correlated events for a wallet."""
events = self._recent_events.get(wallet, [])
cutoff = time.time() - (window_hours * 3600)
return [e for e in events if e.get("timestamp", 0) > cutoff]
# Singleton
_processor: IntelligentWebhookProcessor | None = None
def get_intelligent_processor() -> IntelligentWebhookProcessor:
global _processor
if _processor is None:
_processor = IntelligentWebhookProcessor()
return _processor