Merge pull request #43 from pselamy/fix/12
feat: add known entity registry for CEX and bridge detection (#12)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user