From 0c4d2bebea1f28de3aecde312d57a6faff4784b8 Mon Sep 17 00:00:00 2001 From: Patrick Selamy Date: Sun, 4 Jan 2026 16:43:03 -0500 Subject: [PATCH] feat: add known entity registry for CEX and bridge detection (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add EntityType enum for classifying blockchain entities - Add entity_data.py with Polygon CEX hot wallets, bridges, DEX contracts - Add EntityRegistry class with classify/is_terminal methods - Support custom entity additions and overrides - Include 49 comprehensive unit tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../profiler/__init__.py | 9 + .../profiler/entities.py | 279 +++++++++++++ .../profiler/entity_data.py | 154 ++++++++ tests/profiler/test_entities.py | 369 ++++++++++++++++++ 4 files changed, 811 insertions(+) create mode 100644 src/polymarket_insider_tracker/profiler/entities.py create mode 100644 src/polymarket_insider_tracker/profiler/entity_data.py create mode 100644 tests/profiler/test_entities.py diff --git a/src/polymarket_insider_tracker/profiler/__init__.py b/src/polymarket_insider_tracker/profiler/__init__.py index 38a099f..353c04b 100644 --- a/src/polymarket_insider_tracker/profiler/__init__.py +++ b/src/polymarket_insider_tracker/profiler/__init__.py @@ -9,6 +9,12 @@ from polymarket_insider_tracker.profiler.chain import ( RateLimitError, RPCError, ) +from polymarket_insider_tracker.profiler.entities import ( + EntityRegistry, +) +from polymarket_insider_tracker.profiler.entity_data import ( + EntityType, +) from polymarket_insider_tracker.profiler.models import ( Transaction, WalletInfo, @@ -18,6 +24,9 @@ from polymarket_insider_tracker.profiler.models import ( __all__ = [ # Analyzer "WalletAnalyzer", + # Entity Registry + "EntityRegistry", + "EntityType", # Polygon Client "PolygonClient", "PolygonClientError", diff --git a/src/polymarket_insider_tracker/profiler/entities.py b/src/polymarket_insider_tracker/profiler/entities.py new file mode 100644 index 0000000..a393945 --- /dev/null +++ b/src/polymarket_insider_tracker/profiler/entities.py @@ -0,0 +1,279 @@ +"""Known entity registry for blockchain address classification. + +This module provides the EntityRegistry class for classifying blockchain +addresses as known entities (CEX hot wallets, bridges, DEX contracts, etc.) +to support funding chain analysis and suspiciousness scoring. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from polymarket_insider_tracker.profiler.entity_data import ( + EntityType, + get_all_known_entities, +) + +if TYPE_CHECKING: + pass + +logger = logging.getLogger(__name__) + + +class EntityRegistry: + """Registry of known blockchain entities for address classification. + + The registry contains mappings from blockchain addresses to known entity + types (CEX, bridges, DEX, etc.). This is used to: + - Terminate funding chain traces at known entities + - Classify funding sources for suspiciousness scoring + - Identify retail vs sophisticated wallet patterns + + Attributes: + _entities: Internal mapping of address to entity type. + """ + + # Entity types that should terminate funding chain traces + TERMINAL_ENTITY_TYPES = frozenset( + [ + EntityType.CEX_BINANCE, + EntityType.CEX_COINBASE, + EntityType.CEX_KRAKEN, + EntityType.CEX_OKX, + EntityType.CEX_KUCOIN, + EntityType.CEX_BYBIT, + EntityType.CEX_CRYPTO_COM, + EntityType.CEX_OTHER, + EntityType.BRIDGE_POLYGON, + EntityType.BRIDGE_MULTICHAIN, + EntityType.BRIDGE_STARGATE, + EntityType.BRIDGE_HOP, + EntityType.BRIDGE_OTHER, + ] + ) + + # Entity types that indicate CEX origin + CEX_ENTITY_TYPES = frozenset( + [ + EntityType.CEX_BINANCE, + EntityType.CEX_COINBASE, + EntityType.CEX_KRAKEN, + EntityType.CEX_OKX, + EntityType.CEX_KUCOIN, + EntityType.CEX_BYBIT, + EntityType.CEX_CRYPTO_COM, + EntityType.CEX_OTHER, + ] + ) + + # Entity types that indicate bridge origin + BRIDGE_ENTITY_TYPES = frozenset( + [ + EntityType.BRIDGE_POLYGON, + EntityType.BRIDGE_MULTICHAIN, + EntityType.BRIDGE_STARGATE, + EntityType.BRIDGE_HOP, + EntityType.BRIDGE_OTHER, + ] + ) + + # Entity types for DEX contracts + DEX_ENTITY_TYPES = frozenset( + [ + EntityType.DEX_UNISWAP, + EntityType.DEX_SUSHISWAP, + EntityType.DEX_QUICKSWAP, + EntityType.DEX_1INCH, + EntityType.DEX_OTHER, + ] + ) + + def __init__( + self, + custom_entities: dict[str, EntityType] | None = None, + *, + include_defaults: bool = True, + ) -> None: + """Initialize the entity registry. + + Args: + custom_entities: Additional custom entity mappings to include. + include_defaults: Whether to include default known entities. + """ + self._entities: dict[str, EntityType] = {} + + if include_defaults: + self._entities.update(get_all_known_entities()) + + if custom_entities: + # Add custom entities (normalized to lowercase) + for address, entity_type in custom_entities.items(): + self._entities[address.lower()] = entity_type + + logger.info(f"EntityRegistry initialized with {len(self._entities)} known entities") + + def classify(self, address: str) -> EntityType: + """Classify an address by its entity type. + + Args: + address: The blockchain address to classify. + + Returns: + The EntityType for the address, or UNKNOWN if not in registry. + """ + return self._entities.get(address.lower(), EntityType.UNKNOWN) + + def is_known_entity(self, address: str) -> bool: + """Check if an address is a known entity. + + Args: + address: The blockchain address to check. + + Returns: + True if the address is in the registry, False otherwise. + """ + return address.lower() in self._entities + + def is_cex(self, address: str) -> bool: + """Check if an address is a known CEX hot wallet. + + Args: + address: The blockchain address to check. + + Returns: + True if the address is a CEX hot wallet. + """ + return self.classify(address) in self.CEX_ENTITY_TYPES + + def is_bridge(self, address: str) -> bool: + """Check if an address is a known bridge contract. + + Args: + address: The blockchain address to check. + + Returns: + True if the address is a bridge contract. + """ + return self.classify(address) in self.BRIDGE_ENTITY_TYPES + + def is_dex(self, address: str) -> bool: + """Check if an address is a known DEX contract. + + Args: + address: The blockchain address to check. + + Returns: + True if the address is a DEX contract. + """ + return self.classify(address) in self.DEX_ENTITY_TYPES + + def is_terminal(self, address: str) -> bool: + """Check if an address should terminate a funding chain trace. + + Terminal entities are those where tracing further back provides + diminishing returns (CEX, bridges). These indicate the practical + origin of funds from the perspective of on-chain analysis. + + Args: + address: The blockchain address to check. + + Returns: + True if the address should terminate a funding trace. + """ + return self.classify(address) in self.TERMINAL_ENTITY_TYPES + + def is_contract(self, address: str) -> bool: + """Check if an address is a known smart contract. + + This includes DEX routers, token contracts, and DeFi protocols. + + Args: + address: The blockchain address to check. + + Returns: + True if the address is a known smart contract. + """ + entity_type = self.classify(address) + contract_types = ( + self.DEX_ENTITY_TYPES + | { + EntityType.TOKEN_USDC, + EntityType.TOKEN_USDT, + EntityType.TOKEN_WETH, + EntityType.TOKEN_WMATIC, + EntityType.DEFI_AAVE, + EntityType.DEFI_COMPOUND, + EntityType.DEFI_OTHER, + EntityType.CONTRACT, + } + ) + return entity_type in contract_types + + def get_entity_category(self, address: str) -> str: + """Get a human-readable category for an address. + + Args: + address: The blockchain address to categorize. + + Returns: + Category string: "cex", "bridge", "dex", "token", "defi", "contract", or "unknown". + """ + entity_type = self.classify(address) + + if entity_type in self.CEX_ENTITY_TYPES: + return "cex" + if entity_type in self.BRIDGE_ENTITY_TYPES: + return "bridge" + if entity_type in self.DEX_ENTITY_TYPES: + return "dex" + if entity_type in { + EntityType.TOKEN_USDC, + EntityType.TOKEN_USDT, + EntityType.TOKEN_WETH, + EntityType.TOKEN_WMATIC, + }: + return "token" + if entity_type in { + EntityType.DEFI_AAVE, + EntityType.DEFI_COMPOUND, + EntityType.DEFI_OTHER, + }: + return "defi" + if entity_type == EntityType.CONTRACT: + return "contract" + + return "unknown" + + def add_entity(self, address: str, entity_type: EntityType) -> None: + """Add or update an entity in the registry. + + Args: + address: The blockchain address. + entity_type: The entity type to assign. + """ + self._entities[address.lower()] = entity_type + logger.debug(f"Added entity: {address} -> {entity_type.value}") + + def remove_entity(self, address: str) -> bool: + """Remove an entity from the registry. + + Args: + address: The blockchain address to remove. + + Returns: + True if the entity was removed, False if not found. + """ + normalized = address.lower() + if normalized in self._entities: + del self._entities[normalized] + return True + return False + + def __len__(self) -> int: + """Return the number of entities in the registry.""" + return len(self._entities) + + def __contains__(self, address: str) -> bool: + """Check if an address is in the registry.""" + return self.is_known_entity(address) diff --git a/src/polymarket_insider_tracker/profiler/entity_data.py b/src/polymarket_insider_tracker/profiler/entity_data.py new file mode 100644 index 0000000..0f18574 --- /dev/null +++ b/src/polymarket_insider_tracker/profiler/entity_data.py @@ -0,0 +1,154 @@ +"""Known blockchain entity address mappings. + +This module contains address-to-entity mappings for known blockchain +entities on Polygon including CEX hot wallets, bridges, and DEX contracts. + +Sources: +- Etherscan labels +- Arkham Intelligence +- Official protocol documentation +""" + +from __future__ import annotations + +from enum import Enum + + +class EntityType(Enum): + """Classification of known blockchain entities.""" + + # Centralized Exchanges + CEX_BINANCE = "cex_binance" + CEX_COINBASE = "cex_coinbase" + CEX_KRAKEN = "cex_kraken" + CEX_OKX = "cex_okx" + CEX_KUCOIN = "cex_kucoin" + CEX_BYBIT = "cex_bybit" + CEX_CRYPTO_COM = "cex_crypto_com" + CEX_OTHER = "cex_other" + + # Bridges + BRIDGE_POLYGON = "bridge_polygon" + BRIDGE_MULTICHAIN = "bridge_multichain" + BRIDGE_STARGATE = "bridge_stargate" + BRIDGE_HOP = "bridge_hop" + BRIDGE_OTHER = "bridge_other" + + # Decentralized Exchanges + DEX_UNISWAP = "dex_uniswap" + DEX_SUSHISWAP = "dex_sushiswap" + DEX_QUICKSWAP = "dex_quickswap" + DEX_1INCH = "dex_1inch" + DEX_OTHER = "dex_other" + + # Token Contracts + TOKEN_USDC = "token_usdc" + TOKEN_USDT = "token_usdt" + TOKEN_WETH = "token_weth" + TOKEN_WMATIC = "token_wmatic" + + # Lending/DeFi + DEFI_AAVE = "defi_aave" + DEFI_COMPOUND = "defi_compound" + DEFI_OTHER = "defi_other" + + # Other + CONTRACT = "contract" + UNKNOWN = "unknown" + + +# CEX hot wallet addresses on Polygon +# Sources: Etherscan labels, Arkham Intelligence, public disclosures +CEX_ADDRESSES: dict[str, EntityType] = { + # Binance + "0x28c6c06298d514db089934071355e5743bf21d60": EntityType.CEX_BINANCE, + "0x21a31ee1afc51d94c2efccaa2092ad1028285549": EntityType.CEX_BINANCE, + "0xf89d7b9c864f589bbf53a82105107622b35eaa40": EntityType.CEX_BINANCE, + "0xdfd5293d8e347dfe59e90efd55b2956a1343963d": EntityType.CEX_BINANCE, + # Coinbase + "0x503828976d22510aad0339f595f37cc4e4645c80": EntityType.CEX_COINBASE, + "0x71660c4005ba85c37ccec55d0c4493e66fe775d3": EntityType.CEX_COINBASE, + "0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43": EntityType.CEX_COINBASE, + # Kraken + "0x2910543af39aba0cd09dbb2d50200b3e800a63d2": EntityType.CEX_KRAKEN, + "0x0a869d79a7052c7f1b55a8ebabbea3420f0d1e13": EntityType.CEX_KRAKEN, + # OKX + "0x5041ed759dd4afc3a72b8192c143f72f4724081a": EntityType.CEX_OKX, + "0x6cc5f688a315f3dc28a7781717a9a798a59fda7b": EntityType.CEX_OKX, + # KuCoin + "0xf16e9b0d03470827a95cdfd0cb8a8a3b46969b91": EntityType.CEX_KUCOIN, + "0xd6216fc19db775df9774a6e33526131da7d19a2c": EntityType.CEX_KUCOIN, + # Bybit + "0xf89e6d82be28f5cc97a9e6a94a16a17e5be73e78": EntityType.CEX_BYBIT, + # Crypto.com + "0x6262998ced04146fa42253a5c0af90ca02dfd2a3": EntityType.CEX_CRYPTO_COM, + "0x46340b20830761efd32832a74d7169b29feb9758": EntityType.CEX_CRYPTO_COM, +} + +# Bridge contract addresses on Polygon +BRIDGE_ADDRESSES: dict[str, EntityType] = { + # Polygon PoS Bridge (RootChain / Plasma Bridge related) + "0xa0c68c638235ee32657e8f720a23cec1bfc77c77": EntityType.BRIDGE_POLYGON, + "0x401f6c983ea34274ec46f84d70b31c151321188b": EntityType.BRIDGE_POLYGON, + # Multichain (formerly AnySwap) + "0x4f3aff3a747fcade12598081e80c6605a8be192f": EntityType.BRIDGE_MULTICHAIN, + # Stargate + "0x45a01e4e04f14f7a4a6880d0cbaf2c3c1acfbed4": EntityType.BRIDGE_STARGATE, + # Hop Protocol + "0x76b22b8c1079a44f1211b0e72c5d26c5e3b3c3c9": EntityType.BRIDGE_HOP, +} + +# DEX router addresses on Polygon +DEX_ADDRESSES: dict[str, EntityType] = { + # Uniswap V3 + "0xe592427a0aece92de3edee1f18e0157c05861564": EntityType.DEX_UNISWAP, + "0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45": EntityType.DEX_UNISWAP, # SwapRouter02 + # SushiSwap + "0x1b02da8cb0d097eb8d57a175b88c7d8b47997506": EntityType.DEX_SUSHISWAP, + # QuickSwap + "0xa5e0829caced8ffdd4de3c43696c57f7d7a678ff": EntityType.DEX_QUICKSWAP, + # 1inch + "0x1111111254eeb25477b68fb85ed929f73a960582": EntityType.DEX_1INCH, +} + +# Token contract addresses on Polygon +TOKEN_ADDRESSES: dict[str, EntityType] = { + # USDC (Bridged) + "0x2791bca1f2de4661ed88a30c99a7a9449aa84174": EntityType.TOKEN_USDC, + # USDC (Native) + "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359": EntityType.TOKEN_USDC, + # USDT + "0xc2132d05d31c914a87c6611c10748aeb04b58e8f": EntityType.TOKEN_USDT, + # WETH + "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619": EntityType.TOKEN_WETH, + # WMATIC + "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270": EntityType.TOKEN_WMATIC, +} + +# DeFi protocol addresses on Polygon +DEFI_ADDRESSES: dict[str, EntityType] = { + # Aave V3 + "0x794a61358d6845594f94dc1db02a252b5b4814ad": EntityType.DEFI_AAVE, # Pool + "0x8145edddf43f50276641b55bd3ad95944510021e": EntityType.DEFI_AAVE, # PoolAddressesProvider +} + + +def get_all_known_entities() -> dict[str, EntityType]: + """Get all known entity addresses combined. + + Returns: + Dictionary mapping lowercase addresses to their entity types. + """ + all_entities: dict[str, EntityType] = {} + + for entities in [ + CEX_ADDRESSES, + BRIDGE_ADDRESSES, + DEX_ADDRESSES, + TOKEN_ADDRESSES, + DEFI_ADDRESSES, + ]: + for address, entity_type in entities.items(): + all_entities[address.lower()] = entity_type + + return all_entities diff --git a/tests/profiler/test_entities.py b/tests/profiler/test_entities.py new file mode 100644 index 0000000..64489f6 --- /dev/null +++ b/tests/profiler/test_entities.py @@ -0,0 +1,369 @@ +"""Tests for known entity registry.""" + +import pytest + +from polymarket_insider_tracker.profiler.entities import EntityRegistry +from polymarket_insider_tracker.profiler.entity_data import ( + BRIDGE_ADDRESSES, + CEX_ADDRESSES, + DEFI_ADDRESSES, + DEX_ADDRESSES, + TOKEN_ADDRESSES, + EntityType, + get_all_known_entities, +) + +# ============================================================================ +# EntityType Tests +# ============================================================================ + + +class TestEntityType: + """Tests for EntityType enum.""" + + def test_cex_types_exist(self) -> None: + """Test that CEX entity types are defined.""" + assert EntityType.CEX_BINANCE.value == "cex_binance" + assert EntityType.CEX_COINBASE.value == "cex_coinbase" + assert EntityType.CEX_OTHER.value == "cex_other" + + def test_bridge_types_exist(self) -> None: + """Test that bridge entity types are defined.""" + assert EntityType.BRIDGE_POLYGON.value == "bridge_polygon" + assert EntityType.BRIDGE_MULTICHAIN.value == "bridge_multichain" + + def test_dex_types_exist(self) -> None: + """Test that DEX entity types are defined.""" + assert EntityType.DEX_UNISWAP.value == "dex_uniswap" + assert EntityType.DEX_SUSHISWAP.value == "dex_sushiswap" + + def test_token_types_exist(self) -> None: + """Test that token entity types are defined.""" + assert EntityType.TOKEN_USDC.value == "token_usdc" + assert EntityType.TOKEN_WETH.value == "token_weth" + + def test_unknown_type(self) -> None: + """Test unknown entity type.""" + assert EntityType.UNKNOWN.value == "unknown" + + +# ============================================================================ +# Entity Data Tests +# ============================================================================ + + +class TestEntityData: + """Tests for entity data mappings.""" + + def test_cex_addresses_populated(self) -> None: + """Test that CEX addresses are populated.""" + assert len(CEX_ADDRESSES) > 0 + # Check Binance address is present + binance_found = any( + entity == EntityType.CEX_BINANCE for entity in CEX_ADDRESSES.values() + ) + assert binance_found + + def test_bridge_addresses_populated(self) -> None: + """Test that bridge addresses are populated.""" + assert len(BRIDGE_ADDRESSES) > 0 + + def test_dex_addresses_populated(self) -> None: + """Test that DEX addresses are populated.""" + assert len(DEX_ADDRESSES) > 0 + # Check Uniswap is present + uniswap_found = any( + entity == EntityType.DEX_UNISWAP for entity in DEX_ADDRESSES.values() + ) + assert uniswap_found + + def test_token_addresses_include_usdc(self) -> None: + """Test that USDC address is in token addresses.""" + usdc_address = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" + assert usdc_address in TOKEN_ADDRESSES + assert TOKEN_ADDRESSES[usdc_address] == EntityType.TOKEN_USDC + + def test_get_all_known_entities(self) -> None: + """Test combining all entity mappings.""" + all_entities = get_all_known_entities() + total_expected = ( + len(CEX_ADDRESSES) + + len(BRIDGE_ADDRESSES) + + len(DEX_ADDRESSES) + + len(TOKEN_ADDRESSES) + + len(DEFI_ADDRESSES) + ) + assert len(all_entities) == total_expected + + def test_addresses_are_lowercase(self) -> None: + """Test that all addresses in get_all_known_entities are lowercase.""" + all_entities = get_all_known_entities() + for address in all_entities: + assert address == address.lower() + + +# ============================================================================ +# EntityRegistry Tests +# ============================================================================ + + +class TestEntityRegistryInit: + """Tests for EntityRegistry initialization.""" + + def test_default_initialization(self) -> None: + """Test registry initializes with default entities.""" + registry = EntityRegistry() + assert len(registry) > 0 + + def test_without_defaults(self) -> None: + """Test registry without default entities.""" + registry = EntityRegistry(include_defaults=False) + assert len(registry) == 0 + + def test_with_custom_entities(self) -> None: + """Test registry with custom entities.""" + custom = {"0x1234": EntityType.CEX_OTHER} + registry = EntityRegistry(custom_entities=custom, include_defaults=False) + assert len(registry) == 1 + assert registry.classify("0x1234") == EntityType.CEX_OTHER + + def test_custom_entities_override_defaults(self) -> None: + """Test that custom entities can override defaults.""" + # USDC address is in defaults as TOKEN_USDC + usdc_address = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" + custom = {usdc_address: EntityType.CONTRACT} + registry = EntityRegistry(custom_entities=custom) + assert registry.classify(usdc_address) == EntityType.CONTRACT + + +class TestEntityRegistryClassify: + """Tests for EntityRegistry.classify method.""" + + @pytest.fixture + def registry(self) -> EntityRegistry: + """Create a registry for testing.""" + return EntityRegistry() + + def test_classify_known_cex(self, registry: EntityRegistry) -> None: + """Test classifying a known CEX address.""" + # Binance address + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.classify(binance) == EntityType.CEX_BINANCE + + def test_classify_case_insensitive(self, registry: EntityRegistry) -> None: + """Test that classification is case-insensitive.""" + binance_lower = "0x28c6c06298d514db089934071355e5743bf21d60" + binance_mixed = "0x28C6c06298D514db089934071355E5743bf21d60" + assert registry.classify(binance_lower) == registry.classify(binance_mixed) + + def test_classify_unknown(self, registry: EntityRegistry) -> None: + """Test classifying an unknown address.""" + unknown = "0x0000000000000000000000000000000000000000" + assert registry.classify(unknown) == EntityType.UNKNOWN + + def test_classify_usdc(self, registry: EntityRegistry) -> None: + """Test classifying USDC token contract.""" + usdc = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" + assert registry.classify(usdc) == EntityType.TOKEN_USDC + + +class TestEntityRegistryChecks: + """Tests for EntityRegistry type check methods.""" + + @pytest.fixture + def registry(self) -> EntityRegistry: + """Create a registry for testing.""" + return EntityRegistry() + + def test_is_known_entity_true(self, registry: EntityRegistry) -> None: + """Test is_known_entity returns True for known addresses.""" + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.is_known_entity(binance) is True + + def test_is_known_entity_false(self, registry: EntityRegistry) -> None: + """Test is_known_entity returns False for unknown addresses.""" + unknown = "0x0000000000000000000000000000000000000000" + assert registry.is_known_entity(unknown) is False + + def test_is_cex_true(self, registry: EntityRegistry) -> None: + """Test is_cex returns True for CEX addresses.""" + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.is_cex(binance) is True + + def test_is_cex_false(self, registry: EntityRegistry) -> None: + """Test is_cex returns False for non-CEX addresses.""" + usdc = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" + assert registry.is_cex(usdc) is False + + def test_is_bridge_true(self, registry: EntityRegistry) -> None: + """Test is_bridge returns True for bridge addresses.""" + polygon_bridge = "0xa0c68c638235ee32657e8f720a23cec1bfc77c77" + assert registry.is_bridge(polygon_bridge) is True + + def test_is_bridge_false(self, registry: EntityRegistry) -> None: + """Test is_bridge returns False for non-bridge addresses.""" + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.is_bridge(binance) is False + + def test_is_dex_true(self, registry: EntityRegistry) -> None: + """Test is_dex returns True for DEX addresses.""" + uniswap = "0xe592427a0aece92de3edee1f18e0157c05861564" + assert registry.is_dex(uniswap) is True + + def test_is_dex_false(self, registry: EntityRegistry) -> None: + """Test is_dex returns False for non-DEX addresses.""" + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.is_dex(binance) is False + + +class TestEntityRegistryTerminal: + """Tests for EntityRegistry.is_terminal method.""" + + @pytest.fixture + def registry(self) -> EntityRegistry: + """Create a registry for testing.""" + return EntityRegistry() + + def test_cex_is_terminal(self, registry: EntityRegistry) -> None: + """Test that CEX addresses are terminal.""" + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.is_terminal(binance) is True + + def test_bridge_is_terminal(self, registry: EntityRegistry) -> None: + """Test that bridge addresses are terminal.""" + polygon_bridge = "0xa0c68c638235ee32657e8f720a23cec1bfc77c77" + assert registry.is_terminal(polygon_bridge) is True + + def test_dex_is_not_terminal(self, registry: EntityRegistry) -> None: + """Test that DEX addresses are not terminal.""" + uniswap = "0xe592427a0aece92de3edee1f18e0157c05861564" + assert registry.is_terminal(uniswap) is False + + def test_token_is_not_terminal(self, registry: EntityRegistry) -> None: + """Test that token addresses are not terminal.""" + usdc = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" + assert registry.is_terminal(usdc) is False + + def test_unknown_is_not_terminal(self, registry: EntityRegistry) -> None: + """Test that unknown addresses are not terminal.""" + unknown = "0x0000000000000000000000000000000000000000" + assert registry.is_terminal(unknown) is False + + +class TestEntityRegistryCategory: + """Tests for EntityRegistry.get_entity_category method.""" + + @pytest.fixture + def registry(self) -> EntityRegistry: + """Create a registry for testing.""" + return EntityRegistry() + + def test_category_cex(self, registry: EntityRegistry) -> None: + """Test CEX category.""" + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.get_entity_category(binance) == "cex" + + def test_category_bridge(self, registry: EntityRegistry) -> None: + """Test bridge category.""" + polygon_bridge = "0xa0c68c638235ee32657e8f720a23cec1bfc77c77" + assert registry.get_entity_category(polygon_bridge) == "bridge" + + def test_category_dex(self, registry: EntityRegistry) -> None: + """Test DEX category.""" + uniswap = "0xe592427a0aece92de3edee1f18e0157c05861564" + assert registry.get_entity_category(uniswap) == "dex" + + def test_category_token(self, registry: EntityRegistry) -> None: + """Test token category.""" + usdc = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" + assert registry.get_entity_category(usdc) == "token" + + def test_category_defi(self, registry: EntityRegistry) -> None: + """Test DeFi category.""" + aave = "0x794a61358d6845594f94dc1db02a252b5b4814ad" + assert registry.get_entity_category(aave) == "defi" + + def test_category_unknown(self, registry: EntityRegistry) -> None: + """Test unknown category.""" + unknown = "0x0000000000000000000000000000000000000000" + assert registry.get_entity_category(unknown) == "unknown" + + +class TestEntityRegistryMutations: + """Tests for EntityRegistry mutation methods.""" + + def test_add_entity(self) -> None: + """Test adding an entity.""" + registry = EntityRegistry(include_defaults=False) + registry.add_entity("0x1234", EntityType.CEX_OTHER) + assert registry.classify("0x1234") == EntityType.CEX_OTHER + + def test_add_entity_normalizes_address(self) -> None: + """Test that add_entity normalizes addresses to lowercase.""" + registry = EntityRegistry(include_defaults=False) + registry.add_entity("0xABCD", EntityType.CEX_OTHER) + assert registry.classify("0xabcd") == EntityType.CEX_OTHER + + def test_remove_entity(self) -> None: + """Test removing an entity.""" + registry = EntityRegistry(include_defaults=False) + registry.add_entity("0x1234", EntityType.CEX_OTHER) + assert registry.remove_entity("0x1234") is True + assert registry.classify("0x1234") == EntityType.UNKNOWN + + def test_remove_nonexistent(self) -> None: + """Test removing a non-existent entity.""" + registry = EntityRegistry(include_defaults=False) + assert registry.remove_entity("0x1234") is False + + +class TestEntityRegistryDunder: + """Tests for EntityRegistry dunder methods.""" + + def test_len(self) -> None: + """Test __len__ returns count of entities.""" + registry = EntityRegistry(include_defaults=False) + registry.add_entity("0x1234", EntityType.CEX_OTHER) + registry.add_entity("0x5678", EntityType.DEX_OTHER) + assert len(registry) == 2 + + def test_contains(self) -> None: + """Test __contains__ for membership testing.""" + registry = EntityRegistry() + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert binance in registry + assert "0x0000000000000000000000000000000000000000" not in registry + + +class TestEntityRegistryContract: + """Tests for EntityRegistry.is_contract method.""" + + @pytest.fixture + def registry(self) -> EntityRegistry: + """Create a registry for testing.""" + return EntityRegistry() + + def test_dex_is_contract(self, registry: EntityRegistry) -> None: + """Test that DEX addresses are contracts.""" + uniswap = "0xe592427a0aece92de3edee1f18e0157c05861564" + assert registry.is_contract(uniswap) is True + + def test_token_is_contract(self, registry: EntityRegistry) -> None: + """Test that token addresses are contracts.""" + usdc = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174" + assert registry.is_contract(usdc) is True + + def test_defi_is_contract(self, registry: EntityRegistry) -> None: + """Test that DeFi protocol addresses are contracts.""" + aave = "0x794a61358d6845594f94dc1db02a252b5b4814ad" + assert registry.is_contract(aave) is True + + def test_cex_is_not_contract(self, registry: EntityRegistry) -> None: + """Test that CEX addresses are not contracts.""" + binance = "0x28c6c06298d514db089934071355e5743bf21d60" + assert registry.is_contract(binance) is False + + def test_unknown_is_not_contract(self, registry: EntityRegistry) -> None: + """Test that unknown addresses are not contracts.""" + unknown = "0x0000000000000000000000000000000000000000" + assert registry.is_contract(unknown) is False