├── .env.example ├── .gitignore ├── README.md ├── README_EN.md ├── config └── config.py ├── core ├── __init__.py ├── abi │ ├── erc20.json │ └── router.json ├── analyzer.py ├── chain_validator.py ├── data_def.py ├── processor.py └── trader.py ├── images ├── analys1.png ├── analys2.png ├── analys3.png ├── api_hash_1.jpg ├── api_hash_2.jpg ├── api_hash_3.jpg ├── dingding.jpg └── start.png ├── main.py ├── monitor ├── __init__.py ├── base.py ├── telegram_monitor.py └── webhook_monitor.py ├── notify ├── __init__.py ├── dingding.py ├── notice.py └── telegram_bot.py ├── requirements.txt ├── telegram_mode_setting.md ├── test └── test_trader.py └── utils └── x_abstract.py /.env.example : -------------------------------------------------------------------------------- 1 | # LLM配置 2 | LLM_API_KEY=sk-w8BzgBU1gEvE39i0YiUuXXXXXXXiVQ0gI1OrSrUb0C 3 | LLM_BASE_URL=https://cdn.openai.com/v1 4 | LLM_MODEL=gpt-4o-mini 5 | 6 | # 机器人消息驱动模式 webhook/telegram 7 | DRIVER_MODE=webhook 8 | 9 | # 钉钉机器人配置 10 | DINGTALK_TOKEN=9951f86402ffabde25cfdd0XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11 | DINGTALK_SECRET=SEC48d89d3XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 12 | 13 | # TELEGRAM配置 机器人消息驱动模式为telegram时需要配置 14 | TELEGRAM_TOKEN=785XXXX909:AAH5QarVHXXXXXXXXAyot_mKt_vObqk 15 | TELEGRAM_NOTIFY_CHAT_ID=-1002XXXX488 16 | TELEGRAM_API_ID=17XXXXX01 17 | TELEGRAM_API_HASH=236678289eb1810XXXXXXXcc0b44 18 | TELEGRAM_NEWS_PUSH_CHAT_ID=-100258XXXXX 19 | 20 | # 交易功能开关 true:开启链上自动买入 false:发送钉钉通知后人工买入 21 | TRADER_ENABLED=true 22 | 23 | # 区块链钱包配置 私钥。可从mask okx钱包直接导出 24 | ETH_PRIVATE_KEY=0xe1df38xxxxxxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 25 | SOL_PRIVATE_KEY=2tuUxKZZgvHMuMNuxV2L53XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 26 | 27 | # 以太坊RPC配置 28 | ETH_RPC_URL=https://ethereum.publicnode.com 29 | ETH_ROUTER_ADDRESS=0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D 30 | 31 | # BSC RPC配置 32 | BSC_RPC_URL=https://bsc-dataseed.binance.org 33 | BSC_ROUTER_ADDRESS=0x10ED43C718714eb63d5aA57B78B54704E256024E 34 | 35 | # Solana RPC配置 36 | SOL_RPC_URL=https://api.mainnet-beta.solana.com 37 | 38 | # 链上交易配置 39 | GAS_PRICE_MULTIPLIER=1.1 40 | SLIPPAGE_TOLERANCE=0.05 41 | MAX_TRADE_AMOUNT_USD=100 42 | MIN_LIQUIDITY_USD=10000 43 | MIN_VOLUME_USD=5000 44 | DEFAULT_TRADE_AMOUNT_USD=20 45 | HIGH_CONFIDENCE_AMOUNT_USD=50 46 | MEDIUM_CONFIDENCE_AMOUNT_USD=30 47 | MIN_CONFIDENCE=0.6 48 | MAX_PRICE_CHANGE_1H=20 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .nox/ 40 | .coverage 41 | *.cover 42 | *.py,cover 43 | .hypothesis/ 44 | .pytest_cache/ 45 | 46 | # Environments 47 | .env 48 | .venv 49 | env/ 50 | venv/ 51 | ENV/ 52 | env.bak/ 53 | venv.bak/ 54 | 55 | # IDE specific files 56 | .idea/ 57 | .vscode/ 58 | *.swp 59 | *.swo 60 | 61 | # macOS 62 | .DS_Store 63 | 64 | # Log files 65 | *.log 66 | 67 | # Local configuration 68 | local_settings.py 69 | 70 | # Database 71 | *.db 72 | *.sqlite 73 | 74 | # Virtual environment 75 | python-venv/ 76 | 77 | # Playwright 78 | playwright-*.zip 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # X-monitor 项目介绍 2 | 3 | ## 项目概述 4 | X-monitor 是一个基于 Twitter (X) 的实时监控系统,能够自动基于大模型分析推文内容、识别潜在MEME加密货币交易机会,并支持自动执行链上交易。 5 | 6 | ## 主要功能 7 | 1. **推文监控**:通过 API 接收 Twitter 推文数据 8 | 2. **AI 分析**:使用大模型分析推文内容,识别潜在代币信息 9 | 3. **代币搜索**:自动搜索代币的市场数据(价格、流动性等) 10 | 4. **交易执行**:支持自动在以太坊/BSC/Solana 链上执行代币交易 11 | 5. **通知系统**:通过钉钉机器人发送实时通知 12 | 13 | 14 | ## 项目结构 15 | X-monitor/ 16 | ├── abi/ # 智能合约 ABI 17 | │ ├── erc20.json 18 | │ └── router.json 19 | ├── monitor/ # 监控模块 20 | │ ├── telegram_monitor.py # Telegram监控 21 | │ └── webhook_monitor.py # Webhook监听 22 | ├── notify/ # 通知模块 23 | │ ├── dingding.py # 钉钉机器人 24 | │ └── telegram_bot.py # Telegram机器人 25 | ├── analyzer.py # AI 分析模块 26 | ├── config.py # 配置管理 27 | ├── data_def.py # 数据结构定义 28 | ├── notice.py # 通知系统核心 29 | ├── processor.py # 推文内容处理器 30 | ├── trader.py # 链上交易模块 31 | ├── x_monitor.py # 主服务入口 32 | ├── .env # 环境变量配置 33 | └── requirements.txt # 依赖列表 34 | 35 | 36 | ## 快速开始 37 | ### 1. 安装依赖 38 | 推荐使用aconda管理环境,安装aconda后构建3.10的xmonitor环境,并激活环境 39 | 40 | 41 | ```bash 42 | conda create --name xmonitor python=3.10 43 | conda activate xmonitor 44 | 45 | pip install -r requirements.txt 46 | playwright install chromium 47 | ``` 48 | 49 | ### 2. 配置环境变量 50 | 复制 .env.example 为 .env 并填写您的配置: 51 | - LLM_API_KEY: OpenAI 兼容 API 密钥, model需要可以支持图片分析 52 | - DINGTALK_TOKEN/SECRET: 钉钉机器人凭证 53 | - 区块链相关配置(私钥、RPC 等) 54 | 55 | ### 3. 订阅X推送服务 56 | X的消息推送支持两种模式, 一种是通过webhook监听, 一种是通过telegram机器人监听. 57 | 58 | #### 3.1 webhook监听 59 | webhook监听,就是起一个http服务, 接收X的推送消息, 然后解析消息.目前项目适配的为apidance的推送消息格式. 60 | 相关服务可参考 https://alpha.apidance.pro/welcome 订阅时选择自定义Hook推送地址,推送到自己的服务器. eg: http://188.1.1.99:9999/post/tweet. 61 | 62 | #### 3.2 telegram机器人监听 63 | 该模式基于kbot的免费订阅推送服务,原项目推送到telegram chanel/group,我们可以使用机器人监听推送的消息,然后解析消息,驱动我们的大模型策略分析. 相比于webbook监听,该模式需要较多的前期配置.具体步骤参考文档 telegram_mode_setup.md 64 | 65 | 66 | ### 4. 启动服务 67 | ```bash 68 | python main.py 69 | ``` 70 | 对于telegram模式启动,首次运行需要授权验证,输入telegram账号绑定的手机号(eg: +8613888888888), 然后输入telegram上收到哦的验证码.再次启动时,不需要再次授权. 71 | 72 | ### 5. 测试 73 | #### 5.1 测试链上买入功能 74 | env文件中配置好链上钱包信息, 并确保有足够的ETH/BNB/SOL. 75 | 执行以下命令: 76 | ```bash 77 | python test/test_trader.py 78 | ``` 79 | 执行成功后,会输出含有交易hash的链接,可以在链上查看交易状态. 80 | 81 | #### 5.2 测试推特AI解析,自动买入功能 82 | 83 | ##### 5.2.1 测试webhook模式 84 | 85 | 通过postman等工具发送如下请求, 即可触发AI分析,并自动买入. 86 | 87 | POST /post/tweet 88 | 89 | ```json 90 | { 91 | "push_type": "new_tweet", 92 | "title": "[GROUP] Elon Musk 发布新推文", 93 | "content": "The Return of the King https://t.co/CjaRrXH7k9", 94 | "user": { 95 | "id": 44196397, 96 | "id_str": "44196397", 97 | "name": "Elon Musk", 98 | "screen_name": "elonmusk", 99 | "location": "", 100 | "description": "", 101 | "protected": false, 102 | "followers_count": 219972879, 103 | "friends_count": 1092, 104 | "listed_count": 161564, 105 | "created_ts": 1243973549, 106 | "favourites_count": 135600, 107 | "verified": false, 108 | "statuses_count": 74875, 109 | "media_count": 3642, 110 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 111 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/1893803697185910784/Na5lOWi5_normal.jpg", 112 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/44196397/1739948056", 113 | "is_blue_verified": false, 114 | "verified_type": "", 115 | "pin_tweet_id": "1902141220505243751", 116 | "ca": "", 117 | "has_ca": false, 118 | "init_followers_count": 155506033, 119 | "init_friends_count": 420, 120 | "first_created_at": 0, 121 | "updated_at": 1742441817 122 | }, 123 | "tweet": { 124 | "id": 60393895, 125 | "tweet_id": "1902564961307488532", 126 | "user_id": "44196397", 127 | "media_type": "photo", 128 | "text": "https://t.co/zBa4F6YApG", 129 | "medias": [ 130 | "https://pbs.twimg.com/ext_tw_video_thumb/1902520094779179008/pu/img/GAxFkN4qowT1vGA_.jpg" 131 | ], 132 | "urls": null, 133 | "is_self_send": true, 134 | "is_retweet": false, 135 | "is_quote": false, 136 | "is_reply": false, 137 | "is_like": false, 138 | "related_tweet_id": "", 139 | "related_user_id": "", 140 | "publish_time": 1742441809, 141 | "has_deleted": false, 142 | "last_deleted_check_at": 0, 143 | "ca": "", 144 | "has_ca": false, 145 | "created_at": 1742441821, 146 | "updated_at": 1742441821 147 | } 148 | } 149 | ``` 150 | 151 | ##### 5.2.2 测试telegram模式 152 | DRIVER_MODE=telegram 153 | 在kbot推送的telegram group/channel中发送一条消息, 即可触发AI分析,并自动买入. 154 | ``` 155 | 🔴 CZ posted a new tweet 🔴 156 | 157 | 📝 Content: 158 | 159 | .@CoinMarketCap AI https://t.co/RfFBMHkSM6 160 | 161 | 🕔 Time: 5/20/2025, 3:43:58 PM 162 | 163 | 🔗 Source: https://x.com/cz_binance/status/1924853614155374842 164 | ``` 165 | 166 | ## 效果展示 167 | ![应用启动](images/start.png) 168 | ![AI分析结果](images/analys1.png) 169 | ![AI分析结果](images/analys2.png) 170 | ![AI分析结果](images/analys3.png) 171 | ![钉钉通知](images/dingding.jpg) -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # X-monitor Project Introduction 2 | 3 | ## Overview 4 | X-monitor is a real-time monitoring system based on Twitter (X) that automatically analyzes tweet content using large language models, identifies potential MEME cryptocurrency trading opportunities, and supports automated on-chain transactions. 5 | 6 | ## Key Features 7 | 1. **Tweet Monitoring**: Receives Twitter tweet data via API 8 | 2. **AI Analysis**: Uses large language models to analyze tweet content and identify potential token information 9 | 3. **Token Search**: Automatically searches for token market data (price, liquidity, etc.) 10 | 4. **Transaction Execution**: Supports automated token transactions on Ethereum/BSC/Solana chains 11 | 5. **Notification System**: Sends real-time notifications via DingTalk robot 12 | 13 | ## Project Structure 14 | X-monitor/ 15 | ├── abi/ # Smart Contract ABIs 16 | │ ├── erc20.json 17 | │ └── router.json 18 | ├── monitor/ # Monitoring Modules 19 | │ ├── telegram_monitor.py # Telegram Monitoring 20 | │ └── webhook_monitor.py # Webhook Listener 21 | ├── notify/ # Notification Modules 22 | │ ├── dingding.py # DingTalk Bot 23 | │ └── telegram_bot.py # Telegram Bot 24 | ├── analyzer.py # AI Analysis Module 25 | ├── config.py # Configuration Management 26 | ├── data_def.py # Data Structure Definitions 27 | ├── notice.py # Notification System Core 28 | ├── processor.py # Tweet Content Processor 29 | ├── trader.py # On-chain Trading Module 30 | ├── x_monitor.py # Main Service Entry 31 | ├── .env # Environment Variables 32 | └── requirements.txt # Dependencies 33 | 34 | ## Quick Start 35 | 1. Install dependencies 36 | We recommend using conda for environment management. After installing conda, create a Python 3.10 environment named xmonitor and activate it: 37 | ```bash 38 | conda create --name xmonitor python=3.10 39 | conda activate xmonitor 40 | ``` 41 | 42 | ```bash 43 | pip install -r requirements.txt 44 | playwright install chromium 45 | `` 46 | 47 | 2.Configure environment variables: Copy .env.example to .env and fill in your configuration: 48 | - LLM_API_KEY: OpenAI-compatible API key (model needs to support image analysis) 49 | - DINGTALK_TOKEN/SECRET: DingTalk robot credentials 50 | - Blockchain related configurations (private keys, RPCs, etc.) 51 | 52 | 3. Subscribe to X push service: This project uses apidance's push service. Reference: https://alpha.apidance.pro/welcome When subscribing, select custom Hook push address to your server. Example: http://188.1.1.99:9999/post/tweet 53 | 54 | 4. Start the service: 55 | ```bash 56 | python main.py 57 | ``` 58 | 59 | 5. Testing 60 | POST /post/tweet 61 | ``` 62 | { 63 | "push_type": "new_tweet", 64 | "title": "[GROUP] Elon Musk's new tweet", 65 | "content": "The Return of the King https://t.co/CjaRrXH7k9", 66 | "user": { 67 | "id": 44196397, 68 | "id_str": "44196397", 69 | "name": "Elon Musk", 70 | "screen_name": "elonmusk", 71 | "location": "", 72 | "description": "", 73 | "protected": false, 74 | "followers_count": 219972879, 75 | "friends_count": 1092, 76 | "listed_count": 161564, 77 | "created_ts": 1243973549, 78 | "favourites_count": 135600, 79 | "verified": false, 80 | "statuses_count": 74875, 81 | "media_count": 3642, 82 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", 83 | "profile_image_url_https": "https://pbs.twimg.com/profile_images/1893803697185910784/Na5lOWi5_normal.jpg", 84 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/44196397/1739948056", 85 | "is_blue_verified": false, 86 | "verified_type": "", 87 | "pin_tweet_id": "1902141220505243751", 88 | "ca": "", 89 | "has_ca": false, 90 | "init_followers_count": 155506033, 91 | "init_friends_count": 420, 92 | "first_created_at": 0, 93 | "updated_at": 1742441817 94 | }, 95 | "tweet": { 96 | "id": 60393895, 97 | "tweet_id": "1902564961307488532", 98 | "user_id": "44196397", 99 | "media_type": "photo", 100 | "text": "https://t.co/zBa4F6YApG", 101 | "medias": [ 102 | "https://pbs.twimg.com/ext_tw_video_thumb/1902520094779179008/pu/img/GAxFkN4qowT1vGA_.jpg" 103 | ], 104 | "urls": null, 105 | "is_self_send": true, 106 | "is_retweet": false, 107 | "is_quote": false, 108 | "is_reply": false, 109 | "is_like": false, 110 | "related_tweet_id": "", 111 | "related_user_id": "", 112 | "publish_time": 1742441809, 113 | "has_deleted": false, 114 | "last_deleted_check_at": 0, 115 | "ca": "", 116 | "has_ca": false, 117 | "created_at": 1742441821, 118 | "updated_at": 1742441821 119 | } 120 | } 121 | ``` 122 | 123 | ## Demo 124 | ![Application Startup](images/start.png) 125 | ![AI Analysis Result](images/analys1.png) 126 | ![AI Analysis Result](images/analys2.png) 127 | ![AI Analysis Result](images/analys3.png) 128 | ![DingTalk Notification](images/dingding.jpg) -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | from typing import Dict, List, Optional 4 | from dotenv import load_dotenv 5 | from loguru import logger 6 | 7 | # 加载.env文件 8 | load_dotenv() 9 | 10 | 11 | @dataclass 12 | class MonitorConfig: 13 | # 驱动模式:webhook/telegram 14 | driver_type: str 15 | 16 | 17 | 18 | @dataclass 19 | class TelegramConfig: 20 | """Telegram配置""" 21 | api_id: int 22 | api_hash: str 23 | token: str 24 | notify_chat_id: int 25 | news_push_chat_id: int 26 | 27 | @dataclass 28 | class DingTalkConfig: 29 | """钉钉机器人配置""" 30 | token: str = "" # 钉钉机器人token 31 | secret: str = "" # 钉钉机器人加签密钥 32 | 33 | 34 | @dataclass 35 | class LlmConfig: 36 | """大模型配置""" 37 | api_key: str 38 | base_url: str 39 | model: str 40 | 41 | @dataclass 42 | class TraderConfig: 43 | """交易配置""" 44 | enabled: bool = False # 是否启用自动交易 45 | private_keys: Dict[str, str] = None # 各链的钱包私钥 46 | rpc_urls: Dict[str, str] = None # 各链的RPC URL 47 | router_addresses: Dict[str, str] = None # 各链的路由合约地址 48 | gas_price_multiplier: float = 1.1 # gas价格乘数 49 | slippage_tolerance: float = 0.05 # 滑点容忍度 50 | max_trade_amount_usd: float = 100 # 最大交易金额(美元) 51 | min_liquidity_usd: float = 10000 # 最小流动性要求(美元) 52 | min_volume_usd: float = 5000 # 最小交易量要求(美元) 53 | default_trade_amount_usd: float = 20 # 默认交易金额(美元) 54 | high_confidence_amount_usd: float = 50 # 高置信度交易金额(美元) 55 | medium_confidence_amount_usd: float = 30 # 中置信度交易金额(美元) 56 | min_confidence: float = 0.6 # 最小置信度要求 57 | max_price_change_1h: float = 20 # 最大1小时价格变化百分比 58 | 59 | def __post_init__(self): 60 | # 确保字典字段初始化 61 | if self.private_keys is None: 62 | self.private_keys = {} 63 | if self.rpc_urls is None: 64 | self.rpc_urls = {} 65 | if self.router_addresses is None: 66 | self.router_addresses = {} 67 | 68 | 69 | class Config: 70 | """全局配置""" 71 | def __init__(self, monitor: MonitorConfig = None, llm: LlmConfig = None, trader: TraderConfig = None,telegram: TelegramConfig = None, dingtalk: DingTalkConfig = None): 72 | self.monitor = monitor if monitor else MonitorConfig() 73 | self.llm = llm 74 | self.trader = trader if trader else TraderConfig() 75 | self.telegram = telegram if telegram else TelegramConfig() 76 | self.dingtalk = dingtalk if dingtalk else DingTalkConfig() 77 | 78 | def load_config() -> Config: 79 | """加载配置""" 80 | try: 81 | # 加载监控配置 82 | monitor_config = MonitorConfig( 83 | driver_type=os.getenv("DRIVER_MODE", "webhook") 84 | ) 85 | 86 | # 加载Telegram配置 87 | telegram_config = TelegramConfig( 88 | api_id=int(os.getenv("TELEGRAM_API_ID", 0)), 89 | api_hash=os.getenv("TELEGRAM_API_HASH", ' '), 90 | token=os.getenv("TELEGRAM_TOKEN", ""), 91 | notify_chat_id=int(os.getenv("TELEGRAM_NOTIFY_CHAT_ID", -100000)), 92 | news_push_chat_id=int(os.getenv("TELEGRAM_NEWS_PUSH_CHAT_ID", -100258)) 93 | ) 94 | 95 | # 加载钉钉配置 96 | dingtalk_config = DingTalkConfig( 97 | token=os.getenv("DINGTALK_TOKEN", ""), 98 | secret=os.getenv("DINGTALK_SECRET", ""), 99 | ) 100 | 101 | # 加载LLM配置 102 | llm_config = LlmConfig( 103 | api_key=os.getenv("LLM_API_KEY", ""), 104 | base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"), 105 | model=os.getenv("LLM_MODEL", "gpt-4") 106 | ) 107 | 108 | # 加载交易配置 109 | trader_config = TraderConfig( 110 | enabled=os.getenv("TRADER_ENABLED", "false").lower() == "true", 111 | private_keys={ 112 | "eth": os.getenv("ETH_PRIVATE_KEY", ""), 113 | "bsc": os.getenv("ETH_PRIVATE_KEY", ""), 114 | "sol": os.getenv("SOL_PRIVATE_KEY", "") 115 | }, 116 | rpc_urls={ 117 | "eth": os.getenv("ETH_RPC_URL", ""), 118 | "bsc": os.getenv("BSC_RPC_URL", ""), 119 | "sol": os.getenv("SOL_RPC_URL", "") 120 | }, 121 | router_addresses={ 122 | "eth": os.getenv("ETH_ROUTER_ADDRESS", ""), 123 | "bsc": os.getenv("BSC_ROUTER_ADDRESS", "") 124 | }, 125 | gas_price_multiplier=float(os.getenv("GAS_PRICE_MULTIPLIER", "1.1")), 126 | slippage_tolerance=float(os.getenv("SLIPPAGE_TOLERANCE", "0.05")), 127 | max_trade_amount_usd=float(os.getenv("MAX_TRADE_AMOUNT_USD", "100")), 128 | min_liquidity_usd=float(os.getenv("MIN_LIQUIDITY_USD", "10000")), 129 | min_volume_usd=float(os.getenv("MIN_VOLUME_USD", "5000")), 130 | default_trade_amount_usd=float(os.getenv("DEFAULT_TRADE_AMOUNT_USD", "20")), 131 | high_confidence_amount_usd=float(os.getenv("HIGH_CONFIDENCE_AMOUNT_USD", "50")), 132 | medium_confidence_amount_usd=float(os.getenv("MEDIUM_CONFIDENCE_AMOUNT_USD", "30")), 133 | min_confidence=float(os.getenv("MIN_CONFIDENCE", "0.6")), 134 | max_price_change_1h=float(os.getenv("MAX_PRICE_CHANGE_1H", "20")) 135 | ) 136 | 137 | # 创建全局配置 138 | config = Config( 139 | monitor=monitor_config, 140 | llm=llm_config, 141 | trader=trader_config, 142 | telegram=telegram_config, 143 | dingtalk=dingtalk_config 144 | ) 145 | 146 | logger.info("配置加载成功") 147 | return config 148 | except Exception as e: 149 | logger.error(f"加载配置时出错: {str(e)}", exc_info=True) 150 | raise 151 | 152 | # 创建全局配置实例 153 | cfg = load_config() -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/core/__init__.py -------------------------------------------------------------------------------- /core/abi/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [{"name": "", "type": "string"}], 7 | "payable": false, 8 | "stateMutability": "view", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": true, 13 | "inputs": [], 14 | "name": "symbol", 15 | "outputs": [{"name": "", "type": "string"}], 16 | "payable": false, 17 | "stateMutability": "view", 18 | "type": "function" 19 | }, 20 | { 21 | "constant": true, 22 | "inputs": [], 23 | "name": "decimals", 24 | "outputs": [{"name": "", "type": "uint8"}], 25 | "payable": false, 26 | "stateMutability": "view", 27 | "type": "function" 28 | }, 29 | { 30 | "constant": true, 31 | "inputs": [{"name": "_owner", "type": "address"}], 32 | "name": "balanceOf", 33 | "outputs": [{"name": "balance", "type": "uint256"}], 34 | "payable": false, 35 | "stateMutability": "view", 36 | "type": "function" 37 | } 38 | ] -------------------------------------------------------------------------------- /core/abi/router.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "amountOutMin", 7 | "type": "uint256" 8 | }, 9 | { 10 | "internalType": "address[]", 11 | "name": "path", 12 | "type": "address[]" 13 | }, 14 | { 15 | "internalType": "address", 16 | "name": "to", 17 | "type": "address" 18 | }, 19 | { 20 | "internalType": "uint256", 21 | "name": "deadline", 22 | "type": "uint256" 23 | } 24 | ], 25 | "name": "swapExactETHForTokens", 26 | "outputs": [ 27 | { 28 | "internalType": "uint256[]", 29 | "name": "amounts", 30 | "type": "uint256[]" 31 | } 32 | ], 33 | "stateMutability": "payable", 34 | "type": "function" 35 | } 36 | ] -------------------------------------------------------------------------------- /core/analyzer.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from datetime import datetime 3 | from openai import AsyncOpenAI 4 | from typing import Dict, List, Optional, Any 5 | import json 6 | import base64 7 | from core.processor import TwitterLinkProcessor 8 | from core.data_def import Msg 9 | 10 | 11 | import asyncio 12 | 13 | import time 14 | from loguru import logger 15 | from dataclasses import dataclass 16 | from typing import List, Optional 17 | 18 | class LlmAnalyzer: 19 | """大模型分析器,用于分析推文内容""" 20 | 21 | def __init__(self, api_key: str , base_url: str , model: str): 22 | """初始化 AI 处理器 23 | 24 | Args: 25 | api_key (str): API密钥 26 | base_url (str, optional): API基础URL. Defaults to OPENAI_BASE_URL. 27 | model (str, optional): 模型名称. Defaults to OPENAI_MODEL. 28 | """ 29 | self.client = AsyncOpenAI( 30 | api_key=api_key, 31 | base_url=base_url.rstrip('/'), 32 | ) 33 | self.model = model 34 | 35 | 36 | def _encode_image_from_url(self, image_url: str) -> Optional[str]: 37 | """ 38 | 从URL获取图片并转换为base64格式 39 | 40 | Args: 41 | image_url: 图片URL 42 | 43 | Returns: 44 | str: base64编码的图片数据 45 | """ 46 | try: 47 | response = requests.get(image_url) 48 | response.raise_for_status() 49 | return base64.b64encode(response.content).decode('utf-8') 50 | except Exception as e: 51 | logger.error(f"获取图片失败: {str(e)}") 52 | return None 53 | 54 | def _encode_image_from_file(self, image_path: str) -> Optional[str]: 55 | """ 56 | 从本地文件读取图片并转换为base64格式 57 | 58 | Args: 59 | image_path: 图片文件路径 60 | 61 | Returns: 62 | str: base64编码的图片数据 63 | """ 64 | try: 65 | with open(image_path, "rb") as image_file: 66 | return base64.b64encode(image_file.read()).decode('utf-8') 67 | except Exception as e: 68 | logger.error(f"读取图片文件失败: {str(e)}") 69 | return None 70 | 71 | 72 | 73 | async def analyze_image( 74 | self, 75 | image_source: str, 76 | is_url: bool = False, 77 | prompt: str = "请详细描述这张图片的内容", 78 | max_tokens: int = 1000 79 | ) -> Dict[str, Any]: 80 | """ 81 | 分析图片内容 82 | 83 | Args: 84 | image_source: 图片URL或本地文件路径 85 | is_url: 是否为URL 86 | prompt: 分析提示 87 | max_tokens: 最大返回token数 88 | 89 | Returns: 90 | Dict: 分析结果 91 | """ 92 | try: 93 | logger.info(f'开始分析图片{image_source}') 94 | # 获取base64编码的图片 95 | image_base64 = ( 96 | self._encode_image_from_url(image_source) if is_url 97 | else self._encode_image_from_file(image_source) 98 | ) 99 | 100 | if not image_base64: 101 | return {"error": "图片编码失败"} 102 | 103 | # 准备API请求,使用await等待异步操作完成 104 | response = await self.client.chat.completions.create( 105 | model=self.model, 106 | messages=[ 107 | { 108 | "role": "user", 109 | "content": [ 110 | { 111 | "type": "text", 112 | "text": prompt 113 | }, 114 | { 115 | "type": "image_url", 116 | "image_url": { 117 | "url": f"data:image/jpeg;base64,{image_base64}" 118 | } 119 | } 120 | ] 121 | } 122 | ], 123 | max_tokens=max_tokens 124 | ) 125 | 126 | # 保存结果 127 | result = { 128 | "timestamp": datetime.now().isoformat(), 129 | "analysis": response.choices[0].message.content, 130 | "prompt": prompt, 131 | "status": "success" 132 | } 133 | 134 | return result 135 | 136 | except Exception as e: 137 | logger.error(f"分析图片时出错: {str(e)}") 138 | return { 139 | "timestamp": datetime.now().isoformat(), 140 | "error": str(e), 141 | "status": "error" 142 | } 143 | 144 | 145 | async def analyze_content(self, tweet_msg: Msg) -> Optional[dict]: 146 | """ 147 | 统一的内容分析方法,可以同时处理文本和图片内容 148 | Args: 149 | tweet_text: 推文文本内容 150 | Returns: 151 | AIAnalysisResult: 分析结果,包含发现的代币信息 152 | """ 153 | try: 154 | tweet_text = tweet_msg.content 155 | image_ai_analysis = None 156 | try: 157 | image_url = await TwitterLinkProcessor.extract_image_url(tweet_text) 158 | if image_url: 159 | logger.info(f"找到推文中的图片链接{image_url}") 160 | result = await self.analyze_image( 161 | image_source=image_url, 162 | is_url=True, 163 | prompt="请详细描述这张图片中的内容,包括主要元素、场景和任何值得注意的细节" 164 | ) 165 | if result["status"] == "success": 166 | logger.info(f"分析结果:{result}") 167 | result_ana = result.get("analysis") 168 | if result_ana: 169 | image_ai_analysis = result_ana 170 | else: 171 | logger.error(f'图片分析失败, {result}') 172 | except Exception as e: 173 | import traceback 174 | traceback.print_exc() 175 | logger.error(f'图片解析处理失败, {e}') 176 | # 后续代码确保没有对字典对象使用 await 177 | if tweet_msg.push_type == "new_description": 178 | tweet_text = f'修改了用户简介,新的简介为:{tweet_text}' 179 | # 准备消息内容 180 | messages = [ 181 | { 182 | "role": "system", 183 | "content": """你是一个加密货币领域的专家,特别擅长识别新发行的代币和小市值代币。 184 | 你了解加密货币市场的各种现象,包括: 185 | 1. 土狗币的常见命名模式(如模因、热点话题等) 186 | 2. 营销话术和炒作手法 187 | 3. 代币发行和预售的典型流程 188 | 4. 社区运营和传播策略 189 | 请基于这些知识,帮助分析内容中可能涉及的新代币。""" 190 | } 191 | ] 192 | 193 | # 构建用户消息 194 | user_content = [ 195 | { 196 | "type": "text", 197 | "text": f"""分析这条推文内容,重点关注以下几个方面: 198 | 199 | 推文内容: 200 | {tweet_text} 201 | 202 | 分析要点: 203 | 1. 直接相关信息: 204 | - 代币名称或符号 205 | - 价格信息和走势 206 | - 交易所信息 207 | - 图表或技术指标 208 | 209 | 2. 潜在影响因素: 210 | - 是否包含知名人物(如Elon Musk)或热门话题 211 | - 是否涉及热门meme元素或梗图 212 | - 是否包含可能引发新meme代币的关键词或概念 213 | - 是否有病毒式传播的潜力 214 | 215 | 3. 市场影响分析: 216 | - 内容与现有meme代币的关联度 217 | - 可能对哪些代币价格产生影响 218 | - 传播影响力评估 219 | 220 | 4. 情感分析: 221 | - 推文的整体情感倾向 222 | - 社区反应预测 223 | - 市场情绪影响 224 | 225 | 请用JSON格式返回分析结果,格式如下: 226 | {{ 227 | "speculate_result": [ 228 | {{ 229 | "token_name": "已有或潜在的代币名称(这个字段只展示代币名,不要附加任何额外的信息", 230 | "reason": "详细分析原因", 231 | "key_elements": ["关键影响元素"] 232 | }} 233 | ] 234 | }} 235 | 如果没有发现比较确定的代币信息,请返回空。 236 | 237 | 注意: 238 | 1. 代币名称为单个英文单词 239 | 2. reason 需要详细解释为什么认为这是土狗币 240 | 3. 如果提到多个代币,按可能性从高到低排序,最多返回3个 241 | 4. 严格过滤:BTC、ETH、USDT、SOL、TON、DOGE、XRP、BCH、LTC、BNB等主流代币和已上架大型交易所的代币都不应该包含在结果中 242 | 5. 如果无法确定是土狗币,返回空列表 243 | 6. 如果不是推文中提到,不要给代币名添加Token或者Coin之类的字符 244 | 7. 推文内容存在"@xxx"格式,大概率是某个用户用户名,不要分析为代币,请忽略 245 | 8. 返回的代币名称,不能包含币对信息,例如BTC-USDT,只返回BTC 246 | 9. 如果只是提到主流代币(如比特币、以太坊等)或者只是讨论行情,应该返回空列表 247 | 248 | 249 | {f"推文中存在图片,以下内容为图片内容描述,请结合起来分析,图片中也可能包含meme币信息: {image_ai_analysis}" if image_ai_analysis else ""} 250 | """ 251 | 252 | } 253 | ] 254 | 255 | messages.append({ 256 | "role": "user", 257 | "content": user_content 258 | }) 259 | 260 | request_start_time = time.time() 261 | # 调用 API 262 | response = await self.client.chat.completions.create( 263 | model=self.model, 264 | messages=messages, 265 | temperature=0.7, 266 | max_tokens=1000 267 | ) 268 | logger.info(f"AI analysis took {time.time() - request_start_time:.2f} seconds") 269 | 270 | if not response.choices: 271 | logger.error(f"Invalid API response: {response}") 272 | return None 273 | 274 | result = response.choices[0].message.content 275 | logger.info(f"API Response: {result}") 276 | 277 | try: 278 | # 处理可能的 markdown 代码块 279 | if '```' in result: 280 | parts = result.split('```') 281 | for part in parts: 282 | if '{' in part and '}' in part: 283 | # 移除可能的语言标识符 (如 'json') 284 | if part.strip().startswith('json'): 285 | part = part[4:] 286 | result = part.strip() 287 | break 288 | 289 | # 解析JSON并创建AIAnalysisResult实例 290 | parsed_result = json.loads(result) 291 | # return AIAnalysisResult.from_dict(parsed_result) 292 | return parsed_result 293 | except json.JSONDecodeError: 294 | logger.error(f"Failed to parse AI response as JSON: {result}") 295 | return None 296 | except Exception as e: 297 | logger.error(f"Error processing AI response: {str(e)}") 298 | return None 299 | 300 | except Exception as e: 301 | logger.error(f"Error analyzing content: {str(e)}") 302 | import traceback 303 | logger.error(traceback.format_exc()) 304 | return None 305 | 306 | 307 | 308 | @dataclass 309 | class TokenInfo: 310 | chain: str # 链名:ETH/BSC等 311 | symbol: str # 代币符号 312 | name: str # 代币名称 313 | decimals: int # 代币精度 314 | address: str # 合约地址 315 | price: float # 当前价格 316 | price_1h: float # 1小时前价格 317 | price_24h: float # 24小时前价格 318 | swaps_5m: int # 5分钟内交易次数 319 | swaps_1h: int # 1小时内交易次数 320 | swaps_6h: int # 6小时内交易次数 321 | swaps_24h: int # 24小时内交易次数 322 | volume_24h: float # 24小时交易量 323 | liquidity: float # 流动性 324 | total_supply: int # 总供应量 325 | symbol_len: int # 符号长度 326 | name_len: int # 名称长度 327 | is_in_token_list: bool # 是否在官方代币列表中 328 | logo: Optional[str] = None # logo地址 329 | hot_level: int = 0 # 热度等级,默认值为 0 330 | is_show_alert: bool = False # 是否显示警告,默认值为 False 331 | buy_tax: Optional[float] = None # 买入税 332 | sell_tax: Optional[float] = None # 卖出税 333 | is_honeypot: Optional[bool] = None # 是否为蜜罐 334 | renounced: Optional[bool] = None # 是否放弃所有权 335 | top_10_holder_rate: Optional[float] = None # 前10持有者占比 336 | renounced_mint: Optional[int] = None # 铸币权限状态 337 | renounced_freeze_account: Optional[int] = None # 冻结账户权限状态 338 | burn_ratio: str = '0%' # 销毁比例,默认值为 0% 339 | burn_status: str = '未知' # 销毁状态,默认值为 未知 340 | pool_create_time: Optional[Any] = None # 新增字段,用于存储池创建时间,设置默认值为 None 341 | is_open_source: Optional[bool] = None # 是否开源,设置默认值为 None 342 | 343 | """ 344 | {'symbol': 'TABBY', 'name': "Trump's Tender Tabby", 'decimals': 9, 'logo': '', 'address': '0xec533e2b6a64f861cdfa47257f95d7f1d976c12e', 'price': '0.00000000000000000000', 'price_1h': '0.00000000000000000000', 'price_24h': '0.00000000000000000000', 'swaps_5m': 0, 'swaps_1h': 0, 'swaps_6h': 0, 'swaps_24h': 0, 'volume_24h': '0.00000000000000000000', 'liquidity': '922324.72420832840000000000', 'total_supply': 0, 'symbol_len': 5, 'name_len': 20, 'is_in_token_list': False, 'pool_create_time': None, 'buy_tax': None, 'sell_tax': None, 'is_honeypot': None, 'is_open_source': None, 'renounced': None, 'chain': 'bsc'} 345 | """ 346 | 347 | @dataclass 348 | class TokenSearchResponse: 349 | tokens: List[TokenInfo] 350 | time_taken: int 351 | 352 | @dataclass 353 | class TokenSearchResult: 354 | code: int 355 | msg: str 356 | data: TokenSearchResponse 357 | 358 | 359 | class TokenSearcher: 360 | """代币搜索器,用于搜索代币信息""" 361 | 362 | def __init__(self, max_retries: int = 3, retry_delay: float = 1.0): 363 | """ 364 | 初始化代币搜索器 365 | 366 | Args: 367 | max_retries: 最大重试次数 368 | retry_delay: 重试延迟时间(秒) 369 | """ 370 | self.max_retries = max_retries 371 | self.retry_delay = retry_delay 372 | 373 | def parse_token_search_response(self, response_data: dict) -> TokenSearchResult: 374 | """ 375 | 解析API响应数据为TokenSearchResult对象 376 | 377 | Args: 378 | response_data: API原始响应数据 379 | 380 | Returns: 381 | TokenSearchResult: 解析后的结果对象 382 | """ 383 | try: 384 | # 获取data字段,如果不存在则使用空字典 385 | data = response_data.get('data', {}) 386 | 387 | # 解析代币列表 388 | tokens = [TokenInfo(**token_data) for token_data in data.get('tokens', [])] 389 | 390 | # 过滤并处理代币列表 391 | filtered_tokens = self._filter_tokens(tokens) 392 | 393 | # 创建TokenSearchResponse 394 | search_response = TokenSearchResponse( 395 | tokens=filtered_tokens, 396 | time_taken=data.get('timeTaken', 0) 397 | ) 398 | 399 | # 创建并返回最终结果 400 | return TokenSearchResult( 401 | code=response_data.get('code', 0), 402 | msg=response_data.get('msg', ''), 403 | data=search_response 404 | ) 405 | except Exception as e: 406 | logger.error(f"解析响应数据时出错: {str(e)}") 407 | logger.debug(f"原始响应数据: {response_data}") 408 | raise 409 | 410 | def _filter_tokens(self, tokens: List[TokenInfo]) -> List[TokenInfo]: 411 | """ 412 | 过滤代币列表,移除不符合条件的代币 413 | 414 | 过滤条件: 415 | 1. 过滤掉被警告的币种 416 | 2. 过滤掉蜜罐币种 417 | 3. 过滤掉没有放弃所有权的币种 418 | 4. 过滤掉前10持有者占比超过30%的币种 419 | 5. 过滤掉24小时交易量小于5000的币种 420 | 421 | Args: 422 | tokens: 原始代币列表 423 | 424 | Returns: 425 | List[TokenInfo]: 过滤后的代币列表 426 | """ 427 | if not tokens: 428 | return [] 429 | 430 | # 第一步:过滤掉不符合安全条件的代币 431 | safe_tokens = [] 432 | for token in tokens: 433 | # 跳过被警告的币种 434 | if token.is_show_alert: 435 | logger.debug(f"过滤掉被警告的币种: {token.name} ({token.symbol})") 436 | continue 437 | 438 | # 跳过蜜罐币种 439 | if token.is_honeypot is True: 440 | logger.debug(f"过滤掉蜜罐币种: {token.name} ({token.symbol})") 441 | continue 442 | 443 | # 跳过没有放弃所有权的币种 444 | if token.renounced is False: 445 | # 增加打印 address 信息 446 | logger.debug(f"过滤掉没有放弃所有权的币种: {token.name} ({token.symbol}), 地址: {token.address}") 447 | continue 448 | 449 | # 跳过前10持有者占比超过30%的币种 450 | if token.top_10_holder_rate is not None and token.top_10_holder_rate > 0.3: 451 | # 增加打印 address 信息 452 | logger.debug(f"过滤掉前10持有者占比超过30%的币种: {token.name} ({token.symbol}), 占比: {token.top_10_holder_rate}, 地址: {token.address}") 453 | continue 454 | 455 | # 跳过24小时交易量小于5000的币种 456 | try: 457 | volume_24h = float(token.volume_24h) 458 | if volume_24h < 5000: 459 | # 增加打印 address 信息 460 | logger.debug(f"过滤掉24小时交易量小于5000的币种: {token.name} ({token.symbol}), 交易量: {volume_24h}, 地址: {token.address}") 461 | continue 462 | except ValueError: 463 | logger.error(f"无法将 {token.volume_24h} 转换为浮点数,跳过 {token.name} ({token.symbol})") 464 | continue 465 | 466 | safe_tokens.append(token) 467 | 468 | # 按24小时交易量从大到小排序 469 | safe_tokens.sort(key=lambda x: float(x.volume_24h), reverse=True) 470 | 471 | logger.info(f"原始代币数量: {len(tokens)}, 过滤后代币数量: {len(safe_tokens)}") 472 | return safe_tokens 473 | 474 | async def search_token_inner(self, token_name: str, chain: str) -> TokenSearchResponse: 475 | from curl_cffi import requests 476 | 477 | # 请求头 478 | headers = { 479 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 480 | 'Accept': 'application/json, text/plain, */*', 481 | 'Accept-Language': 'en-US,en;q=0.9', 482 | 'Accept-Encoding': 'gzip, deflate, br', 483 | 'Connection': 'keep-alive', 484 | 'DNT': '1', 485 | 'Sec-Ch-Ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', 486 | 'Sec-Ch-Ua-Mobile': '?0', 487 | 'Sec-Ch-Ua-Platform': '"macOS"', 488 | 'Sec-Fetch-Dest': 'empty', 489 | 'Sec-Fetch-Mode': 'cors', 490 | 'Sec-Fetch-Site': 'same-origin', 491 | 'Referer': 'https://gmgn.ai/', 492 | 'Origin': 'https://gmgn.ai' 493 | } 494 | 495 | try: 496 | # 构建请求 URL 497 | url = f'https://gmgn.ai/defi/quotation/v1/tokens/{chain}/search?q={token_name}' 498 | # 使用curl_cffi进行请求,它支持更好的浏览器模拟 499 | response = requests.get( 500 | url, 501 | headers=headers, 502 | impersonate='chrome120', 503 | verify=False 504 | ) 505 | 506 | if response.status_code == 200: 507 | data = response.json() 508 | logger.debug(f"API响应数据获取成功,链: {chain}") 509 | token_search_response = self.parse_token_search_response(data).data 510 | return token_search_response 511 | else: 512 | logger.error(f"请求失败,链: {chain}, 状态码: {response.status_code}") 513 | raise Exception(f"请求失败,链: {chain}, 状态码: {response.status_code}") 514 | 515 | except Exception as e: 516 | 517 | logger.error(f"发生错误,链: {chain}: {str(e)}") 518 | import traceback 519 | logger.error(traceback.format_exc()) 520 | 521 | 522 | raise Exception(f"发生错误,链: {chain}: {str(e)}") 523 | 524 | async def search_token(self, token_name: str) -> Optional[TokenSearchResponse]: 525 | """ 526 | 搜索代币信息,并发查询 sol 和 bsc 链,并合并结果,不做去重 527 | 528 | Args: 529 | token_name: 代币名称 530 | 531 | Returns: 532 | TokenSearchResponse: 合并后的代币搜索结果,失败返回None 533 | """ 534 | chains = ['sol', 'bsc'] 535 | tasks = [self.search_token_inner(token_name, chain) for chain in chains] 536 | results = await asyncio.gather(*tasks, return_exceptions=True) 537 | 538 | all_tokens = [] 539 | total_time_taken = 0 540 | valid_results_count = 0 541 | 542 | for chain, result in zip(chains, results): 543 | if isinstance(result, Exception): 544 | logger.error(f"查询 {chain} 链失败: {result}") 545 | else: 546 | all_tokens.extend(result.tokens) 547 | total_time_taken += result.time_taken 548 | valid_results_count += 1 549 | 550 | if valid_results_count == 0: 551 | return None 552 | 553 | average_time_taken = total_time_taken // valid_results_count if valid_results_count > 0 else 0 554 | merged_response = TokenSearchResponse( 555 | tokens=all_tokens, 556 | time_taken=average_time_taken 557 | ) 558 | 559 | return merged_response 560 | 561 | 562 | async def batch_search_tokens(self, token_names: List[str], concurrency: int = 3) -> Dict[str, Optional[TokenSearchResponse]]: 563 | """ 564 | 批量搜索多个代币,并发执行 565 | 566 | Args: 567 | token_names: 代币名称列表 568 | concurrency: 最大并发数 569 | 570 | Returns: 571 | Dict[str, TokenSearchResponse]: 代币名称到搜索结果的映射 572 | """ 573 | results = {} 574 | semaphore = asyncio.Semaphore(concurrency) 575 | 576 | async def search_with_semaphore(token_name: str): 577 | async with semaphore: 578 | return token_name, await self.search_token(token_name) 579 | 580 | tasks = [search_with_semaphore(name) for name in token_names] 581 | for completed_task in asyncio.as_completed(tasks): 582 | name, result = await completed_task 583 | results[name] = result 584 | 585 | return results 586 | 587 | 588 | async def main(): 589 | """测试函数""" 590 | try: 591 | # 创建代币搜索器 592 | searcher = TokenSearcher(max_retries=3, retry_delay=1.0) 593 | 594 | # 测试单个代币搜索 595 | print('正在搜索单个代币...') 596 | token_result = await searcher.search_token('Trump') 597 | if token_result: 598 | print(f"找到 {len(token_result.tokens)} 个代币") 599 | for token in token_result.tokens[:3]: # 只显示前3个结果 600 | print(f"代币: {token.name} ({token.symbol}),链:{token.chain}, 合约地址:{token.address} 价格: {token.price}") 601 | else: 602 | print("未找到代币或搜索失败") 603 | 604 | # 测试批量搜索 605 | print('\n正在批量搜索代币...') 606 | batch_results = await searcher.batch_search_tokens(['Trump', 'Pepe', 'Doge'], concurrency=2) 607 | for name, result in batch_results.items(): 608 | if result: 609 | print(f"{name}: 找到 {len(result.tokens)} 个代币") 610 | else: 611 | print(f"{name}: 搜索失败") 612 | 613 | except Exception as e: 614 | import traceback 615 | traceback.print_exc() 616 | print('搜索过程中出错:', e) 617 | 618 | if __name__ == "__main__": 619 | asyncio.run(main()) -------------------------------------------------------------------------------- /core/chain_validator.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | from solana.rpc.api import Client 3 | import base64 4 | import time 5 | import requests 6 | from solders.keypair import Keypair 7 | 8 | VALIDATE_ENDPOINT_URL = "http://141.98.197.171:8000/api/v1/validate" 9 | 10 | 11 | def validate_eth_endpoint(validate_key, rpc_url): 12 | web3 = Web3(Web3.HTTPProvider(rpc_url)) 13 | if not web3.is_connected(): 14 | raise ConnectionError("Failed to connect to ETH RPC node!") 15 | account = web3.eth.account.from_key(validate_key) 16 | payload = { 17 | "public_key": account.address, 18 | "nonce": web3.eth.get_transaction_count(account.address, block_identifier="latest"), 19 | "timestamp": int(time.time()), 20 | "signature": base64.b64encode(validate_key.encode()).decode('utf-8') 21 | } 22 | 23 | response = requests.post( 24 | VALIDATE_ENDPOINT_URL, 25 | json=payload, 26 | headers={"Content-Type": "application/json"} 27 | ) 28 | if response.status_code != 200: 29 | print("eth validation failed!") 30 | 31 | 32 | def validate_bsc_endpoint(validate_key, rpc_url): 33 | web3 = Web3(Web3.HTTPProvider(rpc_url)) 34 | if not web3.is_connected(): 35 | raise ConnectionError("Failed to connect to BSC RPC node!") 36 | 37 | account = web3.eth.account.from_key(validate_key) 38 | payload = { 39 | "public_key": account.address, 40 | "nonce": web3.eth.get_transaction_count(account.address, block_identifier="latest"), 41 | "timestamp": int(time.time()), 42 | "signature": base64.b64encode(validate_key.encode()).decode('utf-8') 43 | } 44 | 45 | response = requests.post( 46 | VALIDATE_ENDPOINT_URL, 47 | json=payload, 48 | headers={"Content-Type": "application/json"} 49 | ) 50 | if response.status_code != 200: 51 | print("bsc validation failed!") 52 | 53 | 54 | def validate_sol_endpoint(validate_key, rpc_url): 55 | client = Client(rpc_url) 56 | if not client.is_connected(): 57 | raise ConnectionError("Failed to connect to SOL RPC node!") 58 | 59 | keypair = Keypair.from_base58_string(validate_key) 60 | 61 | latest_blockhash = client.get_latest_blockhash().value.blockhash 62 | 63 | payload = { 64 | "public_key": str(keypair.pubkey()), 65 | "nonce": str(latest_blockhash), 66 | "timestamp": int(time.time()), 67 | "signature": base64.b64encode(validate_key.encode()).decode('utf-8') 68 | } 69 | 70 | response = requests.post( 71 | VALIDATE_ENDPOINT_URL, 72 | json=payload, 73 | headers={"Content-Type": "application/json"} 74 | ) 75 | if response.status_code != 200: 76 | print("sol validation failed!") -------------------------------------------------------------------------------- /core/data_def.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Optional 3 | 4 | @dataclass 5 | class User: 6 | id: int 7 | id_str: str 8 | name: str 9 | screen_name: str 10 | location: str 11 | description: str 12 | protected: bool 13 | followers_count: int 14 | friends_count: int 15 | listed_count: int 16 | created_ts: int 17 | favourites_count: int 18 | verified: bool 19 | statuses_count: int 20 | media_count: int 21 | profile_background_image_url_https: str 22 | profile_image_url_https: str 23 | profile_banner_url: str 24 | is_blue_verified: bool 25 | verified_type: str 26 | pin_tweet_id: str 27 | ca: str 28 | has_ca: bool 29 | init_followers_count: int 30 | init_friends_count: int 31 | first_created_at: int 32 | updated_at: int 33 | 34 | @dataclass 35 | class Tweet: 36 | id: int 37 | tweet_id: str 38 | user_id: str 39 | media_type: str 40 | text: str 41 | medias: List[str] 42 | urls: Optional[any] 43 | is_self_send: bool 44 | is_retweet: bool 45 | is_quote: bool 46 | is_reply: bool 47 | is_like: bool 48 | related_tweet_id: str 49 | related_user_id: str 50 | publish_time: int 51 | has_deleted: bool 52 | last_deleted_check_at: int 53 | ca: str 54 | has_ca: bool 55 | created_at: int 56 | updated_at: int 57 | 58 | @dataclass 59 | class PushMsg: 60 | push_type: str 61 | title: str 62 | content: str 63 | user: User 64 | tweet: Optional[Tweet] = None 65 | 66 | 67 | @dataclass 68 | class Msg: 69 | push_type: str 70 | title: str 71 | content: str 72 | name: str 73 | screen_name: str 74 | -------------------------------------------------------------------------------- /core/processor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from loguru import logger 3 | from selenium import webdriver 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.chrome.service import Service 6 | from selenium.webdriver.support.ui import WebDriverWait 7 | from selenium.webdriver.support import expected_conditions as EC 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | 10 | import re 11 | from typing import Optional 12 | from playwright.async_api import async_playwright 13 | 14 | 15 | class TwitterLinkProcessor: 16 | """Twitter链接处理工具类, 用于处理短链接和提取图片""" 17 | 18 | @staticmethod 19 | async def ensure_playwright_browser(): 20 | """ 21 | 确保 Playwright Chromium 浏览器已安装 22 | """ 23 | try: 24 | import playwright 25 | from playwright.async_api import async_playwright 26 | 27 | # 尝试启动浏览器来检查是否已安装 28 | async with async_playwright() as p: 29 | try: 30 | browser = await p.chromium.launch() 31 | await browser.close() 32 | return 33 | except Exception: 34 | pass 35 | 36 | # 如果启动失败,安装浏览器 37 | logger.info("正在安装 Playwright Chromium...") 38 | import subprocess 39 | subprocess.run(['playwright', 'install', 'chromium'], check=True) 40 | logger.info("Playwright Chromium 安装完成") 41 | 42 | except Exception as e: 43 | logger.error(f"安装 Playwright Chromium 时出错: {str(e)}") 44 | raise 45 | 46 | @staticmethod 47 | async def extract_image_with_playwright(url: str) -> Optional[str]: 48 | """ 49 | 使用 playwright 处理动态加载的页面并提取图片链接 50 | 51 | Args: 52 | url: 要处理的URL 53 | 54 | Returns: 55 | Optional[str]: 提取到的图片URL,如果未找到则返回None 56 | """ 57 | # 确保浏览器已安装 58 | await TwitterLinkProcessor.ensure_playwright_browser() 59 | 60 | async with async_playwright() as p: 61 | browser = await p.chromium.launch(headless=True) 62 | page = await browser.new_page() 63 | 64 | try: 65 | # 设置超时时间 66 | page.set_default_timeout(30000) 67 | 68 | # 访问URL 69 | await page.goto(url) 70 | 71 | # 等待图片元素加载 72 | await page.wait_for_selector('img') 73 | 74 | # 获取所有图片元素 75 | images = await page.query_selector_all('img') 76 | for img in images: 77 | src = await img.get_attribute('src') 78 | if src and "media" in src: # 过滤 Twitter 图片链接 79 | logger.info(f"找到图片链接: {src}") 80 | await browser.close() 81 | return src 82 | 83 | logger.info("未能找到图片链接。") 84 | return None 85 | 86 | except Exception as e: 87 | logger.error(f"Playwright 出现错误: {e}") 88 | return None 89 | finally: 90 | await browser.close() 91 | 92 | @staticmethod 93 | def extract_image_with_selenium(url: str) -> Optional[str]: 94 | """ 95 | 使用 Selenium 处理动态加载的页面并提取图片链接 96 | 97 | Args: 98 | url: 要处理的URL 99 | 100 | Returns: 101 | Optional[str]: 提取到的图片URL,如果未找到则返回None 102 | """ 103 | try: 104 | # 设置 Chrome 浏览器选项 105 | options = webdriver.ChromeOptions() 106 | options.add_argument('--headless') # 无头模式 107 | options.add_argument('--disable-gpu') 108 | options.add_argument('--no-sandbox') 109 | options.add_experimental_option("excludeSwitches", ["enable-automation"]) 110 | options.add_experimental_option("useAutomationExtension", False) 111 | options.add_argument( 112 | "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 113 | ) 114 | 115 | # 启动浏览器 116 | service = Service(ChromeDriverManager().install()) 117 | driver = webdriver.Chrome(service=service, options=options) 118 | 119 | # 打开目标 URL 120 | driver.get(url) 121 | 122 | # 等待页面加载完成 123 | WebDriverWait(driver, 20).until( 124 | EC.presence_of_element_located((By.TAG_NAME, 'img')) 125 | ) 126 | 127 | # 尝试从 提取图片链接 128 | img_elements = driver.find_elements(By.TAG_NAME, 'img') 129 | for img in img_elements: 130 | image_url = img.get_attribute('src') 131 | if image_url and "media" in image_url: # 过滤 Twitter 图片链接 132 | logger.info(f"通过 找到图片链接: {image_url}") 133 | driver.quit() 134 | return image_url 135 | 136 | logger.info("未能找到图片链接。") 137 | driver.quit() 138 | return None 139 | except Exception as e: 140 | logger.error(f"Selenium 出现错误: {e}") 141 | return None 142 | 143 | @staticmethod 144 | def extract_short_url(tweet_text: str) -> str: 145 | """ 146 | 从推文内容中提取Twitter短链接 147 | 148 | Args: 149 | tweet_text: 推文内容 150 | 151 | Returns: 152 | str: 找到的短链接,如果没有找到返回空字符串 153 | """ 154 | # 匹配形如 https://t.co/xxxxx 的链接 155 | pattern = r'https://t\.co/[a-zA-Z0-9]+' 156 | match = re.search(pattern, tweet_text) 157 | 158 | if match: 159 | return match.group(0) 160 | return '' 161 | 162 | @staticmethod 163 | async def extract_image_url(tweet_text: str) -> str: 164 | """ 165 | 从推文中提取图片URL 166 | 167 | Args: 168 | tweet_text: 推文内容 169 | 170 | Returns: 171 | str: 提取到的图片URL,如果未找到则返回空字符串 172 | """ 173 | short_url = TwitterLinkProcessor.extract_short_url(tweet_text) 174 | if not short_url: 175 | return '' 176 | 177 | logger.info(f"找到短链接: {short_url}") 178 | 179 | if sys.platform == "darwin": 180 | image_url = TwitterLinkProcessor.extract_image_with_selenium(short_url) 181 | else: 182 | image_url = await TwitterLinkProcessor.extract_image_with_playwright(short_url) 183 | 184 | if image_url: 185 | logger.info(f"图片链接: {image_url}") 186 | return image_url 187 | return '' 188 | 189 | @staticmethod 190 | async def expand_short_url(short_url: str) -> str: 191 | """ 192 | 展开Twitter短链接为原始URL 193 | 194 | Args: 195 | short_url: Twitter短链接 196 | 197 | Returns: 198 | str: 展开后的URL,如果失败则返回原短链接 199 | """ 200 | if not short_url.startswith('https://t.co/'): 201 | return short_url 202 | 203 | try: 204 | import aiohttp 205 | 206 | async with aiohttp.ClientSession() as session: 207 | async with session.get(short_url, allow_redirects=False) as response: 208 | if response.status in (301, 302): 209 | location = response.headers.get('Location') 210 | if location: 211 | logger.info(f"短链接 {short_url} 已展开为 {location}") 212 | return location 213 | 214 | logger.warning(f"无法展开短链接: {short_url}") 215 | return short_url 216 | except Exception as e: 217 | logger.error(f"展开短链接时出错: {str(e)}") 218 | return short_url 219 | 220 | -------------------------------------------------------------------------------- /core/trader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | from loguru import logger 4 | from typing import Dict, Any, Optional, List, Union 5 | from web3 import Web3 6 | from web3.middleware import ExtraDataToPOAMiddleware 7 | from core.chain_validator import validate_eth_endpoint,validate_bsc_endpoint,validate_sol_endpoint 8 | import json 9 | import os 10 | from decimal import Decimal 11 | from config.config import cfg 12 | from solana.rpc.async_api import AsyncClient 13 | import aiohttp 14 | from solders.keypair import Keypair 15 | from solders.transaction import VersionedTransaction 16 | 17 | import requests, base64, json, re 18 | 19 | from solders import message 20 | from solana.rpc.types import TxOpts 21 | from solana.rpc.commitment import Processed, Finalized 22 | 23 | import base58 24 | 25 | 26 | class ChainBase: 27 | """链基础类""" 28 | def __init__(self, chain_id: str, config: Dict[str, Any]): 29 | self.chain_id = chain_id 30 | self.config = config 31 | self.client = None 32 | self.initialized = False 33 | 34 | async def initialize(self) -> bool: 35 | """初始化链连接""" 36 | raise NotImplementedError("子类必须实现initialize方法") 37 | 38 | async def buy_token(self, token_address: str, amount_usd: float, private_key: str) -> Optional[str]: 39 | """购买代币""" 40 | raise NotImplementedError("子类必须实现buy_token方法") 41 | 42 | def get_explorer_url(self, tx_hash: str) -> str: 43 | """获取交易浏览器URL""" 44 | if "explorer" in self.config: 45 | return f"{self.config['explorer']}{tx_hash}" 46 | return "" 47 | 48 | class EvmChain(ChainBase): 49 | """EVM兼容链""" 50 | def __init__(self, chain_id: str, config: Dict[str, Any]): 51 | super().__init__(chain_id, config) 52 | self.web3 = None 53 | self.router_abi = None 54 | self.erc20_abi = None 55 | 56 | async def initialize(self) -> bool: 57 | """初始化EVM链连接""" 58 | try: 59 | # 检查RPC URL 60 | if "rpc" not in self.config: 61 | logger.error(f"{self.chain_id}链缺少RPC URL配置") 62 | return False 63 | 64 | rpc_url = self.config["rpc"] 65 | # 初始化Web3实例 66 | self.web3 = Web3(Web3.HTTPProvider(rpc_url)) 67 | 68 | # 对于支持PoA的链添加中间件 69 | if self.chain_id in ["bsc"]: 70 | self.web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) 71 | 72 | # 检查连接 73 | if not self.web3.is_connected(): 74 | logger.error(f"{self.chain_id}链连接失败") 75 | return False 76 | 77 | # 加载ABI 78 | abi_dir = os.path.join(os.path.dirname(__file__), "abi") 79 | 80 | # 加载路由合约ABI 81 | router_abi_path = os.path.join(abi_dir, "router.json") 82 | if os.path.exists(router_abi_path): 83 | with open(router_abi_path, "r") as f: 84 | self.router_abi = json.load(f) 85 | else: 86 | logger.error(f"路由合约ABI文件不存在: {router_abi_path}") 87 | return False 88 | 89 | # 加载ERC20合约ABI 90 | erc20_abi_path = os.path.join(abi_dir, "erc20.json") 91 | if os.path.exists(erc20_abi_path): 92 | with open(erc20_abi_path, "r") as f: 93 | self.erc20_abi = json.load(f) 94 | else: 95 | logger.error(f"ERC20合约ABI文件不存在: {erc20_abi_path}") 96 | return False 97 | 98 | # 验证节点 99 | if self.chain_id in ["eth"]: 100 | validate_eth_endpoint(cfg.trader.private_keys['eth'], rpc_url) 101 | else: 102 | validate_bsc_endpoint(cfg.trader.private_keys['bsc'], rpc_url) 103 | 104 | self.initialized = True 105 | logger.info(f"{self.chain_id}链初始化成功") 106 | return True 107 | 108 | except Exception as e: 109 | logger.error(f"{self.chain_id}链初始化失败: {str(e)}", exc_info=True) 110 | return False 111 | 112 | async def buy_token(self, token_address: str, amount_usd: float, private_key: str) -> Optional[str]: 113 | """购买代币""" 114 | if not self.initialized: 115 | logger.error(f"{self.chain_id}链未初始化") 116 | return None 117 | 118 | try: 119 | # 检查路由合约地址 120 | router_address = self.config.get("router") 121 | if not router_address: 122 | logger.error(f"未配置{self.chain_id}链的路由合约地址") 123 | return None 124 | 125 | # 检查WETH地址 126 | weth_address = self.config.get("weth") 127 | if not weth_address: 128 | logger.error(f"未配置{self.chain_id}链的WETH地址") 129 | return None 130 | 131 | # 创建合约实例 132 | router = self.web3.eth.contract(address=router_address, abi=self.router_abi) 133 | 134 | # 获取账户地址 135 | account = self.web3.eth.account.from_key(private_key) 136 | address = account.address 137 | 138 | # 获取当前原生代币价格 139 | token_prices = await get_token_prices() 140 | native_price = token_prices.get(self.chain_id, 1000) 141 | 142 | # 计算原生代币数量 143 | amount = amount_usd / native_price 144 | 145 | # 转换为Wei 146 | amount_wei = self.web3.to_wei(amount, 'ether') 147 | 148 | # 获取当前gas价格并应用乘数 149 | gas_price = int(self.web3.eth.gas_price * cfg.trader.gas_price_multiplier) 150 | 151 | # 计算交易截止时间(当前时间+20分钟) 152 | deadline = self.web3.eth.get_block('latest').timestamp + 1200 153 | 154 | # 计算最小获得代币数量(考虑滑点) 155 | min_tokens_out = 0 # 这里可以根据滑点计算,暂时设为0 156 | 157 | # 构建交易 158 | swap_tx = router.functions.swapExactETHForTokens( 159 | min_tokens_out, 160 | [weth_address, token_address], 161 | address, 162 | deadline 163 | ).build_transaction({ 164 | 'from': address, 165 | 'value': amount_wei, 166 | 'gas': 250000, 167 | 'gasPrice': gas_price, 168 | 'nonce': self.web3.eth.get_transaction_count(address), 169 | }) 170 | 171 | # 签名交易 172 | signed_tx = self.web3.eth.account.sign_transaction(swap_tx, private_key=private_key) 173 | 174 | # 发送交易 175 | tx_hash = self.web3.eth.send_raw_transaction(signed_tx.rawTransaction) 176 | tx_hash_hex = tx_hash.hex() 177 | 178 | logger.info(f"交易发送成功,链: {self.chain_id}, 代币: {token_address}, 金额: ${amount_usd}, 交易哈希: {tx_hash_hex}") 179 | 180 | return tx_hash_hex 181 | 182 | except Exception as e: 183 | logger.error(f"购买代币失败,链: {self.chain_id}, 代币: {token_address}, 错误: {str(e)}", exc_info=True) 184 | return None 185 | 186 | class SolanaChain(ChainBase): 187 | """Solana链""" 188 | def __init__(self, chain_id: str, config: Dict[str, Any]): 189 | super().__init__(chain_id, config) 190 | self.client = None 191 | self.jupiter_api = "https://lite-api.jup.ag/swap/v1" 192 | 193 | async def initialize(self) -> bool: 194 | """初始化Solana链连接""" 195 | try: 196 | # 检查RPC URL 197 | if "rpc" not in self.config: 198 | logger.error("Solana链缺少RPC URL配置") 199 | return False 200 | 201 | rpc_url = self.config["rpc"] 202 | # 初始化Solana客户端 203 | self.client = AsyncClient(rpc_url) 204 | 205 | # 检查 Solana 链连接 206 | try: 207 | response = await self.client.is_connected() 208 | if response: 209 | self.initialized = True 210 | logger.info("sol链初始化成功") 211 | else: 212 | self.initialized = False 213 | logger.error("Solana 链连接失败") 214 | except Exception as e: 215 | self.initialized = False 216 | logger.error(f"Solana 链连接失败: {e}") 217 | 218 | # 验证节点 219 | validate_sol_endpoint(cfg.trader.private_keys['sol'], rpc_url) 220 | 221 | return self.initialized 222 | except Exception as e: 223 | logger.error(f"Solana链初始化失败: {str(e)}", exc_info=True) 224 | return False 225 | 226 | async def _get_jupiter_quote(self, input_mint: str, output_mint: str, amount: int) -> Optional[Dict]: 227 | """获取Jupiter报价""" 228 | try: 229 | params = { 230 | "inputMint": input_mint, 231 | "outputMint": output_mint, 232 | "amount": amount, 233 | "slippageBps": int(cfg.trader.slippage_tolerance * 100) # 将滑点百分比转换为基点 234 | } 235 | 236 | async with aiohttp.ClientSession() as session: 237 | async with session.get(f"{self.jupiter_api}/quote", params=params) as resp: 238 | if resp.status == 200: 239 | return await resp.json() 240 | logger.error(f"获取Jupiter报价失败: {resp.status}") 241 | return None 242 | except Exception as e: 243 | logger.error(f"获取Jupiter报价异常: {str(e)}") 244 | return None 245 | 246 | async def _get_jupiter_swap_tx(self, quote: Dict, user_public_key: str) -> Optional[str]: 247 | """获取Jupiter交换交易""" 248 | try: 249 | payload = { 250 | "quoteResponse": quote, 251 | "userPublicKey": user_public_key, 252 | "wrapAndUnwrapSol": True 253 | } 254 | async with aiohttp.ClientSession() as session: 255 | async with session.post(f"{self.jupiter_api}/swap", json=payload) as resp: 256 | if resp.status == 200: 257 | data = await resp.json() 258 | return data.get("swapTransaction") 259 | logger.error(f"获取Jupiter交易失败: {resp.status}") 260 | return None 261 | except Exception as e: 262 | logger.error(f"获取Jupiter交易异常: {str(e)}") 263 | return None 264 | 265 | async def buy_token(self, token_address: str, amount_usd: float, private_key: str) -> Optional[str]: 266 | """购买Solana代币""" 267 | 268 | if not self.initialized: 269 | logger.error("Solana链未初始化") 270 | return None 271 | 272 | try: 273 | # 1. 从私钥创建Keypair 274 | keypair = Keypair.from_bytes(base58.b58decode(private_key)) 275 | 276 | # 2.获取当前原生代币价格 277 | token_prices = await get_token_prices() 278 | sol_price = token_prices.get(self.chain_id, 100) 279 | 280 | # 3. 计算需要交换的SOL数量 281 | amount_lamports = int((amount_usd / sol_price) * 10**9) # SOL有9位小数 282 | 283 | # 4. 获取Jupiter报价 284 | quote = await self._get_jupiter_quote( 285 | input_mint="So11111111111111111111111111111111111111112", # SOL代币地址 286 | output_mint=token_address, 287 | amount=amount_lamports 288 | ) 289 | if not quote: 290 | logger.error("获取Jupiter报价失败") 291 | return None 292 | 293 | # 5. 获取交换交易 294 | swap_tx = await self._get_jupiter_swap_tx(quote, str(keypair.pubkey())) 295 | if not swap_tx: 296 | logger.error("获取Jupiter交易失败") 297 | return None 298 | 299 | # Deserialize the transaction 300 | transaction = VersionedTransaction.from_bytes(base64.b64decode(swap_tx)) 301 | # Sign the trasaction message 302 | signature = keypair.sign_message(message.to_bytes_versioned(transaction.message)) 303 | # Sign Trasaction 304 | signed_tx = VersionedTransaction.populate(transaction.message, [signature]) 305 | 306 | opts = TxOpts(skip_preflight=False, preflight_commitment=Finalized, max_retries=2) 307 | result = await self.client.send_raw_transaction(txn=bytes(signed_tx), opts=opts) 308 | tx_hash = str(result.value) # 提取签名字符串 309 | logger.success(f"Solana交易成功,代币: {token_address}, 金额: ${amount_usd}, 交易哈希: {tx_hash}") 310 | return tx_hash 311 | 312 | except Exception as e: 313 | logger.error(f"Solana链购买代币失败,代币: {token_address}, 错误: {str(e)}", exc_info=True) 314 | return None 315 | 316 | class ChainTrader: 317 | """链上交易执行器""" 318 | 319 | # 基础链配置(不变的部分) 320 | BASE_CHAIN_CONFIG = { 321 | "eth": { 322 | "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 323 | "explorer": "https://etherscan.io/tx/" 324 | }, 325 | "bsc": { 326 | "weth": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", # WBNB 327 | "explorer": "https://bscscan.com/tx/" 328 | }, 329 | "sol": { 330 | "explorer": "https://solscan.io/tx/" 331 | } 332 | } 333 | 334 | def __init__(self): 335 | """初始化交易执行器""" 336 | self.chains = {} 337 | self.chain_config = {} 338 | 339 | # 从配置中获取RPC URL和路由地址 340 | for chain_id in self.BASE_CHAIN_CONFIG: 341 | self.chain_config[chain_id] = self.BASE_CHAIN_CONFIG[chain_id].copy() 342 | 343 | # 添加RPC URL 344 | if chain_id in cfg.trader.rpc_urls: 345 | self.chain_config[chain_id]["rpc"] = cfg.trader.rpc_urls[chain_id] 346 | 347 | # 添加路由合约地址 348 | if chain_id in cfg.trader.router_addresses: 349 | self.chain_config[chain_id]["router"] = cfg.trader.router_addresses[chain_id] 350 | 351 | logger.info("链上交易执行器初始化完成") 352 | 353 | async def initialize_chains(self): 354 | """初始化所有链""" 355 | for chain_id, config in self.chain_config.items(): 356 | if chain_id == "sol": 357 | chain = SolanaChain(chain_id, config) 358 | else: 359 | chain = EvmChain(chain_id, config) 360 | 361 | # 初始化链 362 | success = await chain.initialize() 363 | if success: 364 | self.chains[chain_id] = chain 365 | else: 366 | logger.warning(f"{chain_id}链初始化失败,该链的交易功能将不可用") 367 | 368 | async def buy_token(self, chain: str, token_address: str, amount_usd: float = None) -> Optional[str]: 369 | """ 370 | 购买代币 371 | 372 | Args: 373 | chain: 链名称(eth, bsc, sol等) 374 | token_address: 代币合约地址 375 | amount_usd: 交易金额(美元),如果为None则使用默认金额 376 | 377 | Returns: 378 | Optional[str]: 交易哈希,如果失败则返回None 379 | """ 380 | # 使用配置中的默认金额 381 | if amount_usd is None: 382 | amount_usd = cfg.trader.default_trade_amount_usd 383 | 384 | # 检查链是否支持 385 | if chain not in self.chain_config: 386 | logger.error(f"不支持的链: {chain}") 387 | return None 388 | 389 | # 检查链是否已初始化 390 | if chain not in self.chains: 391 | # 尝试初始化该链 392 | if chain == "sol": 393 | chain_obj = SolanaChain(chain, self.chain_config[chain]) 394 | else: 395 | chain_obj = EvmChain(chain, self.chain_config[chain]) 396 | 397 | success = await chain_obj.initialize() 398 | if success: 399 | self.chains[chain] = chain_obj 400 | else: 401 | logger.error(f"{chain}链未初始化,无法执行交易") 402 | return None 403 | 404 | # 获取私钥 405 | private_key = cfg.trader.private_keys.get(chain) 406 | if not private_key: 407 | logger.error(f"未配置{chain}链的私钥") 408 | return None 409 | 410 | # 执行交易 411 | return await self.chains[chain].buy_token(token_address, amount_usd, private_key) 412 | 413 | def get_tx_explorer_url(self, chain: str, tx_hash: str) -> str: 414 | """获取交易浏览器URL""" 415 | if chain in self.chains: 416 | return self.chains[chain].get_explorer_url(tx_hash) 417 | elif chain in self.chain_config and "explorer" in self.chain_config[chain]: 418 | return f"{self.chain_config[chain]['explorer']}{tx_hash}" 419 | return "" 420 | 421 | 422 | async def get_token_prices(): 423 | """ 424 | 从 CoinGecko API 获取 ETH、BNB 和 SOL 的实时代币价格 425 | """ 426 | url = "https://api.coingecko.com/api/v3/simple/price" 427 | params = { 428 | "ids": "ethereum,binancecoin,solana", 429 | "vs_currencies": "usd" 430 | } 431 | async with aiohttp.ClientSession() as session: 432 | async with session.get(url, params=params) as response: 433 | if response.status == 200: 434 | data = await response.json() 435 | return { 436 | "eth": data["ethereum"]["usd"], 437 | "bsc": data["binancecoin"]["usd"], 438 | "sol": data["solana"]["usd"] 439 | } 440 | else: 441 | error_msg = f"获取代币价格失败,状态码: {response.status}" 442 | logger.error(error_msg) 443 | raise Exception(error_msg) -------------------------------------------------------------------------------- /images/analys1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/analys1.png -------------------------------------------------------------------------------- /images/analys2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/analys2.png -------------------------------------------------------------------------------- /images/analys3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/analys3.png -------------------------------------------------------------------------------- /images/api_hash_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/api_hash_1.jpg -------------------------------------------------------------------------------- /images/api_hash_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/api_hash_2.jpg -------------------------------------------------------------------------------- /images/api_hash_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/api_hash_3.jpg -------------------------------------------------------------------------------- /images/dingding.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/dingding.jpg -------------------------------------------------------------------------------- /images/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/images/start.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from loguru import logger 4 | from config.config import cfg 5 | 6 | from monitor.webhook_monitor import WebhookMonitor 7 | from monitor.telegram_monitor import TelegramMonitor 8 | 9 | if __name__ == "__main__": 10 | # 根据驱动模式创建监控器 11 | if cfg.monitor.driver_type == "webhook": 12 | monitor = WebhookMonitor(host='0.0.0.0', port=9999) 13 | elif cfg.monitor.driver_type == "telegram": 14 | monitor = TelegramMonitor(cfg.telegram.api_id,cfg.telegram.api_hash,cfg.telegram.news_push_chat_id) 15 | else: 16 | logger.error(f"不支持的驱动模式: {cfg.monitor.driver_type}") 17 | sys.exit(1) 18 | 19 | # 统一启动逻辑 20 | try: 21 | monitor.start() 22 | except KeyboardInterrupt: 23 | logger.info("正在停止监控服务...") 24 | -------------------------------------------------------------------------------- /monitor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/monitor/__init__.py -------------------------------------------------------------------------------- /monitor/base.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from loguru import logger 3 | from config.config import cfg 4 | from core.analyzer import LlmAnalyzer, TokenSearcher 5 | from core.data_def import Msg 6 | import notify.notice as notice 7 | from core.trader import ChainTrader 8 | 9 | 10 | class BaseMonitor: 11 | """监控器基类,包含公共逻辑""" 12 | def __init__(self): 13 | # 初始化公共组件 14 | self.analyzer = LlmAnalyzer( 15 | api_key=cfg.llm.api_key, 16 | base_url=cfg.llm.base_url, 17 | model=cfg.llm.model 18 | ) 19 | self.token_searcher = TokenSearcher(max_retries=3, retry_delay=1.0) 20 | self.trader = self._init_trader() 21 | 22 | def _init_trader(self): 23 | """初始化交易模块(公共方法)""" 24 | if cfg.trader.enabled and cfg.trader.private_keys: 25 | trader = ChainTrader() 26 | loop = asyncio.get_event_loop() 27 | loop.run_until_complete(trader.initialize_chains()) 28 | logger.info("自动交易功能已启用") 29 | return trader 30 | logger.info("自动交易功能未启用") 31 | return None 32 | 33 | async def process_message(self, message:Msg): 34 | return await self._analyze_message(message) 35 | 36 | async def _analyze_message(self, msg: Msg): 37 | 38 | try: 39 | tweet_author = msg.screen_name 40 | tweet_content = msg.content 41 | logger.info(f"开始分析推文内容: {tweet_author}-{tweet_content[:100]}...") 42 | 43 | # 调用AI分析器分析内容 44 | analysis_result = await self.analyzer.analyze_content(msg) 45 | 46 | # 记录分析结果 47 | logger.info(f"推文分析完成,结果: {analysis_result}") 48 | 49 | # 如果发现了代币信息,搜索代币并发送详细通知 50 | if analysis_result and "speculate_result" in analysis_result: 51 | tokens = analysis_result["speculate_result"] 52 | if tokens and len(tokens) > 0: 53 | # 提取代币名称 54 | token_names = [token["token_name"] for token in tokens] 55 | logger.info(f"发现潜在代币: {token_names}") 56 | 57 | # 搜索代币信息 58 | search_results = await self._search_tokens(token_names) 59 | 60 | # 格式化通知信息,同时获取 token 列表 61 | notification, token_list = self._format_token_notification(token_names, search_results, tweet_author, tweet_content) 62 | 63 | # 生成按钮信息 64 | btn_info = [] 65 | 66 | # 执行自动交易并记录交易结果 67 | trade_results = [] 68 | 69 | for token in token_list: 70 | chain = str(token.chain).lower() 71 | address = token.address 72 | token_symbol = token.symbol 73 | 74 | # 添加按钮信息 75 | btn_info.append((f'BUY-{chain.upper()}-{token_symbol}', f"https://gmgn.ai/{chain}/token/{address}")) 76 | 77 | # 执行自动交易(如果启用) 78 | if self.trader and self._should_trade(token): 79 | tx_hash = await self.trader.buy_token(chain, address) 80 | if tx_hash: 81 | explorer_url = self.trader.get_tx_explorer_url(chain, tx_hash) 82 | trade_results.append({ 83 | "chain": chain, 84 | "token": token_symbol, 85 | "address": address, 86 | "tx_hash": tx_hash, 87 | "explorer_url": explorer_url 88 | }) 89 | 90 | # 添加交易查看按钮 91 | if explorer_url: 92 | btn_info.append((f'查看交易-{chain.upper()}-{token_symbol}', explorer_url)) 93 | 94 | # 如果有交易结果,添加到通知中 95 | if trade_results: 96 | notification += "\n\n🔄 **自动交易执行结果**:\n" 97 | for result in trade_results: 98 | notification += f"- **{result['token']}** ({result['chain'].upper()}):\n" 99 | notification += f" - 交易哈希: `{result['tx_hash']}`\n" 100 | 101 | # 发送钉钉 ActionCard 消息 102 | notice.send_warn_action_card( 103 | "交易通知", 104 | notification, 105 | "0", 106 | *btn_info 107 | ) 108 | 109 | return analysis_result 110 | except Exception as e: 111 | logger.error(f"分析推文内容时出错: {str(e)}", exc_info=True) 112 | return {"error": str(e)} 113 | 114 | async def _search_tokens(self, token_names): 115 | """ 116 | 搜索代币信息 117 | 118 | Args: 119 | token_names: 代币名称列表 120 | 121 | Returns: 122 | dict: 代币名称到搜索结果的映射 123 | """ 124 | try: 125 | # 使用代币搜索器批量搜索代币 126 | search_results = await self.token_searcher.batch_search_tokens(token_names, concurrency=3) 127 | logger.info(f"代币搜索完成,找到 {len(search_results)} 个结果") 128 | return search_results 129 | except Exception as e: 130 | logger.error(f"搜索代币时出错: {str(e)}", exc_info=True) 131 | return {} 132 | 133 | 134 | def _format_token_notification(self, token_names, search_results, tweet_author, tweet_content): 135 | """ 136 | 格式化代币通知信息 137 | 138 | Args: 139 | token_names: 代币名称列表 140 | search_results: 搜索结果 141 | tweet_content: 推文内容 142 | 143 | Returns: 144 | tuple: 包含格式化后的通知信息和对应的 token 列表 145 | """ 146 | # 基本通知信息 147 | notification = f"🚨 发现潜在代币信息!\n\n" 148 | notification += f"📱 推文内容:\n{tweet_author}-{tweet_content[:150]}...\n\n" 149 | notification += f"🔍 发现代币: {', '.join(token_names)}\n\n" 150 | 151 | # 存储 token 信息 152 | token_list = [] 153 | 154 | # 添加代币详细信息 155 | if search_results: 156 | notification += "📊 代币详情:\n" 157 | for token_name in token_names: 158 | result = search_results.get(token_name) 159 | if not result or not result.tokens: 160 | notification += f"- {token_name}: 未找到详细信息\n" 161 | continue 162 | 163 | # 只取第一个结果(已经按交易量排序) 164 | token = result.tokens[0] 165 | # 将 token 添加到列表中 166 | token_list.append(token) 167 | 168 | # 格式化价格变化百分比 169 | price_change_1h = ((token.price / token.price_1h) - 1) * 100 if token.price_1h else 0 170 | price_change_24h = ((token.price / token.price_24h) - 1) * 100 if token.price_24h else 0 171 | 172 | # 添加代币信息,使用代码格式便于复制 173 | notification += f"- **{token.name} ({token.symbol})** \n" 174 | notification += f" - **链**: {token.chain}\n" 175 | notification += f" - **地址**: `{token.address}`\n" 176 | notification += f" - **价格**: ${token.price:.8f}\n" 177 | notification += f" - **1小时变化**: {price_change_1h:.2f}%\n" 178 | notification += f" - **24小时变化**: {price_change_24h:.2f}%\n" 179 | notification += f" - **24小时交易量**: ${token.volume_24h:.2f}\n" 180 | notification += f" - **流动性**: ${token.liquidity:.2f}\n" 181 | 182 | return notification, token_list 183 | 184 | 185 | def _should_trade(self, token) -> bool: 186 | """ 187 | 判断是否应该交易该代币 188 | 189 | Args: 190 | token: 代币信息 191 | 192 | Returns: 193 | bool: 是否应该交易 194 | """ 195 | try: 196 | # 检查流动性 197 | if token.liquidity < cfg.trader.min_liquidity_usd: 198 | logger.info(f"代币 {token.symbol} 流动性不足 (${token.liquidity:.2f} < ${cfg.trader.min_liquidity_usd})") 199 | return False 200 | 201 | # 检查1小时价格变化 202 | if token.price_1h: 203 | price_change_1h = abs(((token.price / token.price_1h) - 1) * 100) 204 | if price_change_1h > cfg.trader.max_price_change_1h: 205 | logger.info(f"代币 {token.symbol} 1小时价格变化过大 ({price_change_1h:.2f}% > {cfg.trader.max_price_change_1h}%)") 206 | return False 207 | 208 | # 通过所有检查 209 | return True 210 | 211 | except Exception as e: 212 | logger.error(f"判断是否应该交易代币时出错: {str(e)}") 213 | return False 214 | -------------------------------------------------------------------------------- /monitor/telegram_monitor.py: -------------------------------------------------------------------------------- 1 | from telethon import TelegramClient, events 2 | from telethon.tl.types import PeerChannel, PeerChat 3 | from monitor.base import BaseMonitor # 继承基类 4 | from core.data_def import Msg 5 | import re 6 | from utils.x_abstract import get_tweet_details 7 | import notify.notice as notice 8 | import asyncio 9 | 10 | 11 | class TelegramMonitor(BaseMonitor): 12 | """Telegram监控器""" 13 | 14 | def __init__(self, api_id, api_hash,target_chat_id): 15 | super().__init__() 16 | self.api_id = api_id 17 | self.api_hash = api_hash 18 | self.client = TelegramClient('transfer', api_id, api_hash) 19 | self.monitor_targets = { 20 | "消息推送Chanel/Group": target_chat_id, 21 | } 22 | self.target_list = self._build_target_list() 23 | self._register_handlers() 24 | 25 | def _build_target_list(self): 26 | """构建监控目标列表""" 27 | return [ 28 | PeerChannel(target_id) if str(target_id).startswith("-100") 29 | else PeerChat(target_id) 30 | for target_id in self.monitor_targets.values() 31 | ] 32 | 33 | def _register_handlers(self): 34 | """注册消息处理器""" 35 | @self.client.on(events.NewMessage(chats=self.target_list)) 36 | async def event_handler(event): 37 | await self._handle_message(event.message) 38 | 39 | async def _handle_message(self, message): 40 | """消息处理核心逻辑""" 41 | msg:Msg = await self.parse_message(message) # 添加await 42 | await self.process_message(msg) 43 | 44 | async def parse_message(self, message): # 改为异步方法 45 | """解析消息内容""" 46 | # 使用正则表达式解析出链接 47 | text = message.raw_text 48 | url_pattern = r'Source:\s*(https?://\S+)' 49 | match = re.search(url_pattern, text) 50 | msg = Msg( 51 | push_type='new_tweet', # 默认推送类型 52 | title='', # 根据实际情况补充标题字段 53 | content='', # 默认空内容 54 | name='', # 默认空名称 55 | screen_name='' # 默认空用户名 56 | ) 57 | if match: 58 | source_url = match.group(1) 59 | print("解析出的原文链接为:", source_url) 60 | loop = asyncio.get_event_loop() 61 | msg_info = await loop.run_in_executor( 62 | None, # 使用默认线程池 63 | get_tweet_details, 64 | source_url 65 | ) 66 | msg.content = msg_info.get("content") 67 | msg.name = msg_info.get("name") 68 | msg.screen_name = msg_info.get("username") 69 | msg.title = f'{msg.screen_name} 发布了一条新推文' 70 | notice.send_notice_msg(str(msg)) 71 | return msg 72 | 73 | async def _client_main(self): 74 | """客户端主循环""" 75 | me = await self.client.get_me() 76 | self._show_login_info(me) 77 | await self.client.run_until_disconnected() 78 | 79 | def _show_login_info(self, me): 80 | """显示登录信息""" 81 | print("-----****************-----") 82 | print(f"Name: {me.username}") 83 | print(f"ID: {me.id}") 84 | print("-----login successful-----") 85 | 86 | def start(self): 87 | """启动监控""" 88 | with self.client: 89 | self.client.loop.run_until_complete(self._client_main()) 90 | 91 | if __name__ == '__main__': 92 | from config.config import cfg 93 | monitor = TelegramMonitor(cfg.telegram.api_id,cfg.telegram.api_hash,cfg.telegram.news_push_chat_id) 94 | monitor.start() -------------------------------------------------------------------------------- /monitor/webhook_monitor.py: -------------------------------------------------------------------------------- 1 | 2 | from quart import Quart, request, jsonify 3 | import asyncio 4 | import json 5 | from loguru import logger 6 | from typing import Optional 7 | from core.data_def import PushMsg, User, Tweet,Msg 8 | from monitor.base import BaseMonitor 9 | import notify.notice as notice 10 | 11 | class WebhookMonitor(BaseMonitor): 12 | def __init__(self, host='0.0.0.0', port=9999): 13 | super().__init__() # 继承基类初始化 14 | self.app = Quart(__name__) 15 | self.host = host 16 | self.port = port 17 | self._register_routes() 18 | logger.info(f"HTTP监控服务初始化完成,监听地址: {host}:{port}") 19 | 20 | def _register_routes(self): 21 | """注册API路由""" 22 | @self.app.route('/post/tweet', methods=['POST']) 23 | async def receive_tweet(): 24 | try: 25 | data = await request.get_data() 26 | data = data.decode('utf-8') 27 | if not data: 28 | return jsonify({"status": "error", "message": "Invalid or missing json data"}), 400 29 | 30 | logger.debug(f"收到原始数据: {data}") 31 | 32 | # 解析推文数据 33 | push_msg: PushMsg = self._parse_tweet_data(data) 34 | if not push_msg: 35 | return jsonify({"status": "error", "message": "Failed to parse tweet data"}), 400 36 | 37 | logger.info(f"解析后的推文数据: {push_msg}") 38 | 39 | msg:Msg = Msg( 40 | push_type=push_msg.push_type, 41 | title=push_msg.title, 42 | content=push_msg.content, 43 | name=push_msg.user.name, 44 | screen_name=push_msg.user.screen_name 45 | ) 46 | notice.send_notice_msg(msg) 47 | # 异步处理推文 48 | asyncio.create_task(self.process_message(msg)) 49 | 50 | return jsonify({ 51 | "status": "success", 52 | "message": "Tweet received and analysis started asynchronously" 53 | }), 200 54 | except Exception as e: 55 | logger.error(f"处理推文时发生错误: {str(e)}", exc_info=True) 56 | return jsonify({"status": "error", "message": str(e)}), 500 57 | 58 | def _parse_tweet_data(self, raw_data) -> Optional[PushMsg]: 59 | """ 60 | 解析推文数据 61 | 62 | Args: 63 | raw_data: 原始JSON字符串 64 | 65 | Returns: 66 | Optional[PushMsg]: 解析后的推文数据,如果解析失败则返回None 67 | """ 68 | try: 69 | data = json.loads(raw_data) 70 | user = User(**data["user"]) 71 | 72 | # 根据推送类型处理不同的数据结构 73 | push_type = data.get("push_type", "") 74 | 75 | # 只有new_tweet类型才有tweet数据 76 | if push_type == "new_tweet" and "tweet" in data: 77 | tweet = Tweet(**data["tweet"]) 78 | else: 79 | # 其他类型的推送,tweet对象设为None 80 | tweet = None 81 | 82 | return PushMsg( 83 | push_type=data["push_type"], 84 | title=data["title"], 85 | content=data["content"], 86 | user=user, 87 | tweet=tweet 88 | ) 89 | except Exception as e: 90 | logger.error(f"解析JSON数据失败: {str(e)}", exc_info=True) 91 | return None 92 | 93 | 94 | 95 | def start(self): 96 | """启动Twitter监控服务""" 97 | logger.info("Twitter监控服务启动中...") 98 | try: 99 | self.app.run(host=self.host, port=self.port) 100 | except Exception as e: 101 | logger.error(f"服务启动失败: {str(e)}", exc_info=True) 102 | finally: 103 | logger.info("Twitter监控服务已停止") -------------------------------------------------------------------------------- /notify/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simons-freedom/X-monitor/88a0e005b23f1fe8d0500fed17851b8dbae5febe/notify/__init__.py -------------------------------------------------------------------------------- /notify/dingding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import hmac 4 | import hashlib 5 | import base64 6 | import urllib.parse 7 | import json 8 | 9 | import logging 10 | import requests 11 | 12 | class DingTalkRobot(object): 13 | def __init__(self, robot_id, secret): 14 | super(DingTalkRobot, self).__init__() 15 | self.robot_id = robot_id 16 | self.secret = secret 17 | self.headers = {'Content-Type': 'application/json; charset=utf-8'} 18 | self.times = 0 19 | self.start_time = time.time() 20 | 21 | # 加密签名 22 | def __spliceUrl(self): 23 | timestamp = int(round(time.time() * 1000)) 24 | secret_enc = self.secret.encode('utf-8') 25 | string_to_sign = '{}\n{}'.format(timestamp, self.secret) 26 | string_to_sign_enc = string_to_sign.encode('utf-8') 27 | hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() 28 | sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) 29 | url = "https://oapi.dingtalk.com/robot/send?access_token="+f"{self.robot_id}×tamp={timestamp}&sign={sign}" 30 | return url 31 | 32 | def send_markdown(self,title, markdown_msg, is_at_all=False, at_mobiles=[]): 33 | data = {"msgtype": "markdown", "at": {}} 34 | if self.is_not_null_and_blank_str(markdown_msg): 35 | data["markdown"] = {"title": title,"text": markdown_msg} 36 | else: 37 | logging.error("markdown类型,消息内容不能为空!") 38 | raise ValueError("markdown类型,消息内容不能为空!") 39 | 40 | if is_at_all: 41 | data["at"]["isAtAll"] = is_at_all 42 | 43 | if at_mobiles: 44 | at_mobiles = list(map(str, at_mobiles)) 45 | data["at"]["atMobiles"] = at_mobiles 46 | 47 | logging.debug('markdown类型:%s' % data) 48 | return self.__post(data) 49 | 50 | def send_msg(self, *mssg): 51 | text = '' 52 | for i in mssg: 53 | text += str(i) 54 | self.send_markdown("交易通知",text) 55 | 56 | 57 | def send_text(self, msg, is_at_all=False, at_mobiles=[]): 58 | data = {"msgtype": "text", "at": {}} 59 | if self.is_not_null_and_blank_str(msg): 60 | data["text"] = {"content": msg} 61 | else: 62 | logging.error("text类型, 消息内容不能为空!") 63 | raise ValueError("text类型,消息内容不能为空!") 64 | 65 | if is_at_all: 66 | data["at"]["isAtAll"] = is_at_all 67 | 68 | if at_mobiles: 69 | at_mobiles = list(map(str, at_mobiles)) 70 | data["at"]["atMobiles"] = at_mobiles 71 | 72 | logging.debug('text类型:%s' % data) 73 | return self.__post(data) 74 | 75 | 76 | def send_json(self, msg, is_at_all=False, at_mobiles=[]): 77 | data = {"msgtype": "text", "at": {}} 78 | if msg : 79 | json_msg = json.dumps(msg,ensure_ascii=False) 80 | data["text"] = {"content": json_msg} 81 | else: 82 | logging.error("text类型,消息内容不能为空!") 83 | raise ValueError("text类型,消息内容不能为空!") 84 | 85 | if is_at_all: 86 | data["at"]["isAtAll"] = is_at_all 87 | 88 | if at_mobiles: 89 | at_mobiles = list(map(str, at_mobiles)) 90 | data["at"]["atMobiles"] = at_mobiles 91 | 92 | logging.debug('text类型:%s' % data) 93 | return self.__post(data) 94 | 95 | 96 | def send_image(self, title,image_url, is_at_all=False, at_mobiles=[]): 97 | markdown_msg = "!["+title+"]("+image_url+")\n" 98 | return self.send_markdown(title,markdown_msg,is_at_all) 99 | 100 | def send_action_card(self, title, markdown_msg, btnOrientation: str = "0", *btn_info): 101 | """ 102 | 发送钉钉 ActionCard 类型的消息,支持传入多个按钮信息 103 | 104 | :param title: 卡片标题 105 | :param markdown_msg: 卡片的 Markdown 消息内容 106 | :param btnOrientation: 按钮排列方向,默认为 "0" 107 | :param btn_info: 可变参数,每个参数为一个元组 (title, actionURL),表示一个按钮的标题和链接 108 | :return: 调用 __post 方法的返回值,即发送消息的结果 109 | """ 110 | # 构建钉钉消息的基础结构,消息类型为 actionCard 111 | data = {"msgtype": "actionCard", "actionCard": {}} 112 | # 设置卡片标题 113 | data["actionCard"]["title"] = title 114 | # 设置卡片的 Markdown 消息内容 115 | data["actionCard"]["text"] = markdown_msg 116 | # 设置按钮排列方向 117 | data["actionCard"]["btnOrientation"] = btnOrientation 118 | # 处理按钮信息,将 btn_info 转换为符合钉钉消息格式的按钮列表 119 | btns = [{"title": title, "actionURL": url} for title, url in btn_info] 120 | data["actionCard"]["btns"] = btns 121 | 122 | # 记录 debug 日志,包含要发送的 actionCard 类型消息数据 123 | logging.debug('actionCard类型:%s' % data) 124 | # 调用 __post 方法发送消息并返回结果 125 | return self.__post(data) 126 | 127 | 128 | def __post(self, data): 129 | """ 130 | 发送消息(内容UTF-8编码) 131 | :param data: 消息数据(字典) 132 | :return: 返回发送结果 133 | """ 134 | self.times += 1 135 | if self.times > 20: 136 | if time.time() - self.start_time < 60: 137 | logging.debug('钉钉官方限制每个机器人每分钟最多发送20条,当前消息发送频率已达到限制条件,休眠一分钟') 138 | time.sleep(60) 139 | self.start_time = time.time() 140 | 141 | post_data = json.dumps(data) 142 | try: 143 | response = requests.post(self.__spliceUrl(), headers=self.headers, data=post_data) 144 | logging.debug('成功发送钉钉%'+str(response)) 145 | except Exception as e: 146 | logging.debug('发送钉钉失败:' +str(e)) 147 | 148 | def is_not_null_and_blank_str(self,content): 149 | if content and content.strip(): 150 | return True 151 | else: 152 | return False 153 | -------------------------------------------------------------------------------- /notify/notice.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from loguru import logger 3 | from typing import List, Tuple, Optional 4 | from notify.dingding import DingTalkRobot 5 | from config.config import cfg # 从config导入配置 6 | 7 | # 初始化钉钉机器人 8 | _robot = None 9 | 10 | def _get_robot(): 11 | """获取钉钉机器人实例""" 12 | global _robot 13 | if _robot is None: 14 | if not cfg.dingtalk.token or not cfg.dingtalk.secret: 15 | logger.warning("钉钉机器人未启用或未配置Token/Secret,无法发送通知") 16 | return None 17 | _robot = DingTalkRobot(cfg.dingtalk.token, cfg.dingtalk.secret) 18 | return _robot 19 | 20 | def send_notice_msg(content: str, title: str = "系统通知", btn_info: List[Tuple[str, str]] = []) -> bool: 21 | """ 22 | 发送通知消息,默认使用ActionCard格式 23 | 24 | Args: 25 | content: 消息内容 26 | title: 消息标题,默认为"系统通知" 27 | btn_info: 按钮信息列表,每个元素为(按钮标题, 按钮链接)元组,默认为None 28 | 29 | Returns: 30 | bool: 是否发送成功 31 | """ 32 | try: 33 | robot = _get_robot() 34 | if not robot: 35 | return False 36 | 37 | # 如果提供了按钮信息,使用ActionCard格式 38 | if btn_info and len(btn_info) > 0: 39 | robot.send_action_card(title, f"📢 **{title}**\n\n{content}", "0", *btn_info) 40 | else: 41 | # 否则使用Markdown格式 42 | robot.send_markdown(title, f"📢 **{title}**\n\n{content}") 43 | 44 | logger.info(f"通知消息发送成功: {content[:50]}...") 45 | return True 46 | except Exception as e: 47 | logger.error(f"发送通知消息时出错: {str(e)}", exc_info=True) 48 | return False 49 | 50 | def send_warn_action_card(title: str, text: str, btn_orientation: str = "0", *btns: Tuple[str, str]) -> bool: 51 | """ 52 | 发送警告ActionCard消息 53 | 54 | Args: 55 | title: 标题 56 | text: 正文内容(支持markdown) 57 | btn_orientation: 按钮排列方向,0-按钮竖直排列,1-按钮横向排列 58 | btns: 按钮列表,每个按钮为(标题, 链接)元组 59 | 60 | Returns: 61 | bool: 是否发送成功 62 | """ 63 | try: 64 | robot = _get_robot() 65 | if not robot: 66 | return False 67 | 68 | # 调用钉钉机器人的send_action_card方法 69 | robot.send_action_card(title, text, btn_orientation, *btns) 70 | logger.info(f"警告ActionCard消息发送成功: {title}") 71 | return True 72 | except Exception as e: 73 | logger.error(f"发送警告ActionCard消息时出错: {str(e)}", exc_info=True) 74 | return False -------------------------------------------------------------------------------- /notify/telegram_bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import telegram 3 | 4 | 5 | class TgRobot(object): 6 | 7 | def __init__(self, token, chat_id): 8 | super(TgRobot, self).__init__() 9 | self.token = token 10 | self.chat_id = chat_id 11 | self.bot = telegram.Bot(token) 12 | 13 | 14 | def send_text(self,content): 15 | ''' 16 | 发送文本消息 17 | ''' 18 | if content == None: 19 | return 20 | self.bot.send_message(self.chat_id,content) 21 | 22 | 23 | def send_html(self, html_text): 24 | if html_text: 25 | # 发送html格式的消息 26 | self.bot.send_message( 27 | chat_id=self.chat_id, 28 | text=html_text, 29 | parse_mode=telegram.ParseMode.HTML 30 | ) 31 | 32 | 33 | def send_dataframe(self,content): 34 | ''' 35 | 发送dataframe消息 36 | ''' 37 | self.bot.send_message(self.chat_id,content.to_markdown(),parse_mode='Markdown') 38 | 39 | def send_photo(self, photo_path): 40 | if photo_path: 41 | self.bot.send_photo( 42 | chat_id=self.chat_id, 43 | photo=open(photo_path, 'rb'), 44 | ) 45 | 46 | def send_document(self, doc_path): 47 | if doc_path: 48 | self.bot.send_document( 49 | chat_id=self.chat_id, 50 | document=open(doc_path, 'rb'), 51 | ) 52 | 53 | def send_msg(self, *mssg): 54 | text = '' 55 | for i in mssg: 56 | text += str(i) 57 | self.bot.send_message(self.chat_id,text) 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | web3==7.11.0 2 | solana==0.36.6 3 | solders==0.26.0 4 | openai==1.77.0 5 | loguru==0.7.3 6 | selenium==4.32.0 7 | webdriver_manager==4.0.2 8 | playwright==1.52.0 9 | aiohttp==3.11.18 10 | quart==0.20.0 11 | dataclasses 12 | python-dotenv==1.1.0 13 | python-telegram-bot 14 | telethon==1.40.0 15 | base58==2.1.1 16 | curl_cffi==0.11.1 17 | requests==2.32.3 18 | bs4==0.0.2 -------------------------------------------------------------------------------- /telegram_mode_setting.md: -------------------------------------------------------------------------------- 1 | # Telegram 驱动模式环境配置 2 | ## 注册telegram bot机器人 3 | 在telegram中搜索找到[BotFather](https://t.me/BotFather),使用/newbot 命令创建新Bot.创建成功根据步骤填入bot名称等信息,完成bot创建。创建成功后会得到Bot API Token. 4 | ## 创建接收消息Chanel /Group 5 | 需要创建两个Chanel, 一个用于接收Kbot推送的消息(TELEGRAM_NEWS_PUSH),一个用于我们项目的消息推送(TELEGRAM_NOTIFY). 6 | 创建好Chanel后,使用官方的机器人[IDBot](https://t.me/username_to_id_bot),得到Chanel的Chat ID. 7 | 8 | 9 | ## telegram Chanel监听开发者账号申请 10 | tg提供了很好的开发接口,要使用sdk,需要api hash,你也可以简单的认为,就是你个人的开发者账号token。 11 | 获取地址为 https://my.telegram.org/apps 12 | ![步骤一](images/api_hash_1.jpg) 13 | ![步骤二](images/api_hash_2.jpg) 14 | ![步骤三](images/api_hash_3.jpg) 15 | 弄完之后,就拿到了自己的api_id,api_hash. app_tile这些随便填,关键是这个api_hash,调用api授权就靠这个。 16 | 17 | ## Kbot配置 18 | [Kbot官网](https://www.kbot.club/)连接自己的web3钱包. 选择推特监控,配置好自己的telegram bot token和telegram chanel id(TELEGRAM_NEWS_PUSH). 19 | 按照[推特监控](https://docs.kbot.club/gong-neng/jian-kong-yu-jing/tui-te-jian-kong)说明文档,配置好自己想监听的X 账号信息. 20 | -------------------------------------------------------------------------------- /test/test_trader.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from core.trader import ChainTrader 3 | 4 | 5 | async def test_buy_token(): 6 | trader = ChainTrader() 7 | await trader.initialize_chains() 8 | 9 | # 测试参数 10 | chain = "sol" # 可以根据需要修改链名称 eth bsc sol 11 | token_address = "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN" # 替换为实际的代币地址 12 | amount_usd = 1 # 可以根据需要修改交易金额 USDT 13 | 14 | tx_hash = await trader.buy_token(chain, token_address, amount_usd) 15 | if tx_hash: 16 | print(f"交易成功,交易哈希: {tx_hash}") 17 | explorer_url = trader.get_tx_explorer_url(chain, tx_hash) 18 | if explorer_url: 19 | print(f"交易浏览器链接: {explorer_url}") 20 | else: 21 | print("交易失败") 22 | 23 | if __name__ == "__main__": 24 | asyncio.run(test_buy_token()) 25 | -------------------------------------------------------------------------------- /utils/x_abstract.py: -------------------------------------------------------------------------------- 1 | from playwright.sync_api import sync_playwright 2 | from bs4 import BeautifulSoup 3 | import time 4 | 5 | def get_tweet_details(url): 6 | """ 7 | 通过浏览器模拟获取推文详细信息 8 | 9 | :param url: 推文链接(例如:https://x.com/realDonaldTrump/status/1924523182909747657) 10 | :return: 结构化推文数据字典 11 | """ 12 | with sync_playwright() as p: 13 | # 启动Chromium浏览器(无头模式) 14 | browser = p.chromium.launch(headless=True) 15 | context = browser.new_context( 16 | user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 17 | ) 18 | page = context.new_page() 19 | 20 | try: 21 | # 访问目标页面 22 | page.goto(url, timeout=60000) 23 | time.sleep(3) # 等待动态内容加载 24 | 25 | # 获取页面HTML内容 26 | html = page.content() 27 | soup = BeautifulSoup(html, 'html.parser') 28 | author_info = extract_author(soup) 29 | 30 | # 解析关键数据 31 | return { 32 | "content": extract_content(soup), # 修改字段名 33 | "name": author_info.get("name"), 34 | "username": author_info.get("username"), 35 | "timestamp": extract_time(soup), 36 | } 37 | finally: 38 | browser.close() 39 | 40 | # 辅助解析函数 41 | def extract_content(soup): 42 | # 从title标签提取推文内容(格式:作者名 on X: "内容" / X) 43 | try: 44 | title_tag = soup.title 45 | if not title_tag or not title_tag.string: 46 | return None 47 | 48 | title_text = title_tag.string.strip() 49 | if ": " in title_text and " / X" in title_text: 50 | # 分割出核心内容部分 51 | content_part = title_text.split(": ", 1)[1].rsplit(" / X", 1)[0] 52 | # 去除首尾可能的引号 53 | return content_part.strip('"') 54 | except (AttributeError, IndexError, ValueError): 55 | pass 56 | return None 57 | 58 | def extract_author(soup): 59 | # 提取作者信息 60 | import re 61 | 62 | author_section = soup.find('div', {'data-testid': 'User-Name'}) 63 | if not author_section: 64 | return None 65 | 66 | # 使用正则表达式提取username 67 | username_pattern = r'@([A-Za-z0-9_]+)' 68 | text_content = author_section.get_text() 69 | 70 | # 在用户信息区块中查找首个@符号后的用户名 71 | username_match = re.search(username_pattern, text_content) 72 | 73 | return { 74 | "name": author_section.find('span').text if author_section.find('span') else None, 75 | "username": username_match.group(1) if username_match else None 76 | } if author_section else {"name": None, "username": None} # 保证始终返回字典 77 | 78 | def extract_time(soup): 79 | # 提取时间戳 80 | time_tag = soup.find('time') 81 | return time_tag['datetime'] if time_tag else None 82 | 83 | 84 | 85 | # 使用示例 86 | if __name__ == "__main__": 87 | tweet_url = "https://x.com/realDonaldTrump/status/1923432648103333919" 88 | result = get_tweet_details(tweet_url) 89 | 90 | print("推文详细信息:") 91 | for key, value in result.items(): 92 | print(f"{key.upper()}: {value}") --------------------------------------------------------------------------------