rmi-backend/app/wallet_clustering.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

884 lines
33 KiB
Python

"""
Wallet Clustering Engine - Advanced Wallet Relationship Detection
================================================================
Detects wallet clusters using multiple forensic signals:
- Transaction pattern analysis
- Temporal proximity detection
- Common counterparty identification
- Fund flow tracing
- Behavioral fingerprinting
"""
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class Transaction:
"""Represents a blockchain transaction."""
signature: str
timestamp: datetime
from_address: str
to_address: str
amount: float
token: str
program: str
success: bool = True
@property
def is_transfer(self) -> bool:
return self.program in ["spl-token", "system", "transfer"]
@dataclass
class WalletProfile:
"""Profile of a wallet's behavior."""
address: str
first_seen: datetime | None = None
last_seen: datetime | None = None
total_transactions: int = 0
total_volume: float = 0.0
unique_counterparties: set[str] = field(default_factory=set)
token_holdings: dict[str, float] = field(default_factory=dict)
transaction_times: list[datetime] = field(default_factory=list)
programs_used: set[str] = field(default_factory=set)
# Behavioral metrics
avg_transaction_size: float = 0.0
transaction_frequency: float = 0.0 # tx per day
preferred_hours: list[int] = field(default_factory=list) # Hours of day most active
def calculate_metrics(self):
"""Calculate behavioral metrics from transaction data."""
if self.transaction_times:
self.transaction_times.sort()
self.first_seen = self.transaction_times[0]
self.last_seen = self.transaction_times[-1]
# Calculate frequency
days_active = (self.last_seen - self.first_seen).days + 1
if days_active > 0:
self.transaction_frequency = len(self.transaction_times) / days_active
# Preferred hours
hours = [t.hour for t in self.transaction_times]
hour_counts = defaultdict(int)
for h in hours:
hour_counts[h] += 1
self.preferred_hours = sorted(hour_counts.keys(), key=lambda x: hour_counts[x], reverse=True)[:3]
@dataclass
class Cluster:
"""A detected wallet cluster."""
cluster_id: str
wallets: set[str]
confidence: float
detection_methods: list[str]
center_wallet: str | None = None
total_volume: float = 0.0
common_tokens: set[str] = field(default_factory=set)
common_counterparties: set[str] = field(default_factory=set)
first_activity: datetime | None = None
last_activity: datetime | None = None
def to_dict(self) -> dict:
return {
"cluster_id": self.cluster_id,
"wallets": list(self.wallets),
"wallet_count": len(self.wallets),
"confidence": round(self.confidence, 3),
"detection_methods": self.detection_methods,
"center_wallet": self.center_wallet,
"total_volume": self.total_volume,
"common_tokens": list(self.common_tokens),
"common_counterparties": list(self.common_counterparties),
"first_activity": self.first_activity.isoformat() if self.first_activity else None,
"last_activity": self.last_activity.isoformat() if self.last_activity else None,
}
@dataclass
class Connection:
"""Connection between two wallets."""
wallet_a: str
wallet_b: str
strength: float # 0-1
connection_types: list[str]
evidence: list[dict]
total_volume: float = 0.0
transaction_count: int = 0
first_connection: datetime | None = None
last_connection: datetime | None = None
class WalletClusteringEngine:
"""
Advanced wallet clustering engine using multiple forensic signals.
"""
# Thresholds for clustering
TEMPORAL_PROXIMITY_MINUTES = 5 # Transactions within 5 min considered coordinated
MIN_COMMON_COUNTERPARTIES = 3 # Min shared counterparties for cluster
MIN_TRANSACTION_SIMILARITY = 0.7 # Min similarity score for pattern match
MIN_CONNECTION_STRENGTH = 0.3 # Min strength for bubble map connection
def __init__(self):
self.wallets: dict[str, WalletProfile] = {}
self.transactions: list[Transaction] = []
self.connections: dict[tuple[str, str], Connection] = {}
self.clusters: dict[str, Cluster] = {}
def add_transaction(self, tx: Transaction):
"""Add a transaction to the engine."""
self.transactions.append(tx)
# Update sender profile
if tx.from_address not in self.wallets:
self.wallets[tx.from_address] = WalletProfile(address=tx.from_address)
sender = self.wallets[tx.from_address]
sender.total_transactions += 1
sender.total_volume += tx.amount
sender.unique_counterparties.add(tx.to_address)
sender.transaction_times.append(tx.timestamp)
sender.programs_used.add(tx.program)
# Update receiver profile
if tx.to_address not in self.wallets:
self.wallets[tx.to_address] = WalletProfile(address=tx.to_address)
receiver = self.wallets[tx.to_address]
receiver.total_transactions += 1
receiver.total_volume += tx.amount
receiver.unique_counterparties.add(tx.from_address)
receiver.transaction_times.append(tx.timestamp)
receiver.programs_used.add(tx.program)
# Update or create connection
pair = tuple(sorted([tx.from_address, tx.to_address]))
if pair not in self.connections:
self.connections[pair] = Connection(
wallet_a=pair[0], wallet_b=pair[1], strength=0.0, connection_types=[], evidence=[]
)
conn = self.connections[pair]
conn.transaction_count += 1
conn.total_volume += tx.amount
conn.evidence.append(
{
"signature": tx.signature,
"timestamp": tx.timestamp.isoformat(),
"amount": tx.amount,
"token": tx.token,
}
)
if conn.first_connection is None or tx.timestamp < conn.first_connection:
conn.first_connection = tx.timestamp
if conn.last_connection is None or tx.timestamp > conn.last_connection:
conn.last_connection = tx.timestamp
def load_from_helius(self, helius_data: list[dict]):
"""Load transactions from Helius API format."""
for item in helius_data:
tx = Transaction(
signature=item.get("signature", ""),
timestamp=datetime.fromisoformat(item.get("timestamp", datetime.now().isoformat())),
from_address=item.get("from", ""),
to_address=item.get("to", ""),
amount=item.get("amount", 0.0),
token=item.get("token", "SOL"),
program=item.get("program", "unknown"),
success=item.get("success", True),
)
self.add_transaction(tx)
# Recalculate all metrics
for wallet in self.wallets.values():
wallet.calculate_metrics()
def detect_temporal_clusters(self, time_window_minutes: int | None = None) -> list[Cluster]:
"""
Detect clusters based on temporal proximity of transactions.
Wallets that transact at the same time may be coordinated.
"""
time_window = time_window_minutes or self.TEMPORAL_PROXIMITY_MINUTES
clusters = []
# Group transactions by time windows
time_groups = defaultdict(list)
for tx in self.transactions:
if not tx.success:
continue
# Round to time window
window_key = tx.timestamp.replace(
minute=(tx.timestamp.minute // time_window) * time_window, second=0, microsecond=0
)
time_groups[window_key].append(tx)
# Find wallets active in same time windows
cluster_id = 0
processed_wallets = set()
for window, txs in time_groups.items():
if len(txs) < 2:
continue
# Get all wallets in this window
window_wallets = set()
for tx in txs:
window_wallets.add(tx.from_address)
window_wallets.add(tx.to_address)
# Skip if already processed
unprocessed = window_wallets - processed_wallets
if len(unprocessed) < 2:
continue
# Check for common patterns
common_tokens = set()
common_programs = set()
for tx in txs:
common_tokens.add(tx.token)
common_programs.add(tx.program)
# Create cluster if significant
if len(unprocessed) >= 2:
cluster = Cluster(
cluster_id=f"temporal_{cluster_id}",
wallets=unprocessed,
confidence=min(0.9, 0.5 + len(unprocessed) * 0.1),
detection_methods=["temporal_proximity"],
common_tokens=common_tokens,
first_activity=window,
last_activity=window,
)
clusters.append(cluster)
processed_wallets.update(unprocessed)
cluster_id += 1
return clusters
def detect_common_counterparty_clusters(self) -> list[Cluster]:
"""
Detect clusters based on shared counterparties.
Wallets that send/receive from the same addresses may be related.
"""
clusters = []
# Build counterparty -> wallets mapping
counterparty_wallets = defaultdict(set)
for wallet in self.wallets.values():
for counterparty in wallet.unique_counterparties:
counterparty_wallets[counterparty].add(wallet.address)
# Find wallets sharing multiple counterparties
wallet_pairs = defaultdict(set)
for counterparty, wallets in counterparty_wallets.items():
if len(wallets) < 2:
continue
wallet_list = list(wallets)
for i in range(len(wallet_list)):
for j in range(i + 1, len(wallet_list)):
pair = tuple(sorted([wallet_list[i], wallet_list[j]]))
wallet_pairs[pair].add(counterparty)
# Group into clusters
cluster_map = defaultdict(set)
for (w1, w2), counterparties in wallet_pairs.items():
if len(counterparties) >= self.MIN_COMMON_COUNTERPARTIES:
cluster_map[w1].add(w2)
cluster_map[w2].add(w1)
# Find connected components
visited = set()
cluster_id = 0
for wallet in cluster_map:
if wallet in visited:
continue
# BFS to find connected wallets
cluster_wallets = set()
queue = [wallet]
while queue:
current = queue.pop(0)
if current in visited:
continue
visited.add(current)
cluster_wallets.add(current)
queue.extend(cluster_map[current] - visited)
if len(cluster_wallets) >= 2:
# Find common counterparties for this cluster
common_cp = None
for w in cluster_wallets:
if common_cp is None:
common_cp = self.wallets[w].unique_counterparties
else:
common_cp = common_cp & self.wallets[w].unique_counterparties
cluster = Cluster(
cluster_id=f"counterparty_{cluster_id}",
wallets=cluster_wallets,
confidence=min(0.95, 0.6 + len(cluster_wallets) * 0.05),
detection_methods=["common_counterparties"],
common_counterparties=common_cp or set(),
center_wallet=self._find_center_wallet(cluster_wallets),
)
clusters.append(cluster)
cluster_id += 1
return clusters
def detect_pattern_clusters(self) -> list[Cluster]:
"""
Detect clusters based on similar transaction patterns.
Similar behavior may indicate the same operator.
"""
clusters = []
# Calculate behavioral fingerprints
fingerprints = {}
for address, wallet in self.wallets.items():
if wallet.total_transactions < 5: # Need enough data
continue
fingerprint = {
"avg_size": wallet.avg_transaction_size or (wallet.total_volume / wallet.total_transactions),
"frequency": wallet.transaction_frequency,
"preferred_hours": wallet.preferred_hours,
"program_diversity": len(wallet.programs_used),
"counterparty_count": len(wallet.unique_counterparties),
}
fingerprints[address] = fingerprint
# Find similar fingerprints
similarity_matrix = {}
addresses = list(fingerprints.keys())
for i in range(len(addresses)):
for j in range(i + 1, len(addresses)):
w1, w2 = addresses[i], addresses[j]
sim = self._calculate_fingerprint_similarity(fingerprints[w1], fingerprints[w2])
if sim >= self.MIN_TRANSACTION_SIMILARITY:
similarity_matrix[(w1, w2)] = sim
# Group similar wallets
cluster_map = defaultdict(set)
for (w1, w2), sim in similarity_matrix.items(): # noqa: B007
cluster_map[w1].add(w2)
cluster_map[w2].add(w1)
# Find connected components
visited = set()
cluster_id = 0
for wallet in cluster_map:
if wallet in visited:
continue
cluster_wallets = set()
queue = [wallet]
while queue:
current = queue.pop(0)
if current in visited:
continue
visited.add(current)
cluster_wallets.add(current)
queue.extend(cluster_map[current] - visited)
if len(cluster_wallets) >= 2:
cluster = Cluster(
cluster_id=f"pattern_{cluster_id}",
wallets=cluster_wallets,
confidence=min(0.85, 0.5 + len(cluster_wallets) * 0.05),
detection_methods=["behavioral_pattern"],
center_wallet=self._find_center_wallet(cluster_wallets),
)
clusters.append(cluster)
cluster_id += 1
return clusters
def detect_funding_clusters(self) -> list[Cluster]:
"""
Detect clusters based on common funding sources.
Wallets funded from the same source may be related.
"""
clusters = []
# Find funding transactions (first transaction to each wallet)
funding_sources = {}
for wallet in self.wallets.values():
if wallet.transaction_times:
first_tx_time = min(wallet.transaction_times)
# Find first incoming transaction
for tx in self.transactions:
if tx.to_address == wallet.address and tx.timestamp == first_tx_time:
funding_sources[wallet.address] = tx.from_address
break
# Group by funding source
source_wallets = defaultdict(set)
for wallet, source in funding_sources.items():
source_wallets[source].add(wallet)
# Create clusters for wallets with same funder
cluster_id = 0
for source, wallets in source_wallets.items():
if len(wallets) >= 2:
cluster = Cluster(
cluster_id=f"funding_{cluster_id}",
wallets=wallets,
confidence=0.8 if len(wallets) >= 5 else 0.6,
detection_methods=["common_funding_source"],
center_wallet=source,
common_counterparties={source},
)
clusters.append(cluster)
cluster_id += 1
return clusters
def find_all_clusters(self) -> list[Cluster]:
"""Run all clustering methods and merge results."""
all_clusters = []
# Run all detection methods
all_clusters.extend(self.detect_temporal_clusters())
all_clusters.extend(self.detect_common_counterparty_clusters())
all_clusters.extend(self.detect_pattern_clusters())
all_clusters.extend(self.detect_funding_clusters())
# Merge overlapping clusters
merged = self._merge_clusters(all_clusters)
# Store and return
for cluster in merged:
self.clusters[cluster.cluster_id] = cluster
return merged
def _merge_clusters(self, clusters: list[Cluster]) -> list[Cluster]:
"""Merge clusters that share wallets."""
if not clusters:
return []
# Build wallet -> clusters mapping
wallet_clusters = defaultdict(set)
for i, cluster in enumerate(clusters):
for wallet in cluster.wallets:
wallet_clusters[wallet].add(i)
# Find connected cluster groups
visited = set()
merged_clusters = []
for i, cluster in enumerate(clusters): # noqa: B007
if i in visited:
continue
# BFS to find all connected clusters
group_indices = set()
queue = [i]
while queue:
current = queue.pop(0)
if current in visited:
continue
visited.add(current)
group_indices.add(current)
# Find connected clusters through shared wallets
for wallet in clusters[current].wallets:
for connected in wallet_clusters[wallet]:
if connected not in visited:
queue.append(connected)
# Merge this group
all_wallets = set()
all_methods = set()
all_tokens = set()
all_counterparties = set()
max_confidence = 0
for idx in group_indices:
c = clusters[idx]
all_wallets.update(c.wallets)
all_methods.update(c.detection_methods)
all_tokens.update(c.common_tokens)
all_counterparties.update(c.common_counterparties)
max_confidence = max(max_confidence, c.confidence)
merged = Cluster(
cluster_id=f"merged_{len(merged_clusters)}",
wallets=all_wallets,
confidence=min(0.98, max_confidence + len(all_methods) * 0.05),
detection_methods=list(all_methods),
common_tokens=all_tokens,
common_counterparties=all_counterparties,
center_wallet=self._find_center_wallet(all_wallets),
)
merged_clusters.append(merged)
return merged_clusters
def _calculate_fingerprint_similarity(self, fp1: dict, fp2: dict) -> float:
"""Calculate similarity between two behavioral fingerprints."""
scores = []
# Average transaction size similarity (normalized)
if fp1["avg_size"] > 0 and fp2["avg_size"] > 0:
size_ratio = min(fp1["avg_size"], fp2["avg_size"]) / max(fp1["avg_size"], fp2["avg_size"])
scores.append(size_ratio)
# Frequency similarity
if fp1["frequency"] > 0 and fp2["frequency"] > 0:
freq_ratio = min(fp1["frequency"], fp2["frequency"]) / max(fp1["frequency"], fp2["frequency"])
scores.append(freq_ratio)
# Preferred hours overlap
hours1 = set(fp1["preferred_hours"])
hours2 = set(fp2["preferred_hours"])
if hours1 and hours2:
hour_overlap = len(hours1 & hours2) / len(hours1 | hours2)
scores.append(hour_overlap)
# Program diversity similarity
if fp1["program_diversity"] > 0 and fp2["program_diversity"] > 0:
prog_ratio = min(fp1["program_diversity"], fp2["program_diversity"]) / max(
fp1["program_diversity"], fp2["program_diversity"]
)
scores.append(prog_ratio)
return sum(scores) / len(scores) if scores else 0
def _find_center_wallet(self, wallets: set[str]) -> str | None:
"""Find the most connected wallet in a cluster (center)."""
if not wallets:
return None
max_connections = 0
center = None
for wallet in wallets:
if wallet in self.wallets:
connections = len(self.wallets[wallet].unique_counterparties & wallets)
if connections > max_connections:
max_connections = connections
center = wallet
return center or next(iter(wallets))
def get_connections_for_bubble_map(
self,
center_wallet: str,
depth: int = 2,
min_strength: float | None = None,
max_wallets: int = 250,
) -> tuple[list[str], list[Connection]]:
"""
Get connections for bubble map visualization.
Supports deep linking up to 250 wallets for advanced forensics.
Returns:
Tuple of (all_wallets, connections)
"""
min_str = min_strength or self.MIN_CONNECTION_STRENGTH
# Calculate connection strengths
for conn in self.connections.values():
# Strength based on transaction count and volume
count_score = min(1.0, conn.transaction_count / 100)
volume_score = min(1.0, conn.total_volume / 10000)
time_score = 0.5 # Base score
if conn.first_connection and conn.last_connection:
duration = (conn.last_connection - conn.first_connection).days
time_score = min(1.0, duration / 30) # Longer = stronger
conn.strength = count_score * 0.4 + volume_score * 0.4 + time_score * 0.2
# BFS to find connected wallets up to depth, capped at max_wallets
all_wallets = {center_wallet}
relevant_connections = []
current_level = {center_wallet}
for _d in range(depth):
if len(all_wallets) >= max_wallets:
break
next_level = set()
for wallet in current_level:
for pair, conn in self.connections.items():
if wallet in pair and conn.strength >= min_str:
other = pair[1] if pair[0] == wallet else pair[0]
if other not in all_wallets:
if len(all_wallets) < max_wallets:
next_level.add(other)
all_wallets.add(other)
if conn not in relevant_connections:
relevant_connections.append(conn)
current_level = next_level
if not current_level:
break
return list(all_wallets), relevant_connections
def generate_bubble_map_data(self, center_wallet: str, depth: int = 2, max_wallets: int = 250) -> dict:
"""
Generate data for interactive bubble map visualization.
Supports up to 250 wallets deep for comprehensive cluster analysis.
Returns JSON-ready data structure for D3.js or similar.
"""
wallets, connections = self.get_connections_for_bubble_map(center_wallet, depth, max_wallets=max_wallets)
# Build nodes
nodes = []
for _i, wallet in enumerate(wallets):
profile = self.wallets.get(wallet)
# Determine node type
if wallet == center_wallet:
node_type = "center"
color = "#ff6b6b" # Red
elif wallet in self._get_known_scammer_wallets():
node_type = "scammer"
color = "#ff0000" # Dark red
elif profile and len(profile.unique_counterparties) > 50:
node_type = "exchange"
color = "#4dabf7" # Blue
else:
node_type = "wallet"
color = "#69db7c" # Green
# Size based on volume
volume = profile.total_volume if profile else 0
size = min(50, max(10, volume / 100))
nodes.append(
{
"id": wallet,
"type": node_type,
"size": size,
"color": color,
"volume": volume,
"transactions": profile.total_transactions if profile else 0,
"label": f"{wallet[:8]}...",
}
)
# Build links
links = []
for conn in connections:
links.append(
{
"source": conn.wallet_a,
"target": conn.wallet_b,
"strength": round(conn.strength, 3),
"volume": conn.total_volume,
"transactions": conn.transaction_count,
"value": conn.strength * 10, # For D3 force simulation
}
)
return {
"center_wallet": center_wallet,
"depth": depth,
"nodes": nodes,
"links": links,
"total_wallets": len(nodes),
"total_connections": len(links),
"generated_at": datetime.now().isoformat(),
}
def _get_known_scammer_wallets(self) -> set[str]:
"""Get set of known scammer wallets."""
# This would come from your database
return set() # Placeholder
def generate_ai_forensic_breakdown(
self,
center_wallet: str,
initial_depth: int = 2,
initial_max_wallets: int = 250,
max_expansion_depth: int = 5,
absolute_max_wallets: int = 1000,
) -> dict:
"""
AI-driven forensic breakdown that dynamically pulls deeper if necessary.
This is a premium feature that analyzes the initial cluster for risk vectors.
If complex layering, high-risk patterns, or obfuscation tactics are detected,
it automatically expands the search depth (up to absolute_max_wallets) to
provide a comprehensive forensic breakdown that competitors lack.
Returns a rich, AI-ready context payload for LLM analysis.
"""
# Step 1: Get initial cluster data
initial_wallets, initial_connections = self.get_connections_for_bubble_map(
center_wallet, depth=initial_depth, max_wallets=initial_max_wallets
)
# Step 2: Analyze for risk vectors that warrant deeper investigation
risk_score = 0.0
risk_vectors = []
# Check for complex layering (many intermediate wallets)
intermediate_count = sum(1 for w in initial_wallets if w != center_wallet)
if intermediate_count > 50:
risk_score += 0.3
risk_vectors.append("Complex layering detected (>50 intermediate wallets)")
# Check for high transaction velocity
total_txs = sum(self.wallets[w].total_transactions for w in initial_wallets if w in self.wallets)
if total_txs > 500:
risk_score += 0.4
risk_vectors.append("High transaction velocity detected")
# Check for common funding sources (potential sybil or coordinated attack)
funding_sources = set()
for w in initial_wallets:
profile = self.wallets.get(w)
if profile and profile.total_transactions > 0:
# Simplified: check if they share counterparties
funding_sources.update(profile.unique_counterparties)
if len(funding_sources) < len(initial_wallets) * 0.5:
risk_score += 0.3
risk_vectors.append("Concentrated funding sources detected (potential sybil cluster)")
# Step 3: Dynamically expand if risk score is high
needs_expansion = risk_score >= 0.5
expanded_wallets = initial_wallets
expanded_connections = initial_connections
expansion_depth_used = initial_depth
if needs_expansion:
# Expand depth up to max_expansion_depth, capped at absolute_max_wallets
expanded_wallets, expanded_connections = self.get_connections_for_bubble_map(
center_wallet, depth=max_expansion_depth, max_wallets=absolute_max_wallets
)
expansion_depth_used = max_expansion_depth
risk_vectors.append(
f"AI auto-expanded analysis to depth {expansion_depth_used} ({len(expanded_wallets)} wallets) due to high risk indicators"
)
# Step 4: Build AI-ready forensic context
wallet_profiles = []
for w in expanded_wallets:
profile = self.wallets.get(w)
if profile:
wallet_profiles.append(
{
"address": w,
"is_center": w == center_wallet,
"total_transactions": profile.total_transactions,
"total_volume": profile.total_volume,
"unique_counterparties_count": len(profile.unique_counterparties),
"first_seen": profile.first_seen.isoformat() if profile.first_seen else None,
"last_seen": profile.last_seen.isoformat() if profile.last_seen else None,
"preferred_hours": profile.preferred_hours,
}
)
connection_summary = []
for conn in expanded_connections:
connection_summary.append(
{
"source": conn.wallet_a,
"target": conn.wallet_b,
"strength": round(conn.strength, 3),
"total_volume": conn.total_volume,
"transaction_count": conn.transaction_count,
}
)
return {
"center_wallet": center_wallet,
"analysis_mode": "expanded_deep_forensics" if needs_expansion else "standard_cluster",
"risk_score": round(risk_score, 2),
"risk_vectors": risk_vectors,
"expansion_triggered": needs_expansion,
"depth_used": expansion_depth_used,
"total_wallets_analyzed": len(expanded_wallets),
"total_connections_analyzed": len(expanded_connections),
"wallet_profiles": wallet_profiles,
"connection_summary": connection_summary,
"ai_prompt_context": f"Analyze this wallet cluster centered on {center_wallet}. "
f"Risk score: {risk_score:.2f}. "
f"Vectors: {', '.join(risk_vectors)}. "
f"The cluster contains {len(expanded_wallets)} wallets and {len(expanded_connections)} connections. "
f"Identify the ultimate beneficiary, obfuscation tactics, and provide a clear forensic breakdown.",
"generated_at": datetime.now().isoformat(),
}
def get_cluster_report(self, cluster_id: str) -> dict | None:
"""Get detailed report for a cluster."""
cluster = self.clusters.get(cluster_id)
if not cluster:
return None
# Get wallet details
wallet_details = []
for wallet in cluster.wallets:
profile = self.wallets.get(wallet)
if profile:
wallet_details.append(
{
"address": wallet,
"transactions": profile.total_transactions,
"volume": profile.total_volume,
"counterparties": len(profile.unique_counterparties),
"first_seen": profile.first_seen.isoformat() if profile.first_seen else None,
"last_seen": profile.last_seen.isoformat() if profile.last_seen else None,
}
)
report = cluster.to_dict()
report["wallet_details"] = wallet_details
report["internal_connections"] = len(
[
conn
for conn in self.connections.values()
if conn.wallet_a in cluster.wallets and conn.wallet_b in cluster.wallets
]
)
return report
# Global engine instance
_clustering_engine = None
def get_clustering_engine() -> WalletClusteringEngine:
"""Get global clustering engine instance."""
global _clustering_engine
if _clustering_engine is None:
_clustering_engine = WalletClusteringEngine()
return _clustering_engine
if __name__ == "__main__":
print("=" * 70)
print("WALLET CLUSTERING ENGINE")
print("=" * 70)
engine = get_clustering_engine()
print("\n🔍 Clustering Methods:")
print(" 1. Temporal Proximity - Transactions within 5 minutes")
print(" 2. Common Counterparties - Shared senders/recipients")
print(" 3. Behavioral Patterns - Similar transaction patterns")
print(" 4. Common Funding - Same funding source")
print("\n📊 Bubble Map Features:")
print(" - Size = Transaction volume")
print(" - Color = Wallet type (center/scammer/exchange/unknown)")
print(" - Line thickness = Connection strength")
print(" - Interactive depth control")
print("\n" + "=" * 70)