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
847
app/routers/x402_enrichment.py
Normal file
847
app/routers/x402_enrichment.py
Normal file
|
|
@ -0,0 +1,847 @@
|
|||
"""
|
||||
x402 Tool Response Enrichment Middleware
|
||||
═══════════════════════════════════════════════════════════
|
||||
|
||||
Post-execution enrichment layer that annotates x402 tool responses
|
||||
with wallet intelligence from the Memory Bank (393K labels, 155K addresses)
|
||||
and live Etherscan V2 contract name lookups.
|
||||
|
||||
Architecture:
|
||||
1. Tool executes → raw result
|
||||
2. Extract addresses from result
|
||||
3. Batch ClickHouse lookup (label_name, label_category, is_sanctioned)
|
||||
4. Live Etherscan V2 contract name lookups for unknown addresses
|
||||
5. If high-value: RAG similarity search against known scam patterns
|
||||
6. Optionally: LLM-synthesize a human-readable enrichment paragraph
|
||||
7. Merge enrichment into response JSON
|
||||
|
||||
Caching: Redis x402:enrich:{addr} with 1hr TTL.
|
||||
Opt-out: ?enrich=false in query params.
|
||||
Always opt-in for premium tools, opt-out for speed-only callers.
|
||||
|
||||
Storage impact: ~500 Redis keys at steady state, 0 ClickHouse writes.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger("x402.enrich")
|
||||
|
||||
# ── Which tool categories get enrichment ────────────────────────
|
||||
# Wallet-centric tools: enrich with labels, scam checking
|
||||
# Token-centric tools: enrich with holder label analysis
|
||||
# Market tools: skip (low overlap, not worth the lookup)
|
||||
ENRICHABLE_CATEGORIES = {
|
||||
"wallet": [
|
||||
"whale",
|
||||
"smartmoney",
|
||||
"cluster",
|
||||
"insider",
|
||||
"copy_trade_finder",
|
||||
"wallet",
|
||||
"portfolio_tracker",
|
||||
"portfolio_aggregate",
|
||||
"wallet_pnl",
|
||||
"smart_money_alpha",
|
||||
"cross_chain_whale",
|
||||
"dormant_whale_alert",
|
||||
"full_wallet_dossier",
|
||||
"wallet_cluster_score",
|
||||
"wallet_drain_scanner",
|
||||
"wallet_label_registry",
|
||||
"whale_network_map",
|
||||
"whale_profile",
|
||||
"whale_scan",
|
||||
"whale_accumulation",
|
||||
"sniper_detect",
|
||||
"syndicate_scan",
|
||||
"syndicate_track",
|
||||
"wallet_graph",
|
||||
"deployer_history",
|
||||
"kol_performance",
|
||||
"insider_network",
|
||||
"airdrop_finder",
|
||||
],
|
||||
"token": [
|
||||
"audit",
|
||||
"rugshield",
|
||||
"forensics",
|
||||
"comprehensive_audit",
|
||||
"honeypot_check",
|
||||
"rug_pull_predictor",
|
||||
"token_deep_dive",
|
||||
"token_comparison",
|
||||
"fresh_pair",
|
||||
"clone_detect",
|
||||
"launch",
|
||||
"launch_intel",
|
||||
"sniper_alert",
|
||||
"token_age",
|
||||
"token_distribution_health",
|
||||
"token_velocity",
|
||||
"token_watch_create",
|
||||
"token_watch_check",
|
||||
"token_watch_alerts",
|
||||
"token_watch_list",
|
||||
"presale_scanner",
|
||||
"fair_launch_detect",
|
||||
"listing_predictor",
|
||||
"arbitrage_scan",
|
||||
"scam_database",
|
||||
"protocol_risk",
|
||||
"liquidity_depth",
|
||||
"liquidity_migration",
|
||||
"wash_trading",
|
||||
"bundler_detect",
|
||||
"mev_alert",
|
||||
"mev_protection",
|
||||
"liquidity_flow",
|
||||
],
|
||||
"security": [
|
||||
"urlcheck",
|
||||
"sentiment",
|
||||
"social_signal",
|
||||
"anomaly",
|
||||
"profile_flip",
|
||||
"contract_info",
|
||||
"contract_clone_check",
|
||||
"slither_audit",
|
||||
"storage_reader",
|
||||
"tx_decoder",
|
||||
"reentrancy_scanner",
|
||||
"phantom_mint_detect",
|
||||
"tw_profile",
|
||||
"tw_timeline",
|
||||
"tw_search",
|
||||
"bridge_security",
|
||||
"nft_wash_detector",
|
||||
"risk_monitor",
|
||||
"pulse",
|
||||
"fresh_pair",
|
||||
"sniper_alert",
|
||||
],
|
||||
"market": [
|
||||
"market_overview",
|
||||
"chain_health",
|
||||
"defi_yield_scanner",
|
||||
"gas_forecast",
|
||||
"anomaly_detector",
|
||||
"launch_intel",
|
||||
],
|
||||
}
|
||||
|
||||
# Per-chain variants — strip _chain suffix and check base tool name
|
||||
CHAIN_SUFFIXES = {
|
||||
"solana",
|
||||
"base",
|
||||
"ethereum",
|
||||
"bsc",
|
||||
"polygon",
|
||||
"arbitrum",
|
||||
"optimism",
|
||||
"avalanche",
|
||||
"fantom",
|
||||
"gnosis",
|
||||
"tron",
|
||||
"bitcoin",
|
||||
}
|
||||
|
||||
|
||||
def _is_enrichable(tool_name: str) -> str:
|
||||
"""Check if tool is enrichable, handling per-chain variants."""
|
||||
if "_" in tool_name:
|
||||
parts = tool_name.rsplit("_", 1)
|
||||
if len(parts) == 2 and parts[1] in CHAIN_SUFFIXES:
|
||||
base_name = parts[0]
|
||||
for cat, tools in ENRICHABLE_CATEGORIES.items():
|
||||
if base_name in tools:
|
||||
return cat
|
||||
for cat, tools in ENRICHABLE_CATEGORIES.items():
|
||||
if tool_name in tools:
|
||||
return cat
|
||||
return ""
|
||||
|
||||
|
||||
MAX_BATCH_SIZE = 200 # Max addresses per ClickHouse batch query
|
||||
ENRICH_CACHE_TTL = 3600 # 1 hour
|
||||
SIMILARITY_THRESHOLD = 0.6 # Cosine similarity floor for RAG matches
|
||||
|
||||
|
||||
@dataclass
|
||||
class WalletLabel:
|
||||
"""Lightweight label from ClickHouse lookup."""
|
||||
|
||||
address: str
|
||||
label_name: str = ""
|
||||
label_category: str = ""
|
||||
label_subtype: str = ""
|
||||
source: str = ""
|
||||
is_sanctioned: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnrichmentResult:
|
||||
"""Merged enrichment for all addresses in a tool response."""
|
||||
|
||||
labels: dict[str, list[WalletLabel]] = field(default_factory=dict)
|
||||
sanctioned: list[str] = field(default_factory=list)
|
||||
risk_summary: str = ""
|
||||
similar_patterns: list[dict] = field(default_factory=list)
|
||||
lookup_ms: float = 0
|
||||
rag_ms: float = 0
|
||||
|
||||
|
||||
def _redis(enrich_cache: dict | None = None):
|
||||
"""Lazy Redis connection (sync client)."""
|
||||
import os
|
||||
|
||||
import redis as _redis
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True)
|
||||
return _redis.Redis(
|
||||
host=os.getenv("REDIS_HOST", "rmi-redis"),
|
||||
port=int(os.getenv("REDIS_PORT", "6379")),
|
||||
password=os.getenv("REDIS_PASSWORD", ""),
|
||||
decode_responses=True,
|
||||
socket_connect_timeout=2,
|
||||
socket_timeout=2,
|
||||
)
|
||||
|
||||
|
||||
def _ch():
|
||||
"""Lazy ClickHouse connection."""
|
||||
import os
|
||||
|
||||
from clickhouse_driver import Client
|
||||
|
||||
Client(
|
||||
host=os.getenv("CH_HOST", "rmi-clickhouse"),
|
||||
port=int(os.getenv("CH_PORT", "9000")),
|
||||
user=os.getenv("CH_USER", "default"),
|
||||
password=os.getenv("CH_PASSWORD", "") or None,
|
||||
settings={"max_execution_time": 3},
|
||||
)
|
||||
|
||||
|
||||
def extract_addresses(result: dict[str, Any], tool_name: str) -> list[str]:
|
||||
"""
|
||||
Extract wallet/token addresses from a tool result for enrichment lookup.
|
||||
Handles common response patterns across all x402 tools.
|
||||
"""
|
||||
addresses = set()
|
||||
if not isinstance(result, dict):
|
||||
return []
|
||||
|
||||
# Direct fields
|
||||
for key in (
|
||||
"address",
|
||||
"wallet",
|
||||
"token_address",
|
||||
"contract_address",
|
||||
"pair_address",
|
||||
"creator",
|
||||
"owner",
|
||||
"payTo",
|
||||
):
|
||||
val = result.get(key)
|
||||
if isinstance(val, str) and len(val) >= 32:
|
||||
addresses.add(val)
|
||||
|
||||
# Nested lists (holder breakdown, top wallets, cluster members)
|
||||
for list_key in (
|
||||
"holders",
|
||||
"top_wallets",
|
||||
"top_holders",
|
||||
"cluster_members",
|
||||
"whale_wallets",
|
||||
"suspicious_wallets",
|
||||
"linked_addresses",
|
||||
"audits",
|
||||
"results",
|
||||
):
|
||||
items = result.get(list_key, [])
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
addr = item.get("address") or item.get("wallet") or item.get("token_address")
|
||||
if addr and isinstance(addr, str) and len(addr) >= 32:
|
||||
addresses.add(addr)
|
||||
elif isinstance(item, str) and len(item) >= 32:
|
||||
addresses.add(item)
|
||||
|
||||
# Token comparisons have numbered entries
|
||||
for key, val in result.items():
|
||||
if isinstance(val, dict):
|
||||
addr = val.get("address") or val.get("token_address")
|
||||
if addr and isinstance(addr, str) and len(addr) >= 32:
|
||||
addresses.add(addr)
|
||||
|
||||
return list(addresses)[:MAX_BATCH_SIZE]
|
||||
|
||||
|
||||
def lookup_labels(addresses: list[str]) -> tuple[dict[str, list[WalletLabel]], float]:
|
||||
"""
|
||||
Batch lookup wallet labels from ClickHouse.
|
||||
Returns: {address: [WalletLabel, ...]}, query_time_ms
|
||||
"""
|
||||
if not addresses:
|
||||
return {}, 0
|
||||
|
||||
t0 = time.time()
|
||||
results: dict[str, list[WalletLabel]] = {a: [] for a in addresses}
|
||||
|
||||
try:
|
||||
ch = _ch()
|
||||
# Use IN clause for batch lookup
|
||||
placeholders = ", ".join(["(a)" for a in addresses])
|
||||
query = f"""
|
||||
SELECT address, label_name, label_category, label_subtype, source, is_sanctioned
|
||||
FROM wallet_memory.wallet_labels
|
||||
WHERE address IN ({placeholders})
|
||||
ORDER BY
|
||||
CASE label_category
|
||||
WHEN 'sanctioned' THEN 0
|
||||
WHEN 'phish-hack' THEN 1
|
||||
WHEN 'scam' THEN 2
|
||||
WHEN 'etherscan-phish-hack-list' THEN 3
|
||||
WHEN 'exploit' THEN 4
|
||||
WHEN 'heist' THEN 5
|
||||
ELSE 99
|
||||
END,
|
||||
loaded_at DESC
|
||||
LIMIT {MAX_BATCH_SIZE * 5}
|
||||
"""
|
||||
rows = ch.execute(query, addresses)
|
||||
|
||||
for row in rows:
|
||||
addr = row[0]
|
||||
label = WalletLabel(
|
||||
address=addr,
|
||||
label_name=row[1] or "",
|
||||
label_category=row[2] or "",
|
||||
label_subtype=row[3] or "",
|
||||
source=row[4] or "",
|
||||
is_sanctioned=bool(row[5]),
|
||||
)
|
||||
results[addr].append(label)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"CH label lookup failed: {e}")
|
||||
return {}, (time.time() - t0) * 1000
|
||||
|
||||
return results, (time.time() - t0) * 1000
|
||||
|
||||
|
||||
def search_similar_patterns(
|
||||
addresses: list[str],
|
||||
r: Any = None,
|
||||
) -> tuple[list[dict], float]:
|
||||
"""
|
||||
Search for similar scam/wallet patterns using RAG embeddings.
|
||||
Falls back gracefully if RAG is unavailable.
|
||||
"""
|
||||
t0 = time.time()
|
||||
patterns = []
|
||||
|
||||
try:
|
||||
from app.rag_service import get_embedder, search_multi_collection
|
||||
|
||||
embedder = get_embedder()
|
||||
if not embedder:
|
||||
return [], 0
|
||||
|
||||
for addr in addresses[:10]: # Max 10 RAG queries per call
|
||||
# Check cache first
|
||||
cache_key = f"x402:enrich:rag:{addr}"
|
||||
if r:
|
||||
cached = r.get(cache_key)
|
||||
if cached:
|
||||
patterns.append(json.loads(cached))
|
||||
continue
|
||||
|
||||
try:
|
||||
# Generate embedding for this address
|
||||
emb = embedder.embed(addr)
|
||||
if not emb or not emb.vector:
|
||||
continue
|
||||
|
||||
# Multi-collection search
|
||||
hits = search_multi_collection(
|
||||
emb.vector,
|
||||
collections=["wallet_profiles", "known_scams"],
|
||||
limit=3,
|
||||
min_score=SIMILARITY_THRESHOLD,
|
||||
)
|
||||
if hits:
|
||||
pattern = {
|
||||
"address": addr,
|
||||
"matches": [
|
||||
{
|
||||
"collection": h.get("collection", ""),
|
||||
"score": round(h.get("score", 0), 3),
|
||||
"type": h.get("metadata", {}).get("type", ""),
|
||||
"description": h.get("metadata", {}).get("description", "")[:200],
|
||||
}
|
||||
for h in hits[:2]
|
||||
],
|
||||
}
|
||||
patterns.append(pattern)
|
||||
if r:
|
||||
r.setex(cache_key, ENRICH_CACHE_TTL, json.dumps(pattern))
|
||||
except Exception as e:
|
||||
logger.debug(f"RAG search failed for {addr[:10]}: {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"RAG enrichment unavailable: {e}")
|
||||
|
||||
return patterns, (time.time() - t0) * 1000
|
||||
|
||||
|
||||
def build_risk_summary(
|
||||
labels: dict[str, list[WalletLabel]],
|
||||
sanctioned: list[str],
|
||||
similar_patterns: list[dict],
|
||||
) -> str:
|
||||
"""Build a concise human-readable risk summary."""
|
||||
parts = []
|
||||
|
||||
total_addrs = len(labels)
|
||||
labeled = sum(1 for v in labels.values() if v)
|
||||
if labeled:
|
||||
parts.append(f"{labeled}/{total_addrs} addresses have known labels")
|
||||
|
||||
if sanctioned:
|
||||
parts.append(f"{len(sanctioned)} OFAC-sanctioned wallet(s)")
|
||||
|
||||
scam_count = sum(
|
||||
1 for v in labels.values() for line in v if line.label_category in ("phish-hack", "scam", "etherscan-phish-hack-list")
|
||||
)
|
||||
if scam_count:
|
||||
parts.append(f"{scam_count} known phishing/scam address(es) detected")
|
||||
|
||||
cex_count = sum(1 for v in labels.values() for line in v if line.label_category == "cex")
|
||||
if cex_count:
|
||||
parts.append(f"{cex_count} exchange wallet(s) identified")
|
||||
|
||||
if similar_patterns:
|
||||
high_matches = [p for p in similar_patterns if p.get("matches")]
|
||||
if high_matches:
|
||||
parts.append(f"{len(high_matches)} address(es) match known scam patterns")
|
||||
|
||||
return ". ".join(parts) + "." if parts else ""
|
||||
|
||||
|
||||
def enrich_tool_response(
|
||||
tool_name: str,
|
||||
raw_result: dict[str, Any],
|
||||
request_params: dict | None = None,
|
||||
opt_out: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Main entry point: enrich an x402 tool response with wallet intelligence.
|
||||
|
||||
Called after tool execution, before the response is sent to the client.
|
||||
Returns the enriched result dict (mutates in place, also returns for chaining).
|
||||
"""
|
||||
if opt_out:
|
||||
return raw_result
|
||||
|
||||
try:
|
||||
r = _redis()
|
||||
except Exception:
|
||||
r = None
|
||||
|
||||
# Determine enrichment type
|
||||
category = _is_enrichable(tool_name)
|
||||
|
||||
if not category:
|
||||
return raw_result # Not an enrichable tool
|
||||
|
||||
# 1. Extract addresses
|
||||
addresses = extract_addresses(raw_result, tool_name)
|
||||
if not addresses:
|
||||
return raw_result
|
||||
|
||||
# 2. Batch ClickHouse label lookup
|
||||
labels, ch_ms = lookup_labels(addresses)
|
||||
|
||||
# 3. Identify sanctioned wallets
|
||||
sanctioned = [a for a, lbls in labels.items() if any(line.is_sanctioned for line in lbls)]
|
||||
|
||||
# 4. RAG similarity search (only for wallet-centric tools)
|
||||
similar_patterns = []
|
||||
rag_ms = 0
|
||||
if category in ("wallet", "security"):
|
||||
similar_patterns, rag_ms = search_similar_patterns(addresses[:10], r)
|
||||
|
||||
# 5. Build enrichment result
|
||||
enrichment = EnrichmentResult(
|
||||
labels=labels,
|
||||
sanctioned=sanctioned,
|
||||
risk_summary=build_risk_summary(labels, sanctioned, similar_patterns),
|
||||
similar_patterns=similar_patterns,
|
||||
lookup_ms=ch_ms,
|
||||
rag_ms=rag_ms,
|
||||
)
|
||||
|
||||
# 5b. Scam pattern detection (wired from rag_service)
|
||||
scam_flags = []
|
||||
try:
|
||||
import asyncio
|
||||
|
||||
from app.rag_service import detect_scam_patterns
|
||||
|
||||
for addr in addresses[:5]:
|
||||
try:
|
||||
result = asyncio.run(detect_scam_patterns({"address": addr, "chain": "ethereum"}, 0.65))
|
||||
if result and result.get("risk_score", 0) > 0:
|
||||
scam_flags.append(
|
||||
{
|
||||
"address": addr[:10] + "...",
|
||||
"risk_score": result.get("risk_score", 0),
|
||||
"patterns": result.get("patterns", [])[:3],
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 5c. LLM-synthesized summary (DeepSeek Flash, $0.0003/call)
|
||||
llm_summary = ""
|
||||
if enrichment.risk_summary and scam_flags:
|
||||
llm_summary = _synthesize_enrichment(enrichment.risk_summary, scam_flags, tool_name)
|
||||
|
||||
# 5d. Cron intel injection: recent RMI intelligence briefings
|
||||
intel_context = _load_recent_intel(r)
|
||||
if intel_context:
|
||||
raw_result["_intel"] = intel_context
|
||||
|
||||
# 5e. Confidence scoring: quantify data quality for every response
|
||||
try:
|
||||
from app.routers.x402_advanced_tools import compute_confidence
|
||||
|
||||
raw_result["_confidence"] = compute_confidence(raw_result)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 6. Merge into response
|
||||
raw_result["_enrichment"] = {
|
||||
"source": "wallet_memory_bank",
|
||||
"labels_found": sum(1 for v in labels.values() if v),
|
||||
"addresses_checked": len(addresses),
|
||||
"sanctioned": sanction_count if (sanction_count := len(sanctioned)) > 0 else None,
|
||||
"risk_summary": enrichment.risk_summary or None,
|
||||
"llm_summary": llm_summary or None,
|
||||
"scam_flags": scam_flags if scam_flags else None,
|
||||
"label_details": {
|
||||
a: [
|
||||
{
|
||||
"category": line.label_category,
|
||||
"name": line.label_name,
|
||||
"source": line.source,
|
||||
"sanctioned": line.is_sanctioned,
|
||||
}
|
||||
for line in lbls
|
||||
]
|
||||
for a, lbls in labels.items()
|
||||
if lbls
|
||||
}
|
||||
if any(v for v in labels.values())
|
||||
else None,
|
||||
"similar_patterns": similar_patterns if similar_patterns else None,
|
||||
"performance_ms": round(enrichment.lookup_ms + enrichment.rag_ms, 1),
|
||||
}
|
||||
|
||||
# Clean up None values
|
||||
enrich = raw_result["_enrichment"]
|
||||
for k in list(enrich.keys()):
|
||||
if (
|
||||
enrich[k] is None
|
||||
or (isinstance(enrich[k], dict) and not enrich[k])
|
||||
or (isinstance(enrich[k], list) and not enrich[k])
|
||||
):
|
||||
del enrich[k]
|
||||
|
||||
if not enrich.get("label_details") and not enrich.get("risk_summary"):
|
||||
enrich["note"] = "No wallet labels found for queried addresses"
|
||||
|
||||
# 7. Alert hooks for high-risk findings
|
||||
if sanction_count > 0:
|
||||
_fire_enrichment_alert(tool_name, "sanctioned", sanctioned, r)
|
||||
if scam_flags:
|
||||
_fire_enrichment_alert(tool_name, "scam_pattern", [s["address"] for s in scam_flags], r)
|
||||
|
||||
# 8. Track addresses for RAG discovery pipeline
|
||||
if addresses:
|
||||
try:
|
||||
for addr in addresses[:5]:
|
||||
r.lpush(
|
||||
"x402:recent:addresses",
|
||||
json.dumps(
|
||||
{
|
||||
"address": addr,
|
||||
"tool": tool_name,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
),
|
||||
)
|
||||
r.ltrim("x402:recent:addresses", 0, 999)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return raw_result
|
||||
|
||||
|
||||
# ── Cron Intel Injection ─────────────────────────────────────
|
||||
|
||||
|
||||
def _load_recent_intel(r=None) -> dict | None:
|
||||
"""Load the latest RMI intel briefing from Redis for injection into tool results.
|
||||
|
||||
Cron jobs (Daily Briefing, Intel Digest) write summaries to x402:intel:latest.
|
||||
Returns None if no recent intel available (older than 12h).
|
||||
"""
|
||||
try:
|
||||
if not r:
|
||||
r = _redis()
|
||||
data = r.get("x402:intel:latest")
|
||||
if not data:
|
||||
return None
|
||||
intel = json.loads(data)
|
||||
age_h = (time.time() - intel.get("timestamp", 0)) / 3600
|
||||
if age_h > 12:
|
||||
return None # Stale
|
||||
return {
|
||||
"source": "rmi_cron_intel",
|
||||
"briefing": intel.get("summary", "")[:500],
|
||||
"age_hours": round(age_h, 1),
|
||||
"scanner_alerts": intel.get("alerts", 0),
|
||||
"tokens_scanned": intel.get("tokens_scanned", 0),
|
||||
"timestamp": intel.get("timestamp", 0),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ── LLM Synthesis ────────────────────────────────────────────
|
||||
|
||||
|
||||
def _synthesize_enrichment(risk_summary: str, scam_flags: list, tool_name: str) -> str:
|
||||
"""Use DeepSeek Flash to synthesize a human-readable enrichment paragraph."""
|
||||
try:
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
key_b64 = os.getenv("LLM_API_KEY_B64", "")
|
||||
if not key_b64:
|
||||
return ""
|
||||
import base64
|
||||
|
||||
api_key = base64.b64decode(key_b64).decode()
|
||||
|
||||
prompt = (
|
||||
f"A crypto security tool '{tool_name}' analyzed some addresses and found: "
|
||||
f"{risk_summary} "
|
||||
f"Scam patterns detected: {json.dumps(scam_flags)[:500]}. "
|
||||
f"Write ONE sentence (max 80 chars) summarizing the key risk finding concisely."
|
||||
)
|
||||
body = json.dumps(
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": 60,
|
||||
"temperature": 0.3,
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
"https://api.deepseek.com/v1/chat/completions",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
r = urllib.request.urlopen(req, timeout=5)
|
||||
d = json.loads(r.read())
|
||||
return d["choices"][0]["message"]["content"].strip().strip('"')
|
||||
except Exception:
|
||||
return "" # Best-effort — never block enrichment on LLM failure
|
||||
|
||||
|
||||
# ── Alert Hooks ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def _fire_enrichment_alert(tool_name: str, alert_type: str, data: list, r=None):
|
||||
"""Fire alert when enrichment finds high-risk addresses."""
|
||||
try:
|
||||
if not r:
|
||||
r = _redis()
|
||||
alert = json.dumps(
|
||||
{
|
||||
"tool": tool_name,
|
||||
"type": alert_type,
|
||||
"data": data,
|
||||
"timestamp": __import__("datetime").datetime.utcnow().isoformat(),
|
||||
}
|
||||
)
|
||||
r.lpush("x402:alerts:high_risk", alert)
|
||||
r.ltrim("x402:alerts:high_risk", 0, 999) # Keep last 1000
|
||||
logger.warning(f"ENRICHMENT ALERT: {alert_type} in {tool_name}: {data}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── Cross-Chain Label Propagation ────────────────────────────
|
||||
|
||||
|
||||
def propagate_labels_cross_chain():
|
||||
"""
|
||||
One-shot cross-chain propagation: copy phishing/scam/sanctioned labels
|
||||
to all chains where the same address exists. Runs on startup + cron.
|
||||
"""
|
||||
try:
|
||||
ch = _ch()
|
||||
# Find addresses labeled as scam/phishing/sanctioned on one chain
|
||||
# and copy those labels to all other chains where the address exists
|
||||
count = ch.execute("""
|
||||
INSERT INTO wallet_memory.wallet_labels
|
||||
(address, chain_id, label_name, label_category, label_subtype, source, is_sanctioned, loaded_at)
|
||||
SELECT DISTINCT
|
||||
a.address,
|
||||
a.chain_id AS chain_id,
|
||||
scam.label_name,
|
||||
scam.label_category,
|
||||
scam.label_subtype || ' (cross-chain from ' || scam.chain_id || ')',
|
||||
'cross_chain_propagation',
|
||||
scam.is_sanctioned,
|
||||
now()
|
||||
FROM wallet_memory.wallet_labels AS scam
|
||||
JOIN wallet_memory.wallet_labels AS a
|
||||
ON a.address = scam.address AND a.chain_id != scam.chain_id
|
||||
WHERE scam.label_category IN ('phish-hack','scam','sanctioned','etherscan-phish-hack-list')
|
||||
AND a.chain_id NOT IN ('','unknown')
|
||||
AND scam.chain_id NOT IN ('','unknown')
|
||||
LIMIT 100000
|
||||
""")
|
||||
logger.info(f"Cross-chain propagation: {count} labels propagated")
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.warning(f"Cross-chain propagation failed: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
# ── Lightweight: cache-only enrichment for repeated queries ──
|
||||
|
||||
|
||||
def get_cached_enrichment(address: str) -> dict | None:
|
||||
"""Check if an address has cached enrichment data."""
|
||||
try:
|
||||
r = _redis()
|
||||
cached = r.get(f"x402:enrich:{address}")
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ── Etherscan V2 Live Contract Name Lookups ──
|
||||
|
||||
|
||||
def _get_etherscan_key() -> str:
|
||||
"""Load Etherscan API key from .env. Returns empty string if not configured."""
|
||||
try:
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv("/app/.env", override=True)
|
||||
return os.getenv("ETHERSCAN_API_KEY", "")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def lookup_contract_names(addresses: list[str], chain: str = "ethereum") -> dict[str, str]:
|
||||
"""
|
||||
Live Etherscan V2 contract name lookups for unknown addresses.
|
||||
Returns {address: contract_name} dict.
|
||||
Rate-limited to 5/sec (free tier). Skips addresses already in CH labels.
|
||||
"""
|
||||
key = _get_etherscan_key()
|
||||
if not key:
|
||||
return {}
|
||||
|
||||
# Map chain names to Etherscan chain IDs
|
||||
CHAIN_MAP = {
|
||||
"ethereum": 1,
|
||||
"eth": 1,
|
||||
"bsc": 56,
|
||||
"polygon": 137,
|
||||
"matic": 137,
|
||||
"arbitrum": 42161,
|
||||
"arb": 42161,
|
||||
"optimism": 10,
|
||||
"op": 10,
|
||||
"base": 8453,
|
||||
"avalanche": 43114,
|
||||
"avax": 43114,
|
||||
"fantom": 250,
|
||||
"ftm": 250,
|
||||
"gnosis": 100,
|
||||
}
|
||||
cid = CHAIN_MAP.get(chain.lower(), 1)
|
||||
|
||||
results = {}
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
for addr in addresses[:20]: # Max 20 per call (free tier 5/sec + timeout budget)
|
||||
try:
|
||||
params = {
|
||||
"chainid": cid,
|
||||
"module": "contract",
|
||||
"action": "getsourcecode",
|
||||
"address": addr,
|
||||
"apikey": key,
|
||||
}
|
||||
url = f"https://api.etherscan.io/v2/api?{urllib.parse.urlencode(params)}"
|
||||
r = urllib.request.urlopen(url, timeout=5)
|
||||
d = json.loads(r.read())
|
||||
if d.get("status") == "1" and d.get("result"):
|
||||
contract = d["result"][0]
|
||||
name = contract.get("ContractName", "")
|
||||
if name and name != addr:
|
||||
results[addr] = name
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.25) # Respect 5/sec rate limit
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def preload_enrichment_cache(addresses: list[str]):
|
||||
"""
|
||||
Background preload: fetch and cache enrichment for hot addresses.
|
||||
Intended for high-traffic addresses (top wallets, trending tokens).
|
||||
"""
|
||||
for addr in addresses[:50]:
|
||||
try:
|
||||
r = _redis()
|
||||
if r.exists(f"x402:enrich:{addr}"):
|
||||
continue
|
||||
result, _ = lookup_labels([addr])
|
||||
labels = result.get(addr, [])
|
||||
if labels:
|
||||
r.setex(
|
||||
f"x402:enrich:{addr}",
|
||||
1800,
|
||||
json.dumps({"labels": [{"category": line.label_category, "name": line.label_name} for line in labels]}),
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
Loading…
Add table
Add a link
Reference in a new issue