├── .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 | 
168 | 
169 | 
170 | 
171 | 
--------------------------------------------------------------------------------
/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 | 
125 | 
126 | 
127 | 
128 | 
--------------------------------------------------------------------------------
/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 = "\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 | 
13 | 
14 | 
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}")
--------------------------------------------------------------------------------