diff --git a/.env.example b/.env.example index e3e8a46..ec28ee3 100644 --- a/.env.example +++ b/.env.example @@ -61,17 +61,7 @@ ALPHAX_ONCHAIN_CANDIDATE_ENABLED=1 ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE=70 ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE=70 ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS=6 -ALPHAX_ONCHAIN_DEXSCREENER_ENABLED=0 -ALPHAX_ONCHAIN_DEX_VOLUME_SPIKE_PCT=80 -ALPHAX_ONCHAIN_DEX_MIN_LIQUIDITY_USD=100000 -ALPHAX_ONCHAIN_DEX_MIN_VOLUME_24H_USD=100000 -ALPHAX_ONCHAIN_LIQUIDITY_ADD_PCT=25 -ALPHAX_ONCHAIN_LIQUIDITY_REMOVE_PCT=-25 ALPHAX_ONCHAIN_WHALE_TX_USD=250000 -ALPHAX_ETHERSCAN_ENABLED=0 -ALPHAX_ETHERSCAN_API_KEY= -ALPHAX_HELIUS_ENABLED=0 -ALPHAX_HELIUS_API_KEY= ALPHAX_SYSTEM_ERROR_FEISHU_ENABLED=0 ALPHAX_SYSTEM_ERROR_FEISHU_WEBHOOK= diff --git a/AGENTS.md b/AGENTS.md index 559ee4e..cbe5d35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,7 +102,8 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组 - 当前链上主数据源是 NodeReal,入口在 `app/services/nodereal_client.py` 和 `app/services/onchain_monitor.py`。 - 默认只跑 `ALPHAX_ONCHAIN_PROVIDER=nodereal`,并通过 `ALPHAX_NODEREAL_API_KEY` 访问 EVM JSON-RPC / Enhanced API。 -- DEX Screener、Etherscan、Helius 已从默认链路关闭,只保留历史兼容函数和旧数据展示,不应再作为新增链上逻辑的主入口。 +- DEX Screener、Etherscan、Helius 已从运行时代码链路移除;当前只做 Ethereum / BSC 的 NodeReal 采集。 +- NodeReal 原始 Transfer 会先记录到 `onchain_raw_events`,再通过 ERC-20 `symbol/name/decimals` 自动尝试映射到交易所 `XXX/USDT`,人工 `ALPHAX_ONCHAIN_TOKEN_MAPPINGS` 只作为兜底。 - 新增链上信号优先落到 `onchain_token_metrics` / `onchain_events`,不要直接创建推荐;高质量事件仍通过 `event_news` 进入技术检查。 ### 4.2 Web/API diff --git a/README_DOCKER.md b/README_DOCKER.md index 22305ea..0fb4331 100644 --- a/README_DOCKER.md +++ b/README_DOCKER.md @@ -7,8 +7,8 @@ - Web 默认暴露到宿主机 `8191`,容器内端口 `8190`。 - 运行时数据库是 PostgreSQL,compose 内置 `postgres:16` 服务。 - `DATABASE_URL` 是应用唯一运行时数据库连接入口。 -- 链上主数据源是 NodeReal;`.env` 中配置 `ALPHAX_NODEREAL_API_KEY` 后,`python -m app.cli onchain` 才会产出 NodeReal 链上事件。 -- 生产若出现 `nodereal_no_mappings`,说明 `onchain_token_map` 没有可用合约映射;可在配置中心 `system/onchain.token_mappings` 或 `.env` 的 `ALPHAX_ONCHAIN_TOKEN_MAPPINGS` 写入 `symbol/chain/contract_address` 种子。 +- 链上主数据源是 NodeReal,只采集 Ethereum / BSC;`.env` 中配置 `ALPHAX_NODEREAL_API_KEY` 后,`python -m app.cli onchain` 才会产出链上事件。 +- NodeReal 原始 Transfer 会先进入 `onchain_raw_events` 展示,再自动读取 ERC-20 metadata 尝试映射交易所 `XXX/USDT`;`ALPHAX_ONCHAIN_TOKEN_MAPPINGS` 只作为人工兜底。 - 调度器以并发子进程运行,并通过业务锁组避免主推荐写入冲突。 - `.dockerignore` 排除了 `data/`、真实 `.env` 和所有 DB 文件,避免把数据库/密钥打进镜像。 diff --git a/app/config/system_config.py b/app/config/system_config.py index 8d05d29..3fb9528 100644 --- a/app/config/system_config.py +++ b/app/config/system_config.py @@ -90,22 +90,7 @@ def default_onchain_config(default_chains=("ethereum", "bsc")): "candidate_min_score": _env_float("ALPHAX_ONCHAIN_CANDIDATE_MIN_SCORE", 70), "candidate_min_confidence": _env_int("ALPHAX_ONCHAIN_CANDIDATE_MIN_CONFIDENCE", 70), "candidate_cooldown_hours": _env_float("ALPHAX_ONCHAIN_CANDIDATE_COOLDOWN_HOURS", 6), - "dexscreener_enabled": _env_bool("ALPHAX_ONCHAIN_DEXSCREENER_ENABLED", False), - "dex_volume_spike_pct": _env_float("ALPHAX_ONCHAIN_DEX_VOLUME_SPIKE_PCT", 80), - "dex_min_liquidity_usd": _env_float("ALPHAX_ONCHAIN_DEX_MIN_LIQUIDITY_USD", 100000), - "dex_min_volume_24h_usd": _env_float("ALPHAX_ONCHAIN_DEX_MIN_VOLUME_24H_USD", 100000), - "dex_min_hour_volume_usd": _env_float("ALPHAX_ONCHAIN_DEX_MIN_HOUR_VOLUME_USD", 50000), - "dex_hour_volume_share_pct": _env_float("ALPHAX_ONCHAIN_DEX_HOUR_VOLUME_SHARE_PCT", 8), - "liquidity_add_pct": _env_float("ALPHAX_ONCHAIN_LIQUIDITY_ADD_PCT", 25), - "liquidity_remove_pct": _env_float("ALPHAX_ONCHAIN_LIQUIDITY_REMOVE_PCT", -25), "whale_tx_usd": _env_float("ALPHAX_ONCHAIN_WHALE_TX_USD", 250000), - "etherscan_enabled": _env_bool("ALPHAX_ETHERSCAN_ENABLED", False), - "etherscan_chains": _env_list("ALPHAX_ETHERSCAN_CHAINS", ("ethereum",)), - "helius_enabled": _env_bool("ALPHAX_HELIUS_ENABLED", False), - "etherscan_base_url": _env_str("ALPHAX_ETHERSCAN_BASE_URL", "https://api.etherscan.io/v2/api"), - "helius_base_url": _env_str("ALPHAX_HELIUS_BASE_URL", "https://api.helius.xyz"), - "etherscan_api_key_env": "ALPHAX_ETHERSCAN_API_KEY", - "helius_api_key_env": "ALPHAX_HELIUS_API_KEY", } diff --git a/app/db/onchain_db.py b/app/db/onchain_db.py index d7a5c86..54c8f0c 100644 --- a/app/db/onchain_db.py +++ b/app/db/onchain_db.py @@ -31,28 +31,10 @@ SIGNAL_LABELS = { } RAW_EVENT_TYPE_LABELS = { - "token_profile_latest": "DEX 新币资料变更", - "token_boost_latest": "DEX 付费曝光新增", - "token_boost_top": "DEX 付费曝光榜", "evm_transfer": "EVM 原始转账", } RAW_EVENT_EXPLAINERS = { - "token_profile_latest": { - "plain": "项目方或社区刚在 DEX Screener 更新了代币资料、图标或链接。", - "meaning": "只代表曝光资料发生变化,信号较弱,通常不能单独说明有资金买入。", - "priority": "low", - }, - "token_boost_latest": { - "plain": "有人为这个代币购买了 DEX Screener 曝光位。", - "meaning": "代表短期推广热度上升,可能吸引散户注意,但也可能只是营销。", - "priority": "medium", - }, - "token_boost_top": { - "plain": "这个代币出现在 DEX Screener 付费曝光榜前列。", - "meaning": "代表平台内关注度较高,需要再看成交量、流动性和是否能映射交易对。", - "priority": "medium", - }, "evm_transfer": { "plain": "NodeReal 捕捉到 EVM 链上的 ERC-20 Transfer 原始日志。", "meaning": "这代表链上确实有资金转移,但没有完成币种映射前,不能直接进入策略候选。", @@ -476,11 +458,6 @@ def get_onchain_provider_status(hours=24): hours = int(hours or 24) cutoff = (datetime.now() - timedelta(hours=hours)).isoformat() nodereal_env = str(cfg.get("nodereal_api_key_env") or "ALPHAX_NODEREAL_API_KEY") - etherscan_env = str(cfg.get("etherscan_api_key_env") or "ALPHAX_ETHERSCAN_API_KEY") - helius_env = str(cfg.get("helius_api_key_env") or "ALPHAX_HELIUS_API_KEY") - etherscan_chains = cfg.get("etherscan_chains") or ["ethereum"] - if isinstance(etherscan_chains, str): - etherscan_chains = [x.strip().lower() for x in etherscan_chains.split(",") if x.strip()] conn = get_conn() try: raw_total = conn.execute("SELECT COUNT(*) FROM onchain_raw_events WHERE detected_at >= %s", (cutoff,)).fetchone()[0] @@ -544,72 +521,25 @@ def get_onchain_provider_status(hours=24): summary = _load(last_onchain.get("summary_json") if last_onchain else "{}", {}) if last_onchain else {} last_error = last_onchain.get("error_message") if last_onchain else "" provider = str(cfg.get("provider") or "nodereal").strip().lower() - dexscreener_enabled = bool(cfg.get("dexscreener_enabled", False)) and provider != "nodereal" - etherscan_enabled = bool(cfg.get("etherscan_enabled", False)) and provider != "nodereal" - helius_enabled = bool(cfg.get("helius_enabled", False)) and provider != "nodereal" + nodereal_enabled = bool(cfg.get("nodereal_enabled", True)) and provider == "nodereal" + nodereal_metrics = int(sum(row["count"] for row in metric_sources if row["source"] == "nodereal")) + nodereal_signals = int(sum(row["count"] for row in signal_sources if row["source"] == "nodereal")) providers = [ { "provider": "nodereal", "label": "NodeReal", - "enabled": bool(cfg.get("nodereal_enabled", True)) and provider == "nodereal", + "enabled": nodereal_enabled, "api_key_present": bool(os.getenv(nodereal_env, "").strip()), "implemented": True, "role": "EVM 主链上数据源:Transfer 日志、大额转账、holder 变化", - "raw_events": 0, - "metrics": int(sum(row["count"] for row in metric_sources if row["source"] == "nodereal")), - "signals": int(sum(row["count"] for row in signal_sources if row["source"] == "nodereal")), - "status": _provider_status_label( - bool(cfg.get("nodereal_enabled", True)), - True, - int(sum(row["count"] for row in metric_sources if row["source"] == "nodereal")) - + int(sum(row["count"] for row in signal_sources if row["source"] == "nodereal")), - last_error if "nodereal" in str(last_error).lower() else "", - ), - }, - { - "provider": "dexscreener", - "label": "DEX Screener", - "enabled": dexscreener_enabled, - "api_key_present": True, - "implemented": True, - "role": "低优先级曝光源:Token 资料、付费推广、已映射合约的 DEX 成交量与流动性", "raw_events": int(raw_total or 0), - "metrics": int(metric_total or 0), - "signals": int(sum(row["count"] for row in signal_sources if row["source"] == "dexscreener")), - "status": _provider_status_label(dexscreener_enabled, True, raw_total + metric_total, last_error), - }, - { - "provider": "etherscan", - "label": "Etherscan", - "enabled": etherscan_enabled, - "api_key_present": bool(os.getenv(etherscan_env, "").strip()), - "implemented": True, - "role": "EVM 已映射合约的 ERC20 大额转账,当前链: " + ", ".join(etherscan_chains or ["ethereum"]), - "raw_events": 0, - "metrics": 0, - "signals": int(sum(row["count"] for row in signal_sources if row["source"] == "etherscan")), + "metrics": nodereal_metrics, + "signals": nodereal_signals, "status": _provider_status_label( - etherscan_enabled, + nodereal_enabled, True, - int(sum(row["count"] for row in signal_sources if row["source"] == "etherscan")), - last_error if "etherscan" in str(last_error).lower() else "", - ), - }, - { - "provider": "helius", - "label": "Helius", - "enabled": helius_enabled, - "api_key_present": bool(os.getenv(helius_env, "").strip()), - "implemented": True, - "role": "Solana 已映射 mint 的解析交易与大额 token 活动", - "raw_events": 0, - "metrics": 0, - "signals": int(sum(row["count"] for row in signal_sources if row["source"] == "helius")), - "status": _provider_status_label( - helius_enabled, - True, - int(sum(row["count"] for row in signal_sources if row["source"] == "helius")), - last_error if "helius" in str(last_error).lower() else "", + int(raw_total or 0) + nodereal_metrics + nodereal_signals, + last_error if "nodereal" in str(last_error).lower() else "", ), }, ] diff --git a/app/services/onchain_monitor.py b/app/services/onchain_monitor.py index 6beadb2..49b6032 100644 --- a/app/services/onchain_monitor.py +++ b/app/services/onchain_monitor.py @@ -8,9 +8,6 @@ but it never creates recommendations or changes recommendation state directly. import json import os from datetime import datetime, timedelta -from urllib.parse import urlencode - -import requests from app.config.system_config import onchain_config from app.db import onchain_db @@ -36,16 +33,6 @@ from app.services.nodereal_client import DEFAULT_CHAIN_ENDPOINTS, NodeRealClient DEFAULT_CHAINS = ("ethereum", "bsc") -ETHERSCAN_CHAIN_IDS = { - "ethereum": "1", - "bsc": "56", - "base": "8453", - "arbitrum": "42161", -} -SOLANA_AUTO_ALLOWLIST = { - "WIF", "BONK", "JUP", "RAY", "PYTH", "PENGU", "JTO", "MEW", "POPCAT", "PNUT", - "FARTCOIN", "RENDER", "HNT", "MOBILE", "ORCA", "KMNO", "DRIFT", "TNSR", "IO", -} NON_TARGET_NATIVE_BASES = { "AVAX", "FIL", "SUI", "APT", "DOT", "ADA", "XRP", "LTC", "BCH", "ATOM", "NEAR", "SEI", "INJ", "TON", "ETC", "ICP", "HBAR", "ALGO", "VET", "TRX", "XLM", "KAS", @@ -55,23 +42,6 @@ BRIDGED_TOKEN_MARKERS = ( "wrapped", "wormhole", "portal", "bridged", "bridge", "axelar", "allbridge", "binance-peg", "multichain", "layerzero", "lz", "wavax", "wfil", ) -DEX_CHAIN_ALIASES = { - "ethereum": "ethereum", - "eth": "ethereum", - "bsc": "bsc", - "bnb": "bsc", - "base": "base", - "arbitrum": "arbitrum", - "arb": "arbitrum", - "solana": "solana", - "sol": "solana", -} - -DEXSCREENER_RAW_ENDPOINTS = ( - ("token_profile_latest", "https://api.dexscreener.com/token-profiles/latest/v1"), - ("token_boost_latest", "https://api.dexscreener.com/token-boosts/latest/v1"), - ("token_boost_top", "https://api.dexscreener.com/token-boosts/top/v1"), -) TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" ERC20_SYMBOL_SELECTOR = "0x95d89b41" ERC20_NAME_SELECTOR = "0x06fdde03" @@ -107,15 +77,8 @@ def get_onchain_params(): chains = [x.strip().lower() for x in chains_raw.split(",") if x.strip()] else: chains = [str(x).strip().lower() for x in chains_raw if str(x).strip()] - etherscan_env = str(cfg.get("etherscan_api_key_env") or "ALPHAX_ETHERSCAN_API_KEY") - helius_env = str(cfg.get("helius_api_key_env") or "ALPHAX_HELIUS_API_KEY") nodereal_env = str(cfg.get("nodereal_api_key_env") or "ALPHAX_NODEREAL_API_KEY") token_mappings_env = str(cfg.get("token_mappings_env") or "ALPHAX_ONCHAIN_TOKEN_MAPPINGS") - etherscan_chains_raw = cfg.get("etherscan_chains") or ["ethereum"] - if isinstance(etherscan_chains_raw, str): - etherscan_chains = [x.strip().lower() for x in etherscan_chains_raw.split(",") if x.strip()] - else: - etherscan_chains = [str(x).strip().lower() for x in etherscan_chains_raw if str(x).strip()] return { "enabled": bool(cfg.get("enabled", False)), "provider": str(cfg.get("provider") or "nodereal").strip().lower(), @@ -138,22 +101,7 @@ def get_onchain_params(): "candidate_min_score": float(cfg.get("candidate_min_score") or 70), "candidate_min_confidence": int(cfg.get("candidate_min_confidence") or 70), "candidate_cooldown_hours": float(cfg.get("candidate_cooldown_hours") or 6), - "dexscreener_enabled": bool(cfg.get("dexscreener_enabled", True)), - "etherscan_enabled": bool(cfg.get("etherscan_enabled", True)), - "etherscan_chains": etherscan_chains or ["ethereum"], - "helius_enabled": bool(cfg.get("helius_enabled", True)), - "dex_volume_spike_pct": float(cfg.get("dex_volume_spike_pct") or 80), - "dex_min_liquidity_usd": float(cfg.get("dex_min_liquidity_usd") or 100000), - "dex_min_volume_24h_usd": float(cfg.get("dex_min_volume_24h_usd") or 100000), - "liquidity_add_pct": float(cfg.get("liquidity_add_pct") or 25), - "liquidity_remove_pct": float(cfg.get("liquidity_remove_pct") or -25), - "dex_hour_volume_share_pct": float(cfg.get("dex_hour_volume_share_pct") or 8), - "dex_min_hour_volume_usd": float(cfg.get("dex_min_hour_volume_usd") or 50000), "whale_tx_usd": float(cfg.get("whale_tx_usd") or 250000), - "etherscan_base_url": str(cfg.get("etherscan_base_url") or "https://api.etherscan.io/v2/api").strip(), - "helius_base_url": str(cfg.get("helius_base_url") or "https://api.helius.xyz").strip().rstrip("/"), - "etherscan_api_key": os.getenv(etherscan_env, "").strip(), - "helius_api_key": os.getenv(helius_env, "").strip(), } @@ -228,13 +176,6 @@ def _now(): return datetime.now() -def _request_json(url, params=None, timeout=15): - resp = requests.get(url, params=params or {}, timeout=timeout, headers={"User-Agent": "AlphaX-Agent-Crypto/1.0"}) - if resp.status_code >= 400: - raise RuntimeError(f"http_{resp.status_code}:{resp.text[:200]}") - return resp.json() - - def _safe_float(value, default=0.0): try: return float(value or 0) @@ -242,14 +183,6 @@ def _safe_float(value, default=0.0): return default -def _safe_pct_change(new_value, old_value): - new_value = _safe_float(new_value) - old_value = _safe_float(old_value) - if old_value <= 0: - return 0.0 - return (new_value - old_value) / old_value * 100 - - def _safe_int(value, default=0): try: return int(float(value or 0)) @@ -257,11 +190,6 @@ def _safe_int(value, default=0): return default -def _chain_alias(value): - key = str(value or "").lower() - return DEX_CHAIN_ALIASES.get(key, key) - - def _chain_explorer_tx_url(chain, tx_hash): tx_hash = str(tx_hash or "").strip() if not tx_hash: @@ -293,308 +221,7 @@ def _latest_metric(symbol, chain, contract_address): return dict(row) if row else None -def _event_amount(item): - return _safe_float(item.get("amount")) - - -def _event_total_amount(item): - return _safe_float(item.get("totalAmount") or item.get("total_amount")) - - -def _raw_importance(event_type, item): - amount = _event_amount(item) - total = _event_total_amount(item) - if event_type == "token_boost_top": - return max(total, amount, 1) - if event_type == "token_boost_latest": - return max(amount, total * 0.5, 1) - return 1 - - -def normalize_dexscreener_raw_event(item, event_type, cfg=None): - cfg = cfg or get_onchain_params() - chain = _chain_alias(item.get("chainId")) - if chain not in set(cfg.get("chains") or DEFAULT_CHAINS): - return None - token_address = str(item.get("tokenAddress") or "").strip() - if not token_address: - return None - mapping = find_mapping_by_contract(chain, token_address) - links = item.get("links") or [] - symbol_guess = "" - name = "" - if isinstance(links, list): - for link in links: - if not isinstance(link, dict): - continue - if not name and link.get("label"): - name = str(link.get("label") or "") - raw = { - "chainId": item.get("chainId"), - "tokenAddress": token_address, - "url": item.get("url") or "", - "description": item.get("description") or "", - "icon": item.get("icon") or "", - "header": item.get("header") or "", - "links": links, - "amount": item.get("amount"), - "totalAmount": item.get("totalAmount"), - } - title = "DEX Screener" - if event_type == "token_profile_latest": - title = "DEX 新币资料变更" - elif event_type == "token_boost_latest": - title = "DEX 付费曝光新增" - elif event_type == "token_boost_top": - title = "DEX 付费曝光榜" - return { - "source": "dexscreener", - "chain": chain, - "event_type": event_type, - "token_address": token_address, - "symbol_guess": symbol_guess, - "name": name, - "title": title, - "description": item.get("description") or "", - "url": item.get("url") or "", - "icon": item.get("icon") or "", - "amount": _event_amount(item), - "total_amount": _event_total_amount(item), - "importance": _raw_importance(event_type, item), - "mapped_symbol": mapping.get("symbol") if mapping else "", - "mapping_status": "mapped" if mapping else "unmapped", - "detected_at": _now().isoformat(timespec="seconds"), - "raw": raw, - } - - -def fetch_dexscreener_raw_events(limit=80): - cfg = get_onchain_params() - if not cfg.get("dexscreener_enabled", True): - return {"raw_events": [], "errors": ["dexscreener_disabled"]} - inserted = [] - errors = [] - per_source_limit = max(1, int(limit or 80)) - for event_type, url in DEXSCREENER_RAW_ENDPOINTS: - try: - data = _request_json(url, timeout=cfg.get("timeout", 15)) - items = data if isinstance(data, list) else data.get("items") or data.get("data") or [] - for item in items[:per_source_limit]: - if not isinstance(item, dict): - continue - event = normalize_dexscreener_raw_event(item, event_type, cfg=cfg) - if not event: - continue - if insert_onchain_raw_event(event): - inserted.append(event) - except Exception as exc: - errors.append(f"{event_type}:{str(exc)[:160]}") - return {"raw_events": inserted, "errors": errors} - - -def _discover_seed_symbols(limit=120): - conn = get_conn() - symbols = [] - try: - rows = conn.execute( - """ - SELECT DISTINCT symbol - FROM recommendation - WHERE status='active' AND COALESCE(display_bucket,'watch_pool') != 'history' - ORDER BY rec_time DESC - LIMIT %s - """, - (int(limit or 120),), - ).fetchall() - symbols.extend([row["symbol"] for row in rows if row["symbol"]]) - except Exception: - pass - try: - rows = conn.execute( - """ - SELECT DISTINCT symbol - FROM coin_state - WHERE state != '过期' - ORDER BY detected_at DESC - LIMIT %s - """, - (int(limit or 120),), - ).fetchall() - symbols.extend([row["symbol"] for row in rows if row["symbol"]]) - except Exception: - pass - conn.close() - seen = set() - ordered = [] - for symbol in symbols: - norm = normalize_symbol(symbol) - if not norm or norm in seen or not _tradable_symbol(norm): - continue - seen.add(norm) - ordered.append(norm) - return ordered[: int(limit or 120)] - - -def _score_pair_candidate(pair, requested_symbol, chains): - base = (pair.get("baseToken") or {}) - quote = (pair.get("quoteToken") or {}) - base_symbol = str(base.get("symbol") or "").upper() - req_base = str(requested_symbol or "").split("/")[0].upper() - liquidity = _safe_float((pair.get("liquidity") or {}).get("usd")) - volume = _safe_float((pair.get("volume") or {}).get("h24")) - chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or "").lower(), str(pair.get("chainId") or "").lower()) - score = 0 - if base_symbol == req_base: - score += 50 - if chain in set(chains or []): - score += 15 - if quote.get("symbol") in ("USDT", "USDC", "USD", "FDUSD", "USDE", "DAI", "USDS"): - score += 10 - if liquidity >= 100000: - score += 10 - if volume >= 100000: - score += 10 - if liquidity >= 500000: - score += 5 - return score - - -def _pair_rejection_reason(pair, requested_symbol, chains): - base = pair.get("baseToken") or {} - quote = pair.get("quoteToken") or {} - req_base = str(requested_symbol or "").split("/")[0].upper() - base_symbol = str(base.get("symbol") or "").upper() - base_name = str(base.get("name") or "").lower() - pair_url = str(pair.get("url") or "").lower() - chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or "").lower(), str(pair.get("chainId") or "").lower()) - - if base_symbol != req_base: - return "symbol_mismatch" - if chain not in set(chains or []): - return "chain_not_supported" - if req_base in NON_TARGET_NATIVE_BASES: - return "native_chain_not_in_scope" - if chain == "solana" and req_base not in SOLANA_AUTO_ALLOWLIST: - return "solana_not_allowlisted" - text = " ".join([base_name, base_symbol.lower(), str(quote.get("symbol") or "").lower(), pair_url]) - if any(marker in text for marker in BRIDGED_TOKEN_MARKERS): - return "bridged_or_wrapped_token" - return "" - - -def discover_token_mappings(limit=60): - cfg = get_onchain_params() - chains = set(cfg.get("chains") or DEFAULT_CHAINS) - seeds = _discover_seed_symbols(limit=limit) - if not seeds: - return {"inserted": 0, "candidates": [], "errors": ["no_seed_symbols"]} - inserted = [] - errors = [] - for symbol in seeds: - existing = get_token_mappings(symbol, min_confidence=1, active_only=False) - if existing: - continue - base = symbol.split("/")[0] - try: - data = _request_json("https://api.dexscreener.com/latest/dex/search", params={"q": base}, timeout=cfg.get("timeout", 15)) - pairs = data.get("pairs") or [] - pair_candidates = [] - for pair in pairs: - chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or "").lower(), str(pair.get("chainId") or "").lower()) - if chain not in chains: - continue - if _pair_rejection_reason(pair, symbol, chains): - continue - pair_candidates.append((pair, _score_pair_candidate(pair, symbol, chains))) - if not pair_candidates: - continue - pair_candidates.sort(key=lambda x: (x[1], _safe_float((x[0].get("liquidity") or {}).get("usd")), _safe_float((x[0].get("volume") or {}).get("h24"))), reverse=True) - best, score = pair_candidates[0] - if score < 55: - continue - base_token = best.get("baseToken") or {} - chain = DEX_CHAIN_ALIASES.get(str(best.get("chainId") or "").lower(), str(best.get("chainId") or "").lower()) - contract = str(base_token.get("address") or "").strip() - if not contract: - continue - confidence = min(95, 60 + score) - mapping_id = onchain_db.upsert_token_mapping( - symbol=symbol, - chain=chain, - contract_address=contract, - source="dexscreener_search", - confidence=confidence, - raw={ - "search_query": base, - "matched_pair": { - "pairAddress": best.get("pairAddress") or "", - "dexId": best.get("dexId") or "", - "url": best.get("url") or "", - "liquidity": best.get("liquidity") or {}, - "volume": best.get("volume") or {}, - "priceChange": best.get("priceChange") or {}, - "baseToken": base_token, - "quoteToken": best.get("quoteToken") or {}, - }, - }, - is_active=True, - ) - if mapping_id: - inserted.append({"symbol": symbol, "chain": chain, "contract_address": contract, "confidence": confidence}) - except Exception as exc: - errors.append(f"{symbol}:{str(exc)[:160]}") - return {"inserted": len(inserted), "candidates": inserted, "errors": errors} - - -def _score_metric(metric): - score = 0.0 - risk = 0.0 - vol_change = _safe_float(metric.get("dex_volume_change_pct")) - liq_change = _safe_float(metric.get("liquidity_change_pct")) - netflow = _safe_float(metric.get("exchange_netflow_usd")) - whale = _safe_float(metric.get("whale_accumulation_usd")) - smart = _safe_float(metric.get("smart_money_score")) - if vol_change > 0: - score += min(35, vol_change / 4) - if liq_change > 0: - score += min(20, liq_change / 3) - if netflow < 0: - score += min(20, abs(netflow) / 100000) - if whale > 0: - score += min(20, whale / 100000) - score += min(20, smart) - if liq_change < 0: - risk += min(40, abs(liq_change)) - if netflow > 0: - risk += min(35, netflow / 100000) - metric["onchain_score"] = round(min(score, 100), 2) - metric["risk_score"] = round(min(risk, 100), 2) - return metric - - -def derive_dex_signals(metric, cfg=None): - cfg = cfg or get_onchain_params() - signals = [] - vol_change = _safe_float(metric.get("dex_volume_change_pct")) - liq_change = _safe_float(metric.get("liquidity_change_pct")) - volume_1h = _safe_float(metric.get("dex_volume_1h_usd")) - volume_24h = _safe_float(metric.get("dex_volume_usd")) - hour_share_pct = (volume_1h / volume_24h * 100) if volume_24h > 0 else 0 - if vol_change >= cfg.get("dex_volume_spike_pct", 80): - signals.append("dex_volume_spike") - elif ( - volume_1h >= cfg.get("dex_min_hour_volume_usd", 50000) - and hour_share_pct >= cfg.get("dex_hour_volume_share_pct", 8) - ): - signals.append("dex_volume_spike") - if liq_change >= cfg.get("liquidity_add_pct", 25): - signals.append("liquidity_add") - if liq_change <= cfg.get("liquidity_remove_pct", -25): - signals.append("liquidity_remove_risk") - return signals - - -def _event_from_metric(metric, signal_code, source="dexscreener"): +def _event_from_metric(metric, signal_code, source="nodereal"): direction = signal_direction(signal_code) severity = "RISK" if direction == "risk" else "A" if _safe_float(metric.get("onchain_score")) >= 75 else "B" return { @@ -615,143 +242,6 @@ def _event_from_metric(metric, signal_code, source="dexscreener"): } -def normalize_dexscreener_pair(pair, mapping, cfg=None): - cfg = cfg or get_onchain_params() - symbol = normalize_symbol(mapping.get("symbol")) - chain = DEX_CHAIN_ALIASES.get(str(pair.get("chainId") or mapping.get("chain") or "").lower(), str(mapping.get("chain") or "").lower()) - contract = mapping.get("contract_address") or (pair.get("baseToken") or {}).get("address") or "" - liquidity = _safe_float((pair.get("liquidity") or {}).get("usd")) - volume_map = pair.get("volume") or {} - volume = _safe_float(volume_map.get("h24")) - volume_1h = _safe_float(volume_map.get("h1")) - volume_5m = _safe_float(volume_map.get("m5")) - volume_6h = _safe_float(volume_map.get("h6")) - txns_map = pair.get("txns") or {} - txns_h1 = txns_map.get("h1") or {} - prev = _latest_metric(symbol, chain, contract) - prev_volume = _safe_float(prev.get("dex_volume_usd") if prev else 0) - prev_liquidity = _safe_float(prev.get("liquidity_usd") if prev else 0) - metric = { - "symbol": symbol, - "chain": chain, - "contract_address": contract, - "window": "1h", - "metric_time": _now().isoformat(timespec="seconds"), - "dex_volume_usd": volume, - "dex_volume_1h_usd": volume_1h, - "dex_volume_5m_usd": volume_5m, - "dex_volume_6h_usd": volume_6h, - "dex_volume_1h_share_pct": round(volume_1h / volume * 100, 2) if volume > 0 else 0, - "dex_volume_change_pct": _safe_pct_change(volume, prev_volume), - "liquidity_usd": liquidity, - "liquidity_change_pct": _safe_pct_change(liquidity, prev_liquidity), - "exchange_netflow_usd": 0, - "whale_accumulation_usd": 0, - "holder_delta": 0, - "smart_money_score": 0, - "source": "dexscreener", - "url": pair.get("url") or "", - "raw": { - "pair_address": pair.get("pairAddress") or "", - "dex_id": pair.get("dexId") or "", - "price_usd": pair.get("priceUsd") or "", - "fdv": pair.get("fdv") or 0, - "txns": pair.get("txns") or {}, - "price_change": pair.get("priceChange") or {}, - "volume": pair.get("volume") or {}, - "liquidity": pair.get("liquidity") or {}, - "derived": { - "dex_volume_1h_usd": volume_1h, - "dex_volume_5m_usd": volume_5m, - "dex_volume_6h_usd": volume_6h, - "dex_volume_1h_share_pct": round(volume_1h / volume * 100, 2) if volume > 0 else 0, - "h1_buys": int(txns_h1.get("buys") or 0), - "h1_sells": int(txns_h1.get("sells") or 0), - }, - }, - } - return _score_metric(metric) - - -def fetch_dexscreener_metrics(limit=60): - cfg = get_onchain_params() - if not cfg.get("dexscreener_enabled", True): - return {"metrics": [], "events": [], "errors": ["dexscreener_disabled"]} - mappings = get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE) - bootstrap = None - if not mappings: - bootstrap = discover_token_mappings(limit=limit) - mappings = get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE) - metrics = [] - events = [] - errors = [] - if bootstrap: - errors.extend(bootstrap.get("errors") or []) - for mapping in mappings[: int(limit or 60)]: - symbol = normalize_symbol(mapping.get("symbol")) - if not symbol or not _tradable_symbol(symbol): - continue - try: - url = "https://api.dexscreener.com/latest/dex/tokens/" + str(mapping.get("contract_address") or "").strip() - data = _request_json(url, timeout=cfg.get("timeout", 15)) - pairs = data.get("pairs") or [] - wanted_chain = DEX_CHAIN_ALIASES.get(str(mapping.get("chain") or "").lower(), str(mapping.get("chain") or "").lower()) - pairs = [p for p in pairs if DEX_CHAIN_ALIASES.get(str(p.get("chainId") or "").lower(), str(p.get("chainId") or "").lower()) == wanted_chain] - if not pairs: - continue - best = max(pairs, key=lambda p: _safe_float((p.get("liquidity") or {}).get("usd"))) - metric = normalize_dexscreener_pair(best, mapping, cfg=cfg) - if metric.get("liquidity_usd", 0) < cfg.get("dex_min_liquidity_usd", 100000) and metric.get("dex_volume_usd", 0) < cfg.get("dex_min_volume_24h_usd", 100000): - insert_token_metric(metric) - metrics.append(metric) - continue - insert_token_metric(metric) - metrics.append(metric) - for code in derive_dex_signals(metric, cfg): - event = _event_from_metric(metric, code, source="dexscreener") - if insert_onchain_event(event): - events.append(event) - except Exception as exc: - errors.append(f"{symbol}:{str(exc)[:160]}") - return {"metrics": metrics, "events": events, "errors": errors} - - -def _event_from_etherscan_transfer(row, mapping, cfg=None): - cfg = cfg or get_onchain_params() - decimals = _safe_int(row.get("tokenDecimal"), 18) - amount = _safe_float(row.get("value")) / (10 ** decimals if decimals >= 0 else 1) - price_usd = _latest_price_from_metric(mapping) - value_usd = amount * price_usd if price_usd > 0 else 0 - threshold = _safe_float(cfg.get("whale_tx_usd"), 250000) - if value_usd > 0 and value_usd < threshold: - return None - if value_usd <= 0 and amount <= 0: - return None - tx_hash = str(row.get("hash") or "").strip() - chain = str(mapping.get("chain") or "").lower() - return { - "chain": chain, - "symbol": mapping.get("symbol"), - "contract_address": mapping.get("contract_address") or "", - "event_type": "token_transfer", - "signal_code": "whale_accumulation" if value_usd >= threshold else "large_token_transfer", - "signal_label": signal_label("whale_accumulation" if value_usd >= threshold else "large_token_transfer"), - "direction": "positive" if value_usd >= threshold else "neutral", - "value_usd": value_usd, - "amount": amount, - "tx_hash": tx_hash, - "wallet_address": row.get("to") or "", - "wallet_label": "EVM 接收地址", - "counterparty_label": "EVM 发送地址 " + _short_addr(row.get("from") or ""), - "confidence": 74 if value_usd >= threshold else 58, - "severity": "A" if value_usd >= threshold else "B", - "detected_at": _ts_to_iso(row.get("timeStamp")), - "source": "etherscan", - "url": _chain_explorer_tx_url(chain, tx_hash), - "raw": row, - } - - def _latest_price_from_metric(mapping): symbol = normalize_symbol(mapping.get("symbol")) chain = str(mapping.get("chain") or "").lower() @@ -1179,141 +669,6 @@ def fetch_nodereal_raw_events(client=None, cfg=None, limit=60): return {"raw_events": inserted, "errors": errors} -def fetch_etherscan_events(limit=60): - cfg = get_onchain_params() - if not cfg.get("etherscan_enabled", True): - return {"events": [], "errors": ["etherscan_disabled"]} - api_key = cfg.get("etherscan_api_key") or "" - if not api_key: - return {"events": [], "errors": ["etherscan_api_key_missing"]} - allowed_chains = set(cfg.get("etherscan_chains") or ["ethereum"]) - mappings = [ - m for m in get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE) - if m.get("chain") in ETHERSCAN_CHAIN_IDS and m.get("chain") in allowed_chains - ] - events = [] - errors = [] - for mapping in mappings[: int(limit or 60)]: - chain = str(mapping.get("chain") or "").lower() - contract = str(mapping.get("contract_address") or "").strip() - if not contract: - continue - params = { - "chainid": ETHERSCAN_CHAIN_IDS[chain], - "module": "account", - "action": "tokentx", - "contractaddress": contract, - "page": 1, - "offset": 25, - "sort": "desc", - "apikey": api_key, - } - try: - data = _request_json(cfg.get("etherscan_base_url"), params=params, timeout=cfg.get("timeout", 15)) - status = str(data.get("status") or "") - message = str(data.get("message") or "") - rows = data.get("result") or [] - if status == "0" and not isinstance(rows, list): - if "No transactions found" not in str(rows) and "No records" not in str(rows): - errors.append(f"{mapping.get('symbol')}:etherscan_{message}:{str(rows)[:100]}") - continue - if not isinstance(rows, list): - continue - for row in rows: - if not isinstance(row, dict): - continue - event = _event_from_etherscan_transfer(row, mapping, cfg=cfg) - if not event: - continue - if insert_onchain_event(event): - events.append(event) - except Exception as exc: - errors.append(f"{mapping.get('symbol')}:etherscan:{str(exc)[:160]}") - return {"events": events, "errors": errors} - - -def _event_from_helius_tx(tx, mapping, cfg=None): - cfg = cfg or get_onchain_params() - mint = str(mapping.get("contract_address") or "") - symbol = normalize_symbol(mapping.get("symbol")) - signature = str(tx.get("signature") or "") - token_transfers = tx.get("tokenTransfers") or [] - native_transfers = tx.get("nativeTransfers") or [] - matched = [t for t in token_transfers if str(t.get("mint") or "") == mint] - if not matched: - return None - amount = max(_safe_float(t.get("tokenAmount")) for t in matched) - price_usd = _latest_price_from_metric(mapping) - value_usd = amount * price_usd if price_usd > 0 else 0 - sol_amount = max([_safe_float(t.get("amount")) / 1_000_000_000 for t in native_transfers] or [0]) - threshold = _safe_float(cfg.get("whale_tx_usd"), 250000) - if value_usd > 0 and value_usd < threshold and sol_amount < 100: - return None - signal = "whale_accumulation" if value_usd >= threshold or sol_amount >= 100 else "large_token_transfer" - return { - "chain": "solana", - "symbol": symbol, - "contract_address": mint, - "event_type": "solana_token_activity", - "signal_code": signal, - "signal_label": signal_label(signal), - "direction": "positive" if signal == "whale_accumulation" else "neutral", - "value_usd": value_usd, - "amount": amount, - "tx_hash": signature, - "wallet_address": (matched[0].get("toUserAccount") or matched[0].get("userAccount") or ""), - "wallet_label": "Solana 接收地址", - "counterparty_label": "Solana 发送地址 " + _short_addr(matched[0].get("fromUserAccount") or ""), - "confidence": 74 if signal == "whale_accumulation" else 58, - "severity": "A" if signal == "whale_accumulation" else "B", - "detected_at": _ts_to_iso(tx.get("timestamp")), - "source": "helius", - "url": _chain_explorer_tx_url("solana", signature), - "raw": tx, - } - - -def fetch_helius_events(limit=60): - cfg = get_onchain_params() - if not cfg.get("helius_enabled", True): - return {"events": [], "errors": ["helius_disabled"]} - api_key = cfg.get("helius_api_key") or "" - if not api_key: - return {"events": [], "errors": ["helius_api_key_missing"]} - mappings = [m for m in get_token_mappings(min_confidence=MIN_MAPPING_CONFIDENCE) if m.get("chain") == "solana"] - events = [] - errors = [] - for mapping in mappings[: int(limit or 60)]: - mint = str(mapping.get("contract_address") or "").strip() - if not mint: - continue - query = urlencode({"api-key": api_key, "limit": 25}) - url = f"{cfg.get('helius_base_url')}/v0/addresses/{mint}/transactions?{query}" - try: - data = _request_json(url, timeout=cfg.get("timeout", 15)) - rows = data if isinstance(data, list) else data.get("transactions") or [] - for tx in rows: - if not isinstance(tx, dict): - continue - event = _event_from_helius_tx(tx, mapping, cfg=cfg) - if not event: - continue - if insert_onchain_event(event): - events.append(event) - except Exception as exc: - errors.append(f"{mapping.get('symbol')}:helius:{str(exc)[:160]}") - return {"events": events, "errors": errors} - - -def _ts_to_iso(value): - try: - if value: - return datetime.fromtimestamp(float(value)).isoformat(timespec="seconds") - except Exception: - pass - return _now().isoformat(timespec="seconds") - - def _short_addr(value): value = str(value or "") if len(value) <= 12: @@ -1502,16 +857,10 @@ def run_once(limit=60): __all__ = [ "POSITIVE_SIGNALS", "RISK_SIGNALS", - "derive_dex_signals", "enqueue_onchain_candidates", - "fetch_dexscreener_metrics", - "fetch_dexscreener_raw_events", - "fetch_etherscan_events", - "fetch_helius_events", "fetch_nodereal_events", "get_onchain_params", "ingest_normalized_events", - "normalize_dexscreener_pair", "run_once", "seed_configured_token_mappings", ] diff --git a/tests/test_onchain_tracking.py b/tests/test_onchain_tracking.py index 656430d..e538b08 100644 --- a/tests/test_onchain_tracking.py +++ b/tests/test_onchain_tracking.py @@ -36,66 +36,12 @@ def test_mapping_requires_confidence_and_preserves_multi_chain(monkeypatch, tmp_ assert usable[0]["contract_address"] == "0xaaa" -def test_dex_signal_codes_from_metric(monkeypatch, tmp_path): - _temp_db(monkeypatch, tmp_path) - cfg = { - "dex_volume_spike_pct": 80, - "dex_min_hour_volume_usd": 50000, - "dex_hour_volume_share_pct": 8, - "liquidity_add_pct": 25, - "liquidity_remove_pct": -25, - } - - assert onchain_monitor.derive_dex_signals({"dex_volume_change_pct": 120, "liquidity_change_pct": 40}, cfg) == [ - "dex_volume_spike", - "liquidity_add", - ] - assert onchain_monitor.derive_dex_signals({"dex_volume_change_pct": 10, "liquidity_change_pct": -35}, cfg) == [ - "liquidity_remove_risk" - ] - assert onchain_monitor.derive_dex_signals( - {"dex_volume_usd": 600000, "dex_volume_1h_usd": 80000, "dex_volume_change_pct": 0, "liquidity_change_pct": 0}, - cfg, - ) == ["dex_volume_spike"] - - def test_auto_mapping_rejects_non_target_native_and_wrapped_tokens(monkeypatch, tmp_path): _temp_db(monkeypatch, tmp_path) - monkeypatch.setenv("ALPHAX_ONCHAIN_CHAINS", "ethereum,solana") - cfg = onchain_monitor.get_onchain_params() - chains = set(cfg.get("chains") or []) - - avax_pair = { - "chainId": "solana", - "baseToken": {"symbol": "AVAX", "name": "Avalanche"}, - "quoteToken": {"symbol": "USDC"}, - "liquidity": {"usd": 500000}, - "volume": {"h24": 1000000}, - "url": "https://example.com", - } - wrapped_pair = { - "chainId": "ethereum", - "baseToken": {"symbol": "FIL", "name": "Wrapped Filecoin"}, - "quoteToken": {"symbol": "USDT"}, - "liquidity": {"usd": 500000}, - "volume": {"h24": 1000000}, - "url": "https://example.com/wrapped-filecoin", - } - - assert onchain_monitor._pair_rejection_reason(avax_pair, "AVAX/USDT", chains) == "native_chain_not_in_scope" - assert onchain_monitor._pair_rejection_reason(wrapped_pair, "FIL/USDT", chains) == "native_chain_not_in_scope" - assert onchain_monitor._pair_rejection_reason( - { - "chainId": "solana", - "baseToken": {"symbol": "UNKNOWN", "name": "Unknown"}, - "quoteToken": {"symbol": "USDC"}, - "liquidity": {"usd": 500000}, - "volume": {"h24": 1000000}, - "url": "https://example.com", - }, - "UNKNOWN/USDT", - chains, - ) == "solana_not_allowlisted" + assert onchain_monitor._is_auto_mapping_symbol_allowed("STORJ", "Storj") is True + assert onchain_monitor._is_auto_mapping_symbol_allowed("AVAX", "Avalanche") is False + assert onchain_monitor._is_auto_mapping_symbol_allowed("FIL", "Wrapped Filecoin") is False + assert onchain_monitor._is_auto_mapping_symbol_allowed("USDT", "Tether USD") is False def test_onchain_candidate_enqueues_event_news_not_recommendation(monkeypatch, tmp_path): @@ -210,58 +156,16 @@ def test_onchain_api_and_page(monkeypatch, tmp_path): assert provider_status.json()["coverage"]["metrics"] == 1 -def test_raw_dexscreener_events_store_without_mapping(monkeypatch, tmp_path): - _temp_db(monkeypatch, tmp_path) - monkeypatch.setenv("ALPHAX_ONCHAIN_ENABLED", "1") - monkeypatch.setenv("ALPHAX_ONCHAIN_CHAINS", "ethereum,solana") - monkeypatch.setenv("ALPHAX_ONCHAIN_DEXSCREENER_ENABLED", "1") - - def fake_request(url, params=None, timeout=15): - if "token-profiles" in url: - return [ - { - "chainId": "ethereum", - "tokenAddress": "0xraw", - "url": "https://dexscreener.com/ethereum/0xraw", - "description": "Unmapped token started trending", - "icon": "https://example.com/icon.png", - } - ] - if "token-boosts/latest" in url: - return [ - { - "chainId": "solana", - "tokenAddress": "So111", - "url": "https://dexscreener.com/solana/So111", - "amount": 25, - "totalAmount": 100, - } - ] - if "token-boosts/top" in url: - return [] - return {"pairs": []} - - monkeypatch.setattr(onchain_monitor, "_request_json", fake_request) - - result = onchain_monitor.fetch_dexscreener_raw_events(limit=10) - - assert len(result["raw_events"]) == 2 - raw = onchain_db.list_onchain_raw_events(hours=24) - assert raw["total"] == 2 - assert raw["items"][0]["mapping_status"] == "unmapped" - assert raw["items"][0]["event_label"] - - def test_raw_event_api_and_overview_counts(monkeypatch, tmp_path): _temp_db(monkeypatch, tmp_path) - onchain_db.upsert_token_mapping("ABC", "base", "0xabc", source="manual", confidence=95) + onchain_db.upsert_token_mapping("ABC", "ethereum", "0xabc", source="manual", confidence=95) onchain_db.insert_onchain_raw_event( { - "source": "dexscreener", - "chain": "base", - "event_type": "token_boost_top", + "source": "nodereal", + "chain": "ethereum", + "event_type": "evm_transfer", "token_address": "0xabc", - "title": "DEX Boost 榜单", + "title": "NodeReal ERC-20 原始转账", "amount": 10, "total_amount": 80, "importance": 80, @@ -283,9 +187,9 @@ def test_raw_event_api_and_overview_counts(monkeypatch, tmp_path): assert events.status_code == 200 assert events.json()["items"][0]["mapped_symbol"] == "ABC/USDT" assert important.status_code == 200 - assert important.json()["total"] == 0 + assert important.json()["total"] == 1 assert low.status_code == 200 - assert low.json()["total"] == 1 + assert low.json()["total"] == 0 def test_nodereal_events_generate_metrics_and_normalized_event(monkeypatch, tmp_path): @@ -515,16 +419,6 @@ def test_nodereal_seeds_configured_token_mappings_from_env(monkeypatch, tmp_path assert mappings[0]["contract_address"] == "0xabc" -def test_legacy_helius_is_disabled_by_default(monkeypatch, tmp_path): - _temp_db(monkeypatch, tmp_path) - monkeypatch.setenv("ALPHAX_HELIUS_API_KEY", "test-key") - onchain_db.upsert_token_mapping("SOLX", "solana", "Mint111", source="manual", confidence=95) - result = onchain_monitor.fetch_helius_events(limit=10) - - assert result["events"] == [] - assert result["errors"] == ["helius_disabled"] - - def test_scheduler_seeds_onchain_job(monkeypatch, tmp_path): _temp_db(monkeypatch, tmp_path) sched_path = tmp_path / "scheduler_state.db" diff --git a/tests/test_runtime_config.py b/tests/test_runtime_config.py index 3d1a23f..4f54615 100644 --- a/tests/test_runtime_config.py +++ b/tests/test_runtime_config.py @@ -216,25 +216,24 @@ def test_llm_system_config_overrides_env_defaults(monkeypatch): def test_onchain_system_config_overrides_env(monkeypatch): monkeypatch.setenv("ALPHAX_ONCHAIN_ENABLED", "0") - monkeypatch.setenv("TEST_ETHERSCAN_KEY", "etherscan-secret") + monkeypatch.setenv("TEST_NODEREAL_KEY", "nodereal-secret") set_config("system", "onchain", { "enabled": True, - "chains": ["base", "solana"], + "chains": ["ethereum", "bsc"], "timeout": 9, "candidate_min_score": 88, - "dex_min_liquidity_usd": 123456, - "etherscan_api_key_env": "TEST_ETHERSCAN_KEY", - "helius_api_key_env": "TEST_HELIUS_KEY", + "nodereal_api_key_env": "TEST_NODEREAL_KEY", + "nodereal_raw_max_logs_per_chain": 12, }) params = get_onchain_params() assert params["enabled"] is True - assert params["chains"] == ["base", "solana"] + assert params["chains"] == ["ethereum", "bsc"] assert params["timeout"] == 9 assert params["candidate_min_score"] == 88 - assert params["dex_min_liquidity_usd"] == 123456 - assert params["etherscan_api_key"] == "etherscan-secret" + assert params["nodereal_api_key"] == "nodereal-secret" + assert params["nodereal_raw_max_logs_per_chain"] == 12 def test_paper_trading_system_config_controls_account_model(monkeypatch):