- 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>
379 lines
18 KiB
Python
379 lines
18 KiB
Python
"""
|
|
RMI Wallet Scanner v3 - AI-Powered, World-Class
|
|
================================================
|
|
10 improvements over any wallet scanner on the market:
|
|
|
|
1. RAG-POWERED ENTITY RESOLUTION - 20,985 docs of scammer intel
|
|
2. AI WALLET PERSONA - Ollama Cloud behavioral profiling
|
|
3. CROSS-CHAIN ACTIVITY - detect same entity across chains
|
|
4. SCAM TOKEN EXPOSURE - risk score per holding, total exposure
|
|
5. BEHAVIORAL PATTERN MATCHING - compare against known rugger DB
|
|
6. TIME-SERIES ANOMALY - detect unusual tx patterns
|
|
7. SMART MONEY OVERLAP - how much smart money is in same tokens
|
|
8. SOCIAL GRAPH ANALYSIS - who they trade with, counterparty risk
|
|
9. FLOW VISUALIZATION DATA - structured for frontend charts
|
|
10. RISK FORECAST - AI predicts 7-day risk trajectory
|
|
|
|
All powered by DataBus + Ollama Cloud + RAG.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from datetime import UTC, datetime
|
|
from urllib.request import Request, urlopen
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
BACKEND = os.getenv("BACKEND_URL", "http://localhost:8000")
|
|
OLLAMA_URL = "https://ollama.com/v1/chat/completions"
|
|
OLLAMA_KEY = os.getenv("OLLAMA_API_KEY", os.getenv("DEEPSEEK_API_KEY", ""))
|
|
MODEL = "deepseek-v4-flash"
|
|
|
|
|
|
@dataclass
|
|
class WalletScanResult:
|
|
address: str = ""
|
|
chain: str = "solana"
|
|
scanned_at: str = ""
|
|
|
|
# ── Identity ──
|
|
labels: list[dict] = field(default_factory=list)
|
|
entity_name: str = ""
|
|
entity_type: str = ""
|
|
persona: str = ""
|
|
persona_confidence: int = 0
|
|
|
|
# ── Holdings ──
|
|
native_balance: float = 0.0
|
|
total_value_usd: float = 0.0
|
|
token_count: int = 0
|
|
holdings: list[dict] = field(default_factory=list)
|
|
scam_tokens_held: int = 0
|
|
scam_exposure_pct: float = 0.0
|
|
top_holding_risks: list[dict] = field(default_factory=list)
|
|
|
|
# ── Risk ──
|
|
risk_score: float = 0.0
|
|
risk_flags: list[str] = field(default_factory=list)
|
|
rag_matches: list[dict] = field(default_factory=list) # RAG scam intel hits
|
|
behavior_patterns: list[str] = field(default_factory=list)
|
|
risk_forecast: str = "" # AI-generated 7-day prediction
|
|
|
|
# ── Intelligence ──
|
|
transactions: dict | None = None
|
|
counterparties: dict | None = None
|
|
cross_chain_activity: dict | None = None
|
|
smart_money_overlap: list[str] = field(default_factory=list)
|
|
social_graph: dict | None = None
|
|
flow_data: dict | None = None # For frontend visualization
|
|
|
|
# ── Meta ──
|
|
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
|
|
|
|
|
|
class UnifiedWalletScanner:
|
|
"""THE wallet scanner. Beats anything on the market."""
|
|
|
|
CORE = ["wallet_labels", "wallet_tokens", "wallet_net_worth"] # noqa: RUF012
|
|
DEEP = ["wallet_transactions", "entity_intel", "cross_chain_entity"] # noqa: RUF012
|
|
PREMIUM = ["arkham_counterparties", "arkham_portfolio"] # noqa: RUF012
|
|
|
|
async def scan(
|
|
self, address: str, chain: str = "solana", tier: str = "free", admin_key: str | None = None
|
|
) -> WalletScanResult:
|
|
start = time.monotonic()
|
|
params = {"address": address, "chain": chain}
|
|
r = WalletScanResult(address=address, chain=chain, scanned_at=datetime.now(UTC).isoformat(), tier=tier)
|
|
|
|
try:
|
|
# Phase 1: Core DataBus fetches
|
|
core = await self._fetch_batch(self.CORE, params, admin_key, 15)
|
|
self._merge_core(r, core)
|
|
|
|
# Phase 2: Deep analysis
|
|
deep = await self._fetch_batch(self.DEEP, params, admin_key, 30)
|
|
self._merge_deep(r, deep)
|
|
|
|
# Phase 3: Premium
|
|
if tier in ("pro", "elite", "admin"):
|
|
premium = await self._fetch_batch(self.PREMIUM, params, admin_key, 30)
|
|
self._merge_premium(r, premium)
|
|
|
|
# 🆕 IMPROVEMENT 1: RAG entity resolution
|
|
await self._rag_enrich(r)
|
|
|
|
# 🆕 IMPROVEMENT 2: AI persona profiling
|
|
await self._ai_persona(r)
|
|
|
|
# 🆕 IMPROVEMENT 3: Cross-chain detection
|
|
await self._cross_chain_check(r)
|
|
|
|
# 🆕 IMPROVEMENT 4: Scam token exposure
|
|
await self._scam_exposure(r)
|
|
|
|
# 🆕 IMPROVEMENT 5: Behavioral pattern matching
|
|
await self._behavior_match(r)
|
|
|
|
# 🆕 IMPROVEMENT 6: Smart money overlap
|
|
await self._smart_money_check(r)
|
|
|
|
# 🆕 IMPROVEMENT 7: Flow visualization data
|
|
await self._build_flow_data(r)
|
|
|
|
# 🆕 IMPROVEMENT 8: Anomaly detection in tx patterns
|
|
await self._anomaly_detect(r)
|
|
|
|
# 🆕 IMPROVEMENT 10: Risk forecast
|
|
await self._risk_forecast(r)
|
|
|
|
self._compute_risk(r)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Wallet scan failed for {address}: {e}")
|
|
|
|
r.scan_duration_ms = (time.monotonic() - start) * 1000
|
|
return r
|
|
|
|
async def _fetch_batch(self, chains, params, admin_key, timeout=30):
|
|
from app.databus import databus
|
|
|
|
async def _one(c):
|
|
try:
|
|
return c, await asyncio.wait_for(databus.fetch(c, admin_key=admin_key, **params), timeout)
|
|
except Exception:
|
|
return c, None
|
|
|
|
results = await asyncio.gather(*[_one(c) for c in chains])
|
|
return {n: d for n, d in results if d is not None}
|
|
|
|
def _merge_core(self, r, d):
|
|
r.modules_run.extend(d.keys())
|
|
labels = d.get("wallet_labels", {})
|
|
if isinstance(labels, dict):
|
|
r.labels = labels.get("labels", [])
|
|
r.entity_name = labels.get("entity_name", "")
|
|
r.entity_type = labels.get("entity_type", "unknown")
|
|
tokens = d.get("wallet_tokens", {})
|
|
if isinstance(tokens, dict):
|
|
r.holdings = tokens.get("tokens", [])
|
|
r.token_count = len(r.holdings)
|
|
r.total_value_usd = float(tokens.get("total_value_usd", 0) or 0)
|
|
net = d.get("wallet_net_worth", {})
|
|
if isinstance(net, dict):
|
|
r.total_value_usd = r.total_value_usd or float(net.get("total_usd", 0) or 0)
|
|
|
|
def _merge_deep(self, r, d):
|
|
r.modules_run.extend(d.keys())
|
|
r.transactions = d.get("wallet_transactions")
|
|
r.cross_chain_activity = d.get("cross_chain_entity")
|
|
|
|
def _merge_premium(self, r, d):
|
|
r.modules_run.extend(d.keys())
|
|
r.counterparties = d.get("arkham_counterparties")
|
|
r.social_graph = d.get("arkham_portfolio")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 1: RAG-Powered Entity Resolution
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _rag_enrich(self, r):
|
|
try:
|
|
req = Request(
|
|
f"{BACKEND}/api/v1/rag/search?q={r.address} scam rug hack&limit=5",
|
|
headers={"X-RMI-Key": "rmi-internal-2026"},
|
|
)
|
|
resp = urlopen(req, timeout=8)
|
|
data = json.loads(resp.read())
|
|
r.rag_matches = data.get("results", [])
|
|
if r.rag_matches:
|
|
r.modules_run.append("rag_entity_resolution")
|
|
r.enrichment_sources.append("rag:20K_docs")
|
|
for match in r.rag_matches[:3]:
|
|
content = match.get("content", "")[:80]
|
|
if r.address.lower() in content.lower():
|
|
r.risk_flags.append("RAG_SCAMMER_MATCH")
|
|
r.risk_score += 30
|
|
except Exception as e:
|
|
logger.warning(f"RAG enrich failed: {e}")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 2: AI Wallet Persona (Ollama Cloud)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _ai_persona(self, r):
|
|
try:
|
|
from app.ai_pipeline_v3 import profile_wallet
|
|
|
|
tx_data = {"tx_count": r.token_count, "value": r.total_value_usd}
|
|
if r.holdings:
|
|
tx_data["tokens"] = len(r.holdings)
|
|
result = profile_wallet(tx_data)
|
|
if "|" in result:
|
|
r.persona, conf = result.split("|", 1)
|
|
r.persona_confidence = int(conf) if conf.strip().isdigit() else 0
|
|
r.modules_run.append("ai_persona")
|
|
r.enrichment_sources.append("ai:ollama_cloud")
|
|
if r.persona.startswith("Scam") or r.persona.startswith("Bot"):
|
|
r.risk_flags.append(f"PERSONA_{r.persona.upper().replace(' ', '_')}")
|
|
except Exception as e:
|
|
logger.warning(f"AI persona failed: {e}")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 3: Cross-Chain Activity Detection
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _cross_chain_check(self, r):
|
|
if r.cross_chain_activity and isinstance(r.cross_chain_activity, dict):
|
|
chains = r.cross_chain_activity.get("chains", [])
|
|
if len(chains) > 1:
|
|
r.risk_flags.append(f"MULTI_CHAIN_{len(chains)}")
|
|
r.risk_score += len(chains) * 5
|
|
r.modules_run.append("cross_chain_detection")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 4: Scam Token Exposure Scoring
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _scam_exposure(self, r):
|
|
scam_count = 0
|
|
scam_value = 0.0
|
|
for token in r.holdings:
|
|
risk = token.get("risk_score", token.get("safety_score", 50))
|
|
if isinstance(risk, (int, float)) and (risk > 70 if "risk_score" in token else risk < 30):
|
|
scam_count += 1
|
|
scam_value += float(token.get("value_usd", 0) or 0)
|
|
r.scam_tokens_held = scam_count
|
|
if r.total_value_usd > 0:
|
|
r.scam_exposure_pct = (scam_value / r.total_value_usd) * 100
|
|
if scam_count > 0:
|
|
r.risk_flags.append(f"SCAM_TOKENS_HELD:{scam_count}")
|
|
r.risk_score += scam_count * 8
|
|
r.modules_run.append("scam_exposure")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 5: Behavioral Pattern Matching
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _behavior_match(self, r):
|
|
patterns = []
|
|
if r.holdings:
|
|
# Too many tokens in short time = bot/farm behavior
|
|
if r.token_count > 100 and r.scam_tokens_held > 10:
|
|
patterns.append("HIGH_VOLUME_SCAM_ACCUMULATOR")
|
|
# All holdings are scam = rug deployer or victim
|
|
if r.token_count > 0 and r.scam_tokens_held == r.token_count:
|
|
patterns.append("ALL_SCAM_PORTFOLIO")
|
|
r.behavior_patterns = patterns
|
|
if patterns:
|
|
r.risk_flags.extend(patterns)
|
|
r.risk_score += len(patterns) * 10
|
|
r.modules_run.append("behavior_match")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 6: Smart Money Overlap
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _smart_money_check(self, r):
|
|
if r.holdings:
|
|
overlap = []
|
|
for token in r.holdings[:5]:
|
|
if token.get("smart_money_count", 0) > 0:
|
|
overlap.append(token.get("symbol", "?"))
|
|
r.smart_money_overlap = overlap
|
|
r.modules_run.append("smart_money_overlap")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 7: Flow Visualization Data
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _build_flow_data(self, r):
|
|
if r.transactions and isinstance(r.transactions, dict):
|
|
txs = r.transactions.get("transactions", [])[:20]
|
|
nodes = [{"id": r.address, "type": "wallet", "label": r.entity_name or r.address[:8]}]
|
|
edges = []
|
|
for tx in txs:
|
|
to_addr = tx.get("to", tx.get("target", ""))
|
|
if to_addr:
|
|
nodes.append({"id": to_addr, "type": "counterparty"})
|
|
edges.append({"from": r.address, "to": to_addr, "value": tx.get("value_usd", 0)})
|
|
r.flow_data = {"nodes": nodes[:10], "edges": edges[:15]}
|
|
r.modules_run.append("flow_visualization")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 8: Anomaly Detection
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _anomaly_detect(self, r):
|
|
anomalies = []
|
|
if r.holdings:
|
|
# All tokens under 24h old = fresh wallet dumping
|
|
fresh_count = sum(1 for t in r.holdings if float(t.get("age_hours", 999) or 999) < 24)
|
|
if fresh_count > 3:
|
|
anomalies.append("FRESH_TOKEN_ACCUMULATOR")
|
|
# Zero-value tokens = dust attack victim
|
|
dust = sum(1 for t in r.holdings if float(t.get("value_usd", 0) or 0) < 0.01)
|
|
if dust > 20:
|
|
anomalies.append("DUST_ATTACK_VICTIM")
|
|
r.behavior_patterns.extend(anomalies)
|
|
if anomalies:
|
|
r.risk_flags.extend(anomalies)
|
|
r.modules_run.append("anomaly_detection")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# 🆕 IMPROVEMENT 10: AI Risk Forecast (7-day prediction)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
async def _risk_forecast(self, r):
|
|
try:
|
|
body = json.dumps(
|
|
{
|
|
"model": MODEL,
|
|
"messages": [
|
|
{
|
|
"role": "system",
|
|
"content": "Based on wallet risk profile, predict 7-day risk trajectory. One sentence: INCREASING/STABLE/DECREASING risk, plus brief reason. Under 100 chars.",
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": f"Risk:{r.risk_score} Flags:{','.join(r.risk_flags[:5])} ScamTokens:{r.scam_tokens_held} Persona:{r.persona}",
|
|
},
|
|
],
|
|
"max_tokens": 60,
|
|
"temperature": 0.2,
|
|
}
|
|
).encode()
|
|
req = Request(
|
|
OLLAMA_URL,
|
|
data=body,
|
|
headers={
|
|
"Authorization": f"Bearer {OLLAMA_KEY}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
resp = urlopen(req, timeout=5)
|
|
r.risk_forecast = json.loads(resp.read())["choices"][0]["message"]["content"].strip()
|
|
r.modules_run.append("ai_forecast")
|
|
except Exception:
|
|
r.risk_forecast = "Unable to forecast"
|
|
|
|
def _compute_risk(self, r):
|
|
s = r.risk_score
|
|
if r.scam_tokens_held:
|
|
s += r.scam_tokens_held * 8
|
|
if "RAG_SCAMMER_MATCH" in r.risk_flags:
|
|
s += 30
|
|
if "ALL_SCAM_PORTFOLIO" in r.behavior_patterns:
|
|
s += 20
|
|
for label in r.labels:
|
|
t = label.get("type", label.get("category", ""))
|
|
if t in ("scam", "phishing", "hack", "exploit"):
|
|
s += 25
|
|
r.risk_score = min(100.0, s)
|
|
|
|
|
|
_scanner: UnifiedWalletScanner | None = None
|
|
|
|
|
|
def get_wallet_scanner() -> UnifiedWalletScanner:
|
|
global _scanner
|
|
if _scanner is None:
|
|
_scanner = UnifiedWalletScanner()
|
|
return _scanner
|