- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
"""
|
|
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
|