""" SENTINEL — Enhanced Bundle & Sniper Detection ============================================= Detects coordinated wallet groups that purchase simultaneously at token launch, funded by the same source, often in the same block. Detection heuristics: - Same-block purchase (very high confidence) - Deployer funding chain (5-hop trace, very high) - Multi-hop funding (high) - Identical buy sizes (medium) - Coordinated timing (medium) - Shared RPC/Jito endpoint (medium) - Compute budget pattern analysis (Solana-specific) Uses direct API calls: Helius (transaction/signature data), DexScreener (pairs/trades), Solscan (account transfers, transaction detail). """ import logging import os from dataclasses import dataclass, field import httpx from app.chain_client import ChainClient from app.chain_registry import is_solana from app.free_solscan_client import FreeSolscanClient logger = logging.getLogger("bundle_detector") @dataclass class FundingStep: address: str amount: float token: str # "SOL", "ETH", etc. timestamp: int # unix ms tx_signature: str = "" @dataclass class BundledWallet: address: str funding_chain: list[FundingStep] buy_tx: str | None = None buy_amount: float = 0.0 buy_block: int = 0 confidence: str = "MEDIUM" # LOW, MEDIUM, HIGH, VERY_HIGH evidence: list[str] = field(default_factory=list) @dataclass class SameBlockSniper: address: str block_number: int buy_amount: float = 0.0 compute_units: int = 0 # Solana-specific priority_fee: float = 0.0 is_jito: bool = False @dataclass class BehaviorCluster: wallets: list[str] pattern: str # "same_size", "same_time", "same_dex" combined_percentage: float confidence: float @dataclass class BundleReport: token_address: str chain: str deployer: str deploy_block: int deploy_timestamp: int total_bundled_percentage: float deployer_funded: list[BundledWallet] same_block_snipers: list[SameBlockSniper] behavior_clusters: list[BehaviorCluster] compute_budget_patterns: dict[str, int] = field(default_factory=dict) jito_bundles_detected: int = 0 risk_score: int = 0 # 0-100 risk_level: str = "LOW" # LOW, MEDIUM, HIGH, CRITICAL warnings: list[str] = field(default_factory=list) class BundleDetector: """Enhanced bundle and sniper detection with funding chain analysis. Fetches data directly from Helius, DexScreener, and Solscan. """ # Known Jito tip program IDs JITO_PROGRAMS = { "Jito4APyf6rDt1pD1jH3nD3is4v7TwzFNWjjMP7B2RK", "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", "ADuCadRmgjMe6UzXVxR1Kn9CS2CXAf8bjiKkC4xFcegX", } # Known bundler program IDs (Smithii, Printr, etc.) BUNDLER_PROGRAMS = { "Smithii": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", "PumpFun": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", "LaunchLab": "LaunchLabzbP1j4R7KzEFmYSF3EXGnZ6tY7AZ8eg3P2qJ", } def __init__(self): self._http = httpx.AsyncClient(timeout=15.0) self._chain = ChainClient() self._solscan = FreeSolscanClient() self._helius_key = os.getenv("HELIUS_API_KEY", "") async def close(self): await self._http.aclose() # ── Direct API fetchers ─────────────────────────────────────────── async def _fetch_dexscreener_token(self, token_address: str) -> dict | None: """Fetch token pair data from DexScreener (free, no key).""" try: resp = await self._http.get(f"https://api.dexscreener.com/latest/dex/tokens/{token_address}") if resp.status_code == 200: data = resp.json() pairs = data.get("pairs") or [] return pairs[0] if pairs else None except Exception as e: logger.warning(f"DexScreener fetch failed for {token_address}: {e}") return None async def _fetch_solscan_token_data(self, token_address: str) -> dict | None: """Fetch token metadata from Solscan (Solana).""" try: return self._solscan.token_data(token_address) except Exception as e: logger.warning(f"Solscan token data failed for {token_address}: {e}") return None async def _fetch_helius_signatures(self, address: str, limit: int = 50) -> list[dict]: """Fetch recent signatures for an address via Helius RPC.""" result = await self._chain.rpc_call("getSignaturesForAddress", [address, {"limit": limit}]) if result and "result" in result: return result["result"] return [] async def _fetch_helius_transaction(self, tx_signature: str) -> dict | None: """Fetch full transaction details via Helius RPC.""" result = await self._chain.rpc_call( "getTransaction", [tx_signature, {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0}], ) if result and "result" in result: return result["result"] return None async def _fetch_solscan_transactions(self, address: str, page: int = 1, page_size: int = 40) -> list[dict]: """Fetch transaction history from Solscan.""" try: return self._solscan.account_transactions(address, page=page, page_size=page_size) or [] except Exception as e: logger.warning(f"Solscan transactions failed for {address}: {e}") return [] async def _fetch_solscan_transaction_detail(self, tx_hash: str) -> dict | None: """Fetch transaction detail from Solscan.""" try: return self._solscan.transaction_detail(tx_hash) except Exception as e: logger.warning(f"Solscan transaction detail failed for {tx_hash}: {e}") return None # ── Deployment info ──────────────────────────────────────────────── async def _get_deployment_info(self, token: str, chain: str, deploy_tx: str | None) -> tuple[str, int, int]: """Fetch deployment transaction info using direct APIs. Strategy: 1. If deploy_tx is provided, fetch that transaction directly 2. Otherwise, look up the token on DexScreener (has pairCreation metadata) 3. Fallback: use Solscan token data for creation info """ # If explicit deploy_tx provided, fetch it if deploy_tx: if is_solana(chain): tx_data = await self._fetch_helius_transaction(deploy_tx) if tx_data: # Extract deployer from the transaction msg = tx_data.get("transaction", {}).get("message", {}) account_keys = msg.get("accountKeys", []) signers = [a for a in account_keys if a.get("signer", a.get("writable", False))] deployer = "" if signers: deployer = signers[0].get("pubkey", signers[0] if isinstance(signers[0], str) else "") elif account_keys: first = account_keys[0] deployer = first.get("pubkey", first) if isinstance(first, dict) else first block_time = tx_data.get("blockTime", 0) or 0 slot = tx_data.get("slot", 0) or 0 return (deployer, slot, block_time * 1000 if block_time else 0) # Try DexScreener for pair creation info dex_pair = await self._fetch_dexscreener_token(token) if dex_pair: info = dex_pair.get("info", {}) or {} deployer = info.get("creator", dex_pair.get("pairCreator", "")) block_time = dex_pair.get("pairCreatedAt", 0) if deployer: return (deployer, 0, block_time) # Try Solscan token data for creation info on Solana if is_solana(chain): sol_data = await self._fetch_solscan_token_data(token) if sol_data: deployer = sol_data.get("creator", sol_data.get("owner", "")) creation_time = sol_data.get("blockTime", 0) or 0 if deployer: return (deployer, 0, creation_time) return "unknown", 0, 0 async def _get_early_buyers(self, token: str, chain: str, deploy_block: int) -> list: """Get wallets that bought within first 10 blocks of deployment. Uses DexScreener boosts/transactions + Solscan account data. For Solana: queries Helius for early signatures after deploy. """ early_buyers = [] if is_solana(chain) and deploy_block > 0: # Get signatures for the token address (mint account) sigs = await self._fetch_helius_signatures(token, limit=50) for sig_info in sigs[:20]: slot = sig_info.get("slot", 0) # Only consider transactions within ~10 blocks of deployment if 0 < slot - deploy_block <= 10: early_buyers.append( _EarlyBuyer( address=sig_info.get("signer", ""), first_buy_tx=sig_info.get("signature", ""), first_buy_block=slot, ) ) return early_buyers async def _trace_funding_chain( self, wallet: str, chain: str, max_depth: int = 5, before_time: int = 0 ) -> list[FundingStep]: """Trace SOL/token transfers backward from a wallet to find funding source. Uses Solscan top transfers + Helius RPC for on-chain data. """ chain_result: list[FundingStep] = [] if is_solana(chain): # Try Solscan funding sources first funding_sources = self._solscan.get_wallet_funding_sources(wallet, days=30) if funding_sources: for src in funding_sources[:max_depth]: chain_result.append( FundingStep( address=src.get("from", ""), amount=float(src.get("amount", 0)), token=src.get("symbol", "SOL"), timestamp=int(src.get("time", 0) or 0), tx_signature=src.get("tx_hash", ""), ) ) return chain_result # ── Pure computation methods (unchanged) ────────────────────────── def _cluster_by_behavior(self, buyers: list) -> list[BehaviorCluster]: """Cluster wallets by identical trading patterns.""" # Placeholder — requires transaction-level analysis # Real implementation groups by: same DEX, same trade size, same slippage, same route return [] def _analyze_compute_budgets(self, buyers: list) -> tuple[dict[str, int], int]: """Analyze Solana compute budget patterns to identify bot networks. Key signals: - Casual traders: variable compute, random priority fees - Bot operators: identical compute units, fixed priority fee - Sniper bots: max compute (1.4M CU), max priority fee """ patterns = {} jito_count = 0 for buyer in buyers: cu = getattr(buyer, "compute_units", 0) pf = getattr(buyer, "priority_fee", 0.0) is_jito = getattr(buyer, "is_jito", False) if is_jito: jito_count += 1 # Group by compute budget pattern if cu > 1_300_000: # Max compute = sniper pattern = "max_compute_sniper" elif cu > 200_000 and pf < 0.001: # Fixed low fee = bot pattern = "fixed_fee_bot" elif cu < 200_000: # Normal trader pattern = "normal_trader" else: pattern = "standard_bot" patterns[pattern] = patterns.get(pattern, 0) + 1 return patterns, jito_count def _calculate_risk( self, deployer_funded: list, same_block: list, behavior_clusters: list, jito_count: int, bundled_pct: float, ) -> tuple[int, str, list[str]]: """Calculate overall bundle risk score and warnings.""" score = 0 warnings = [] # Deployer-funded wallets (strongest signal) if deployer_funded: fund_pct = sum(b.buy_amount for b in deployer_funded) / max(bundled_pct, 0.01) * 100 score += min(50, len(deployer_funded) * 10) warnings.append( f"DEPLOYER_FUNDED: {len(deployer_funded)} wallets funded by deployer ({fund_pct:.1f}% of supply)" ) # Same-block snipers if same_block: score += min(30, len(same_block) * 5) warnings.append(f"SAME_BLOCK: {len(same_block)} wallets bought in deployment block") # Jito bundles if jito_count > 0: score += min(20, jito_count * 5) warnings.append(f"JITO_BUNDLE: {jito_count} wallets used Jito tips") # Behavior clusters if behavior_clusters: score += min(15, len(behavior_clusters) * 5) warnings.append(f"BEHAVIOR_CLUSTER: {len(behavior_clusters)} coordinated wallet groups detected") # Bundled percentage if bundled_pct > 20: score += 20 warnings.append(f"BUNDLED_SUPPLY: {bundled_pct:.1f}% of supply in bundled wallets") elif bundled_pct > 10: score += 10 score = min(100, score) if score >= 70: level = "CRITICAL" elif score >= 45: level = "HIGH" elif score >= 20: level = "MEDIUM" else: level = "LOW" return score, level, warnings # ── Main analysis entry point ───────────────────────────────────── async def detect_bundles(self, token_address: str, chain: str, deploy_tx: str | None = None) -> BundleReport: """Full bundle detection for a token launch. Steps: 1. Find deployment transaction and deployer 2. Get all buyers in first 10 blocks 3. Trace funding chains for each buyer (5 hops) 4. Identify deployer-funded wallets 5. Identify same-block snipers 6. Analyze compute budget patterns (Solana) 7. Cluster by behavioral patterns 8. Calculate risk score """ # Fetch deployment info via direct APIs deployer, deploy_block, deploy_ts = await self._get_deployment_info(token_address, chain, deploy_tx) if not deployer or deployer == "unknown": return BundleReport( token_address=token_address, chain=chain, deployer="unknown", deploy_block=0, deploy_timestamp=0, total_bundled_percentage=0, deployer_funded=[], same_block_snipers=[], behavior_clusters=[], warnings=["Could not determine deployer"], ) # Get early buyers (first 10 blocks) early_buyers = await self._get_early_buyers(token_address, chain, deploy_block) # Trace funding chains deployer_funded = [] for buyer in early_buyers: chain_trace = await self._trace_funding_chain(buyer.address, chain, max_depth=5, before_time=deploy_ts) chain_addrs = [step.address for step in chain_trace] if deployer in chain_addrs: deployer_funded.append( BundledWallet( address=buyer.address, funding_chain=chain_trace, buy_tx=buyer.first_buy_tx, buy_amount=buyer.buy_amount, buy_block=buyer.first_buy_block, confidence="VERY_HIGH", evidence=[f"Funded by deployer via {len(chain_trace)} hops"], ) ) # Same-block analysis same_block = [ SameBlockSniper( address=b.address, block_number=b.first_buy_block, buy_amount=b.buy_amount, compute_units=getattr(b, "compute_units", 0), priority_fee=getattr(b, "priority_fee", 0.0), is_jito=getattr(b, "is_jito", False), ) for b in early_buyers if b.first_buy_block == deploy_block ] # Behavior clustering behavior_clusters = self._cluster_by_behavior(early_buyers) # Compute budget analysis (Solana) compute_patterns = {} jito_count = 0 if is_solana(chain): compute_patterns, jito_count = self._analyze_compute_budgets(early_buyers) # Calculate totals total_bundled_pct = sum(b.buy_amount for b in deployer_funded) total_supply = sum(b.buy_amount for b in early_buyers) or 1 total_bundled_pct = (total_bundled_pct / total_supply * 100) if total_supply else 0 # Risk score risk_score, risk_level, warnings = self._calculate_risk( deployer_funded, same_block, behavior_clusters, jito_count, total_bundled_pct ) return BundleReport( token_address=token_address, chain=chain, deployer=deployer, deploy_block=deploy_block, deploy_timestamp=deploy_ts, total_bundled_percentage=round(total_bundled_pct, 2), deployer_funded=deployer_funded, same_block_snipers=same_block, behavior_clusters=behavior_clusters, compute_budget_patterns=compute_patterns, jito_bundles_detected=jito_count, risk_score=risk_score, risk_level=risk_level, warnings=warnings, ) # Type alias for early buyer data placeholder from dataclasses import dataclass as _dataclass @_dataclass class _EarlyBuyer: address: str first_buy_tx: str = "" first_buy_block: int = 0 buy_amount: float = 0.0 compute_units: int = 0 priority_fee: float = 0.0 is_jito: bool = False