""" Aggressive Caching Shield - Historical Depth Controller Controls how far back transaction history queries go. Free tier RPC endpoints often rate-limit or block deep history queries. This module caps default queries to shallow depth and gates deep queries. Strategy: - DEFAULT_DEPTH: 20 signatures (fast scan - enough to detect recent activity) - MAX_DEPTH: 100 signatures (deep scan - on-demand only, user clicks button) - MAX_PAGINATED: 200 (absolute maximum across all pages) - Deep queries are cached longer (5min vs 30s) since historical data rarely changes - Per-address cooldown: deep scan limited to once per 5 min per address - Before/signature pagination uses the "before" parameter (not offset-based) Cache integration: - Shallow queries (limit <= 20): TTL 30s (default getSignaturesForAddress) - Deep queries (limit > 20): TTL 300s (5min, historical data is static) - Pagination queries: TTL 180s """ import logging import time from collections import OrderedDict logger = logging.getLogger("history_depth") # Default query depth for fast surface-level scans DEFAULT_DEPTH = 20 # Maximum depth for a single query (prevents abuse) MAX_DEPTH = 100 # Absolute maximum across all pagination MAX_PAGINATED = 200 # Cooldown between deep scans per address (seconds) DEEP_SCAN_COOLDOWN = 300 # 5 minutes # How many addresses to track for cooldown COOLDOWN_TRACK_SIZE = 500 class HistoryDepthController: """Controls transaction history query depth and deep-scan gating. Usage: ctrl = HistoryDepthController() limit = ctrl.clamp_limit(requested_limit=50, is_deep_scan=False) # -> returns 50 (within bounds, allowed) can_proceed, wait = ctrl.check_deep_cooldown("SoL...") if not can_proceed: raise DeepScanThrottled(f"Deep scan cooldown: {wait:.0f}s remaining") """ def __init__(self): # Per-address deep scan cooldown: address -> last_deep_scan_ts self._deep_cooldowns: OrderedDict = OrderedDict() def clamp_limit( self, requested: int = DEFAULT_DEPTH, is_deep_scan: bool = False, is_paginated: bool = False, ) -> int: """Clamp a requested query limit to allowed bounds. Args: requested: The limit the caller wants is_deep_scan: True if this is an explicit deep-scan request (user clicked button) is_paginated: True if this is a continuation (before/after signature pagination) Returns: Clamped limit value """ if is_deep_scan: return min(requested, MAX_DEPTH) elif is_paginated: return min(requested, MAX_PAGINATED) else: # Default shallow scan return min(requested, DEFAULT_DEPTH) def check_deep_cooldown(self, address: str) -> tuple[bool, float]: """Check if a deep scan is allowed for this address. Returns: (allowed, wait_seconds) - if not allowed, wait_seconds is how long to wait """ now = time.monotonic() last = self._deep_cooldowns.get(address) if last is not None: elapsed = now - last if elapsed < DEEP_SCAN_COOLDOWN: wait = DEEP_SCAN_COOLDOWN - elapsed return (False, wait) return (True, 0.0) def record_deep_scan(self, address: str): """Record that a deep scan was performed for this address.""" self._deep_cooldowns[address] = time.monotonic() # Evict oldest if over capacity while len(self._deep_cooldowns) > COOLDOWN_TRACK_SIZE: self._deep_cooldowns.popitem(last=False) def get_deep_cache_ttl(self, depth: int) -> int: """Get appropriate cache TTL based on query depth.""" if depth > DEFAULT_DEPTH: return 300 # 5 min for deep queries elif depth > 10: return 60 # 1 min for medium queries else: return 30 # 30s for shallow queries def stats(self) -> dict: """Return controller statistics.""" return { "addresses_in_cooldown": len(self._deep_cooldowns), "default_depth": DEFAULT_DEPTH, "max_depth": MAX_DEPTH, "max_paginated": MAX_PAGINATED, "deep_scan_cooldown_s": DEEP_SCAN_COOLDOWN, } # ── Singleton ────────────────────────────────────────────────────────────── _controller: HistoryDepthController | None = None def get_history_controller() -> HistoryDepthController: global _controller if _controller is None: _controller = HistoryDepthController() return _controller