""" Arkham Entity Resolver ====================== Map any blockchain address to its real-world owner with multi-source confidence scoring. Entity types detected: 1. CEX/DEX Wallets — Binance, Coinbase, Kraken, OKX, Uniswap, SushiSwap, Curve 2. DeFi Protocols — Aave, Compound, MakerDAO, Lido, EigenLayer, Ethena 3. Institutional Funds — Grayscale, Pantera, a16z, Multicoin, Paradigm 4. Smart Money / Whales — High-value wallets with repeat profitable trades 5. Bridge / Cross-chain — LayerZero, Wormhole, Stargate, Across 6. Scam / Sanctioned — Lazarus, Ronin exploiter, known bad actors 7. NFT / Gaming — BAYC, Opensea, Blur, Magic Eden wallets 8. Unknown / Uncategorized — Addresses with no known entity label Competitive advantage: - Arkham Intelligence paid ($299/mo+) vs our bundled x402 access ($0.10/tool) - Chainalysis/TRM Labs require enterprise contracts (50K+/yr) - Our multi-source hybrid approach (Arkham API + local entity DB + heuristic scoring) means we still return useful results when the API is down - Confidence scoring (0-100) lets users calibrate trust per use case - Integrates with existing RMI tooling (wallet_graph, entity_clustering, reputation_score) Usage: from app.arkham_entity import ArkhamEntityResolver resolver = ArkhamEntityResolver() result = await resolver.resolve("0xBE0eB53FC46b790099138e3d32C721856d41e865") print(f"Entity: {result.entity_name}") print(f"Confidence: {result.confidence}/100") print(f"Category: {result.category}") for label in result.labels: print(f" [{label.source}] {label.label}") CLI: python3 -m app.arkham_entity 0xBE0eB53FC46b790099138e3d32C721856d41e865 """ import asyncio import logging import os import re import sys from dataclasses import dataclass, field, asdict from datetime import datetime, timezone from enum import Enum from typing import Any try: from app.arkham_connector import ArkhamClient, ARKHAM_API_KEY except ImportError: ArkhamClient = None # type: ignore ARKHAM_API_KEY = "" try: from app.entity_registry import KNOWN_CEX_WALLETS except ImportError: KNOWN_CEX_WALLETS = {} try: from app.entity_labeler import EXCHANGES, PROTOCOLS except ImportError: EXCHANGES = {} PROTOCOLS = {} logger = logging.getLogger(__name__) # ── Constants ───────────────────────────────────────────────────────────────── ETH_ADDRESS_RE = re.compile(r"^0x[a-fA-F0-9]{40}$") SOL_ADDRESS_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$") # Known entity database — manual curation supplementing Arkham data KNOWN_ENTITIES: dict[str, dict[str, Any]] = { # Exchanges "0xBE0eB53FC46b790099138e3d32C721856d41e865": { "name": "Binance", "category": "exchange", "label": "Binance Hot Wallet 7", "tags": "cex,hot_wallet,high_volume", }, "0xF977814e90dA44bFA03b6295A0616a897441aceC": { "name": "Binance", "category": "exchange", "label": "Binance Hot Wallet 8", "tags": "cex,hot_wallet,high_volume", }, "0x28C6c06298d514Db089934071355E5743bf21d60": { "name": "Binance", "category": "exchange", "label": "Binance Hot Wallet 14", "tags": "cex,hot_wallet,high_volume", }, "0x503828976D22510aad0201ac7EC88293211D23Da": { "name": "Coinbase", "category": "exchange", "label": "Coinbase Hot Wallet 1", "tags": "cex,hot_wallet", }, "0xddfAbCdc4D8fFC17086Ea2cBcebe71504184443C": { "name": "Coinbase", "category": "exchange", "label": "Coinbase Hot Wallet 2", "tags": "cex,hot_wallet", }, "0x267be1C1D684F78cb4F6a176C4911b741E4Ffdc0": { "name": "Kraken", "category": "exchange", "label": "Kraken Hot Wallet", "tags": "cex,hot_wallet", }, "0x6cC5F688a315f3dC28A7781717a9A798a59fDA7b": { "name": "OKX", "category": "exchange", "label": "OKX Hot Wallet", "tags": "cex,hot_wallet", }, # DeFi Protocols "0x7a250d5630b4cf139281983dce37532e7d5c9196": { "name": "Uniswap V2 Router", "category": "defi", "label": "Uniswap V2 Router", "tags": "dex,router,swap", }, "0x7d2768de32b0b8013e039f31fbacf67128c9c3d8": { "name": "Aave V2 Lending Pool", "category": "defi", "label": "Aave V2 Lending Pool", "tags": "lending,liquidity,pools", }, "0xdAC17F958D2ee523a2206206994597C13D831ec7": { "name": "Tether (USDT)", "category": "token", "label": "Tether USDT Contract", "tags": "stablecoin,erc20,high_volume", }, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { "name": "Circle (USDC)", "category": "token", "label": "USD Coin Contract", "tags": "stablecoin,erc20,high_volume", }, # Known bad actors / scams "0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec": { "name": "Lazarus Group", "category": "scam", "label": "Lazarus Group (Sanctioned)", "tags": "sanctioned,north_korea,exploiter", }, "0x098B716B8Aaf21512996dC57EB0615e2383E2f96": { "name": "Ronin Bridge Exploiter", "category": "scam", "label": "Ronin Bridge Exploiter", "tags": "exploiter,hack,6.2m_eth", }, } # Normalize known entity addresses to lowercase KNOWN_ENTITIES = {k.lower(): v for k, v in KNOWN_ENTITIES.items()} # Entity category weights for confidence scoring CATEGORY_WEIGHTS: dict[str, float] = { "exchange": 0.95, "defi": 0.90, "token": 0.90, "scam": 0.85, "bridge": 0.85, "fund": 0.80, "nft": 0.70, "whale": 0.60, "unknown": 0.20, } # ── Enums ───────────────────────────────────────────────────────────────────── class EntityCategory(str, Enum): """Top-level entity classification.""" EXCHANGE = "exchange" DEFI = "defi" TOKEN = "token" BRIDGE = "bridge" FUND = "fund" WHALE = "whale" NFT = "nft" GAMING = "gaming" SCAM = "scam" SANCTIONED = "sanctioned" UNKNOWN = "unknown" class ResolverSource(str, Enum): """Source that provided the entity resolution.""" ARKHAM_API = "arkham_api" LOCAL_DB = "local_db" ENTITY_REGISTRY = "entity_registry" ENTITY_LABELER = "entity_labeler" HEURISTIC = "heuristic" # ── Dataclasses ─────────────────────────────────────────────────────────────── @dataclass class EntityLabel: """A single label for an address from a specific source.""" label: str source: ResolverSource confidence: float # 0.0 to 1.0 category: str = "unknown" @dataclass class ResolvedEntity: """Complete entity resolution result for a single address.""" address: str chain: str entity_name: str category: EntityCategory confidence: float # 0-100 labels: list[EntityLabel] = field(default_factory=list) tags: list[str] = field(default_factory=list) related_addresses: list[str] = field(default_factory=list) source: ResolverSource = ResolverSource.HEURISTIC arkham_data: dict[str, Any] | None = None resolved_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat()) raw_signals: list[dict[str, Any]] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: return { "address": self.address, "chain": self.chain, "entity_name": self.entity_name, "category": self.category.value if isinstance(self.category, EntityCategory) else self.category, "confidence": self.confidence, "labels": [asdict(l) for l in self.labels], "tags": self.tags, "source": self.source.value if isinstance(self.source, ResolverSource) else self.source, "resolved_at": self.resolved_at, "related_count": len(self.related_addresses), } @dataclass class EntityReport: """Full entity resolution report for one or more addresses.""" query_addresses: list[str] chain: str entities: list[ResolvedEntity] summary: dict[str, Any] generated_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat()) error: str | None = None def to_dict(self) -> dict[str, Any]: return { "query_addresses": self.query_addresses, "chain": self.chain, "entities": [e.to_dict() for e in self.entities], "summary": self.summary, "generated_at": self.generated_at, "error": self.error, } # ── Entity Resolver ─────────────────────────────────────────────────────────── class ArkhamEntityResolver: """ Multi-source entity resolution engine. Pipeline: 1. Local DB lookup (fastest, always available) 2. Entity Registry lookup (CEX wallets, known protocols) 3. Entity Labeler lookup (advanced protocol detection) 4. Arkham API lookup (requires API key, richest data) 5. Heuristic inference (pattern matching, fallback) Each source contributes confidence-weighted labels. Final confidence is aggregated from all sources. """ def __init__(self, cache_ttl: int = 300): self._arkham: ArkhamClient | None = None self._cache_ttl = cache_ttl async def _get_arkham(self) -> ArkhamClient | None: """Lazy-init Arkham client if API key is available.""" if self._arkham is None and ArkhamClient is not None and bool(ARKHAM_API_KEY): try: self._arkham = ArkhamClient(cache_ttl=self._cache_ttl) except Exception as e: logger.warning(f"Failed to init Arkham client: {e}") return self._arkham async def close(self): """Close the underlying Arkham HTTP client.""" if self._arkham is not None: await self._arkham.close() self._arkham = None def _detect_chain(self, address: str) -> str: """Heuristically detect chain from address format.""" if ETH_ADDRESS_RE.match(address): return "ethereum" if SOL_ADDRESS_RE.match(address): return "solana" # Could expand with more chain patterns return "unknown" def _lookup_local_db(self, address: str) -> ResolvedEntity | None: """Step 1: Check hardcoded known entity database.""" addr = address.lower() entry = KNOWN_ENTITIES.get(addr) if not entry: return None labels = [ EntityLabel( label=str(entry.get("label", entry["name"])), source=ResolverSource.LOCAL_DB, confidence=0.95, category=str(entry.get("category", "unknown")), ) ] tags_raw = entry.get("tags", "") tags = [t.strip() for t in tags_raw.split(",") if t.strip()] category = EntityCategory(entry["category"]) if entry["category"] in EntityCategory._value2member_map_ else EntityCategory.UNKNOWN # type: ignore return ResolvedEntity( address=address, chain=self._detect_chain(address), entity_name=str(entry["name"]), category=category, confidence=95.0, labels=labels, tags=tags, source=ResolverSource.LOCAL_DB, ) def _lookup_entity_registry(self, address: str) -> ResolvedEntity | None: """Step 2: Check KNOWN_CEX_WALLETS from entity_registry.""" addr = address.lower() labels: list[EntityLabel] = [] for exchange_name, wallets in KNOWN_CEX_WALLETS.items(): for w in wallets: if w.lower() == addr: labels.append(EntityLabel( label=f"{exchange_name.capitalize()} Wallet", source=ResolverSource.ENTITY_REGISTRY, confidence=0.90, category="exchange", )) if not labels: return None return ResolvedEntity( address=address, chain=self._detect_chain(address), entity_name=labels[0].label.split(" Wallet")[0], category=EntityCategory.EXCHANGE, confidence=90.0, labels=labels, tags=["cex", "exchange"], source=ResolverSource.ENTITY_REGISTRY, ) def _lookup_entity_labeler(self, address: str) -> ResolvedEntity | None: """Step 3: Check EXCHANGES and PROTOCOLS from entity_labeler.""" addr = address.lower() labels: list[EntityLabel] = [] # Check EXCHANGES dict for name, addresses in EXCHANGES.items(): for a in addresses: if isinstance(a, str) and a.lower() == addr: labels.append(EntityLabel( label=f"{name.capitalize()} (Labeler)", source=ResolverSource.ENTITY_LABELER, confidence=0.85, category="exchange", )) # Check PROTOCOLS dict for name, info in PROTOCOLS.items(): if isinstance(info, dict): proto_addr = info.get("address", "") if isinstance(proto_addr, str) and proto_addr.lower() == addr: labels.append(EntityLabel( label=f"{name.capitalize()} (Labeler)", source=ResolverSource.ENTITY_LABELER, confidence=0.90, category="defi", )) if not labels: return None top = labels[0] cat = EntityCategory.EXCHANGE if top.category == "exchange" else EntityCategory.DEFI return ResolvedEntity( address=address, chain=self._detect_chain(address), entity_name=top.label.split(" (Labeler")[0], category=cat, confidence=85.0, labels=labels, tags=[top.category], source=ResolverSource.ENTITY_LABELER, ) async def _lookup_arkham_api(self, address: str) -> ResolvedEntity | None: """Step 4: Query Arkham Intelligence API for entity data.""" arkham = await self._get_arkham() if not arkham: return None try: data = await arkham.get_entity(address) if not data or "error" in data: return None entity_name = data.get("name", data.get("entity", "")) if not entity_name: return None category_raw = data.get("category", "unknown") category = EntityCategory.UNKNOWN if category_raw in EntityCategory._value2member_map_: category = EntityCategory(category_raw) labels = [ EntityLabel( label=data.get("label", entity_name), source=ResolverSource.ARKHAM_API, confidence=min(1.0, float(data.get("confidence", 0.85))), category=category_raw, ) ] return ResolvedEntity( address=address, chain=self._detect_chain(address), entity_name=entity_name, category=category, confidence=min(100.0, float(data.get("confidence", 85)) * 100), labels=labels, tags=data.get("tags", []), source=ResolverSource.ARKHAM_API, arkham_data=data, ) except Exception as e: logger.warning(f"Arkham API lookup failed for {address}: {e}") return None def _heuristic_inference(self, address: str) -> ResolvedEntity | None: """Step 5: Fallback heuristic — pattern-based entity inference.""" addr = address.lower() signals: list[dict[str, Any]] = [] labels: list[EntityLabel] = [] tags: list[str] = [] # Check address patterns if addr.endswith("dead") or addr.endswith("0000"): signals.append({"type": "burn_pattern", "detail": "Address ends with dead/0000", "weight": 0.3}) labels.append(EntityLabel( label="Likely Burn Address", source=ResolverSource.HEURISTIC, confidence=0.6, category="burn", )) tags.append("burn") if addr[:6] == "0x0000" and len(addr) == 42: signals.append({"type": "zero_prefix", "detail": "Heavy zero-prefixed address", "weight": 0.2}) # Check if it looks like a contract (common patterns) # Most contracts start with non-random patterns if not labels: return None confidence = max(0.0, min(100.0, 20.0 + sum(s.get("weight", 0) for s in signals) * 100)) return ResolvedEntity( address=address, chain=self._detect_chain(address), entity_name="Unknown (Heuristic Match)", category=EntityCategory.UNKNOWN, confidence=round(confidence, 1), labels=labels, tags=tags, source=ResolverSource.HEURISTIC, raw_signals=signals, ) async def resolve( self, address: str, chain: str | None = None, use_arkham: bool = True, ) -> ResolvedEntity: """ Resolve a single blockchain address to an entity. Pipeline: Local DB → Entity Registry → Entity Labeler → Arkham API → Heuristic Args: address: Blockchain address to resolve. chain: Chain override (auto-detected if None). use_arkham: Whether to attempt Arkham API lookup. Returns: ResolvedEntity with best available data. """ if not chain: chain = self._detect_chain(address) # Validate address format input_entity = address.strip() if not (ETH_ADDRESS_RE.match(input_entity) or SOL_ADDRESS_RE.match(input_entity)): return ResolvedEntity( address=address, chain=chain, entity_name="Invalid Address", category=EntityCategory.UNKNOWN, confidence=0.0, labels=[], tags=["invalid"], source=ResolverSource.HEURISTIC, ) # Pipeline — stop at first positive match result = self._lookup_local_db(input_entity) if result: result.chain = chain or result.chain return result result = self._lookup_entity_registry(input_entity) if result: result.chain = chain or result.chain return result result = self._lookup_entity_labeler(input_entity) if result: result.chain = chain or result.chain return result if use_arkham: result = await self._lookup_arkham_api(input_entity) if result: result.chain = chain or result.chain return result result = self._heuristic_inference(input_entity) if result: result.chain = chain or result.chain return result # No match at all return ResolvedEntity( address=address, chain=chain, entity_name="Unknown", category=EntityCategory.UNKNOWN, confidence=0.0, labels=[], tags=[], source=ResolverSource.HEURISTIC, ) async def resolve_batch( self, addresses: list[str], chain: str | None = None, use_arkham: bool = True, ) -> list[ResolvedEntity]: """ Resolve multiple addresses in sequence. Args: addresses: List of blockchain addresses. chain: Chain override for all addresses. use_arkham: Whether to attempt Arkham API lookup. Returns: List of ResolvedEntity objects. """ results: list[ResolvedEntity] = [] for addr in addresses: result = await self.resolve(addr, chain=chain, use_arkham=use_arkham) results.append(result) return results # ── Aggregation & Reporting ──────────────────────────────────────────────────── async def analyze_addresses( addresses: list[str], chain: str | None = None, use_arkham: bool = True, ) -> EntityReport: """ Main entry point: resolve addresses and produce a structured report. Args: addresses: One or more blockchain addresses to resolve. chain: Chain override (auto-detected if None). use_arkham: Whether to query Arkham API. Returns: EntityReport with resolved entities and summary statistics. """ if not addresses: return EntityReport( query_addresses=[], chain=chain or "unknown", entities=[], summary={"total": 0, "known": 0, "unknown": 0, "avg_confidence": 0.0}, error="No addresses provided", ) resolver = ArkhamEntityResolver() try: if chain is None: chain = resolver._detect_chain(addresses[0]) entities = await resolver.resolve_batch(addresses, chain=chain, use_arkham=use_arkham) known = [e for e in entities if e.confidence >= 50.0] unknown = [e for e in entities if e.confidence < 50.0] categories: dict[str, int] = {} for e in entities: cat = e.category.value if isinstance(e.category, EntityCategory) else str(e.category) categories[cat] = categories.get(cat, 0) + 1 avg_conf = sum(e.confidence for e in entities) / len(entities) if entities else 0.0 summary = { "total": len(entities), "known": len(known), "unknown": len(unknown), "avg_confidence": round(avg_conf, 1), "categories": categories, "sources_used": list({e.source.value if isinstance(e.source, ResolverSource) else e.source for e in entities}), } return EntityReport( query_addresses=addresses, chain=chain, entities=entities, summary=summary, ) finally: await resolver.close() def format_report(report: EntityReport) -> str: """ Format an EntityReport as a human-readable string. Args: report: The report to format. Returns: Formatted string suitable for CLI or API response. """ lines = [ "┌────────────────────────────────────────────────────────────┐", "│ Arkham Entity Report │", "├────────────────────────────────────────────────────────────┤", ] if report.error: lines.append(f"│ ERROR: {report.error}") lines.append("└────────────────────────────────────────────────────────────┘") return "\n".join(lines) lines.append(f"│ Chain: {report.chain}") lines.append(f"│ Addresses: {len(report.query_addresses)}") lines.append(f"│ Generated: {report.generated_at}") lines.append("├────────────────────────────────────────────────────────────┤") for i, entity in enumerate(report.entities): lines.append(f"│ [{i + 1}] {entity.address[:42]}") lines.append(f"│ Entity: {entity.entity_name}") cat_str = entity.category.value if isinstance(entity.category, EntityCategory) else str(entity.category) lines.append(f"│ Category: {cat_str}") lines.append(f"│ Confidence: {entity.confidence:.0f}/100") src_str = entity.source.value if isinstance(entity.source, ResolverSource) else str(entity.source) lines.append(f"│ Source: {src_str}") if entity.tags: lines.append(f"│ Tags: {', '.join(entity.tags[:5])}") if entity.labels: for label in entity.labels: lines.append(f"│ Label: [{label.source.value}] {label.label} (conf: {label.confidence:.0%})") if entity.related_addresses: lines.append(f"│ Related: {len(entity.related_addresses)} addresses") if i < len(report.entities) - 1: lines.append("│" + "─" * 60 + "│") # Summary footer lines.append("├────────────────────────────────────────────────────────────┤") s = report.summary lines.append(f"│ Summary: {s.get('total', 0)} total, {s.get('known', 0)} known, {s.get('unknown', 0)} unknown") lines.append(f"│ Avg Confidence: {s.get('avg_confidence', 0):.1f}/100") cats = s.get("categories", {}) if cats: cat_str = ", ".join(f"{k}={v}" for k, v in sorted(cats.items())) lines.append(f"│ Categories: {cat_str}") lines.append("└────────────────────────────────────────────────────────────┘") return "\n".join(lines) # ── CLI Entry Point ──────────────────────────────────────────────────────────── async def main(): """CLI entry point for entity resolution.""" import argparse parser = argparse.ArgumentParser( description="Arkham Entity Resolver — map addresses to real-world entities", ) parser.add_argument("addresses", nargs="+", help="Blockchain address(es) to resolve") parser.add_argument("--chain", "-c", default=None, help="Chain override (auto-detect if omitted)") parser.add_argument("--no-arkham", action="store_true", help="Skip Arkham API lookup") parser.add_argument("--json", action="store_true", help="Output as JSON") args = parser.parse_args() report = await analyze_addresses( addresses=args.addresses, chain=args.chain, use_arkham=not args.no_arkham, ) if args.json: import json print(json.dumps(report.to_dict(), indent=2)) else: print(format_report(report)) return 0 if not report.error else 1 if __name__ == "__main__": exit(asyncio.run(main()))