""" Unified Label Service - Merges wallet_label_loader + address_labeler into one call. =============================================================================== Sources: Static (loaded at startup into Redis): 1. crypto-wallet-address-labels (105K+ Solana, 16K BSC/ETH, 15K phishing) 2. OFAC crypto-sanctions-list 3. Etherscan malicious labels CSV 4. Internal wallet_database.json (Omega Forensic) Live (API calls on demand): 5. Etherscan API - public address labels 6. RolodETH - public GitHub label database 7. walletLabels.xyz - public address label API 8. bluepages.fyi - public address label API Built-in: 9. CEX hot wallet labels from chain_registry Returns a unified label report with deduplication and confidence scoring. """ import json import logging import os from typing import Any import httpx from app.chain_registry import is_evm, is_solana logger = logging.getLogger("wallet_memory.labels") # ── RolodETH cache ────────────────────────────────────────────── _ROLODETH_CACHE: dict[str, list[dict]] | None = None _ROLODETH_LAST_FETCH: float = 0.0 _ROLODETH_TTL = 3600 # 1 hour class LabelService: """Unified label resolution across static + live + built-in sources.""" def __init__(self): self._http = httpx.AsyncClient(timeout=15.0) self._etherscan_key = os.getenv("ETHERSCAN_API_KEY", "") self._builtin_cex: dict[str, str] = {} self._load_builtin_cex() def _load_builtin_cex(self): """Load CEX hot wallet labels from chain_registry.""" try: from app.chain_registry import CEX_HOT_WALLETS for _chain, wallets in CEX_HOT_WALLETS.items(): for addr, label in wallets.items(): self._builtin_cex[addr.lower()] = label except Exception: # noqa: S110 pass async def resolve(self, address: str, chain: str) -> dict[str, Any] | None: """ Resolve an address across all label sources. Returns: { "address": str, "chain": str, "labels": [{"source": str, "label": str, "category": str, "confidence": float}], "primary_label": str, "primary_category": str, "is_exchange": bool, "is_mev_bot": bool, "is_known_scam": bool, "is_defi_protocol": bool, "is_bridge": bool, "is_sanctioned": bool, "risk_contribution": float, } """ addr = address.lower().strip() all_labels: list[dict[str, Any]] = [] flags = { "is_exchange": False, "is_mev_bot": False, "is_known_scam": False, "is_defi_protocol": False, "is_bridge": False, "is_sanctioned": False, } # ── Source 1: Static Redis labels (wallet_label_loader) ─── try: from .storage import get_storage storage = get_storage() redis = await storage._get_redis() if hasattr(storage, "_get_redis") else None # Try the direct redis method if not redis: await storage._ensure_connections() redis = storage._redis if redis: # Check chain-specific label label_json = await redis.get(f"rmi:label:{chain}:{addr}") if not label_json and not is_solana(chain): # Try ethereum as fallback for EVM chains label_json = await redis.get(f"rmi:label:ethereum:{addr}") if label_json: label_data = json.loads(label_json) all_labels.append( { "source": f"static_{label_data.get('chain', chain)}", "label": label_data.get( "address_name", label_data.get( "dapp_name", label_data.get( "protocol_name", label_data.get("label_type", "") ), ), ), "category": label_data.get( "label_type", label_data.get("label_subtype", "") ), "confidence": 0.95, # Static labels are high confidence } ) # Check OFAC ofac_json = await redis.get(f"rmi:label:ofac:{addr}") if ofac_json: ofac_data = json.loads(ofac_json) flags["is_sanctioned"] = True flags["is_known_scam"] = True all_labels.append( { "source": "ofac", "label": ofac_data.get("name", "OFAC Sanctioned"), "category": "sanctioned", "confidence": 1.0, } ) # Check wallet database (Omega Forensic) wallet_json = await redis.get(f"rmi:wallet:labeled:{addr}") if wallet_json: wallet_data = json.loads(wallet_json) cat = wallet_data.get("category", "") if cat in ("rug_puller", "scammer", "hacker", "phisher"): flags["is_known_scam"] = True all_labels.append( { "source": "omega_forensic", "label": wallet_data.get( "display_name", wallet_data.get("category", "") ), "category": cat, "confidence": 0.9, } ) except Exception as e: logger.debug(f"Static label lookup failed for {addr}: {e}") # ── Source 2: Built-in CEX labels ───────────────────────── if addr in self._builtin_cex: label = self._builtin_cex[addr] flags["is_exchange"] = True all_labels.append( { "source": "builtin_cex", "label": label, "category": "exchange", "confidence": 1.0, } ) # ── Source 3: Etherscan API (live, EVM only) ────────────── if is_evm(chain) and self._etherscan_key: try: etherscan_labels = await self._query_etherscan(addr, chain) all_labels.extend(etherscan_labels) for lbl in etherscan_labels: cat = lbl.get("category", "").lower() if cat == "exchange": flags["is_exchange"] = True elif cat == "mev": flags["is_mev_bot"] = True elif cat in ("phishing", "scam", "hack"): flags["is_known_scam"] = True elif cat == "defi": flags["is_defi_protocol"] = True elif cat == "bridge": flags["is_bridge"] = True except Exception as e: logger.debug(f"Etherscan label failed for {addr}: {e}") # ── Source 4: RolodETH (live, Ethereum, cached) ─────────── if chain in ("ethereum", "eth"): try: rolodeth_labels = await self._query_rolodeth(addr) all_labels.extend(rolodeth_labels) except Exception as e: logger.debug(f"RolodETH failed for {addr}: {e}") # ── Source 5: walletLabels.xyz (live) ──────────────────── try: wl_labels = await self._query_walletlabels_xyz(addr) all_labels.extend(wl_labels) except Exception as e: logger.debug(f"walletLabels.xyz failed for {addr}: {e}") # ── Source 6: bluepages.fyi (live) ──────────────────────── try: bp_labels = await self._query_bluepages(addr) all_labels.extend(bp_labels) except Exception as e: logger.debug(f"bluepages.fyi failed for {addr}: {e}") # ── Deduplicate and rank ───────────────────────────────── deduped = self._deduplicate(all_labels) primary = self._pick_primary(deduped, flags) # Calculate risk contribution from labels risk_contrib = self._label_risk_contribution(deduped, flags) return { "address": addr, "chain": chain, "labels": deduped, "primary_label": primary.get("label", ""), "primary_category": primary.get("category", ""), **flags, "risk_contribution": risk_contrib, } # ── Live API methods ──────────────────────────────────────── async def _query_etherscan(self, address: str, chain: str) -> list[dict]: """Query Etherscan account API for address labels.""" from app.domains.scanners.dev_reputation import ETHERSCAN_NETWORKS network = ETHERSCAN_NETWORKS.get(chain) if not network: return [] url = network["url"] key = network["key"] or self._etherscan_key if not key: return [] try: resp = await self._http.get( url, params={ "module": "account", "action": "getaddressinfo", "address": address, "apikey": key, }, ) if resp.status_code != 200: return [] data = resp.json() if data.get("status") != "1": return [] result = data.get("result", {}) labels = [] if isinstance(result, dict): tag = result.get("tag", "") name = result.get("name", "") if name: labels.append( { "source": "etherscan", "label": name, "category": self._infer_category(tag or name), "confidence": 0.85, } ) return labels except Exception: return [] async def _query_rolodeth(self, address: str) -> list[dict]: """Query RolodETH GitHub label database.""" global _ROLODETH_CACHE, _ROLODETH_LAST_FETCH import time now = time.time() if _ROLODETH_CACHE is None or (now - _ROLODETH_LAST_FETCH) > _ROLODETH_TTL: try: resp = await self._http.get( "https://raw.githubusercontent.com/RoleTiker/rolodETH/main/rolodETH.json" ) if resp.status_code == 200: _ROLODETH_CACHE = resp.json() _ROLODETH_LAST_FETCH = now except Exception: # noqa: S110 pass if not _ROLODETH_CACHE: return [] labels = [] addr_lower = address.lower() for addr_key, entries in _ROLODETH_CACHE.items(): if addr_lower == addr_key.lower(): for entry in entries: labels.append( { "source": "rolodeth", "label": entry.get("name", entry.get("label", "")), "category": self._infer_category(entry.get("tag", "")), "confidence": 0.80, } ) break return labels async def _query_walletlabels_xyz(self, address: str) -> list[dict]: """Query walletLabels.xyz API.""" try: resp = await self._http.get(f"https://api.walletlabels.xyz/v1/address/{address}") if resp.status_code != 200: return [] data = resp.json() labels = [] for item in data.get("labels", data.get("result", [])): if isinstance(item, dict): labels.append( { "source": "walletlabels.xyz", "label": item.get("label", item.get("name", "")), "category": self._infer_category(item.get("category", "")), "confidence": 0.75, } ) return labels except Exception: return [] async def _query_bluepages(self, address: str) -> list[dict]: """Query bluepages.fyi API.""" try: resp = await self._http.get(f"https://bluepages.fyi/api/v1/address/{address}") if resp.status_code != 200: return [] data = resp.json() labels = [] entry = data.get("label", data.get("name", "")) if entry: labels.append( { "source": "bluepages.fyi", "label": entry, "category": self._infer_category(data.get("category", "")), "confidence": 0.70, } ) return labels except Exception: return [] # ── Helpers ────────────────────────────────────────────────── def _infer_category(self, text: str) -> str: """Infer label category from text.""" if not text: return "unknown" t = text.lower() if any(w in t for w in ("exchange", "binance", "coinbase", "kraken", "okx", "bitfinex")): return "exchange" if any(w in t for w in ("mev", "bot", "flashbot")): return "mev" if any(w in t for w in ("phish", "scam", "hack", "exploit", "rug", "drainer")): return "scam" if any(w in t for w in ("defi", "aave", "uniswap", "curve", "compound", "lido")): return "defi" if any(w in t for w in ("bridge", "hop", "across", "synapse", "stargate")): return "bridge" if any(w in t for w in ("deployer", "creator", "factory")): return "deployer" if any(w in t for w in ("sanction", "ofac")): return "sanctioned" return "wallet" def _deduplicate(self, labels: list[dict]) -> list[dict]: """Remove duplicate labels from different sources.""" seen = set() deduped = [] for lbl in labels: key = f"{lbl.get('label', '').lower()}:{lbl.get('category', '').lower()}" if key not in seen: seen.add(key) deduped.append(lbl) return deduped def _pick_primary(self, labels: list[dict], flags: dict) -> dict: """Pick the most informative primary label.""" if not labels: return {"label": "", "category": "unknown"} # Priority: sanctioned > scam > exchange > defi > bridge > mev > wallet priority = { "sanctioned": 0, "scam": 1, "exchange": 2, "defi": 3, "bridge": 4, "mev": 5, "deployer": 6, "wallet": 7, "unknown": 8, } sorted_labels = sorted( labels, key=lambda line: ( priority.get(line.get("category", "unknown"), 8), -line.get("confidence", 0), ), ) return sorted_labels[0] if sorted_labels else {"label": "", "category": "unknown"} def _label_risk_contribution(self, labels: list[dict], flags: dict) -> float: """Calculate risk score contribution from labels (0-40 of 100).""" score = 0.0 # Flags contribute directly if flags.get("is_sanctioned"): score += 40 elif flags.get("is_known_scam"): score += 30 if flags.get("is_exchange"): score -= 5 # Exchanges are generally safe if flags.get("is_defi_protocol"): score -= 3 # Known DeFi is slightly positive # Label categories contribute for lbl in labels: cat = lbl.get("category", "").lower() if cat == "sanctioned": score = max(score, 40) elif cat == "scam": score += 15 elif cat == "mev": score += 5 return max(0, min(40, score)) # ── Global singleton ──────────────────────────────────────────── _label_service: LabelService | None = None def get_label_service() -> LabelService: global _label_service if _label_service is None: _label_service = LabelService() return _label_service