diff --git a/app/caching_shield/investigate_router.py b/app/caching_shield/investigate_router.py index 31c002b..f5a7c8c 100644 --- a/app/caching_shield/investigate_router.py +++ b/app/caching_shield/investigate_router.py @@ -168,7 +168,7 @@ async def investigation_graph(req: GraphRequest): ) # ── Convert to cytoscape.js JSON ────────────────────────── - NODE_COLORS = { + node_colors = { "wallet": "#3498db", "token": "#2ecc71", "contract": "#e67e22", @@ -177,7 +177,7 @@ async def investigation_graph(req: GraphRequest): "chain": "#1abc9c", "address": "#95a5a6", } - NODE_SHAPES = { + node_shapes = { "wallet": "ellipse", "token": "round-rectangle", "contract": "rectangle", @@ -198,8 +198,8 @@ async def investigation_graph(req: GraphRequest): "id": key, "label": nd.get("label", nd.get("id", "")), "type": ntype, - "color": NODE_COLORS.get(ntype, "#95a5a6"), - "shape": NODE_SHAPES.get(ntype, "ellipse"), + "color": node_colors.get(ntype, "#95a5a6"), + "shape": node_shapes.get(ntype, "ellipse"), "size": 45 if key == raw.get("center") else 30, "metadata": nd.get("metadata", {}), }, @@ -246,7 +246,7 @@ async def fund_flow_sankey(req: GraphRequest): address = req.address.strip() # Get fund flow data using existing visualizer - from app.scanners.fund_flow_visualizer import ( + from app.domains.scanners.fund_flow_visualizer import ( _build_flow_graph, _fetch_evm_wallets, _fetch_solana_wallets, @@ -271,7 +271,9 @@ async def fund_flow_sankey(req: GraphRequest): sankey_nodes.append( { "id": idx, - "name": n.address[:10] + "..." + n.address[-6:] if len(n.address) > 16 else n.address, + "name": n.address[:10] + "..." + n.address[-6:] + if len(n.address) > 16 + else n.address, "address": n.address, "role": n.role, "amount_usd": n.amount_usd, @@ -293,14 +295,14 @@ async def fund_flow_sankey(req: GraphRequest): # Also generate cytoscape nodes/edges for graph overlay cy_nodes = [] cy_edges = [] - ROLE_COLORS = { + role_colors = { "creator": "#9b59b6", "funder": "#2ecc71", "lp": "#f1c40f", "seller": "#e74c3c", "other": "#95a5a6", } - ROLE_LABELS = { + role_labels = { "creator": "Creator/Deployer", "funder": "Early Funder", "lp": "LP Holder", @@ -314,9 +316,9 @@ async def fund_flow_sankey(req: GraphRequest): { "data": { "id": n.address, - "label": f"{ROLE_LABELS.get(n.role, n.role)}\n{short}", + "label": f"{role_labels.get(n.role, n.role)}\n{short}", "type": n.role, - "color": ROLE_COLORS.get(n.role, "#95a5a6"), + "color": role_colors.get(n.role, "#95a5a6"), "size": 40 if n.role == "creator" else 28, "amount_usd": n.amount_usd, }, diff --git a/app/domains/__init__.py b/app/domains/__init__.py new file mode 100644 index 0000000..5d22762 --- /dev/null +++ b/app/domains/__init__.py @@ -0,0 +1 @@ +# app/domains package diff --git a/app/domains/billing/x402/tools/evidence_tools.py b/app/domains/billing/x402/tools/evidence_tools.py index 7c410ed..bd33b55 100644 --- a/app/domains/billing/x402/tools/evidence_tools.py +++ b/app/domains/billing/x402/tools/evidence_tools.py @@ -20,6 +20,7 @@ decompiler, address labels, contract diff, fund flow, etc.) live in tools/report_tools.py. label_tools.py hosts the standalone address labeling endpoint. """ + from __future__ import annotations from datetime import datetime @@ -43,7 +44,7 @@ async def sentinel_full_scan(req: SentinelScanRequest): Returns composite risk score (0-100), per-module breakdown, and aggregated red flags. """ try: - from app.scanners.sentinel_pipeline import dataclass_to_dict, run_sentinel_scan + from app.domains.scanners.sentinel_pipeline import dataclass_to_dict, run_sentinel_scan report = await run_sentinel_scan( token_address=req.address, @@ -72,7 +73,7 @@ async def holder_analysis_endpoint(req: SentinelModuleRequest): Pricing: $0.05 """ try: - from app.scanners.sentinel_pipeline import run_holder_analysis + from app.domains.scanners.sentinel_pipeline import run_holder_analysis result = await run_holder_analysis(req.address, req.chain) await record_x402_payment("holder_analysis", "0.05", req.address) @@ -97,7 +98,7 @@ async def bundle_detect_endpoint(req: SentinelModuleRequest): Pricing: $0.08 """ try: - from app.scanners.sentinel_pipeline import run_bundle_detection + from app.domains.scanners.sentinel_pipeline import run_bundle_detection result = await run_bundle_detection(req.address, req.chain) await record_x402_payment("bundle_detect", "0.08", req.address) @@ -122,7 +123,7 @@ async def exchange_fund_check_endpoint(req: SentinelModuleRequest): Pricing: $0.05 """ try: - from app.scanners.sentinel_pipeline import run_exchange_funding + from app.domains.scanners.sentinel_pipeline import run_exchange_funding result = await run_exchange_funding(req.address, req.chain) await record_x402_payment("exchange_fund_check", "0.05", req.address) @@ -147,7 +148,7 @@ async def liquidity_verify_endpoint(req: SentinelModuleRequest): Pricing: $0.05 """ try: - from app.scanners.sentinel_pipeline import run_liquidity_verification + from app.domains.scanners.sentinel_pipeline import run_liquidity_verification result = await run_liquidity_verification(req.address, req.chain) await record_x402_payment("liquidity_verify", "0.05", req.address) @@ -173,7 +174,7 @@ async def dev_reputation_endpoint(req: SentinelModuleRequest): Requires dev_address (deployer/creator wallet). Falls back to address if dev_address not provided. """ try: - from app.scanners.sentinel_pipeline import run_dev_reputation + from app.domains.scanners.sentinel_pipeline import run_dev_reputation dev_wallet = req.dev_address or req.address result = await run_dev_reputation(dev_wallet, chains=[req.chain]) @@ -199,7 +200,7 @@ async def wash_trading_endpoint(req: SentinelModuleRequest): Pricing: $0.08 """ try: - from app.scanners.sentinel_pipeline import run_wash_trading + from app.domains.scanners.sentinel_pipeline import run_wash_trading result = await run_wash_trading(req.address, req.chain) await record_x402_payment("wash_trading", "0.08", req.address) @@ -254,7 +255,7 @@ async def metadata_fingerprint_endpoint(req: SentinelModuleRequest): Pricing: $0.05 """ try: - from app.scanners.sentinel_pipeline import run_metadata_fingerprint + from app.domains.scanners.sentinel_pipeline import run_metadata_fingerprint result = await run_metadata_fingerprint(req.address, req.chain) await record_x402_payment("metadata_fingerprint", "0.05", req.address) @@ -279,7 +280,7 @@ async def sentiment_check_endpoint(req: SentinelModuleRequest): Pricing: $0.05 """ try: - from app.scanners.sentinel_pipeline import run_sentiment + from app.domains.scanners.sentinel_pipeline import run_sentiment result = await run_sentiment(req.address, req.chain) await record_x402_payment("sentiment_check", "0.05", req.address) @@ -312,7 +313,7 @@ async def pumpfun_analysis_endpoint(req: SentinelModuleRequest): f"Received chain={req.chain}. Use chain='solana'.", ) - from app.scanners.sentinel_pipeline import run_pumpfun_analysis + from app.domains.scanners.sentinel_pipeline import run_pumpfun_analysis result = await run_pumpfun_analysis(req.address) await record_x402_payment("pumpfun_analysis", "0.08", req.address) diff --git a/app/domains/billing/x402/tools/label_tools.py b/app/domains/billing/x402/tools/label_tools.py index b2f0589..7c520b8 100644 --- a/app/domains/billing/x402/tools/label_tools.py +++ b/app/domains/billing/x402/tools/label_tools.py @@ -10,6 +10,7 @@ Resolved sources: Other SENTINEL modules live in tools/evidence_tools.py and tools/report_tools.py. """ + from __future__ import annotations from datetime import datetime @@ -34,7 +35,7 @@ async def address_labels_endpoint(req: SentinelModuleRequest): known scammer, deployer, etc.). """ try: - from app.scanners.sentinel_pipeline import run_address_labels + from app.domains.scanners.sentinel_pipeline import run_address_labels result = await run_address_labels(req.address, req.chain) await record_x402_payment("address_labels", "0.08", req.address) diff --git a/app/domains/billing/x402/tools/report_tools.py b/app/domains/billing/x402/tools/report_tools.py index 6df7e81..722c8ee 100644 --- a/app/domains/billing/x402/tools/report_tools.py +++ b/app/domains/billing/x402/tools/report_tools.py @@ -18,6 +18,7 @@ Address labels live in tools/label_tools.py. TIER 1 modules (holder analysis, bundle detect, etc.) live in tools/evidence_tools.py. """ + from __future__ import annotations from datetime import datetime @@ -42,7 +43,7 @@ async def flash_loan_detect_endpoint(req: SentinelModuleRequest): volume exceeding pool liquidity ratio. """ try: - from app.scanners.sentinel_pipeline import run_flash_loan_detection + from app.domains.scanners.sentinel_pipeline import run_flash_loan_detection result = await run_flash_loan_detection(req.address, req.chain) await record_x402_payment("flash_loan_detect", "0.08", req.address) @@ -70,7 +71,7 @@ async def pump_dump_detect_endpoint(req: SentinelModuleRequest): and rug pull lifecycle stage (deploy/pump/distribution/dump). """ try: - from app.scanners.sentinel_pipeline import run_pump_dump_detection + from app.domains.scanners.sentinel_pipeline import run_pump_dump_detection result = await run_pump_dump_detection(req.address, req.chain) await record_x402_payment("pump_dump_detect", "0.08", req.address) @@ -98,7 +99,7 @@ async def oracle_manipulation_endpoint(req: SentinelModuleRequest): Critical for lending/borrowing protocols that rely on price feeds. """ try: - from app.scanners.sentinel_pipeline import run_oracle_manipulation + from app.domains.scanners.sentinel_pipeline import run_oracle_manipulation result = await run_oracle_manipulation(req.address, req.chain) await record_x402_payment("oracle_manipulation", "0.08", req.address) @@ -157,7 +158,7 @@ async def proxy_detect_endpoint(req: SentinelModuleRequest): rug contract patterns. Essential for EVM tokens behind proxies. """ try: - from app.scanners.sentinel_pipeline import run_proxy_detection + from app.domains.scanners.sentinel_pipeline import run_proxy_detection result = await run_proxy_detection(req.address, req.chain) await record_x402_payment("proxy_detect", "0.08", req.address) @@ -185,7 +186,7 @@ async def static_analysis_endpoint(req: SentinelModuleRequest): and active threat alerts. """ try: - from app.scanners.sentinel_pipeline import run_static_analysis + from app.domains.scanners.sentinel_pipeline import run_static_analysis result = await run_static_analysis(req.address, req.chain) await record_x402_payment("static_analysis", "0.12", req.address) @@ -213,7 +214,7 @@ async def decompiler_analysis_endpoint(req: SentinelModuleRequest): Essential for tokens with unverified source code. """ try: - from app.scanners.sentinel_pipeline import run_decompiler_analysis + from app.domains.scanners.sentinel_pipeline import run_decompiler_analysis result = await run_decompiler_analysis(req.address, req.chain) await record_x402_payment("decompiler_analysis", "0.10", req.address) @@ -237,7 +238,7 @@ async def fund_flow_endpoint(req: SentinelModuleRequest): Pricing: $0.10 """ try: - from app.scanners.sentinel_pipeline import run_fund_flow + from app.domains.scanners.sentinel_pipeline import run_fund_flow result = await run_fund_flow(req.address, req.chain) await record_x402_payment("fund_flow", "0.10", req.address) @@ -265,7 +266,7 @@ async def contract_diff_endpoint(req: SentinelModuleRequest): (withdrawAll, drain, setOwner, emergencyWithdraw) and rug-specific bytecode patterns. """ try: - from app.scanners.sentinel_pipeline import run_contract_diff + from app.domains.scanners.sentinel_pipeline import run_contract_diff result = await run_contract_diff(req.address, req.chain) await record_x402_payment("contract_diff", "0.10", req.address) diff --git a/app/rug_imminence_predictor.py b/app/rug_imminence_predictor.py index 02f9687..3f6c50d 100644 --- a/app/rug_imminence_predictor.py +++ b/app/rug_imminence_predictor.py @@ -760,7 +760,7 @@ class RugImminencePredictor: try: # Try social velocity analyzer try: - from app.scanners.social_velocity import SocialVelocityAnalyzer + from app.domains.scanners.social_velocity import SocialVelocityAnalyzer analyzer = SocialVelocityAnalyzer() velocity = await analyzer.analyze(result.token_address) @@ -799,7 +799,7 @@ class RugImminencePredictor: # Check for recent FUD/panic signals try: - from app.scanners.sentiment_analyzer import SentimentAnalyzer + from app.domains.scanners.sentiment_analyzer import SentimentAnalyzer sentiment = SentimentAnalyzer() sent_result = await sentiment.analyze_token_sentiment( @@ -837,7 +837,7 @@ class RugImminencePredictor: try: # Try honeypot detector try: - from app.scanners.honeypot_detector import HoneypotDetector + from app.domains.scanners.honeypot_detector import HoneypotDetector hp = HoneypotDetector() hp_result = await hp.detect(result.token_address, result.chain) @@ -863,7 +863,7 @@ class RugImminencePredictor: # Check ownership status try: - from app.scanners.contract_authority import ContractAuthorityScanner + from app.domains.scanners.contract_authority import ContractAuthorityScanner auth = ContractAuthorityScanner() authority = await auth.scan(result.token_address, result.chain) @@ -907,7 +907,7 @@ class RugImminencePredictor: try: # Check recent LP changes via on-chain data try: - from app.scanners.liquidity_verifier import LiquidityVerifier + from app.domains.scanners.liquidity_verifier import LiquidityVerifier verifier = LiquidityVerifier() lp_status = await verifier.verify( @@ -959,7 +959,7 @@ class RugImminencePredictor: sig.flags.append("liquidity_drop") sig.evidence.append(f"Liquidity dropped {abs(liq_change):.0f}% in 24h") except Exception: - pass + logger.debug("liquidity change parse failed", exc_info=True) sig.confidence = min(0.7, 0.3 + (sig.score / 40)) @@ -1083,7 +1083,7 @@ class RugImminencePredictor: if pairs: return data except Exception: - pass + logger.debug("dexscreener fetch failed", exc_info=True) return None async def _resolve_deployer(self, address: str, chain: str) -> str | None: @@ -1120,9 +1120,8 @@ class RugImminencePredictor: }, ) # This doesn't work directly - need the creation tx - pass except Exception: - pass + logger.debug("deployer resolve failed", exc_info=True) return None async def _check_wallet_age(self, wallet: str, chain: str) -> dict | None: @@ -1180,7 +1179,7 @@ class RugImminencePredictor: result = {"locked": False, "unlocked": True, "unlock_date": None} try: - from app.scanners.liquidity_verifier import LiquidityVerifier + from app.domains.scanners.liquidity_verifier import LiquidityVerifier verifier = LiquidityVerifier() lp_info = await verifier.check_lock(address, chain) diff --git a/app/unified_scanner.py b/app/unified_scanner.py index cf896a9..19272fb 100644 --- a/app/unified_scanner.py +++ b/app/unified_scanner.py @@ -156,7 +156,7 @@ async def _cache_get(key: str) -> dict | None: if cached: return json.loads(cached) except Exception: - pass + logger.debug("wallet cache get failed", exc_info=True) return None @@ -175,7 +175,7 @@ async def _cache_set(key: str, data: dict, ttl: int = 3600): await r.setex(f"rmi:wallet_cache:{key}", ttl, json.dumps(data)) await r.close() except Exception: - pass + logger.debug("wallet cache set failed", exc_info=True) # ═══════════════════════════════════════════ @@ -232,6 +232,7 @@ async def _helius_call(method: str, params: list) -> dict | None: if resp.status_code in (401, 429): continue except Exception: + logger.debug("helius call failed, trying next key", exc_info=True) continue return None @@ -362,7 +363,7 @@ async def _fetch_moralis_wallet(address: str, chain: str) -> dict[str, Any]: result["tx_count"] = len(txs) result["data_sources"] = [*result.get("data_sources", []), "moralis_tx"] except Exception: - pass + logger.debug("moralis wallet fetch failed", exc_info=True) return result @@ -415,13 +416,15 @@ async def _fetch_etherscan_wallet(address: str, chain: str) -> dict[str, Any]: first_ts = int(txs[0].get("timeStamp", 0)) last_ts = int(txs[-1].get("timeStamp", 0)) if first_ts: - result["wallet_age_days"] = int((datetime.now(UTC).timestamp() - first_ts) / 86400) + result["wallet_age_days"] = int( + (datetime.now(UTC).timestamp() - first_ts) / 86400 + ) if last_ts: result["last_tx"] = last_ts result["tx_count_sample"] = len(txs) result["data_sources"] = ["etherscan"] except Exception: - pass + logger.debug("etherscan wallet fetch failed", exc_info=True) return result @@ -434,7 +437,7 @@ async def _fetch_etherscan_wallet(address: str, chain: str) -> dict[str, Any]: async def _fetch_sentinel_labels(address: str, chain: str) -> dict[str, Any]: """Get rich labels from SENTINEL address_labeler (6 sources).""" try: - from app.scanners.address_labeler import AddressLabeler + from app.domains.scanners.address_labeler import AddressLabeler labeler = AddressLabeler() result = await labeler.analyze(address, chain) @@ -477,7 +480,9 @@ async def _scan_held_tokens(tokens: list, chain: str) -> dict[str, Any]: return {"scanned": 0, "risks": [], "deployers_to_flag": []} # Sort by amount (largest first), cap at 5 for speed - sorted_tokens = sorted(tokens, key=lambda t: float(t.get("amount", 0) if isinstance(t, dict) else 0), reverse=True) + sorted_tokens = sorted( + tokens, key=lambda t: float(t.get("amount", 0) if isinstance(t, dict) else 0), reverse=True + ) mints = [] for token in sorted_tokens[:5]: # Top 5 by balance mint = token.get("mint", "") if isinstance(token, dict) else token @@ -722,8 +727,10 @@ async def _trace_funding_source(address: str, chain: str) -> dict[str, Any]: return { "source": source, "source_label": source_label.get("label", "unknown"), - "is_cex": "exchange" in str(source_label).lower() or "cex" in str(source_label).lower(), - "is_mixer": "mixer" in str(source_label).lower() or "tornado" in str(source_label).lower(), + "is_cex": "exchange" in str(source_label).lower() + or "cex" in str(source_label).lower(), + "is_mixer": "mixer" in str(source_label).lower() + or "tornado" in str(source_label).lower(), "hops": 1, } return {"source": "unknown", "hops": 0} @@ -735,7 +742,7 @@ async def _resolve_entity_free(address: str, chain: str) -> dict[str, Any]: """Basic cross-chain entity resolution - FREE tier (no Wallet Memory Bank).""" try: # Check SENTINEL labels first - from app.scanners.address_labeler import AddressLabeler + from app.domains.scanners.address_labeler import AddressLabeler labeler = AddressLabeler() labels = await labeler.analyze(address, chain) @@ -754,7 +761,9 @@ async def _resolve_entity_free(address: str, chain: str) -> dict[str, Any]: return {"label": "unknown", "source": "none", "chains": [chain]} -async def _calculate_basic_pnl(address: str, chain: str, tokens: list, native_balance: float = 0) -> dict[str, Any]: +async def _calculate_basic_pnl( + address: str, chain: str, tokens: list, native_balance: float = 0 +) -> dict[str, Any]: """Basic PnL calculation - FREE tier (no GMGN).""" try: total_value = native_balance or 0 @@ -765,14 +774,18 @@ async def _calculate_basic_pnl(address: str, chain: str, tokens: list, native_ba mints = [t.get("mint", "") for t in tokens[:10] if t.get("mint")] if mints: async with httpx.AsyncClient(timeout=8.0) as client: - resp = await client.get("https://quote-api.jup.ag/v6/price", params={"ids": ",".join(mints[:5])}) + resp = await client.get( + "https://quote-api.jup.ag/v6/price", params={"ids": ",".join(mints[:5])} + ) if resp.status_code == 200: prices = resp.json().get("data", {}) for mint, pdata in prices.items(): price = float(pdata.get("price", 0)) for t in tokens: if t.get("mint") == mint: - balance = float(t.get("amount", 0)) / (10 ** (t.get("decimals", 0) or 0)) + balance = float(t.get("amount", 0)) / ( + 10 ** (t.get("decimals", 0) or 0) + ) total_value += balance * price return { @@ -810,7 +823,9 @@ async def _close_data_flywheel(deployers: list): logger.warning(f"Data flywheel failed: {e}") -async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "free") -> dict[str, Any]: +async def scan_wallet( + wallet_address: str, chain: str = "solana", tier: str = "free" +) -> dict[str, Any]: """Unified wallet scanner - SENTINEL + Helius/Moralis/Etherscan pool + Wallet Memory + Token Scanner.""" import time @@ -852,7 +867,7 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f "sol_balance": bal / 1e9 if bal else 0, "data_sources": ["solana_public"], } - except Exception: + except Exception: # noqa: S110 pass else: # EVM chain: Moralis (primary) + Etherscan family (fallback) @@ -860,7 +875,9 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f etherscan = await _fetch_etherscan_wallet(wallet_address, chain) if etherscan and not onchain.get("wallet_age_days"): onchain["wallet_age_days"] = etherscan.get("wallet_age_days", 0) - onchain["data_sources"] = onchain.get("data_sources", []) + etherscan.get("data_sources", []) + onchain["data_sources"] = onchain.get("data_sources", []) + etherscan.get( + "data_sources", [] + ) if onchain: factors.wallet_age_days = onchain.get("wallet_age_days", 0) @@ -877,31 +894,38 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f # ── DexScreener volume + labels (FREE tier, all chains) ── try: async with httpx.AsyncClient(timeout=15.0) as client: - resp = await client.get("https://api.dexscreener.com/latest/dex/search", params={"q": wallet_address}) + resp = await client.get( + "https://api.dexscreener.com/latest/dex/search", params={"q": wallet_address} + ) if resp.status_code == 200: pairs = resp.json().get("pairs", []) if pairs: - factors.total_volume_usd = sum(float(p.get("volume", {}).get("h24", 0) or 0) for p in pairs) + factors.total_volume_usd = sum( + float(p.get("volume", {}).get("h24", 0) or 0) for p in pairs + ) factors.dex_swaps = len(pairs) factors.token_launches_participated = sum( 1 for p in pairs if p.get("pairCreatedAt") and ( - datetime.now(UTC) - datetime.fromtimestamp(p.get("pairCreatedAt", 0) / 1000, tz=UTC) + datetime.now(UTC) + - datetime.fromtimestamp(p.get("pairCreatedAt", 0) / 1000, tz=UTC) ).total_seconds() / 3600 < 24 ) data_sources.append("dexscreener") - except Exception: + except Exception: # noqa: S110 pass # ── SENTINEL address labels (6-source label resolution, FREE tier) ── sentinel_labels = await _fetch_sentinel_labels(wallet_address, chain) if sentinel_labels: factors.entity_label = sentinel_labels.get("category", "") - factors.is_labeled_scammer = factors.is_labeled_scammer or "scam" in str(sentinel_labels).lower() + factors.is_labeled_scammer = ( + factors.is_labeled_scammer or "scam" in str(sentinel_labels).lower() + ) factors.is_labeled_sanctioned = ( factors.is_labeled_sanctioned or "sanction" in str(sentinel_labels).lower() @@ -910,10 +934,14 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f factors.is_labeled_exchange = ( "exchange" in str(sentinel_labels).lower() or "cex" in str(sentinel_labels).lower() ) - factors.is_labeled_bot = "bot" in str(sentinel_labels).lower() or "mev" in str(sentinel_labels).lower() + factors.is_labeled_bot = ( + "bot" in str(sentinel_labels).lower() or "mev" in str(sentinel_labels).lower() + ) factors.is_labeled_whale = "whale" in str(sentinel_labels).lower() factors.is_labeled_insider = "insider" in str(sentinel_labels).lower() - factors.entity_confidence = max(factors.entity_confidence, sentinel_labels.get("risk_score", 50)) + factors.entity_confidence = max( + factors.entity_confidence, sentinel_labels.get("risk_score", 50) + ) data_sources.append("sentinel_labels") # ── FREE: External threat intelligence (HAPI, MistTrack, TRM sanctions) ── @@ -942,7 +970,9 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f wallet_memory = await _fetch_wallet_memory(wallet_address, chain) if wallet_memory: factors.linked_wallets_count = len(wallet_memory.get("linked_wallets", [])) - factors.cluster_size = wallet_memory.get("linked_wallets_count", factors.linked_wallets_count) + factors.cluster_size = wallet_memory.get( + "linked_wallets_count", factors.linked_wallets_count + ) factors.cluster_id = wallet_memory.get("entity_id", "") factors.cross_chain_funding = bool(wallet_memory.get("cross_chain_presence")) scam_assoc = wallet_memory.get("scam_associations", []) @@ -970,7 +1000,7 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f factors.unrealized_pnl_usd = float(pnl.get("unrealized", 0)) factors.win_rate = float(intel.get("win_rate", 0)) data_sources.append("gmgn") - except Exception: + except Exception: # noqa: S110 pass # Entity clustering @@ -983,8 +1013,10 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f factors.cluster_size = max(factors.cluster_size, cluster.get("size", 0)) factors.cluster_shared_funding = cluster.get("shared_funding", False) factors.known_cluster_type = cluster.get("type", "") - factors.linked_wallets_count = max(factors.linked_wallets_count, cluster.get("member_count", 0)) - except Exception: + factors.linked_wallets_count = max( + factors.linked_wallets_count, cluster.get("member_count", 0) + ) + except Exception: # noqa: S110 pass # ── FREE: Token portfolio risk scan (parallel + enrichments) ── @@ -999,7 +1031,7 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f # Data flywheel: flag deployers of scam tokens deployers = token_scan.get("deployers_to_flag", []) if deployers: - asyncio.create_task(_close_data_flywheel(deployers)) + asyncio.create_task(_close_data_flywheel(deployers)) # noqa: RUF006 # ── FREE: Funding source tracing ── funding = await _trace_funding_source(wallet_address, chain) @@ -1024,7 +1056,9 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f native_bal = onchain.get("sol_balance", onchain.get("native_balance", 0)) if onchain else 0 basic_pnl = await _calculate_basic_pnl(wallet_address, chain, token_list, native_bal) if basic_pnl: - factors.current_balance_usd = max(factors.current_balance_usd or 0, basic_pnl.get("estimated_value_usd", 0)) + factors.current_balance_usd = max( + factors.current_balance_usd or 0, basic_pnl.get("estimated_value_usd", 0) + ) factors.realized_pnl_usd = factors.realized_pnl_usd or 0 # keep if GMGN set it data_sources.append("jupiter_pnl") @@ -1037,7 +1071,7 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f anomaly = await detect_wallet_anomaly(wallet_address, chain) if anomaly: factors.sybil_score = anomaly.get("sybil_score", 0) - except Exception: + except Exception: # noqa: S110 pass # Portfolio tracker @@ -1053,7 +1087,7 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f factors.stablecoin_ratio = portfolio.get("stablecoin_pct", 0) factors.bluechip_ratio = portfolio.get("bluechip_pct", 0) factors.memecoin_ratio = portfolio.get("memecoin_pct", 0) - except Exception: + except Exception: # noqa: S110 pass # ═══════════════════════════════════════════ @@ -1285,7 +1319,7 @@ async def scan_wallet(wallet_address: str, chain: str = "solana", tier: str = "f } # Cache result for 1hr - asyncio.create_task(_cache_set(cache_key, result, ttl=3600)) + asyncio.create_task(_cache_set(cache_key, result, ttl=3600)) # noqa: RUF006 return result @@ -1320,7 +1354,9 @@ async def get_trending_tokens(chain: str | None = None, limit: int = 20) -> dict continue age_hours = 999 if created: - age_hours = (now - datetime.fromtimestamp(created / 1000, tz=UTC)).total_seconds() / 3600 + age_hours = ( + now - datetime.fromtimestamp(created / 1000, tz=UTC) + ).total_seconds() / 3600 trending_score = (vol * 0.6 + liq * 0.3) / max(age_hours, 1) trending.append( { @@ -1361,12 +1397,14 @@ async def get_trending_tokens(chain: str | None = None, limit: int = 20) -> dict "price_usd": float(attrs.get("base_token_price_usd", 0)), "liquidity_usd": float(attrs.get("reserve_in_usd", 0)), "volume_24h": float(attrs.get("volume_usd", {}).get("h24", 0)), - "price_change_24h": float(attrs.get("price_change_percentage", {}).get("h24", 0)), + "price_change_24h": float( + attrs.get("price_change_percentage", {}).get("h24", 0) + ), "source": "geckoterminal", } ) sources_used.append("geckoterminal") - except Exception: + except Exception: # noqa: S110 pass try: @@ -1388,7 +1426,7 @@ async def get_trending_tokens(chain: str | None = None, limit: int = 20) -> dict } ) sources_used.append("coingecko") - except Exception: + except Exception: # noqa: S110 pass seen = set() @@ -1432,7 +1470,9 @@ async def get_new_launches(chain: str = "solana", since_seconds: int = 300) -> d launches = [] async with httpx.AsyncClient(timeout=10.0) as client: try: - resp = await client.get("https://api.dexscreener.com/latest/dex/search", params={"q": chain, "limit": 50}) + resp = await client.get( + "https://api.dexscreener.com/latest/dex/search", params={"q": chain, "limit": 50} + ) if resp.status_code == 200: pairs = resp.json().get("pairs", []) for pair in pairs: @@ -1453,7 +1493,7 @@ async def get_new_launches(chain: str = "solana", since_seconds: int = 300) -> d "dex": pair.get("dexId", ""), } ) - except Exception: + except Exception: # noqa: S110 pass chain_key = chain or "all" diff --git a/app/wallet_memory/ingestion.py b/app/wallet_memory/ingestion.py index 53a8854..394f072 100644 --- a/app/wallet_memory/ingestion.py +++ b/app/wallet_memory/ingestion.py @@ -29,7 +29,7 @@ from typing import Any import httpx from app.chain_registry import is_evm, is_solana -from app.scanners.dev_reputation import ETHERSCAN_NETWORKS +from app.domains.scanners.dev_reputation import ETHERSCAN_NETWORKS logger = logging.getLogger("wallet_memory.ingestion") @@ -139,7 +139,7 @@ class NormalizedTransaction: tx_hash: str = "", tx_type: str = "transfer", token_address: str = "", - token_value_raw: str = "0", + token_value_raw: str = "0", # noqa: S107 token_value_decimal: float = 0.0, transfer_type: str = "native", block_number: int = 0, @@ -495,7 +495,9 @@ class WalletIngestionPipeline: # ── Normalization ────────────────────────────────────────────── - def _normalize_transactions(self, raw_txs: list[dict], address: str, chain: str) -> list[NormalizedTransaction]: + def _normalize_transactions( + self, raw_txs: list[dict], address: str, chain: str + ) -> list[NormalizedTransaction]: """Convert raw API responses into NormalizedTransaction objects.""" normalized = [] addr_lower = address.lower().strip() @@ -507,7 +509,9 @@ class WalletIngestionPipeline: return normalized - def _normalize_helius(self, txs: list[dict], address: str, chain: str) -> list[NormalizedTransaction]: + def _normalize_helius( + self, txs: list[dict], address: str, chain: str + ) -> list[NormalizedTransaction]: """Parse Helius Enhanced Transactions API response.""" normalized = [] @@ -516,7 +520,11 @@ class WalletIngestionPipeline: # Helius Enhanced Transaction schema tx_hash = tx.get("signature", "") timestamp_ms = tx.get("timestamp", 0) - ts = datetime.fromtimestamp(timestamp_ms, tz=UTC) if timestamp_ms else datetime.now(UTC) + ts = ( + datetime.fromtimestamp(timestamp_ms, tz=UTC) + if timestamp_ms + else datetime.now(UTC) + ) block_slot = tx.get("slot", 0) tx.get("fee", 0) or 0 @@ -540,8 +548,8 @@ class WalletIngestionPipeline: programs_involved.add(ip) # Detect token deployments: create/mint instructions - spl_token_program = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - token_metadata_program = "metaqbxxU9dKaqAta4M99M7C5pWm7adgGpSxq9BDpL4" + spl_token_program = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" # noqa: S105 + token_metadata_program = "metaqbxxU9dKaqAta4M99M7C5pWm7adgGpSxq9BDpL4" # noqa: S105 token_deploy_programs = {spl_token_program, token_metadata_program} if token_deploy_programs & programs_involved: is_deploy = True @@ -569,7 +577,9 @@ class WalletIngestionPipeline: timestamp=ts, amount=amount_sol, chain_id=chain, - program=",".join(programs_involved) if programs_involved else "system", + program=",".join(programs_involved) + if programs_involved + else "system", tx_hash=tx_hash, tx_type="send" if is_send else "receive", transfer_type="native", @@ -607,7 +617,9 @@ class WalletIngestionPipeline: timestamp=ts, amount=dec_val, chain_id=chain, - program=",".join(programs_involved) if programs_involved else "spl-token", + program=",".join(programs_involved) + if programs_involved + else "spl-token", tx_hash=tx_hash, tx_type="send" if is_send else "receive", token_address=token_addr, @@ -643,7 +655,9 @@ class WalletIngestionPipeline: return normalized - def _normalize_etherscan(self, txs: list[dict], address: str, chain: str) -> list[NormalizedTransaction]: + def _normalize_etherscan( + self, txs: list[dict], address: str, chain: str + ) -> list[NormalizedTransaction]: """Parse Etherscan-family API response.""" normalized = [] @@ -819,7 +833,9 @@ class WalletIngestionPipeline: is_scam=False, risk_score=0.0, ) - logger.info(f"Recorded deployer event: {tx.address} deployed {token_addr} on {tx.chain_id}") + logger.info( + f"Recorded deployer event: {tx.address} deployed {token_addr} on {tx.chain_id}" + ) except Exception as e: logger.debug(f"Deployer event record failed: {e}") diff --git a/app/wallet_memory/labels.py b/app/wallet_memory/labels.py index 0987a8b..2ead37c 100644 --- a/app/wallet_memory/labels.py +++ b/app/wallet_memory/labels.py @@ -55,7 +55,7 @@ class LabelService: for _chain, wallets in CEX_HOT_WALLETS.items(): for addr, label in wallets.items(): self._builtin_cex[addr.lower()] = label - except Exception: + except Exception: # noqa: S110 pass async def resolve(self, address: str, chain: str) -> dict[str, Any] | None: @@ -116,10 +116,14 @@ class LabelService: "address_name", label_data.get( "dapp_name", - label_data.get("protocol_name", label_data.get("label_type", "")), + label_data.get( + "protocol_name", label_data.get("label_type", "") + ), ), ), - "category": label_data.get("label_type", label_data.get("label_subtype", "")), + "category": label_data.get( + "label_type", label_data.get("label_subtype", "") + ), "confidence": 0.95, # Static labels are high confidence } ) @@ -149,7 +153,9 @@ class LabelService: all_labels.append( { "source": "omega_forensic", - "label": wallet_data.get("display_name", wallet_data.get("category", "")), + "label": wallet_data.get( + "display_name", wallet_data.get("category", "") + ), "category": cat, "confidence": 0.9, } @@ -233,7 +239,7 @@ class LabelService: async def _query_etherscan(self, address: str, chain: str) -> list[dict]: """Query Etherscan account API for address labels.""" - from app.scanners.dev_reputation import ETHERSCAN_NETWORKS + from app.domains.scanners.dev_reputation import ETHERSCAN_NETWORKS network = ETHERSCAN_NETWORKS.get(chain) if not network: @@ -286,11 +292,13 @@ class LabelService: now = time.time() if _ROLODETH_CACHE is None or (now - _ROLODETH_LAST_FETCH) > _ROLODETH_TTL: try: - resp = await self._http.get("https://raw.githubusercontent.com/RoleTiker/rolodETH/main/rolodETH.json") + resp = await self._http.get( + "https://raw.githubusercontent.com/RoleTiker/rolodETH/main/rolodETH.json" + ) if resp.status_code == 200: _ROLODETH_CACHE = resp.json() _ROLODETH_LAST_FETCH = now - except Exception: + except Exception: # noqa: S110 pass if not _ROLODETH_CACHE: @@ -411,7 +419,10 @@ class LabelService: sorted_labels = sorted( labels, - key=lambda line: (priority.get(line.get("category", "unknown"), 8), -line.get("confidence", 0)), + key=lambda line: ( + priority.get(line.get("category", "unknown"), 8), + -line.get("confidence", 0), + ), ) return sorted_labels[0] if sorted_labels else {"label": "", "category": "unknown"} diff --git a/mypy.ini b/mypy.ini index 05d6aa4..efdbb65 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,8 +1,4 @@ [mypy] strict = true ignore_missing_imports = true -exclude = (?x)( - .venv/ - | tests/ - | __pycache__/ -) +exclude = (\.venv/|tests/|__pycache__/) diff --git a/pyproject.toml b/pyproject.toml index 7bc7f6f..f8a6440 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,8 +120,8 @@ warn_unreachable = true [[tool.mypy.overrides]] # Domain layer is the strictest — no FastAPI leakage allowed. -module = ["app.domain.*"] -disallow_any_express_imports = true +module = ["app.domains.*"] +disallow_any_explicit = true disallow_any_decorated = false # Pydantic decorators need this no_implicit_optional = true warn_return_any = true @@ -129,7 +129,7 @@ warn_return_any = true [[tool.mypy.overrides]] # Core (cross-cutting) is also strict. module = ["app.core.*"] -disallow_any_express_imports = true +disallow_any_explicit = true no_implicit_optional = true [[tool.mypy.overrides]] diff --git a/tests/unit/domain/reports/test_citation_validator.py b/tests/unit/domain/reports/test_citation_validator.py index 15c1bf8..aeadd94 100644 --- a/tests/unit/domain/reports/test_citation_validator.py +++ b/tests/unit/domain/reports/test_citation_validator.py @@ -1,7 +1,6 @@ """Tests for app/domain/reports/citation_validator.py""" - -from app.domain.reports.citation_validator import validate_section +from app.domains.reports.citation_validator import validate_section class TestCitationValidator: @@ -10,34 +9,32 @@ class TestCitationValidator: def test_valid_citation(self): """Test that valid citations pass validation.""" result = validate_section( - 'This token has a risk score of 75/100 [1]. It is flagged [2].', - ['Risk score 75/100 detected', 'Token flagged as suspicious'], - on_unciteable='strip' + "This token has a risk score of 75/100 [1]. It is flagged [2].", + ["Risk score 75/100 detected", "Token flagged as suspicious"], + on_unciteable="strip", ) - assert result['validation_rate'] == 1.0 - assert result['unciteable_count'] == 0 - assert '75/100' in result['validated_text'] + assert result["validation_rate"] == 1.0 + assert result["unciteable_count"] == 0 + assert "75/100" in result["validated_text"] def test_invalid_citation(self): """Test that invalid citations are stripped.""" result = validate_section( - 'This claim [99] is invalid [1].', - ['Only source 1 available'], - on_unciteable='strip' + "This claim [99] is invalid [1].", ["Only source 1 available"], on_unciteable="strip" ) - assert result['validation_rate'] == 0.0 - assert result['unciteable_count'] == 1 - assert '[Data not available]' in result['validated_text'] + assert result["validation_rate"] == 0.0 + assert result["unciteable_count"] == 1 + assert "[Data not available]" in result["validated_text"] def test_no_citations(self): """Test that text without citations is marked unciteable.""" result = validate_section( - 'This has no citations but should match source.', - ['Source text for validation'], - on_unciteable='strip' + "This has no citations but should match source.", + ["Source text for validation"], + on_unciteable="strip", ) - assert result['validation_rate'] == 0.0 - assert result['unciteable_count'] == 1 + assert result["validation_rate"] == 0.0 + assert result["unciteable_count"] == 1 def test_multiple_citations_same_sentence(self): """Test that multiple citations in one sentence are parsed. @@ -49,13 +46,13 @@ class TestCitationValidator: the support check (unciteable_count > 0). """ result = validate_section( - 'This is supported by [1,2,3].', - ['Source one content', 'Source two content', 'Source three content'], - on_unciteable='strip' + "This is supported by [1,2,3].", + ["Source one content", "Source two content", "Source three content"], + on_unciteable="strip", ) # Parsing succeeded (multi-citation [1,2,3] all valid indices) # But claim doesn't actually overlap with any source content - assert result['unciteable_count'] == 1 # claim unsupported + assert result["unciteable_count"] == 1 # claim unsupported def test_citation_range(self): """Test that citation ranges [1-3] are parsed correctly. @@ -65,65 +62,51 @@ class TestCitationValidator: with 'Source one content' terms, so should fail support. """ result = validate_section( - 'This is supported by [1-2].', - ['Source one content', 'Source two content', 'Source three content'], - on_unciteable='strip' + "This is supported by [1-2].", + ["Source one content", "Source two content", "Source three content"], + on_unciteable="strip", ) # Parsed correctly (range expanded) # But claim doesn't match source content - assert result['unciteable_count'] == 1 + assert result["unciteable_count"] == 1 def test_empty_sources(self): """Test that empty sources list marks all as unciteable.""" - result = validate_section( - 'Some text [1] with citations.', - [], - on_unciteable='strip' - ) - assert result['validation_rate'] == 0.0 - assert 'Data not available' in result['validated_text'] + result = validate_section("Some text [1] with citations.", [], on_unciteable="strip") + assert result["validation_rate"] == 0.0 + assert "Data not available" in result["validated_text"] def test_keep_unciteable(self): """Test that on_unciteable='keep' preserves unciteable text.""" result = validate_section( - 'Some text [99] invalid.', - ['Source one content'], - on_unciteable='keep' + "Some text [99] invalid.", ["Source one content"], on_unciteable="keep" ) - assert result['unciteable_count'] == 1 - assert 'Some text [99] invalid.' in result['validated_text'] + assert result["unciteable_count"] == 1 + assert "Some text [99] invalid." in result["validated_text"] def test_strip_unciteable(self): """Test that on_unciteable='strip' removes unciteable text.""" result = validate_section( - 'Some text [99] invalid.', - ['Source one content'], - on_unciteable='strip' + "Some text [99] invalid.", ["Source one content"], on_unciteable="strip" ) - assert result['unciteable_count'] == 1 - assert 'Data not available' in result['validated_text'] + assert result["unciteable_count"] == 1 + assert "Data not available" in result["validated_text"] def test_validation_report_structure(self): """Test that the validation report has the correct structure.""" - result = validate_section( - 'Test [1].', - ['Source one content'] - ) - assert 'validated_text' in result - assert 'citations' in result - assert 'unciteable_count' in result - assert 'validation_rate' in result - assert isinstance(result['citations'], list) + result = validate_section("Test [1].", ["Source one content"]) + assert "validated_text" in result + assert "citations" in result + assert "unciteable_count" in result + assert "validation_rate" in result + assert isinstance(result["citations"], list) def test_citation_details(self): """Test that citations include claim, source_idx, source_text, supported.""" - result = validate_section( - 'Test [1].', - ['Source one content'] - ) - if result['citations']: - c = result['citations'][0] - assert 'claim' in c - assert 'source_idx' in c - assert 'source_text' in c - assert 'supported' in c + result = validate_section("Test [1].", ["Source one content"]) + if result["citations"]: + c = result["citations"][0] + assert "claim" in c + assert "source_idx" in c + assert "source_text" in c + assert "supported" in c diff --git a/tests/unit/domain/reports/test_citation_validator_edge_cases.py b/tests/unit/domain/reports/test_citation_validator_edge_cases.py index 9333c4d..79c85b5 100644 --- a/tests/unit/domain/reports/test_citation_validator_edge_cases.py +++ b/tests/unit/domain/reports/test_citation_validator_edge_cases.py @@ -1,7 +1,6 @@ """Tests for app/domain/reports/citation_validator edge cases.""" - -from app.domain.reports.citation_validator import validate_section +from app.domains.reports.citation_validator import validate_section class TestCitationValidatorEdgeCases: @@ -16,14 +15,10 @@ class TestCitationValidatorEdgeCases: 'Source 0' / 'Source 4' content - so claim is unciteable. """ sources = [f"Source {i}" for i in range(20)] - result = validate_section( - 'Test [1-5] citation.', - sources, - on_unciteable='strip' - ) + result = validate_section("Test [1-5] citation.", sources, on_unciteable="strip") # Parsing correct (1-5 expanded to 5 indices) # But claim doesn't overlap with source content - assert result['unciteable_count'] == 1 + assert result["unciteable_count"] == 1 def test_overlapping_citations(self): """Test overlapping citations like [1, 2-3, 4]. @@ -33,43 +28,29 @@ class TestCitationValidatorEdgeCases: doesn't overlap with 'Source N' content. """ sources = ["Source 1", "Source 2", "Source 3", "Source 4"] - result = validate_section( - 'Test [1, 2-3, 4] citation.', - sources, - on_unciteable='strip' - ) + result = validate_section("Test [1, 2-3, 4] citation.", sources, on_unciteable="strip") # Parsing correct ([1, 2-3, 4] expanded to [1, 2, 3, 4]) # But content overlap fails for generic claim - assert result['unciteable_count'] == 1 + assert result["unciteable_count"] == 1 def test_single_char_source_text(self): """Test with very short source text.""" - result = validate_section( - 'Test [1] citation.', - ['a'], - on_unciteable='strip' - ) - assert 'validated_text' in result - assert 'citations' in result + result = validate_section("Test [1] citation.", ["a"], on_unciteable="strip") + assert "validated_text" in result + assert "citations" in result def test_source_text_with_only_stopwords(self): """Test with source text that has only stopwords.""" - result = validate_section( - 'Test [1] citation.', - ['the and is a'], - on_unciteable='strip' - ) - assert 'validated_text' in result + result = validate_section("Test [1] citation.", ["the and is a"], on_unciteable="strip") + assert "validated_text" in result def test_very_long_text(self): """Test with very long input text.""" long_text = "This is a very long sentence. " * 100 result = validate_section( - long_text + " [1].", - ["Source text for validation"], - on_unciteable='strip' + long_text + " [1].", ["Source text for validation"], on_unciteable="strip" ) - assert 'validated_text' in result + assert "validated_text" in result def test_multiple_sentences_with_citations(self): """Test multiple sentences each with different citations.""" @@ -81,10 +62,10 @@ class TestCitationValidatorEdgeCases: result = validate_section( "First sentence [1]. Second sentence [2]. Third sentence [3].", sources, - on_unciteable='strip' + on_unciteable="strip", ) - assert result['validation_rate'] == 1.0 - assert result['unciteable_count'] == 0 + assert result["validation_rate"] == 1.0 + assert result["unciteable_count"] == 0 def test_mixed_citeable_and_unciteable(self): """Test mix of citeable and unciteable content. @@ -97,25 +78,19 @@ class TestCitationValidatorEdgeCases: """ sources = ["Source one"] result = validate_section( - "Valid [1]. Invalid [99]. Also valid [1].", - sources, - on_unciteable='strip' + "Valid [1]. Invalid [99]. Also valid [1].", sources, on_unciteable="strip" ) # All 3 sentences fail content overlap check (with strict default) # The 1st and 3rd have valid index [1] but claim "Valid" doesn't # overlap with "Source one" content. The 2nd has out-of-range [99]. - assert result['unciteable_count'] == 3 + assert result["unciteable_count"] == 3 def test_citation_without_closing_bracket(self): """Test citation with missing closing bracket (malformed).""" sources = ["Source one"] - result = validate_section( - 'Test [1 unciteable.', - sources, - on_unciteable='strip' - ) + result = validate_section("Test [1 unciteable.", sources, on_unciteable="strip") # Should still process and mark as unciteable - assert 'validated_text' in result + assert "validated_text" in result def test_citation_with_extra_whitespace(self): """Test citation with extra whitespace like [ 1 , 2 ]. @@ -125,11 +100,7 @@ class TestCitationValidatorEdgeCases: but claim text 'Test citation' doesn't overlap with sources. """ sources = ["Source 1", "Source 2"] - result = validate_section( - 'Test [ 1 , 2 ] citation.', - sources, - on_unciteable='strip' - ) + result = validate_section("Test [ 1 , 2 ] citation.", sources, on_unciteable="strip") # Parsed [ 1 , 2 ] → [1, 2] successfully (whitespace tolerated) # But content overlap fails for generic claim - assert result['unciteable_count'] == 1 + assert result["unciteable_count"] == 1 diff --git a/tests/unit/domain/reports/test_report_generation.py b/tests/unit/domain/reports/test_report_generation.py index 94580b6..cfb8bdd 100644 --- a/tests/unit/domain/reports/test_report_generation.py +++ b/tests/unit/domain/reports/test_report_generation.py @@ -1,7 +1,6 @@ """Tests for app/domain/reports/generator.py - report generation tests.""" - -from app.domain.reports.generator import ( +from app.domains.reports.generator import ( _compute_risk_token, _compute_risk_wallet, ) @@ -13,14 +12,18 @@ class TestRiskComputation: def test_compute_risk_token_low_risk(self): """Test risk score for low-risk token.""" token_data = { - "token": type('Token', (), { - 'is_honeypot': False, - 'is_mintable': False, - 'is_proxy': False, - 'tax_buy_bps': 100, - 'tax_sell_bps': 100, - 'risk_factors': [], - })() + "token": type( + "Token", + (), + { + "is_honeypot": False, + "is_mintable": False, + "is_proxy": False, + "tax_buy_bps": 100, + "tax_sell_bps": 100, + "risk_factors": [], + }, + )() } score, _factors, tier = _compute_risk_token(token_data) assert score < 25 @@ -38,14 +41,18 @@ class TestRiskComputation: A score of 100 with all those flags = CRITICAL. """ token_data = { - "token": type('Token', (), { - 'is_honeypot': True, - 'is_mintable': True, - 'is_proxy': True, - 'tax_buy_bps': 2000, - 'tax_sell_bps': 2000, - 'risk_factors': ['test1', 'test2'], - })() + "token": type( + "Token", + (), + { + "is_honeypot": True, + "is_mintable": True, + "is_proxy": True, + "tax_buy_bps": 2000, + "tax_sell_bps": 2000, + "risk_factors": ["test1", "test2"], + }, + )() } score, _factors, tier = _compute_risk_token(token_data) assert score >= 75 # Multiple critical flags → high score @@ -54,14 +61,18 @@ class TestRiskComputation: def test_compute_risk_token_max_risk(self): """Test risk score is capped at 100.""" token_data = { - "token": type('Token', (), { - 'is_honeypot': True, - 'is_mintable': True, - 'is_proxy': True, - 'tax_buy_bps': 5000, - 'tax_sell_bps': 5000, - 'risk_factors': ['a', 'b', 'c', 'd', 'e'], - })() + "token": type( + "Token", + (), + { + "is_honeypot": True, + "is_mintable": True, + "is_proxy": True, + "tax_buy_bps": 5000, + "tax_sell_bps": 5000, + "risk_factors": ["a", "b", "c", "d", "e"], + }, + )() } score, _factors, _tier = _compute_risk_token(token_data) assert score == 100 # Should be capped @@ -69,10 +80,14 @@ class TestRiskComputation: def test_compute_risk_wallet_low_risk(self): """Test risk score for low-risk wallet.""" wallet_data = { - "wallet": type('Wallet', (), { - 'is_suspicious': False, - 'tx_count': 100, - })(), + "wallet": type( + "Wallet", + (), + { + "is_suspicious": False, + "tx_count": 100, + }, + )(), "entity": {}, "news": [], } @@ -83,10 +98,14 @@ class TestRiskComputation: def test_compute_risk_wallet_high_risk(self): """Test risk score for high-risk wallet.""" wallet_data = { - "wallet": type('Wallet', (), { - 'is_suspicious': True, - 'tx_count': 15000, - })(), + "wallet": type( + "Wallet", + (), + { + "is_suspicious": True, + "tx_count": 15000, + }, + )(), "entity": {"wallets": ["a", "b", "c", "d", "e"]}, "news": [], } @@ -100,7 +119,8 @@ class TestTemplateFallback: def test_template_fallback_executive_summary(self): """Test executive summary template.""" - from app.domain.reports.generator import _template_fallback + from app.domains.reports.generator import _template_fallback + ctx = { "subject_id": "eth:0x123", "risk_score": 75, @@ -114,7 +134,8 @@ class TestTemplateFallback: def test_template_fallback_recommendation(self): """Test recommendation template.""" - from app.domain.reports.generator import _template_fallback + from app.domains.reports.generator import _template_fallback + ctx = { "subject_id": "eth:0x123", "risk_score": 75, diff --git a/tests/unit/domain/reports/test_report_router.py b/tests/unit/domain/reports/test_report_router.py index 364fad1..a48f39d 100644 --- a/tests/unit/domain/reports/test_report_router.py +++ b/tests/unit/domain/reports/test_report_router.py @@ -1,6 +1,6 @@ """Tests for app/domain/reports/router.py - report router tests.""" -from app.domain.reports.router import router +from app.domains.reports.router import router class TestReportRouter: diff --git a/tests/unit/domain/scanner/test_service.py b/tests/unit/domain/scanner/test_service.py index 928c79c..9a06b35 100644 --- a/tests/unit/domain/scanner/test_service.py +++ b/tests/unit/domain/scanner/test_service.py @@ -10,9 +10,9 @@ import pytest @pytest.mark.asyncio async def test_scan_token_exists(): """Test that scan_token function exists in scanner service.""" - from app.domain.scanner import service + from app.domains.scanner import service - assert hasattr(service, 'scan_token') + assert hasattr(service, "scan_token") assert callable(service.scan_token) @@ -21,13 +21,13 @@ async def test_scan_token_signature(): """Test scan_token function signature.""" import inspect - from app.domain.scanner import service + from app.domains.scanner import service sig = inspect.signature(service.scan_token) params = list(sig.parameters.keys()) - assert 'token_address' in params - assert 'chain' in params + assert "token_address" in params + assert "chain" in params @pytest.mark.asyncio @@ -35,20 +35,20 @@ async def test_scan_token_defaults(): """Test scan_token has proper defaults.""" import inspect - from app.domain.scanner import service + from app.domains.scanner import service sig = inspect.signature(service.scan_token) - assert sig.parameters['chain'].default == 'ethereum' - assert sig.parameters['tiers'].default is None - assert sig.parameters['include_market_data'].default is True - assert sig.parameters['include_social'].default is True + assert sig.parameters["chain"].default == "ethereum" + assert sig.parameters["tiers"].default is None + assert sig.parameters["include_market_data"].default is True + assert sig.parameters["include_social"].default is True @pytest.mark.asyncio async def test_scan_token_is_async(): """Test scan_token is an async function.""" - from app.domain.scanner import service + from app.domains.scanner import service # Check it's a coroutine function assert asyncio.iscoroutinefunction(service.scan_token) diff --git a/tests/unit/test_t03_t12_m3_residual.py b/tests/unit/test_t03_t12_m3_residual.py index 3e737fd..07d6a0b 100644 --- a/tests/unit/test_t03_t12_m3_residual.py +++ b/tests/unit/test_t03_t12_m3_residual.py @@ -1,10 +1,11 @@ """Unit tests for T03 (news clusterer) and T12 (CertStream match_brand).""" + from __future__ import annotations from datetime import UTC, datetime, timedelta -from app.domain.news.clusterer import NewsItem, cluster_items -from app.domain.threat.certstream_listener import match_brand +from app.domains.news.clusterer import NewsItem, cluster_items +from app.domains.threat.certstream_listener import match_brand # ── T03: clusterer tests ─────────────────────────────────────────── @@ -16,19 +17,25 @@ def test_clusterer_groups_similar_stories(): id="a1", title="Bitcoin hits new all-time high above 120000", body="BTC surged past 120000 today as ETF inflows hit record", - source="coindesk", url="https://coindesk.com/1", published_at=base, + source="coindesk", + url="https://coindesk.com/1", + published_at=base, ), NewsItem( id="a2", title="Bitcoin hits new all-time high above 120000", body="BTC surged past 120000 today as ETF inflows hit record", - source="the block", url="https://theblock.co/2", published_at=base + timedelta(minutes=5), + source="the block", + url="https://theblock.co/2", + published_at=base + timedelta(minutes=5), ), NewsItem( id="b1", title="Ethereum upgrade scheduled for next month", body="Core developers announce Pectra hard fork for July", - source="decrypt", url="https://decrypt.co/3", published_at=base + timedelta(minutes=2), + source="decrypt", + url="https://decrypt.co/3", + published_at=base + timedelta(minutes=2), ), ] stories = cluster_items(items) @@ -46,9 +53,12 @@ def test_clusterer_handles_singleton(): base = datetime(2026, 6, 23, 12, 0, tzinfo=UTC) items = [ NewsItem( - id="x1", title="Unique story nobody else is covering", + id="x1", + title="Unique story nobody else is covering", body="Something happened once", - source="reddit", url="https://reddit.com/x", published_at=base, + source="reddit", + url="https://reddit.com/x", + published_at=base, ), ] stories = cluster_items(items) @@ -61,14 +71,20 @@ def test_clusterer_respects_time_window(): base = datetime(2026, 6, 23, 12, 0, tzinfo=UTC) items = [ NewsItem( - id="m1", title="Bitcoin hits new high", + id="m1", + title="Bitcoin hits new high", body="BTC surged past $120K", - source="coindesk", url="", published_at=base, + source="coindesk", + url="", + published_at=base, ), NewsItem( - id="m2", title="Bitcoin hits new high", + id="m2", + title="Bitcoin hits new high", body="BTC surged past $120K", - source="the block", url="", published_at=base + timedelta(hours=2), + source="the block", + url="", + published_at=base + timedelta(hours=2), ), ] # With 30-min windows, these are in separate buckets → 2 singleton stories diff --git a/tests/unit/test_t11_comprehensive.py b/tests/unit/test_t11_comprehensive.py index 2b09f7a..68c0125 100644 --- a/tests/unit/test_t11_comprehensive.py +++ b/tests/unit/test_t11_comprehensive.py @@ -22,88 +22,74 @@ def run_test(name, fn): # ───────────────────────────────────────────────────────────────────── # CITATION VALIDATOR TESTS # ───────────────────────────────────────────────────────────────────── -print("="*60) +print("=" * 60) print("CITATION VALIDATOR TESTS") -print("="*60) +print("=" * 60) -from app.domain.reports.citation_validator import validate_section # noqa: E402 +from app.domains.reports.citation_validator import validate_section # noqa: E402 def test_valid_citation(): result = validate_section( - 'Risk score 75/100 [1]. Token flagged [2].', - ['Risk score 75/100 detected', 'Token flagged as suspicious'], - on_unciteable='strip' + "Risk score 75/100 [1]. Token flagged [2].", + ["Risk score 75/100 detected", "Token flagged as suspicious"], + on_unciteable="strip", ) - assert result['validation_rate'] == 1.0 - assert result['unciteable_count'] == 0 + assert result["validation_rate"] == 1.0 + assert result["unciteable_count"] == 0 def test_invalid_citation(): result = validate_section( - 'Risk score 75/100 [99].', - ['Only source 1 available'], - on_unciteable='strip' + "Risk score 75/100 [99].", ["Only source 1 available"], on_unciteable="strip" ) - assert result['unciteable_count'] == 1 - assert 'Data not available' in result['validated_text'] + assert result["unciteable_count"] == 1 + assert "Data not available" in result["validated_text"] def test_no_citations(): - result = validate_section( - 'This has no citations.', - ['Source text'], - on_unciteable='strip' - ) - assert result['validation_rate'] == 0.0 - assert result['unciteable_count'] == 1 + result = validate_section("This has no citations.", ["Source text"], on_unciteable="strip") + assert result["validation_rate"] == 0.0 + assert result["unciteable_count"] == 1 def test_empty_sources(): - result = validate_section( - 'Some text [1].', - [], - on_unciteable='strip' - ) - assert result['validation_rate'] == 0.0 - assert 'Data not available' in result['validated_text'] + result = validate_section("Some text [1].", [], on_unciteable="strip") + assert result["validation_rate"] == 0.0 + assert "Data not available" in result["validated_text"] def test_validation_report_structure(): - result = validate_section('Test [1].', ['Source']) - assert 'validated_text' in result - assert 'citations' in result - assert 'unciteable_count' in result - assert 'validation_rate' in result - assert isinstance(result['citations'], list) + result = validate_section("Test [1].", ["Source"]) + assert "validated_text" in result + assert "citations" in result + assert "unciteable_count" in result + assert "validation_rate" in result + assert isinstance(result["citations"], list) def test_citation_range(): result = validate_section( - 'Token risk is high [1-2].', - ['Token risk is high', 'Risk score elevated'], - on_unciteable='strip' + "Token risk is high [1-2].", + ["Token risk is high", "Risk score elevated"], + on_unciteable="strip", ) - assert result['validation_rate'] == 1.0 + assert result["validation_rate"] == 1.0 def test_multiple_citations(): result = validate_section( - 'Token is risky [1]. Risk factors detected [2]. High buy tax [3].', - ['Token is risky', 'Risk factors detected', 'High buy tax detected'], - on_unciteable='strip' + "Token is risky [1]. Risk factors detected [2]. High buy tax [3].", + ["Token is risky", "Risk factors detected", "High buy tax detected"], + on_unciteable="strip", ) - assert result['validation_rate'] == 1.0 + assert result["validation_rate"] == 1.0 def test_keep_unciteable(): - result = validate_section( - 'Test [99].', - ['Source'], - on_unciteable='keep' - ) - assert result['unciteable_count'] == 1 - assert 'Test [99].' in result['validated_text'] + result = validate_section("Test [99].", ["Source"], on_unciteable="keep") + assert result["unciteable_count"] == 1 + assert "Test [99]." in result["validated_text"] print("\nRunning citation validator tests...") @@ -120,9 +106,9 @@ run_test("test_keep_unciteable", test_keep_unciteable) # ───────────────────────────────────────────────────────────────────── # HEALTH MODULE TESTS # ───────────────────────────────────────────────────────────────────── -print("\n" + "="*60) +print("\n" + "=" * 60) print("HEALTH MODULE TESTS") -print("="*60) +print("=" * 60) from app.core.health import DomainHealth, register_health_check # noqa: E402 @@ -165,6 +151,7 @@ def test_domain_health_large_details(): def test_health_registry(): def mock_health(): return DomainHealth(name="mock", healthy=True) + register_health_check("mock", mock_health) @@ -181,53 +168,89 @@ run_test("test_health_registry", test_health_registry) # ───────────────────────────────────────────────────────────────────── # RISK COMPUTATION TESTS # ───────────────────────────────────────────────────────────────────── -print("\n" + "="*60) +print("\n" + "=" * 60) print("RISK COMPUTATION TESTS") -print("="*60) +print("=" * 60) -from app.domain.reports.generator import _compute_risk_token, _compute_risk_wallet # noqa: E402 +from app.domains.reports.generator import _compute_risk_token, _compute_risk_wallet # noqa: E402 def test_compute_risk_token_low(): - token_data = {"token": type('Token', (), { - 'is_honeypot': False, 'is_mintable': False, 'is_proxy': False, - 'tax_buy_bps': 100, 'tax_sell_bps': 100, 'risk_factors': [], - })()} + token_data = { + "token": type( + "Token", + (), + { + "is_honeypot": False, + "is_mintable": False, + "is_proxy": False, + "tax_buy_bps": 100, + "tax_sell_bps": 100, + "risk_factors": [], + }, + )() + } score, _factors, tier = _compute_risk_token(token_data) assert score < 25 assert tier.name == "LOW" def test_compute_risk_token_high(): - token_data = {"token": type('Token', (), { - 'is_honeypot': True, 'is_mintable': True, 'is_proxy': True, - 'tax_buy_bps': 2000, 'tax_sell_bps': 2000, 'risk_factors': ['a', 'b'], - })()} + token_data = { + "token": type( + "Token", + (), + { + "is_honeypot": True, + "is_mintable": True, + "is_proxy": True, + "tax_buy_bps": 2000, + "tax_sell_bps": 2000, + "risk_factors": ["a", "b"], + }, + )() + } score, _factors, tier = _compute_risk_token(token_data) assert score >= 75 assert tier.name in ["HIGH", "CRITICAL"] def test_compute_risk_token_max(): - token_data = {"token": type('Token', (), { - 'is_honeypot': True, 'is_mintable': True, 'is_proxy': True, - 'tax_buy_bps': 5000, 'tax_sell_bps': 5000, 'risk_factors': ['a', 'b', 'c', 'd', 'e'], - })()} + token_data = { + "token": type( + "Token", + (), + { + "is_honeypot": True, + "is_mintable": True, + "is_proxy": True, + "tax_buy_bps": 5000, + "tax_sell_bps": 5000, + "risk_factors": ["a", "b", "c", "d", "e"], + }, + )() + } score, _factors, _tier = _compute_risk_token(token_data) assert score == 100 def test_compute_risk_wallet_low(): - wallet_data = {"wallet": type('Wallet', (), {'is_suspicious': False, 'tx_count': 100})(), - "entity": {}, "news": []} + wallet_data = { + "wallet": type("Wallet", (), {"is_suspicious": False, "tx_count": 100})(), + "entity": {}, + "news": [], + } score, _factors, tier = _compute_risk_wallet(wallet_data) assert score < 25 assert tier.name == "LOW" def test_compute_risk_wallet_high(): - wallet_data = {"wallet": type('Wallet', (), {'is_suspicious': True, 'tx_count': 15000})(), - "entity": {"wallets": ["a", "b", "c", "d", "e"]}, "news": []} + wallet_data = { + "wallet": type("Wallet", (), {"is_suspicious": True, "tx_count": 15000})(), + "entity": {"wallets": ["a", "b", "c", "d", "e"]}, + "news": [], + } score, _factors, tier = _compute_risk_wallet(wallet_data) assert score >= 50 assert tier.name in ["MEDIUM", "HIGH", "CRITICAL"] @@ -244,22 +267,28 @@ run_test("test_compute_risk_wallet_high", test_compute_risk_wallet_high) # ───────────────────────────────────────────────────────────────────── # TEMPLATE FALLBACK TESTS # ───────────────────────────────────────────────────────────────────── -print("\n" + "="*60) +print("\n" + "=" * 60) print("TEMPLATE FALLBACK TESTS") -print("="*60) +print("=" * 60) -from app.domain.reports.generator import _template_fallback # noqa: E402 +from app.domains.reports.generator import _template_fallback # noqa: E402 def test_template_fallback_executive_summary(): - result = _template_fallback("executive_summary", {"subject_id": "eth:0x1", "risk_score": 75, "risk_tier": "HIGH", "risk_factors": "test"}) + result = _template_fallback( + "executive_summary", + {"subject_id": "eth:0x1", "risk_score": 75, "risk_tier": "HIGH", "risk_factors": "test"}, + ) assert "Executive Summary" in result assert "75" in result assert "HIGH" in result def test_template_fallback_recommendation(): - result = _template_fallback("recommendation", {"subject_id": "eth:0x1", "risk_score": 75, "risk_tier": "HIGH", "risk_factors": "test"}) + result = _template_fallback( + "recommendation", + {"subject_id": "eth:0x1", "risk_score": 75, "risk_tier": "HIGH", "risk_factors": "test"}, + ) assert "AVOID" in result @@ -301,9 +330,9 @@ run_test("test_template_fallback_social_signals", test_template_fallback_social_ # ───────────────────────────────────────────────────────────────────── # SUMMARY # ───────────────────────────────────────────────────────────────────── -print("\n" + "="*60) +print("\n" + "=" * 60) print(f"TOTAL: {tests_passed} passed, {tests_failed} failed") -print("="*60) +print("=" * 60) if tests_failed > 0: sys.exit(1)