408 lines
13 KiB
Python
408 lines
13 KiB
Python
"""
|
|
Entity Labeler - Advanced Wallet Labeling Module
|
|
================================================
|
|
|
|
Exposes:
|
|
• Exchange/Protocol detection (Uniswap, Aave, Binance, etc.)
|
|
• Whale detection (high-value wallets)
|
|
• Contract type detection (fungible, non-fungible, proxy, library)
|
|
• Risk scoring (low/medium/high/critical)
|
|
• Smart contract verification status
|
|
• Token metadata (name, symbol, decimals)
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from app.entity_clustering import EntityClusteringEngine
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ─── KNOWN ENTITIES DATABASE ──────────────────────────────────────────────
|
|
|
|
# Exchange contracts (major CEX/DEX)
|
|
EXCHANGES = {
|
|
"binance": ["0x" + "0" * 39 + "2a", "0x" + "0" * 39 + "3b"], # Placeholder - actual addresses
|
|
"coinbase": ["0x" + "0" * 39 + "4c"],
|
|
"uniswap": ["0x7a250d5630b4cf139281983dce37532e7d5c9196"],
|
|
"sushiswap": ["0xc0a47dfe034b400b26bc65c9f1b2c5ac5b5d7373"],
|
|
"curve": ["0x90e80ecf27b8b9f3a6c5b6694c44515f5b6f5d45"],
|
|
"chainlink": ["0x514910771af9ca656af840dff83e8264ecf986ca"],
|
|
}
|
|
|
|
# Protocol contracts (DeFi protocols)
|
|
PROTOCOLS = {
|
|
"aave": {
|
|
"address": "0x7d2768de32b0b8013e039f31fbacf67128c9c3d8",
|
|
"token": "0x37d819f03d3261550b4e75c5e5c5c5c5c5c5c5c5", # placeholder
|
|
"type": "lending",
|
|
},
|
|
"compound": {
|
|
"address": "0x3fdf4e696732ad248fbfc9d846c252d66b4d945c",
|
|
"type": "lending",
|
|
},
|
|
"uniswap_v3": {
|
|
"address": "0x1f98431c8ad98523631ae4a59f267346ea31f988",
|
|
"type": "amm",
|
|
},
|
|
}
|
|
|
|
# Token categories
|
|
TOKEN_TYPES = {
|
|
"stablecoin": ["usdc", "usdt", "dai", "busd", "ampt"],
|
|
"memecoin": ["pepe", "shib", "doge", "floki"],
|
|
"governance": ["snapshot", "governance", "vote"],
|
|
"yield": ["aave", "comp", "yearn"],
|
|
}
|
|
|
|
|
|
# ─── DETECTION FUNCTIONS ──────────────────────────────────────────────────
|
|
|
|
|
|
def detect_exchange(address: str, name: str = "", symbol: str = "") -> str | None:
|
|
"""Detect if address belongs to a known exchange/protocol."""
|
|
address_lower = address.lower()
|
|
|
|
# Check exchange contracts
|
|
for exchange, contracts in EXCHANGES.items():
|
|
if any(contract.lower() in address_lower for contract in contracts):
|
|
return exchange
|
|
|
|
# Check name/symbol matching
|
|
name_lower = (name + " " + symbol).lower()
|
|
for exchange in EXCHANGES:
|
|
if exchange.lower() in name_lower:
|
|
return exchange
|
|
|
|
return None
|
|
|
|
|
|
def detect_protocol(address: str, name: str = "", symbol: str = "") -> str | None:
|
|
"""Detect if address is a DeFi protocol."""
|
|
# Check protocol contracts
|
|
for protocol, info in PROTOCOLS.items():
|
|
if info["address"].lower() in address.lower():
|
|
return protocol
|
|
|
|
# Check name/symbol
|
|
name_lower = (name + " " + symbol).lower()
|
|
for protocol in PROTOCOLS:
|
|
if protocol.lower() in name_lower:
|
|
return protocol
|
|
|
|
return None
|
|
|
|
|
|
def detect_token_type(symbol: str, name: str = "") -> list[str]:
|
|
"""Detect token type(s) based on name/symbol."""
|
|
symbol_lower = symbol.lower()
|
|
name_lower = name.lower()
|
|
|
|
types = []
|
|
for token_type, keywords in TOKEN_TYPES.items():
|
|
if any(kw in symbol_lower or kw in name_lower for kw in keywords):
|
|
types.append(token_type)
|
|
|
|
return types
|
|
|
|
|
|
def detect_whale(address: str, balance_eth: float = 0, balance_usd: float = 0) -> dict[str, Any] | None:
|
|
"""Detect if address is a whale (high-value wallet)."""
|
|
whale_thresholds = {
|
|
"super_whale": 10000, # $10M+ USD
|
|
"high_whale": 1000, # $1M+ USD
|
|
"medium_whale": 100, # $100k+ USD
|
|
}
|
|
|
|
for level, threshold_usd in whale_thresholds.items():
|
|
if balance_usd >= threshold_usd:
|
|
return {
|
|
"is_whale": True,
|
|
"level": level,
|
|
"balance_usd": balance_usd,
|
|
"balance_eth": balance_eth,
|
|
"rank": "whale",
|
|
}
|
|
|
|
return None
|
|
|
|
|
|
def detect_high_risk(address: str, tx_count: int = 0, age_days: int = 0) -> dict[str, Any]:
|
|
"""Detect high-risk wallet characteristics."""
|
|
risk_factors = []
|
|
risk_score = 0
|
|
|
|
# Low age with high activity = suspicious
|
|
if age_days < 30 and tx_count > 100:
|
|
risk_factors.append("new_wallet_high_activity")
|
|
risk_score += 2
|
|
|
|
# High transaction count (potential bot/spam)
|
|
if tx_count > 1000:
|
|
risk_factors.append("high_frequency")
|
|
risk_score += 1
|
|
|
|
# Zero balance but active (potential scam)
|
|
if tx_count > 50:
|
|
risk_factors.append("活跃但余额为零")
|
|
risk_score += 1
|
|
|
|
return {
|
|
"is_high_risk": risk_score >= 2,
|
|
"risk_score": risk_score,
|
|
"risk_factors": risk_factors,
|
|
"risk_level": "low" if risk_score == 0 else ("medium" if risk_score <= 2 else "high"),
|
|
}
|
|
|
|
|
|
def analyze_contract_type(bytecode: str, source_code: str | None = None) -> dict[str, Any]:
|
|
"""Analyze contract type from bytecode."""
|
|
result = {
|
|
"is_contract": True,
|
|
"contract_type": "unknown",
|
|
"is_verified": source_code is not None,
|
|
"is_proxy": False,
|
|
"proxy_type": None,
|
|
"has_own_code": False,
|
|
}
|
|
|
|
# Check for proxy patterns
|
|
if "0x360894a1" in bytecode: # delegate call to
|
|
result["is_proxy"] = True
|
|
result["proxy_type"] = "delegate"
|
|
elif "0xc5318c17" in bytecode: # implementation storage
|
|
result["is_proxy"] = True
|
|
result["proxy_type"] = "transparent"
|
|
elif "0x70ca07de" in bytecode: # upgradeTo
|
|
result["is_proxy"] = True
|
|
result["proxy_type"] = "upgradeable"
|
|
|
|
# Check for specific contract types
|
|
if "transferFrom" in (source_code or ""):
|
|
result["contract_type"] = "erc20"
|
|
elif "tokenURI" in (source_code or "") or "safeMint" in (source_code or ""):
|
|
result["contract_type"] = "erc721"
|
|
elif "vote" in (source_code or ""):
|
|
result["contract_type"] = "governance"
|
|
|
|
return result
|
|
|
|
|
|
# ─── ENTITY LABELER ENGINE ────────────────────────────────────────────────
|
|
|
|
|
|
class EntityLabeler:
|
|
"""Main entity labeler engine."""
|
|
|
|
def __init__(self, clustering_engine: EntityClusteringEngine | None = None):
|
|
self.clustering = clustering_engine or EntityClusteringEngine()
|
|
self.labels: dict[str, dict[str, Any]] = {}
|
|
self.risk_scores: dict[str, float] = {}
|
|
|
|
def label_address(
|
|
self,
|
|
address: str,
|
|
chain: str = "ethereum",
|
|
name: str = "",
|
|
symbol: str = "",
|
|
balance_eth: float = 0,
|
|
balance_usd: float = 0,
|
|
tx_count: int = 0,
|
|
age_days: int = 0,
|
|
bytecode: str = "",
|
|
source_code: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Comprehensive labeling of an address."""
|
|
label = {
|
|
"address": address,
|
|
"chain": chain,
|
|
"labels": [],
|
|
"labels_detail": [],
|
|
"risk": {"score": 0, "level": "low", "factors": []},
|
|
"whale": None,
|
|
"exchange": None,
|
|
"protocol": None,
|
|
"token_type": [],
|
|
"contract": {},
|
|
"verified_at": datetime.utcnow().isoformat(),
|
|
}
|
|
|
|
# Detect exchange
|
|
exchange = detect_exchange(address, name, symbol)
|
|
if exchange:
|
|
label["exchange"] = exchange
|
|
label["labels"].append(f"exchange:{exchange}")
|
|
label["labels_detail"].append({"type": "exchange", "name": exchange})
|
|
label["risk"]["score"] += 1
|
|
|
|
# Detect protocol
|
|
protocol = detect_protocol(address, name, symbol)
|
|
if protocol:
|
|
label["protocol"] = protocol
|
|
label["labels"].append(f"protocol:{protocol}")
|
|
label["labels_detail"].append({"type": "protocol", "name": protocol})
|
|
|
|
# Detect whale
|
|
whale = detect_whale(address, balance_eth, balance_usd)
|
|
if whale:
|
|
label["whale"] = whale
|
|
label["labels"].append(f"whale:{whale['level']}")
|
|
label["labels_detail"].append({"type": "whale", "level": whale["level"]})
|
|
label["risk"]["score"] += 1 # Whales need monitoring
|
|
|
|
# Detect token type
|
|
token_types = detect_token_type(symbol, name)
|
|
if token_types:
|
|
label["token_type"] = token_types
|
|
label["labels"].extend([f"token_type:{tt}" for tt in token_types])
|
|
|
|
# Detect risk factors
|
|
risk = detect_high_risk(address, tx_count, age_days)
|
|
label["risk"]["score"] += risk["risk_score"]
|
|
label["risk"]["factors"] = risk["risk_factors"]
|
|
|
|
# Adjust risk level
|
|
label["risk"]["level"] = risk["risk_level"]
|
|
|
|
# Analyze contract
|
|
if bytecode:
|
|
contract_info = analyze_contract_type(bytecode, source_code)
|
|
label["contract"] = contract_info
|
|
if contract_info["contract_type"] != "unknown":
|
|
label["labels"].append(f"contract:{contract_info['contract_type']}")
|
|
|
|
# Store label
|
|
self.labels[address] = label
|
|
|
|
# Update risk score
|
|
self.risk_scores[address] = min(label["risk"]["score"], 10)
|
|
|
|
return label
|
|
|
|
def batch_label_addresses(
|
|
self,
|
|
addresses: list[dict[str, Any]],
|
|
) -> dict[str, dict[str, Any]]:
|
|
"""Label multiple addresses at once."""
|
|
results = {}
|
|
for addr_info in addresses:
|
|
address = addr_info.get("address", "")
|
|
result = self.label_address(
|
|
address=address,
|
|
chain=addr_info.get("chain", "ethereum"),
|
|
name=addr_info.get("name", ""),
|
|
symbol=addr_info.get("symbol", ""),
|
|
balance_eth=addr_info.get("balance_eth", 0),
|
|
balance_usd=addr_info.get("balance_usd", 0),
|
|
tx_count=addr_info.get("tx_count", 0),
|
|
age_days=addr_info.get("age_days", 0),
|
|
bytecode=addr_info.get("bytecode", ""),
|
|
source_code=addr_info.get("source_code"),
|
|
)
|
|
results[address] = result
|
|
return results
|
|
|
|
def get_label(self, address: str) -> dict[str, Any] | None:
|
|
"""Get label for an address."""
|
|
return self.labels.get(address.lower())
|
|
|
|
def get_risk_score(self, address: str) -> float:
|
|
"""Get risk score for an address."""
|
|
return self.risk_scores.get(address.lower(), 0)
|
|
|
|
def get_risk_level(self, address: str) -> str:
|
|
"""Get risk level for an address."""
|
|
score = self.get_risk_score(address)
|
|
if score >= 8:
|
|
return "critical"
|
|
elif score >= 5:
|
|
return "high"
|
|
elif score >= 2:
|
|
return "medium"
|
|
return "low"
|
|
|
|
|
|
# ─── MODULE FUNCTIONS ─────────────────────────────────────────────────────
|
|
|
|
_labeler: EntityLabeler | None = None
|
|
|
|
|
|
def get_entity_labeler() -> EntityLabeler:
|
|
"""Get the global entity labeler instance."""
|
|
global _labeler
|
|
if _labeler is None:
|
|
_labeler = EntityLabeler()
|
|
return _labeler
|
|
|
|
|
|
def label_address(
|
|
address: str,
|
|
chain: str = "ethereum",
|
|
name: str = "",
|
|
symbol: str = "",
|
|
balance_eth: float = 0,
|
|
balance_usd: float = 0,
|
|
tx_count: int = 0,
|
|
age_days: int = 0,
|
|
bytecode: str = "",
|
|
source_code: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Label an address with comprehensive metadata."""
|
|
labeler = get_entity_labeler()
|
|
return labeler.label_address(
|
|
address=address,
|
|
chain=chain,
|
|
name=name,
|
|
symbol=symbol,
|
|
balance_eth=balance_eth,
|
|
balance_usd=balance_usd,
|
|
tx_count=tx_count,
|
|
age_days=age_days,
|
|
bytecode=bytecode,
|
|
source_code=source_code,
|
|
)
|
|
|
|
|
|
def batch_label_addresses(addresses: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
"""Label multiple addresses."""
|
|
labeler = get_entity_labeler()
|
|
return labeler.batch_label_addresses(addresses)
|
|
|
|
|
|
def get_risk_score(address: str) -> float:
|
|
"""Get risk score for an address."""
|
|
labeler = get_entity_labeler()
|
|
return labeler.get_risk_score(address)
|
|
|
|
|
|
def get_risk_level(address: str) -> str:
|
|
"""Get risk level for an address."""
|
|
labeler = get_entity_labeler()
|
|
return labeler.get_risk_level(address)
|
|
|
|
|
|
# ─── API RESPONSE MODELS ──────────────────────────────────────────────────
|
|
|
|
|
|
class LabelResponse(BaseModel):
|
|
address: str
|
|
chain: str = "ethereum"
|
|
labels: list[str]
|
|
risk: dict[str, Any]
|
|
whale: dict[str, Any] | None = None
|
|
exchange: str | None = None
|
|
protocol: str | None = None
|
|
token_type: list[str] = []
|
|
contract: dict[str, Any] = {}
|
|
verified_at: str
|
|
|
|
|
|
class RiskResponse(BaseModel):
|
|
address: str
|
|
risk_score: float
|
|
risk_level: str
|
|
risk_factors: list[str]
|