feat: add known entity registry for CEX and bridge detection (#12)
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4111d1ee00
commit
0c4d2bebea
@@ -9,6 +9,12 @@ from polymarket_insider_tracker.profiler.chain import (
|
|||||||
RateLimitError,
|
RateLimitError,
|
||||||
RPCError,
|
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 (
|
from polymarket_insider_tracker.profiler.models import (
|
||||||
Transaction,
|
Transaction,
|
||||||
WalletInfo,
|
WalletInfo,
|
||||||
@@ -18,6 +24,9 @@ from polymarket_insider_tracker.profiler.models import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
# Analyzer
|
# Analyzer
|
||||||
"WalletAnalyzer",
|
"WalletAnalyzer",
|
||||||
|
# Entity Registry
|
||||||
|
"EntityRegistry",
|
||||||
|
"EntityType",
|
||||||
# Polygon Client
|
# Polygon Client
|
||||||
"PolygonClient",
|
"PolygonClient",
|
||||||
"PolygonClientError",
|
"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