"""Label data models for the federated label API. A Label represents one piece of information about an address from one source. Multiple labels from different sources for the same address get merged into a unified view. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import UTC, datetime from enum import StrEnum from typing import Any class LabelSource(StrEnum): """Which source a label came from. Used for provenance + dedup.""" ETH_LABELS_DB = "eth-labels-db" # local SQLite, 115K EVM labels ETHERSCAN_CSV = "etherscan-combined" # local CSV, 51K EVM labels ETHEREUM_LABELS_CSV = "ethereum-labels" # local CSV, 51K EVM labels SOLANA_LABELS_CSV = "solana-labels" # local CSV, 103K SOL labels CLICKHOUSE = "clickhouse-wallet-memory" # live CH wallet_labels table METASLEUTH = "metasleuth" # BlockSec AML API OPEN_LABELS = "open-labels-initiative" # OLI REST API (TBD) CHAINBASE = "chainbase" # Chainbase API (TBD, needs key) INTERNAL = "internal" # our own growing set MBAL = "mbal" # Multi-blockchain Anti-Laundering source @dataclass class Label: """A single label for an address. Multiple labels can exist for the same (address, chain, label_type) from different sources. Dedup logic keeps the highest-confidence one. """ address: str chain: str # "ethereum" | "solana" | "base" | "bitcoin" | etc. label_type: str # "entity" | "tag" | "category" | "risk" | "custom" label_value: str # "Binance" | "Exchange" | "Sanctioned" | etc. source: LabelSource confidence: float = 0.5 # 0.0-1.0 verified_at: datetime = field(default_factory=lambda: datetime.now(UTC)) attributes: dict[str, Any] = field(default_factory=dict) # Optional entity info (for MetaSleuth etc.) entity: str | None = None entity_category: str | None = None def to_dict(self) -> dict[str, Any]: """Serialize for API responses.""" return { "address": self.address, "chain": self.chain, "label_type": self.label_type, "label_value": self.label_value, "source": self.source.value, "confidence": self.confidence, "verified_at": self.verified_at.isoformat(), "attributes": self.attributes, "entity": self.entity, "entity_category": self.entity_category, } @classmethod def dedupe_key(cls, address: str, chain: str, label_type: str, label_value: str) -> tuple: """Generate dedup key. Same key = same label from different sources.""" return (address.lower(), chain.lower(), label_type.lower(), label_value.lower()) @classmethod def merge(cls, labels: list[Label]) -> Label: """Merge multiple labels with same dedup_key into one. Keeps highest confidence, unions attributes, aggregates sources. """ if not labels: raise ValueError("Cannot merge empty label list") # Sort by confidence desc, take best as base sorted_labels = sorted(labels, key=lambda l: -l.confidence) base = sorted_labels[0] sources = list({l.source for l in labels}) merged_attrs = dict(base.attributes) for l in labels[1:]: merged_attrs.update(l.attributes) # Update verified_at to most recent verified_at = max(l.verified_at for l in labels) return cls( address=base.address, chain=base.chain, label_type=base.label_type, label_value=base.label_value, source=base.source, # keep highest-confidence source as primary confidence=base.confidence, verified_at=verified_at, attributes={**merged_attrs, "sources_count": len(sources), "all_sources": [s.value for s in sources]}, entity=base.entity, entity_category=base.entity_category, )