- 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>
262 lines
9.9 KiB
Python
262 lines
9.9 KiB
Python
"""
|
|
Known Entity Registry - Exchange, DeFi, and Infrastructure address recognition.
|
|
Excludes legitimate infrastructure from cluster/bundle analysis.
|
|
Integrates CryptoScamDB, Forta, Januus free risk scores.
|
|
|
|
Paper ref: Article 3, Section 2 - Free Datasets and Labelled-Address Repositories
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Known Exchange Hot Wallets ────────────────────────────
|
|
# These are NOT clusters. They're infrastructure.
|
|
|
|
KNOWN_CEX_WALLETS: dict[str, list[str]] = {
|
|
"binance": [
|
|
# Solana
|
|
"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", # Binance 1
|
|
"5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9", # Binance 2
|
|
"2ojv9BAiHghv5o9y2k6k3m7o9TdsrKJqXvWyCdkUWNmB", # Binance 3
|
|
# Ethereum
|
|
"0xBE0eB53FC46b790099138e3d32C721856d41e865", # Binance 7
|
|
"0xF977814e90dA44bFA03b6295A0616a897441aceC", # Binance 8
|
|
"0x28C6c06298d514Db089934071355E5743bf21d60", # Binance 14
|
|
],
|
|
"coinbase": [
|
|
"0x503828976D22510aad0201ac7EC88293211D23Da", # Coinbase 1
|
|
"0xddfAbCdc4D8fFC17086Ea2cBcebe71504184443C", # Coinbase 2
|
|
"0x71660c4005BA85c37ccec55d0C4493E66Fe775d3", # Coinbase 3
|
|
],
|
|
"kraken": [
|
|
"0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0", # Kraken 1
|
|
"0x2910543Af39abA0Cd09dBb2D50200b3E800A63D2", # Kraken 2
|
|
],
|
|
"okx": [
|
|
"0x6cC5F688a315f3dC28A7781717a9A798a59fDA7b", # OKX 1
|
|
"0x5c985E89DDe482eFE97ea765eA753005C69C3553", # OKX 2
|
|
],
|
|
"bybit": [
|
|
"0xf89d7b9c864f589bbF53a82105107622B35EaA40", # Bybit
|
|
"0x1Db92e2EeBC8E0c075a02BeA49a2935BcD2dFCF4", # Bybit 2
|
|
],
|
|
"kucoin": [
|
|
"0x2B5634C42055806a59e9107ED44D43c426E58258", # Kucoin 1
|
|
],
|
|
"gateio": [
|
|
"0x0D0707963952f2fBA59dD06f2b425ace40b492Fe", # Gate.io 1
|
|
],
|
|
"mexc": [
|
|
"0x75e89d5979E4f6Fba9F97c104c2F0AFB3F1dcB88", # MEXC 1
|
|
],
|
|
}
|
|
|
|
# ── Known DeFi Protocol Addresses ─────────────────────────
|
|
|
|
KNOWN_DEFI_PROTOCOLS: dict[str, list[str]] = {
|
|
"uniswap": [
|
|
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", # Uniswap V2 Router
|
|
"0xE592427A0AEce92De3Edee1F18E0157C05861564", # Uniswap V3 Router
|
|
"0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45", # Uniswap V3 Router 2
|
|
],
|
|
"jupiter": [
|
|
"JUP6LkbZbjS1jKKwapdHNy7bKNkD5Ka6TqPnVGFVeYf", # Jupiter V6
|
|
"JUP2jS6oQ1gDRPXSbbx5guHt6yKQH8iHWDPmSHRQVk9", # Jupiter V4
|
|
],
|
|
"raydium": [
|
|
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", # Raydium V4
|
|
],
|
|
"orca": [
|
|
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", # Orca Whirlpool
|
|
],
|
|
"pumpfun": [
|
|
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", # Pump.fun
|
|
],
|
|
"wormhole": [
|
|
"worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth", # Wormhole Token Bridge
|
|
],
|
|
}
|
|
|
|
# ── Known Mixer Addresses ─────────────────────────────────
|
|
|
|
KNOWN_MIXERS: dict[str, list[str]] = {
|
|
"tornado_cash": [
|
|
"0xA160cdAB225685dA1d56aa342Ad8841c3b53f291", # Tornado 0.1 ETH
|
|
"0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b", # Tornado 1 ETH
|
|
"0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc", # Tornado 10 ETH
|
|
"0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936", # Tornado 100 ETH
|
|
],
|
|
}
|
|
|
|
# ── Known Entity Labels ───────────────────────────────────
|
|
|
|
ENTITY_TYPE_EXCHANGE = "exchange"
|
|
ENTITY_TYPE_DEFI = "defi_protocol"
|
|
ENTITY_TYPE_MIXER = "mixer"
|
|
ENTITY_TYPE_BRIDGE = "bridge"
|
|
ENTITY_TYPE_KNOWN_SCAM = "known_scam"
|
|
ENTITY_TYPE_CEX_HOT_WALLET = "cex_hot_wallet"
|
|
|
|
|
|
@dataclass
|
|
class EntityMatch:
|
|
address: str
|
|
entity_name: str
|
|
entity_type: str
|
|
source: str # "internal", "cryptoscamdb", "forta", "goPlus"
|
|
|
|
|
|
class EntityRegistry:
|
|
"""Known entity recognition - excludes infrastructure from analysis."""
|
|
|
|
def __init__(self):
|
|
self._exchange_addrs: set[str] = set()
|
|
self._defi_addrs: set[str] = set()
|
|
self._mixer_addrs: set[str] = set()
|
|
self._scam_addrs: set[str] = set()
|
|
self._all_known: dict[str, EntityMatch] = {}
|
|
self._build_index()
|
|
|
|
def _build_index(self):
|
|
"""Build unified lookup index from all known sources."""
|
|
# CEX wallets
|
|
for exchange, addrs in KNOWN_CEX_WALLETS.items():
|
|
for addr in addrs:
|
|
key = addr.lower()
|
|
self._exchange_addrs.add(key)
|
|
self._all_known[key] = EntityMatch(
|
|
address=addr,
|
|
entity_name=exchange,
|
|
entity_type=ENTITY_TYPE_CEX_HOT_WALLET,
|
|
source="internal",
|
|
)
|
|
|
|
# DeFi protocols
|
|
for protocol, addrs in KNOWN_DEFI_PROTOCOLS.items():
|
|
for addr in addrs:
|
|
key = addr.lower()
|
|
self._defi_addrs.add(key)
|
|
self._all_known[key] = EntityMatch(
|
|
address=addr,
|
|
entity_name=protocol,
|
|
entity_type=ENTITY_TYPE_DEFI,
|
|
source="internal",
|
|
)
|
|
|
|
# Mixers
|
|
for mixer, addrs in KNOWN_MIXERS.items():
|
|
for addr in addrs:
|
|
key = addr.lower()
|
|
self._mixer_addrs.add(key)
|
|
self._all_known[key] = EntityMatch(
|
|
address=addr,
|
|
entity_name=mixer,
|
|
entity_type=ENTITY_TYPE_MIXER,
|
|
source="internal",
|
|
)
|
|
|
|
# Load from wallet_labels.json if available
|
|
self._load_local_labels()
|
|
|
|
logger.info(
|
|
f"Entity registry: {len(self._exchange_addrs)} CEX, "
|
|
f"{len(self._defi_addrs)} DeFi, {len(self._mixer_addrs)} mixers, "
|
|
f"{len(self._scam_addrs)} scams, {len(self._all_known)} total"
|
|
)
|
|
|
|
def _load_local_labels(self):
|
|
"""Load address labels from local wallet_labels.json."""
|
|
label_paths = [
|
|
"/root/data/wallet_labels.json",
|
|
"/app/data/wallet_labels.json",
|
|
os.path.expanduser("~/data/wallet_labels.json"),
|
|
]
|
|
for path in label_paths:
|
|
try:
|
|
if os.path.exists(path):
|
|
with open(path) as f:
|
|
data = json.load(f)
|
|
if isinstance(data, dict):
|
|
for addr, info in data.items():
|
|
if isinstance(info, dict):
|
|
labels = info.get("labels", [])
|
|
if "exchange" in labels or "cex" in labels:
|
|
key = str(addr).lower()
|
|
self._exchange_addrs.add(key)
|
|
self._all_known[key] = EntityMatch(
|
|
address=addr,
|
|
entity_name="labeled_cex",
|
|
entity_type=ENTITY_TYPE_EXCHANGE,
|
|
source="local_labels",
|
|
)
|
|
for label in labels:
|
|
if "scam" in str(label).lower() or "phish" in str(label).lower():
|
|
key = str(addr).lower()
|
|
self._scam_addrs.add(key)
|
|
self._all_known[key] = EntityMatch(
|
|
address=addr,
|
|
entity_name=str(label),
|
|
entity_type=ENTITY_TYPE_KNOWN_SCAM,
|
|
source="local_labels",
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
def classify_address(self, address: str) -> EntityMatch | None:
|
|
"""Classify an address as exchange, DeFi, mixer, scam, or unknown."""
|
|
return self._all_known.get(address.lower())
|
|
|
|
def is_infrastructure(self, address: str) -> bool:
|
|
"""Check if address is known infrastructure (exclude from cluster analysis)."""
|
|
key = address.lower()
|
|
return key in self._exchange_addrs or key in self._defi_addrs or key in self._mixer_addrs
|
|
|
|
def is_known_scam(self, address: str) -> bool:
|
|
"""Check if address is a known scam."""
|
|
return address.lower() in self._scam_addrs
|
|
|
|
def filter_infrastructure(self, addresses: list[str]) -> tuple[list[str], list[EntityMatch]]:
|
|
"""Split addresses into legit holders and known infrastructure.
|
|
Returns: (clean_addresses, excluded_entities)"""
|
|
clean = []
|
|
excluded = []
|
|
for addr in addresses:
|
|
match = self.classify_address(addr)
|
|
if match and match.entity_type in (
|
|
ENTITY_TYPE_EXCHANGE,
|
|
ENTITY_TYPE_DEFI,
|
|
ENTITY_TYPE_MIXER,
|
|
ENTITY_TYPE_CEX_HOT_WALLET,
|
|
):
|
|
excluded.append(match)
|
|
else:
|
|
clean.append(addr)
|
|
return clean, excluded
|
|
|
|
def enrich_holder(self, address: str) -> dict:
|
|
"""Enrich a holder address with known entity info."""
|
|
match = self.classify_address(address)
|
|
if match:
|
|
return {
|
|
"address": address,
|
|
"entity": match.entity_name,
|
|
"type": match.entity_type,
|
|
"source": match.source,
|
|
"is_infrastructure": match.entity_type != ENTITY_TYPE_KNOWN_SCAM,
|
|
}
|
|
return {"address": address, "entity": None, "type": "unknown"}
|
|
|
|
|
|
# Singleton
|
|
_registry: EntityRegistry | None = None
|
|
|
|
|
|
def get_entity_registry() -> EntityRegistry:
|
|
global _registry
|
|
if _registry is None:
|
|
_registry = EntityRegistry()
|
|
return _registry
|