RMI Platform
nightly-build: arkham_entity β multi-source entity resolver with confidence scoring
9725810 | """ | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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" | |
| 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), | |
| } | |
| 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())) | |