merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
165
app/chain_cache.py
Normal file
165
app/chain_cache.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
"""
|
||||
Chain Data Cache — In-memory TTL cache for RPC results.
|
||||
Minimizes Helius API calls on free tier (100k credits/month).
|
||||
Includes startup warming for hot wallets.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Hot Wallets to Pre-Warm ──────────────────────────────
|
||||
# Top wallets by volume + known infrastructure that get hit on every scan.
|
||||
# Pre-warming saves 5+ API calls per scan that would otherwise be cold misses.
|
||||
|
||||
HOT_WALLETS: list[str] = [
|
||||
# Top Solana DEX/infrastructure (hit on every RugMaps/scan)
|
||||
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", # Raydium V4 AMM
|
||||
"JUP6LkbZbjS1jKKwapdHNy7bKNkD5Ka6TqPnVGFVeYf", # Jupiter V6
|
||||
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", # Orca Whirlpool
|
||||
"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", # Binance 1
|
||||
"5tzFkiKscXHK5ZXCGbXZxdw7gTjjD1mBwuoFbhUvuAi9", # Binance 2
|
||||
"2ojv9BAiHghv5o9y2k6k3m7o9TdsrKJqXvWyCdkUWNmB", # Binance 3
|
||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", # Pump.fun
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # SPL Token Program
|
||||
# Top Ethereum infrastructure (cross-chain)
|
||||
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", # Uniswap V2
|
||||
"0xBE0eB53FC46b790099138e3d32C721856d41e865", # Binance 7
|
||||
"0xF977814e90dA44bFA03b6295A0616a897441aceC", # Binance 8
|
||||
"0x28C6c06298d514Db089934071355E5743bf21d60", # Binance 14
|
||||
]
|
||||
|
||||
HOT_TOKENS: list[str] = [
|
||||
# Top tokens scanned for rugs (Solana)
|
||||
"jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL", # JTO
|
||||
"EPjFWdd5AufqSSqeM2qN1xzybapC8G5wUGxvZq2moaomYR", # USDC
|
||||
"So11111111111111111111111111111111111111112", # SOL wrapped
|
||||
"mSoLzYCxHdYgbz9JUv7WoxX3LEdh3K8VZJIQz3EhR9J", # mSOL
|
||||
"JUPyiwrYJF8UP9YuYuB8mH3WGfnCfNJiQ3W9vHJ7kKo", # JUP
|
||||
# Top tokens (Ethereum chain IDs)
|
||||
"1", # ETH chain
|
||||
"56", # BSC chain
|
||||
"8453", # Base chain
|
||||
]
|
||||
|
||||
|
||||
class ChainCache:
|
||||
"""LRU cache with TTL for chain data. Max 500 entries, 5-min default TTL."""
|
||||
|
||||
def __init__(self, max_size: int = 500, default_ttl: int = 300):
|
||||
self.max_size = max_size
|
||||
self.default_ttl = default_ttl
|
||||
self._cache: OrderedDict[str, tuple[float, Any]] = OrderedDict()
|
||||
self._lock = asyncio.Lock()
|
||||
self._warmed = False
|
||||
|
||||
def _make_key(self, method: str, *args) -> str:
|
||||
return f"{method}:{':'.join(str(a)[:100] for a in args)}"
|
||||
|
||||
async def get(self, method: str, *args) -> Any | None:
|
||||
key = self._make_key(method, *args)
|
||||
async with self._lock:
|
||||
if key in self._cache:
|
||||
expiry, value = self._cache[key]
|
||||
if time.time() < expiry:
|
||||
self._cache.move_to_end(key)
|
||||
return value
|
||||
del self._cache[key]
|
||||
return None
|
||||
|
||||
async def set(self, method: str, value: Any, *args, ttl: int | None = None):
|
||||
key = self._make_key(method, *args)
|
||||
async with self._lock:
|
||||
if key in self._cache:
|
||||
del self._cache[key]
|
||||
elif len(self._cache) >= self.max_size:
|
||||
self._cache.popitem(last=False)
|
||||
self._cache[key] = (time.time() + (ttl or self.default_ttl), value)
|
||||
|
||||
async def stats(self) -> dict:
|
||||
async with self._lock:
|
||||
# Count how many are from warming vs live
|
||||
warmed = sum(1 for k in self._cache if k.startswith("warm:"))
|
||||
return {
|
||||
"size": len(self._cache),
|
||||
"max": self.max_size,
|
||||
"warmed_entries": warmed,
|
||||
"utilization_pct": round(len(self._cache) / self.max_size * 100, 1),
|
||||
}
|
||||
|
||||
async def warm(self):
|
||||
"""Pre-populate cache with known hot addresses to avoid cold-start misses."""
|
||||
if self._warmed:
|
||||
return
|
||||
|
||||
logger.info(f"Warming cache with {len(HOT_WALLETS)} wallets and {len(HOT_TOKENS)} tokens...")
|
||||
|
||||
# Mark entity registry entries as known infrastructure (avoids repeated lookups)
|
||||
try:
|
||||
from app.entity_registry import get_entity_registry
|
||||
|
||||
registry = get_entity_registry()
|
||||
for addr in HOT_WALLETS:
|
||||
match = registry.classify_address(addr)
|
||||
if match and match.entity_type != "unknown":
|
||||
await self.set("warm_entity", match.to_dict(), addr, ttl=600)
|
||||
except Exception as e:
|
||||
logger.debug(f"Entity warm skipped: {e}")
|
||||
|
||||
# Mark hot tokens as known-safe to avoid spam registry checks
|
||||
for token in HOT_TOKENS:
|
||||
await self.set("warm_token", {"address": token, "preloaded": True}, token, ttl=600)
|
||||
|
||||
# Mark spam registry as loaded (it loads on first check anyway)
|
||||
try:
|
||||
from app.spam_registry import get_spam_registry
|
||||
|
||||
sr = get_spam_registry()
|
||||
sr._ensure_loaded() # Force load
|
||||
await self.set("warm_spam", {"loaded": True, "total": sr.stats()["total"]}, ttl=600)
|
||||
except Exception as e:
|
||||
logger.debug(f"Spam warm skipped: {e}")
|
||||
|
||||
# Mark GNN as available
|
||||
try:
|
||||
from app.fraud_gnn import get_fraud_gnn
|
||||
|
||||
gnn = get_fraud_gnn()
|
||||
gnn._ensure_loaded()
|
||||
await self.set("warm_gnn", {"loaded": True, "model": gnn.model_type}, ttl=600)
|
||||
except Exception as e:
|
||||
logger.debug(f"GNN warm skipped: {e}")
|
||||
|
||||
# Warm CoinGecko market data if available
|
||||
try:
|
||||
from app.coingecko_connector import get_coingecko_connector
|
||||
|
||||
cg = get_coingecko_connector()
|
||||
trending = await cg.get_trending()
|
||||
if trending:
|
||||
await self.set("warm_trending", trending, ttl=300)
|
||||
logger.info(f"Warmed trending: {len(trending)} tokens")
|
||||
global_data = await cg.get_global_metrics()
|
||||
if global_data:
|
||||
await self.set("warm_global", global_data, ttl=300)
|
||||
logger.info(f"Warmed global: BTC dom {global_data.get('btc_dominance', '?')}%")
|
||||
except Exception as e:
|
||||
logger.debug(f"CoinGecko warm skipped: {e}")
|
||||
|
||||
self._warmed = True
|
||||
logger.info(f"Cache warmed: {len(self._cache)}/{self.max_size} entries")
|
||||
|
||||
|
||||
# Singleton
|
||||
_cache: ChainCache | None = None
|
||||
|
||||
|
||||
def get_chain_cache() -> ChainCache:
|
||||
global _cache
|
||||
if _cache is None:
|
||||
_cache = ChainCache()
|
||||
return _cache
|
||||
Loading…
Add table
Add a link
Reference in a new issue