rmi-backend/app/unified_token_scanner.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- 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>
2026-07-06 15:43:20 +02:00

417 lines
15 KiB
Python

"""
Unified Token Scanner - v2.0 (DataBus-Powered)
================================================
SINGLE source of truth for ALL token scanning at RMI.
Gone: 45 raw httpx enrichment calls, no caching, no fallbacks.
Here: Parallel DataBus.fetch() calls with 4-layer defense:
cache(SWR) → dedup → local precheck → credit-aware provider chain.
Every enrichment is a DataBus chain. DataBus handles:
- Redis caching with stale-while-revalidate
- Request deduplication (same query within 5s = one call)
- Credit conservation (free providers auto-bumped when paid >80%)
- Automatic fallback chains (DexScreener → Etherscan → Moralis → ...)
- RAG auto-indexing
Architecture:
scan_token(address, chain, tier) → parallel DataBus.fetch() → merge → score → return
"""
import asyncio
import logging
import time
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, ClassVar
logger = logging.getLogger(__name__)
# ═══════════════════════════════════════════════════════════════
# DATA TYPES
# ═══════════════════════════════════════════════════════════════
@dataclass
class ScanResult:
"""Single unified scan result."""
token_address: str
chain: str
scanned_at: str = ""
# Core identity
name: str = ""
symbol: str = ""
price_usd: float = 0.0
market_cap_usd: float = 0.0
liquidity_usd: float = 0.0
volume_24h_usd: float = 0.0
age_hours: float = 0.0
# Security
safety_score: float = 50.0 # 0=scam, 100=safe
confidence: float = 0.0 # 0-100, how much data we had
risk_level: str = "unknown"
risk_flags: list[str] = field(default_factory=list)
green_flags: list[str] = field(default_factory=list)
# Enrichment results (keyed by DataBus chain name)
security: dict | None = None
holders: dict | None = None
holder_health: dict | None = None
liquidity: dict | None = None
volume_auth: dict | None = None
dev_reputation: dict | None = None
rug_patterns: dict | None = None
premium: dict | None = None # bundle/cluster/sniper/mev/wash/copy/insider
smart_money: dict | None = None
whale_alerts: dict | None = None
cross_chain: dict | None = None
launches: dict | None = None
# Metadata
modules_run: list[str] = field(default_factory=list)
enrichment_sources: list[str] = field(default_factory=list)
tier: str = "free"
scan_duration_ms: float = 0.0
# ═══════════════════════════════════════════════════════════════
# SCANNER ORCHESTRATOR
# ═══════════════════════════════════════════════════════════════
class UnifiedTokenScanner:
"""
THE single token scanner.
Usage:
scanner = UnifiedTokenScanner()
result = await scanner.scan("0x...", "ethereum", tier="pro")
"""
# Phase 1: Core identity - always run, needed for scoring
CORE_CHAINS: ClassVar[list] =[
"token_price",
"token_metadata",
"token_security",
]
# Phase 2: Deep analysis - parallel, all free
DEEP_CHAINS: ClassVar[list] =[
"holder_data",
"holder_health",
"liquidity_risk",
"dev_reputation",
"rug_patterns",
"volume_authenticity",
]
# Phase 3: Premium - requires pro/elite tier
PREMIUM_CHAINS: ClassVar[list] =[
"bundle_detect",
"cluster_map",
"dev_finder",
"sniper_detect",
"bot_farm_detect",
"copy_trade_detect",
"insider_detect",
"wash_trade_detect",
"mev_detect",
"fresh_wallet_analysis",
]
# Phase 4: Intelligence - always free, enrich the report
INTEL_CHAINS: ClassVar[list] =[
"smart_money",
"whale_alerts",
"token_launches",
"insider_detection",
"cross_chain_entity",
]
async def scan(
self,
address: str,
chain: str = "solana",
tier: str = "free",
admin_key: str | None = None,
) -> ScanResult:
"""
Full token security scan.
Runs phases in dependency order:
1. Core (price, meta, security) - needed for scoring
2. Deep (holders, liquidity, dev, rug, volume)
3. Premium (bundle, cluster, sniper, MEV, wash - tier-gated)
4. Intel (smart money, whales, launches, cross-chain)
"""
start = time.monotonic()
params = {"address": address, "chain": chain}
result = ScanResult(
token_address=address,
chain=chain,
scanned_at=datetime.now(UTC).isoformat(),
tier=tier,
)
try:
# ── Phase 1: Core (sequential - needed for scoring) ──
core_results = await self._fetch_batch(self.CORE_CHAINS, params, admin_key, timeout=15)
self._merge_core(result, core_results)
# ── Phase 2: Deep (parallel) ──
deep_results = await self._fetch_batch(self.DEEP_CHAINS, params, admin_key, timeout=30)
self._merge_deep(result, deep_results)
# ── Phase 3: Premium (tier-gated) ──
if tier in ("pro", "elite", "admin"):
premium_results = await self._fetch_batch(self.PREMIUM_CHAINS, params, admin_key, timeout=30)
self._merge_premium(result, premium_results)
# ── Phase 4: Intel (free enrichment) ──
intel_results = await self._fetch_batch(self.INTEL_CHAINS, params, admin_key, timeout=20)
self._merge_intel(result, intel_results)
# ── Score ──
self._compute_score(result)
except Exception as e:
logger.error(f"Scan failed for {address}: {e}")
result.risk_flags.append(f"SCAN_ERROR: {str(e)[:100]}")
result.scan_duration_ms = (time.monotonic() - start) * 1000
return result
async def _fetch_batch(
self,
chains: list[str],
params: dict,
admin_key: str | None,
timeout: float = 30,
) -> dict[str, Any]:
"""Parallel DataBus fetch for multiple chains."""
from app.databus import databus
async def _fetch_one(chain_name: str) -> tuple:
try:
result = await asyncio.wait_for(
databus.fetch(chain_name, admin_key=admin_key, **params),
timeout=timeout,
)
return chain_name, result
except TimeoutError:
logger.warning(f"DataBus chain '{chain_name}' timed out after {timeout}s")
return chain_name, None
except Exception as e:
logger.warning(f"DataBus chain '{chain_name}' failed: {e}")
return chain_name, None
tasks = [_fetch_one(c) for c in chains]
gathered = await asyncio.gather(*tasks, return_exceptions=True)
results = {}
for item in gathered:
if isinstance(item, Exception):
continue
name, data = item
if data is not None:
results[name] = data
# Track enrichment sources
if isinstance(data, dict) and data.get("source"):
pass # sources tracked in merge methods
return results
# ── MERGE METHODS ──
def _merge_core(self, r: ScanResult, data: dict):
"""Extract core identity from price, metadata, and security checks."""
r.modules_run.extend(data.keys())
# Price
price_data = data.get("token_price", {})
if isinstance(price_data, dict):
r.price_usd = float(price_data.get("price_usd", 0) or 0)
r.market_cap_usd = float(price_data.get("market_cap_usd", 0) or 0)
r.volume_24h_usd = float(price_data.get("volume_24h", 0) or 0)
r.liquidity_usd = float(price_data.get("liquidity_usd", 0) or 0)
if price_data.get("source"):
r.enrichment_sources.append(f"price:{price_data['source']}")
# Metadata
meta = data.get("token_metadata", {})
if isinstance(meta, dict):
r.name = str(meta.get("name", "") or "")
r.symbol = str(meta.get("symbol", "") or "")
r.age_hours = float(meta.get("age_hours", 0) or 0)
# Security checks (GoPlus - 42 checks)
security = data.get("token_security", {})
if isinstance(security, dict):
r.security = security
checks = security.get("checks", {})
r.modules_run.append(f"security_checks:{len(checks)}")
r.enrichment_sources.append(f"security:{security.get('source', 'goplus')}")
def _merge_deep(self, r: ScanResult, data: dict):
"""Merge deep analysis results."""
r.modules_run.extend(data.keys())
r.holders = data.get("holder_data")
r.holder_health = data.get("holder_health")
r.liquidity = data.get("liquidity_risk")
r.dev_reputation = data.get("dev_reputation")
r.rug_patterns = data.get("rug_patterns")
r.volume_auth = data.get("volume_authenticity")
def _merge_premium(self, r: ScanResult, data: dict):
"""Merge premium-tier results."""
r.modules_run.extend(data.keys())
r.premium = {k: v for k, v in data.items() if k in self.PREMIUM_CHAINS and v is not None}
def _merge_intel(self, r: ScanResult, data: dict):
"""Merge intelligence enrichment results."""
r.modules_run.extend(data.keys())
r.smart_money = data.get("smart_money")
r.whale_alerts = data.get("whale_alerts")
r.cross_chain = data.get("cross_chain_entity")
r.launches = data.get("token_launches")
def _compute_score(self, r: ScanResult):
"""
Compute safety score (100 = safest, 0 = scam).
Starts at 50 (neutral/unknown). Adjusted by security checks, holder data,
liquidity, rug patterns, premium detections.
"""
score = 50.0
confidence = 0.0
flags = []
green = []
# ── Security checks (GoPlus) ──
if r.security:
checks = r.security.get("checks", {})
if checks:
confidence += min(len(checks) * 2, 30)
for check_id, check in checks.items():
status = check.get("status", "")
if status == "fail":
score -= 5
flags.append(check_id)
elif status == "pass":
score += 2
green.append(check_id)
# ── Holder concentration ──
if r.holders:
h = r.holders
top10 = float(h.get("top_10_pct", 0) or 0)
if top10 > 80:
score -= 15
flags.append("HOLDER_CONCENTRATION_HIGH")
elif top10 < 30:
score += 5
green.append("HOLDER_DISTRIBUTED")
confidence += 5
# ── Holder health ──
if r.holder_health:
hh = r.holder_health
gini = float(hh.get("gini", 0) or 0)
if gini > 0.8:
score -= 10
flags.append("HOLDER_GINI_HIGH")
confidence += 5
# ── Liquidity ──
if r.liquidity:
liq = r.liquidity
locked = float(liq.get("locked_pct", 0) or 0)
if locked < 50:
score -= 20
flags.append("LP_LOCK_LOW")
elif locked > 90:
score += 5
green.append("LP_LOCKED")
confidence += 5
# ── Dev reputation ──
if r.dev_reputation:
dev = r.dev_reputation
risk = float(dev.get("risk_score", 0) or 0)
if risk > 70:
score -= 25
flags.append("DEV_HIGH_RISK")
elif risk < 20:
score += 5
green.append("DEV_REPUTABLE")
confidence += 5
# ── Rug patterns ──
if r.rug_patterns:
rp = r.rug_patterns
matches = int(rp.get("match_count", 0) or 0)
if matches > 0:
score -= matches * 10
flags.append(f"RUG_PATTERN_MATCH:{matches}")
confidence += 5
# ── Volume authenticity ──
if r.volume_auth:
va = r.volume_auth
fake_pct = float(va.get("fake_volume_pct", 0) or 0)
if fake_pct > 50:
score -= 20
flags.append("VOLUME_FAKE")
elif fake_pct < 10:
score += 5
green.append("VOLUME_AUTHENTIC")
confidence += 5
# ── Premium detections ──
if r.premium:
premium_flags = 0
for feature, data in r.premium.items():
if data and isinstance(data, dict):
risk = data.get("risk", data.get("risk_level", ""))
if risk in ("HIGH", "CRITICAL"):
score -= 10
flags.append(f"PREMIUM_{feature.upper()}")
premium_flags += 1
confidence += min(premium_flags * 5, 15)
# ── Clamp ──
r.safety_score = max(0.0, min(100.0, score))
r.confidence = min(confidence, 100.0)
# ── Risk level ──
if r.safety_score >= 80:
r.risk_level = "safe"
elif r.safety_score >= 60:
r.risk_level = "low"
elif r.safety_score >= 40:
r.risk_level = "medium"
elif r.safety_score >= 20:
r.risk_level = "high"
else:
r.risk_level = "critical"
r.risk_flags = flags
r.green_flags = green
# ═══════════════════════════════════════════════════════════════
# SINGLETON
# ═══════════════════════════════════════════════════════════════
_scanner: UnifiedTokenScanner | None = None
def get_token_scanner() -> UnifiedTokenScanner:
global _scanner
if _scanner is None:
_scanner = UnifiedTokenScanner()
return _scanner