feat: refactor to multi-protocol event streaming system

Major architectural refactor, upgrading from simple logging system to comprehensive multi-protocol Solana DEX event streaming system:

 New Features:
- Support for 5 DEX protocols: PumpFun, PumpSwap, Bonk, Raydium CPMM, Raydium CLMM
- Implement unified event interface (UnifiedEvent trait) and event factory pattern
- Add dual streaming support: Yellowstone gRPC and ShredStream
- Add Chinese documentation (README_CN.md)

🏗️ Architectural Improvements:
- Refactor event parsing system with modular design
- Implement protocol-specific parsers and event types
- Optimize dependency management, update Cargo.toml
- Remove legacy logging modules, clean up redundant code

📊 Statistics:
- Added 46 files, 4511 lines of code
- Removed 1381 lines of legacy code
- Net addition of 3130 lines of code

Tech Stack:
- Rust async/await for asynchronous processing
- Protocol Buffers support
- Multi-protocol event parsing
- High-performance event stream subscription
This commit is contained in:
ysq
2025-07-19 23:46:42 +08:00
parent a7c9721877
commit 9e34a01874
46 changed files with 4511 additions and 1381 deletions
+8 -39
View File
@@ -1,12 +1,12 @@
[package]
name = "grpc-parsed"
version = "2.4.3"
name = "solana-streamer"
version = "0.1.0"
edition = "2021"
authors = []
repository = ""
description = "Rust SDK to interact with the Pump.fun Solana program."
authors = ["William <byteblock6@gmail.com>", "sgxiang <sgxiang@gmail.com>", "wei <1415121722@qq.com>"]
repository = "https://github.com/0xfnzero/solana-streamer"
description = "A lightweight Rust library for real-time event streaming from Solana DEX trading programs. Supports PumpFun, PumpSwap, Bonk, and Raydium protocols with Yellowstone gRPC and ShredStream."
license = "MIT"
keywords = ["solana", "memecoins", "pumpfun", "grpc-parsed", "pumpbot"]
keywords = ["solana", "streaming", "events", "grpc", "shredstream"]
readme = "README.md"
[lib]
@@ -21,18 +21,12 @@ solana-rpc-client-api = "2.1.16"
solana-transaction-status = "2.1.16"
solana-account-decoder = "2.1.16"
solana-hash = "2.1.16"
solana-security-txt = "1.1.1"
solana-entry = "2.1.16"
solana-rpc-client-nonce-utils = "2.1.16"
solana-perf = "2.1.16"
spl-token = "8.0.0"
spl-token-2022 = { version = "8.0.0", features = ["no-entrypoint"] }
solana-metrics = "2.1.16"
spl-associated-token-account = "6.0.0"
mpl-token-metadata = "5.1.0"
borsh = { version = "1.5.3", features = ["derive"] }
isahc = "1.7.2"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.134"
futures = "0.3.31"
@@ -44,16 +38,11 @@ bincode = "1.3.3"
anyhow = "1.0.90"
yellowstone-grpc-client = { version = "6.0.0" }
yellowstone-grpc-proto = { version = "6.0.0" }
reqwest = { version = "0.12.12", features = ["json", "multipart"] }
tokio = { version = "1.42.0" , features = ["full", "rt-multi-thread"]}
tonic = { version = "0.12.3", features = ["tls", "tls-roots", "tls-webpki-roots"] }
rustls = { version = "0.23.23", features = ["ring"] }
rustls-native-certs = "0.8.1"
tokio-rustls = "0.26.1"
core_affinity = "0.8"
dotenvy = "0.15.7"
pretty_env_logger = "0.5.0"
log = "0.4.22"
chrono = "0.4.39"
regex = "1"
@@ -64,31 +53,11 @@ lazy_static = "1.5.0"
once_cell = "1.20.3"
prost = "0.13.5"
prost-types = "0.13.5"
arrform = { git = "https://github.com/raydium-io/arrform" }
num_enum = "0.7.3"
num-derive = "0.4.2"
num-traits = "0.2.19"
uint = "0.10.0"
clap = { version = "4.5.31", features = ["derive"] }
hex = "0.4.3"
bytemuck = { version = "1.4.0" }
safe-transmute = "0.11.0"
enumflags2 = "0.6.4"
static_assertions = "1.1.0"
demand = "1.2.2"
arrayref = "0.3.6"
default-env = "0.1.1"
borsh-derive = "1.5.5"
axum = { version = "0.8.1", features = ["macros"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tokio-tungstenite = { version = "0.26.1", features = ["native-tls"] }
indicatif = "0.17.11"
toml = "0.8.20"
pumpfun = "4.2.0"
indicatif = "0.17.11"
+218 -73
View File
@@ -1,108 +1,253 @@
# GrpcParsed
# Solana Streamer
[中文](https://github.com/0xfnzero/solana-streamer/blob/main/README_CN.md) | [English](https://github.com/0xfnzero/solana-streamer/blob/main/README.md) | [Telegram](https://t.me/fnzero_group)
A gRPC client implementation for subscribing to and processing Solana transaction data.
A lightweight Rust library for real-time event streaming from Solana DEX trading programs. This library provides efficient event parsing and subscription capabilities for PumpFun, PumpSwap, Bonk, and Raydium CPMM protocols.
## Features
## Project Features
- Real-time subscription to Solana transaction data via gRPC
- Support for processing transaction entries and transactions
- Asynchronous transaction data processing
- Custom callback function support for transaction events
- Built-in error handling mechanism
1. **Real-time Event Streaming**: Subscribe to live trading events from multiple Solana DEX protocols
2. **Yellowstone gRPC Support**: High-performance event subscription using Yellowstone gRPC
3. **ShredStream Support**: Alternative event streaming using ShredStream protocol
4. **Multi-Protocol Support**:
- **PumpFun**: Meme coin trading platform events
- **PumpSwap**: PumpFun's swap protocol events
- **Bonk**: Token launch platform events (letsbonk.fun)
- **Raydium CPMM**: Raydium's Concentrated Pool Market Maker events
- **Raydium CLMM**: Raydium's Concentrated Liquidity Market Maker events
5. **Unified Event Interface**: Consistent event handling across all supported protocols
6. **Event Parsing System**: Automatic parsing and categorization of protocol-specific events
7. **High Performance**: Optimized for low-latency event processing
## Installation
Add the following to your `Cargo.toml`:
Clone this project to your project directory:
```bash
cd your_project_root_directory
git clone https://github.com/0xfnzero/solana-streamer
```
Add the dependency to your `Cargo.toml`:
```toml
[dependencies]
grpc-parsed = { path = ".", version = "0.1.0" }
# Add to your Cargo.toml
solana-streamer = { path = "./solana-streamer", version = "0.1.0" }
```
## Usage Examples
### 1. Initializing the Client
```rust
use grpc_parsed::grpc::YellowstoneGrpc;
use solana_streamer::{
match_event,
streaming::{
event_parser::{
protocols::{
bonk::{BonkPoolCreateEvent, BonkTradeEvent}, pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}, pumpswap::{
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
PumpSwapSellEvent, PumpSwapWithdrawEvent,
}, raydium_clmm::{RaydiumClmmSwapEvent, RaydiumClmmSwapV2Event}, raydium_cpmm::RaydiumCpmmSwapEvent
},
Protocol, UnifiedEvent,
},
ShredStreamGrpc, YellowstoneGrpc,
},
};
async fn setup_client() -> Result<YellowstoneGrpc, Box<dyn std::error::Error>> {
let endpoint = "https://solana-yellowstone-grpc.publicnode.com:443";
let client = YellowstoneGrpc::new(endpoint.to_string(), None)?;
Ok(client)
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
test_grpc().await?;
test_shreds().await?;
Ok(())
}
```
### 2. Subscribing to Transaction Data
async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
println!("Subscribing to GRPC events...");
```rust
use grpc_parsed::common::logs_events::PumpfunEvent;
use solana_sdk::pubkey::Pubkey;
async fn subscribe_to_transactions() -> Result<(), Box<dyn std::error::Error>> {
let client = YellowstoneGrpc::new(
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None,
)?;
let callback = |event: PumpfunEvent| {
match event {
PumpfunEvent::NewToken(token_info) => {
println!("Received new token event: {:?}", token_info);
},
PumpfunEvent::NewDevTrade(trade_info) => {
println!("Received new dev trade event: {:?}", trade_info);
},
PumpfunEvent::NewUserTrade(trade_info) => {
println!("Received new trade event: {:?}", trade_info);
},
PumpfunEvent::NewBotTrade(trade_info) => {
println!("Received new bot trade event: {:?}", trade_info);
},
PumpfunEvent::Error(err) => {
println!("Received error: {}", err);
}
}
};
// Optional: Specify bot wallet address for filtering
let bot_wallet = None;
client.subscribe_pumpfun(callback, bot_wallet).await?;
let callback = create_event_callback();
let protocols = vec![
Protocol::PumpFun,
Protocol::PumpSwap,
Protocol::Bonk,
Protocol::RaydiumCpmm,
Protocol::RaydiumClmm,
];
println!("Listening for events, press Ctrl+C to stop...");
grpc.subscribe_events(protocols, None, None, None, callback)
.await?;
Ok(())
}
```
## Error Handling
async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
println!("Subscribing to ShredStream events...");
```rust
use grpc_parsed::grpc::YellowstoneGrpc;
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
let callback = create_event_callback();
let protocols = vec![
Protocol::PumpFun,
Protocol::PumpSwap,
Protocol::Bonk,
Protocol::RaydiumCpmm,
Protocol::RaydiumClmm,
];
println!("Listening for events, press Ctrl+C to stop...");
shred_stream
.shredstream_subscribe(protocols, None, callback)
.await?;
async fn handle_errors() {
match YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None,
) {
Ok(client) => {
println!("Client initialized successfully");
},
Err(e) => {
eprintln!("Failed to initialize client: {}", e);
}
Ok(())
}
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
BonkPoolCreateEvent => |e: BonkPoolCreateEvent| {
println!("BonkPoolCreateEvent: {:?}", e.base_mint_param.symbol);
},
BonkTradeEvent => |e: BonkTradeEvent| {
println!("BonkTradeEvent: {:?}", e);
},
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
println!("PumpFunTradeEvent: {:?}", e);
},
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
println!("PumpFunCreateTokenEvent: {:?}", e);
},
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
println!("Buy event: {:?}", e);
},
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
println!("Sell event: {:?}", e);
},
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
println!("CreatePool event: {:?}", e);
},
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
println!("Deposit event: {:?}", e);
},
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("RaydiumCpmmSwapEvent: {:?}", e);
},
RaydiumClmmSwapEvent => |e: RaydiumClmmSwapEvent| {
println!("RaydiumClmmSwapEvent: {:?}", e);
},
RaydiumClmmSwapV2Event => |e: RaydiumClmmSwapV2Event| {
println!("RaydiumClmmSwapV2Event: {:?}", e);
}
});
}
}
```
## Important Notes
## Supported Protocols
- Ensure the gRPC server address is correct and accessible
- Callback functions should be thread-safe (Send + Sync)
- Implement appropriate error retry mechanisms in production environments
- Be mindful of memory usage when processing large volumes of transaction data
- **PumpFun**: Primary meme coin trading platform
- **PumpSwap**: PumpFun's swap protocol
- **Bonk**: Token launch platform (letsbonk.fun)
- **Raydium CPMM**: Raydium's Concentrated Pool Market Maker protocol
- **Raydium CLMM**: Raydium's Concentrated Liquidity Market Maker protocol
## Event Streaming Services
- **Yellowstone gRPC**: High-performance Solana event streaming
- **ShredStream**: Alternative event streaming protocol
## Architecture Features
### Unified Event Interface
- **UnifiedEvent Trait**: All protocol events implement a common interface
- **Protocol Enum**: Easy identification of event sources
- **Event Factory**: Automatic event parsing and categorization
### Event Parsing System
- **Protocol-specific Parsers**: Dedicated parsers for each supported protocol
- **Event Factory**: Centralized event creation and parsing
- **Extensible Design**: Easy to add new protocols and event types
### Streaming Infrastructure
- **Yellowstone gRPC Client**: Optimized for Solana event streaming
- **ShredStream Client**: Alternative streaming implementation
- **Async Processing**: Non-blocking event handling
## Project Structure
```
src/
├── common/ # Common functionality and types
├── protos/ # Protocol buffer definitions
├── streaming/ # Event streaming system
│ ├── event_parser/ # Event parsing system
│ │ ├── common/ # Common event parsing tools
│ │ ├── core/ # Core parsing traits and interfaces
│ │ ├── protocols/# Protocol-specific parsers
│ │ │ ├── bonk/ # Bonk event parsing
│ │ │ ├── pumpfun/ # PumpFun event parsing
│ │ │ ├── pumpswap/ # PumpSwap event parsing
│ │ │ ├── raydium_cpmm/ # Raydium CPMM event parsing
│ │ │ └── raydium_clmm/ # Raydium CLMM event parsing
│ │ └── factory.rs # Parser factory
│ ├── shred_stream.rs # ShredStream client
│ ├── yellowstone_grpc.rs # Yellowstone gRPC client
│ └── yellowstone_sub_system.rs # Yellowstone subsystem
├── lib.rs # Main library file
└── main.rs # Example program
```
## Performance Considerations
1. **Connection Management**: Properly handle connection lifecycle and reconnection
2. **Event Filtering**: Use protocol filtering to reduce unnecessary event processing
3. **Memory Management**: Implement proper cleanup for long-running streams
4. **Error Handling**: Robust error handling for network issues and service disruptions
## Configuration Options
### Yellowstone gRPC Configuration
```rust
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None, // Custom configuration options
)?;
```
### ShredStream Configuration
```rust
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
```
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
MIT License
### Telegram group:
https://t.me/fnzero_group
## Contact
- Project Repository: https://github.com/0xfnzero/solana-streamer
- Telegram Group: https://t.me/fnzero_group
## Important Notes
1. **Network Stability**: Ensure stable network connection for continuous event streaming
2. **Rate Limiting**: Be aware of rate limits on public gRPC endpoints
3. **Error Recovery**: Implement proper error handling and reconnection logic
4. **Resource Management**: Monitor memory and CPU usage for long-running streams
5. **Compliance**: Ensure compliance with relevant laws and regulations
## Language Versions
- [English](README.md)
- [中文](README_CN.md)
+253
View File
@@ -0,0 +1,253 @@
# Solana Streamer
[中文](https://github.com/0xfnzero/solana-streamer/blob/main/README_CN.md) | [English](https://github.com/0xfnzero/solana-streamer/blob/main/README.md) | [Telegram](https://t.me/fnzero_group)
一个轻量级的 Rust 库,用于从 Solana DEX 交易程序中实时流式传输事件。该库为 PumpFun、PumpSwap、Bonk 和 Raydium CPMM 协议提供高效的事件解析和订阅功能。
## 项目特性
1. **实时事件流**: 订阅多个 Solana DEX 协议的实时交易事件
2. **Yellowstone gRPC 支持**: 使用 Yellowstone gRPC 进行高性能事件订阅
3. **ShredStream 支持**: 使用 ShredStream 协议进行替代事件流传输
4. **多协议支持**:
- **PumpFun**: 迷因币交易平台事件
- **PumpSwap**: PumpFun 的交换协议事件
- **Bonk**: 代币发布平台事件 (letsbonk.fun)
- **Raydium CPMM**: Raydium 集中池做市商事件
- **Raydium CLMM**: Raydium 集中流动性做市商事件
5. **统一事件接口**: 在所有支持的协议中保持一致的事件处理
6. **事件解析系统**: 自动解析和分类协议特定事件
7. **高性能**: 针对低延迟事件处理进行优化
## 安装
将项目克隆到您的项目目录:
```bash
cd your_project_root_directory
git clone https://github.com/0xfnzero/solana-streamer
```
在您的 `Cargo.toml` 中添加依赖:
```toml
# 添加到您的 Cargo.toml
solana-streamer = { path = "./solana-streamer", version = "0.1.0" }
```
## 使用示例
```rust
use solana_streamer::{
match_event,
streaming::{
event_parser::{
protocols::{
bonk::{BonkPoolCreateEvent, BonkTradeEvent}, pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent}, pumpswap::{
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
PumpSwapSellEvent, PumpSwapWithdrawEvent,
}, raydium_clmm::{RaydiumClmmSwapEvent, RaydiumClmmSwapV2Event}, raydium_cpmm::RaydiumCpmmSwapEvent
},
Protocol, UnifiedEvent,
},
ShredStreamGrpc, YellowstoneGrpc,
},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
test_grpc().await?;
test_shreds().await?;
Ok(())
}
async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
println!("正在订阅 GRPC 事件...");
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None,
)?;
let callback = create_event_callback();
let protocols = vec![
Protocol::PumpFun,
Protocol::PumpSwap,
Protocol::Bonk,
Protocol::RaydiumCpmm,
Protocol::RaydiumClmm,
];
println!("开始监听事件,按 Ctrl+C 停止...");
grpc.subscribe_events(protocols, None, None, None, callback)
.await?;
Ok(())
}
async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
println!("正在订阅 ShredStream 事件...");
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
let callback = create_event_callback();
let protocols = vec![
Protocol::PumpFun,
Protocol::PumpSwap,
Protocol::Bonk,
Protocol::RaydiumCpmm,
Protocol::RaydiumClmm,
];
println!("开始监听事件,按 Ctrl+C 停止...");
shred_stream
.shredstream_subscribe(protocols, None, callback)
.await?;
Ok(())
}
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
BonkPoolCreateEvent => |e: BonkPoolCreateEvent| {
println!("BonkPoolCreateEvent: {:?}", e.base_mint_param.symbol);
},
BonkTradeEvent => |e: BonkTradeEvent| {
println!("BonkTradeEvent: {:?}", e);
},
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
println!("PumpFunTradeEvent: {:?}", e);
},
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
println!("PumpFunCreateTokenEvent: {:?}", e);
},
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
println!("Buy event: {:?}", e);
},
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
println!("Sell event: {:?}", e);
},
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
println!("CreatePool event: {:?}", e);
},
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
println!("Deposit event: {:?}", e);
},
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("RaydiumCpmmSwapEvent: {:?}", e);
},
RaydiumClmmSwapEvent => |e: RaydiumClmmSwapEvent| {
println!("RaydiumClmmSwapEvent: {:?}", e);
},
RaydiumClmmSwapV2Event => |e: RaydiumClmmSwapV2Event| {
println!("RaydiumClmmSwapV2Event: {:?}", e);
}
});
}
}
```
## 支持的协议
- **PumpFun**: 主要迷因币交易平台
- **PumpSwap**: PumpFun 的交换协议
- **Bonk**: 代币发布平台 (letsbonk.fun)
- **Raydium CPMM**: Raydium 集中池做市商协议
- **Raydium CLMM**: Raydium 集中流动性做市商协议
## 事件流服务
- **Yellowstone gRPC**: 高性能 Solana 事件流
- **ShredStream**: 替代事件流协议
## 架构特性
### 统一事件接口
- **UnifiedEvent Trait**: 所有协议事件实现通用接口
- **Protocol Enum**: 轻松识别事件来源
- **Event Factory**: 自动事件解析和分类
### 事件解析系统
- **协议特定解析器**: 每个支持协议的专用解析器
- **事件工厂**: 集中式事件创建和解析
- **可扩展设计**: 易于添加新协议和事件类型
### 流基础设施
- **Yellowstone gRPC 客户端**: 针对 Solana 事件流优化
- **ShredStream 客户端**: 替代流实现
- **异步处理**: 非阻塞事件处理
## 项目结构
```
src/
├── common/ # 通用功能和类型
├── protos/ # Protocol buffer 定义
├── streaming/ # 事件流系统
│ ├── event_parser/ # 事件解析系统
│ │ ├── common/ # 通用事件解析工具
│ │ ├── core/ # 核心解析特征和接口
│ │ ├── protocols/# 协议特定解析器
│ │ │ ├── bonk/ # Bonk 事件解析
│ │ │ ├── pumpfun/ # PumpFun 事件解析
│ │ │ ├── pumpswap/ # PumpSwap 事件解析
│ │ │ ├── raydium_cpmm/ # Raydium CPMM 事件解析
│ │ │ └── raydium_clmm/ # Raydium CLMM 事件解析
│ │ └── factory.rs # 解析器工厂
│ ├── shred_stream.rs # ShredStream 客户端
│ ├── yellowstone_grpc.rs # Yellowstone gRPC 客户端
│ └── yellowstone_sub_system.rs # Yellowstone 子系统
├── lib.rs # 主库文件
└── main.rs # 示例程序
```
## 性能考虑
1. **连接管理**: 正确处理连接生命周期和重连
2. **事件过滤**: 使用协议过滤减少不必要的事件处理
3. **内存管理**: 为长时间运行的流实现适当的清理
4. **错误处理**: 对网络问题和服务中断进行健壮的错误处理
## 配置选项
### Yellowstone gRPC 配置
```rust
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None, // 自定义配置选项
)?;
```
### ShredStream 配置
```rust
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
```
## 许可证
MIT 许可证
## 联系方式
- 项目仓库: https://github.com/0xfnzero/solana-streamer
- Telegram 群组: https://t.me/fnzero_group
## 重要注意事项
1. **网络稳定性**: 确保稳定的网络连接以进行连续的事件流传输
2. **速率限制**: 注意公共 gRPC 端点的速率限制
3. **错误恢复**: 实现适当的错误处理和重连逻辑
4. **资源管理**: 监控长时间运行流的内存和 CPU 使用情况
5. **合规性**: 确保遵守相关法律法规
## 语言版本
- [English](README.md)
- [中文](README_CN.md)
-97
View File
@@ -1,97 +0,0 @@
use borsh::{BorshDeserialize, BorshSerialize};
use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
use crate::error::{ClientError, ClientResult};
#[derive(Debug)]
pub enum DexInstruction {
CreateToken(CreateTokenInfo),
UserTrade(TradeInfo),
BotTrade(TradeInfo),
Other,
}
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct CreateTokenInfo {
pub slot: u64,
pub name: String,
pub symbol: String,
pub uri: String,
pub mint: Pubkey,
pub bonding_curve: Pubkey,
pub user: Pubkey,
}
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct TradeInfo {
pub slot: u64,
pub mint: Pubkey,
pub sol_amount: u64,
pub token_amount: u64,
pub is_buy: bool,
pub user: Pubkey,
pub timestamp: i64,
pub virtual_sol_reserves: u64,
pub virtual_token_reserves: u64,
pub real_sol_reserves: u64,
pub real_token_reserves: u64,
}
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct CompleteInfo {
pub user: Pubkey,
pub mint: Pubkey,
pub bonding_curve: Pubkey,
pub timestamp: u64,
}
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
pub struct SwapBaseInLog {
pub log_type: u8,
// input
pub amount_in: u64,
pub minimum_out: u64,
pub direction: u64,
// user info
pub user_source: u64,
// pool info
pub pool_coin: u64,
pub pool_pc: u64,
// calc result
pub out_amount: u64,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TransferInfo {
pub slot: u64,
pub signature: String,
pub tx: Option<VersionedTransaction>,
}
pub trait EventTrait: Sized + std::fmt::Debug {
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
}
impl EventTrait for CreateTokenInfo {
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
CreateTokenInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
}
}
impl EventTrait for TradeInfo {
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
TradeInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
}
}
impl EventTrait for CompleteInfo {
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
CompleteInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
}
}
impl EventTrait for SwapBaseInLog {
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
SwapBaseInLog::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
}
}
-94
View File
@@ -1,94 +0,0 @@
use base64::engine::general_purpose;
use base64::Engine;
use regex::Regex;
use crate::common::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo};
pub const PROGRAM_DATA: &str = "Program data: ";
#[derive(Debug)]
pub enum PumpfunEvent {
NewToken(CreateTokenInfo),
NewDevTrade(TradeInfo),
NewUserTrade(TradeInfo),
NewBotTrade(TradeInfo),
Error(String),
}
#[derive(Debug)]
pub enum DexEvent {
NewToken(CreateTokenInfo),
NewUserTrade(TradeInfo),
NewBotTrade(TradeInfo),
Error(String),
}
#[derive(Debug)]
pub enum SystemEvent {
NewTransfer(TransferInfo),
Error(String),
}
// #[derive(Debug, Clone, Copy)]
// pub struct PumpEvent {}
impl PumpfunEvent {
pub fn parse_logs(logs: &Vec<String>) -> (Option<CreateTokenInfo>, Option<TradeInfo>) {
let mut create_info: Option<CreateTokenInfo> = None;
let mut trade_info: Option<TradeInfo> = None;
if !logs.is_empty() {
let logs_iter = logs.iter().peekable();
for l in logs_iter.rev() {
if let Some(log) = l.strip_prefix(PROGRAM_DATA) {
let borsh_bytes = general_purpose::STANDARD.decode(log).unwrap();
let slice: &[u8] = &borsh_bytes[8..];
if create_info.is_none() {
if let Ok(e) = CreateTokenInfo::from_bytes(slice) {
create_info = Some(e);
continue;
}
}
if trade_info.is_none() {
if let Ok(e) = TradeInfo::from_bytes(slice) {
trade_info = Some(e);
}
}
}
}
}
(create_info, trade_info)
}
}
#[derive(Debug, Clone, Copy)]
pub struct RaydiumEvent {}
impl RaydiumEvent {
pub fn parse_logs<T: EventTrait + Clone>(logs: &Vec<String>) -> Option<T> {
let mut event: Option<T> = None;
if !logs.is_empty() {
let logs_iter = logs.iter().peekable();
for l in logs_iter.rev() {
let re = Regex::new(r"ray_log: (?P<base64>[A-Za-z0-9+/=]+)").unwrap();
if let Some(caps) = re.captures(l) {
if let Some(base64) = caps.name("base64") {
let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap();
if let Ok(e) = T::from_bytes(&bytes) {
event = Some(e);
}
}
}
}
}
event
}
}
-152
View File
@@ -1,152 +0,0 @@
use crate::common::logs_data::DexInstruction;
use crate::common::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data};
use crate::error::ClientResult;
pub struct LogFilter;
use solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
use solana_sdk::transaction::VersionedTransaction;
impl LogFilter {
const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
/// Parse transaction logs and return instruction type and data
pub fn parse_compiled_instruction(
versioned_tx: VersionedTransaction,
bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
let compiled_instructions = versioned_tx.message.instructions();
let accounts = versioned_tx.message.static_account_keys();
let program_id = Pubkey::from_str(Self::PROGRAM_ID).unwrap_or_default();
let pump_index = accounts.iter().position(|key| key == &program_id);
let mut instructions: Vec<DexInstruction> = Vec::new();
if let Some(index) = pump_index {
for instruction in compiled_instructions {
if instruction.program_id_index as usize == index {
let all_accounts_valid = instruction.accounts.iter()
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
if !all_accounts_valid {
continue;
}
match instruction.data.first() {
// create
Some(&24) => {
if let Ok(token_info) = parse_instruction_create_token_data(instruction, accounts) {
instructions.push(DexInstruction::CreateToken(token_info));
};
}
// buy
Some(&102) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, true) {
if let Some(bot_wallet_pubkey) = bot_wallet {
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
instructions.push(DexInstruction::BotTrade(trade_info));
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
};
}
// sell
Some(&51) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, false) {
if let Some(bot_wallet_pubkey) = bot_wallet {
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
instructions.push(DexInstruction::BotTrade(trade_info));
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
};
}
_ => {}
}
}
}
}
Ok(instructions)
}
/// Parse transaction logs and return instruction type and data
pub fn parse_instruction(logs: &[String], bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
let mut current_instruction = None;
let mut program_data = String::new();
let mut invoke_depth = 0;
let mut last_data_len = 0;
let mut instructions = Vec::new();
for log in logs {
// Check program invocation
if log.contains(&format!("Program {} invoke", Self::PROGRAM_ID)) {
invoke_depth += 1;
if invoke_depth == 1 { // Only reset state at top level call
current_instruction = None;
program_data.clear();
last_data_len = 0;
}
continue;
}
// Skip if not in our program
if invoke_depth == 0 {
continue;
}
// Identify instruction type (only at top level)
if invoke_depth == 1 && log.contains("Program log: Instruction:") {
if log.contains("Create") {
current_instruction = Some("create");
} else if log.contains("Buy") || log.contains("Sell") {
current_instruction = Some("trade");
}
continue;
}
// Collect Program data
if log.starts_with("Program data: ") {
let data = log.trim_start_matches("Program data: ");
if data.len() > last_data_len {
program_data = data.to_string();
last_data_len = data.len();
}
}
// Check if program ends
if log.contains(&format!("Program {} success", Self::PROGRAM_ID)) {
invoke_depth -= 1;
if invoke_depth == 0 { // Only process data when top level program ends
if let Some(instruction_type) = current_instruction {
if !program_data.is_empty() {
match instruction_type {
"create" => {
if let Ok(token_info) = parse_create_token_data(&program_data) {
instructions.push(DexInstruction::CreateToken(token_info));
}
},
"trade" => {
if let Ok(trade_info) = parse_trade_data(&program_data) {
if let Some(bot_wallet_pubkey) = bot_wallet {
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
instructions.push(DexInstruction::BotTrade(trade_info));
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
} else {
instructions.push(DexInstruction::UserTrade(trade_info));
}
}
},
_ => {}
}
}
}
}
}
}
Ok(instructions)
}
}
-236
View File
@@ -1,236 +0,0 @@
use std::str::FromStr;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use crate::error::{ClientError, ClientResult};
use crate::common::{
logs_data::{DexInstruction, CreateTokenInfo, TradeInfo},
logs_filters::LogFilter
};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::instruction::CompiledInstruction;
use std::time::{SystemTime, UNIX_EPOCH};
pub async fn process_logs<F>(
signature: &str,
logs: Vec<String>,
callback: F,
payer: Option<Pubkey>,
) -> ClientResult<()>
where
F: Fn(&str, DexInstruction) + Send + Sync,
{
let instructions = LogFilter::parse_instruction(&logs, payer)?;
for instruction in instructions {
callback(signature, instruction);
}
Ok(())
}
// Add parsing function
pub fn parse_create_token_data(data: &str) -> ClientResult<CreateTokenInfo> {
// First do base64 decoding
let decoded = BASE64.decode(data)
.map_err(|e| ClientError::Other(format!("Failed to decode base64: {}", e)))?;
// Skip prefix bytes (if any)
let mut cursor = if decoded.len() > 8 { 8 } else { 0 };
// Read name length and name
if cursor + 4 > decoded.len() {
return Err(ClientError::Other("Data too short for name length".to_string()));
}
let name_len = read_u32(&decoded[cursor..]) as usize;
cursor += 4;
if cursor + name_len > decoded.len() {
return Err(ClientError::Other(format!("Data too short for name: need {} bytes", name_len)));
}
let name = String::from_utf8(decoded[cursor..cursor + name_len].to_vec())
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in name: {}", e)))?;
cursor += name_len;
// Read symbol length and symbol
if cursor + 4 > decoded.len() {
return Err(ClientError::Other("Data too short for symbol length".to_string()));
}
let symbol_len = read_u32(&decoded[cursor..]) as usize;
cursor += 4;
if cursor + symbol_len > decoded.len() {
return Err(ClientError::Other(format!("Data too short for symbol: need {} bytes", symbol_len)));
}
let symbol = String::from_utf8(decoded[cursor..cursor + symbol_len].to_vec())
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in symbol: {}", e)))?;
cursor += symbol_len;
// Read URI length and URI
if cursor + 4 > decoded.len() {
return Err(ClientError::Other("Data too short for URI length".to_string()));
}
let uri_len = read_u32(&decoded[cursor..]) as usize;
cursor += 4;
if cursor + uri_len > decoded.len() {
return Err(ClientError::Other(format!("Data too short for URI: need {} bytes", uri_len)));
}
let uri = String::from_utf8(decoded[cursor..cursor + uri_len].to_vec())
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in uri: {}", e)))?;
cursor += uri_len;
// Make sure there is enough data to read public keys
if cursor + 32 * 3 > decoded.len() {
return Err(ClientError::Other("Data too short for public keys".to_string()));
}
// Parse Mint Public Key
let mint = bs58::encode(&decoded[cursor..cursor+32]).into_string();
cursor += 32;
// Parse Bonding Curve Public Key
let bonding_curve = bs58::encode(&decoded[cursor..cursor+32]).into_string();
cursor += 32;
// Parse User Public Key
let user = bs58::encode(&decoded[cursor..cursor+32]).into_string();
Ok(CreateTokenInfo {
slot: 0,
name,
symbol,
uri,
mint: Pubkey::from_str(&mint).unwrap(),
bonding_curve: Pubkey::from_str(&bonding_curve).unwrap(),
user: Pubkey::from_str(&user).unwrap(),
})
}
fn read_u32(data: &[u8]) -> u32 {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&data[..4]);
u32::from_le_bytes(bytes)
}
pub fn parse_trade_data(data: &str) -> ClientResult<TradeInfo> {
let engine = base64::engine::general_purpose::STANDARD;
let decoded = engine.decode(data).map_err(|e|
ClientError::Parse(
"Failed to decode base64".to_string(),
e.to_string()
)
)?;
let mut cursor = 8; // Skip prefix
// 1. Mint (32 bytes)
let mint = bs58::encode(&decoded[cursor..cursor + 32]).into_string();
cursor += 32;
// 2. Sol Amount (8 bytes)
let sol_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
cursor += 8;
// 3. Token Amount (8 bytes)
let token_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
cursor += 8;
// 4. Is Buy (1 byte)
let is_buy = decoded[cursor] != 0;
cursor += 1;
// 5. User (32 bytes)
let user = bs58::encode(&decoded[cursor..cursor + 32]).into_string();
cursor += 32;
// 6. Timestamp (8 bytes)
let timestamp = i64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
cursor += 8;
// 7. Virtual Sol Reserves (8 bytes)
let virtual_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
cursor += 8;
// 8. Virtual Token Reserves (8 bytes)
let virtual_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
cursor += 8;
let real_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
cursor += 8;
let real_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
Ok(TradeInfo {
slot: 0,
mint: Pubkey::from_str(&mint).unwrap(),
sol_amount,
token_amount,
is_buy,
user: Pubkey::from_str(&user).unwrap(),
timestamp,
virtual_sol_reserves,
virtual_token_reserves,
real_sol_reserves,
real_token_reserves,
})
}
fn current_timestamp_millis() -> i64 {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
duration.as_millis() as i64
}
pub fn parse_instruction_create_token_data(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> ClientResult<CreateTokenInfo> {
let data = instruction.data.clone();
let mut offset = 0;
offset += 8;
let len1 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
let name = String::from_utf8_lossy(&data[offset..offset + len1]);
offset += len1;
let len2 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
let symbol = String::from_utf8_lossy(&data[offset..offset + len2]);
offset += len2;
let _flag = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
offset += 4;
let hash_start = data.len() - 32;
let ipfs_bytes = &data[offset..hash_start];
let uri = String::from_utf8_lossy(ipfs_bytes);
let mint = accounts[instruction.accounts[0] as usize];
let user = accounts[instruction.accounts[7] as usize];
let bonding_curve= accounts[instruction.accounts[2] as usize];
Ok(CreateTokenInfo {
slot: 0,
name: name.to_string(),
symbol: symbol.to_string(),
uri: uri.to_string(),
mint,
bonding_curve,
user,
})
}
pub fn parse_instruction_trade_data(instruction: &CompiledInstruction, accounts: &[Pubkey], is_buy: bool) -> ClientResult<TradeInfo> {
let data = instruction.data.clone();
let amount = u64::from_le_bytes(data[8..16].try_into().unwrap());
let max_sol_cost_or_min_sol_output = u64::from_le_bytes(data[16..24].try_into().unwrap());
let user = accounts[instruction.accounts[6] as usize];
let mint = accounts[instruction.accounts[2] as usize];
Ok(TradeInfo {
slot: 0,
mint,
sol_amount: max_sol_cost_or_min_sol_output,
token_amount: amount,
is_buy,
user,
timestamp: current_timestamp_millis(),
virtual_sol_reserves: 0,
virtual_token_reserves: 0,
real_sol_reserves: 0,
real_token_reserves: 0,
})
}
-105
View File
@@ -1,105 +0,0 @@
use solana_client::{
nonblocking::pubsub_client::PubsubClient,
rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter}
};
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use futures::StreamExt;
use crate::common::{
logs_data::DexInstruction, logs_filters::LogFilter
};
use super::logs_events::PumpfunEvent;
/// Subscription handle containing task and unsubscribe logic
pub struct SubscriptionHandle {
pub task: JoinHandle<()>,
pub unsub_fn: Box<dyn Fn() + Send>,
}
impl SubscriptionHandle {
pub async fn shutdown(self) {
(self.unsub_fn)();
self.task.abort();
}
}
pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient {
PubsubClient::new(ws_url).await.unwrap()
}
/// 启动订阅
pub async fn tokens_subscription<F>(
ws_url: &str,
commitment: CommitmentConfig,
callback: F,
bot_wallet: Option<Pubkey>,
) -> Result<SubscriptionHandle, Box<dyn std::error::Error>>
where
F: Fn(PumpfunEvent) + Send + Sync + 'static,
{
let program_address = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".to_string();
let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]);
let logs_config = RpcTransactionLogsConfig {
commitment: Some(commitment),
};
// Create PubsubClient
let sub_client = Arc::new(PubsubClient::new(ws_url).await.unwrap());
let sub_client_clone = Arc::clone(&sub_client);
// Create channel for unsubscribe
let (unsub_tx, _) = mpsc::channel(1);
// Start subscription task
let task = tokio::spawn(async move {
let (mut stream, _) = sub_client_clone.logs_subscribe(logs_filter, logs_config).await.unwrap();
loop {
let msg = stream.next().await;
match msg {
Some(msg) => {
if let Some(_err) = msg.value.err {
continue;
}
let instructions = LogFilter::parse_instruction(&msg.value.logs, bot_wallet).unwrap();
for instruction in instructions {
match instruction {
DexInstruction::CreateToken(token_info) => {
callback(PumpfunEvent::NewToken(token_info));
}
DexInstruction::UserTrade(trade_info) => {
callback(PumpfunEvent::NewUserTrade(trade_info));
}
DexInstruction::BotTrade(trade_info) => {
callback(PumpfunEvent::NewBotTrade(trade_info));
}
_ => {}
}
}
}
None => {
println!("Token subscription stream ended");
}
}
}
});
// Return subscription handle and unsubscribe logic
Ok(SubscriptionHandle {
task,
unsub_fn: Box::new(move || {
let _ = unsub_tx.try_send(());
}),
})
}
pub async fn stop_subscription(handle: SubscriptionHandle) {
handle.shutdown().await;
}
Executable → Regular
+2 -6
View File
@@ -1,6 +1,2 @@
pub mod logs_data;
pub mod logs_parser;
pub mod logs_filters;
pub mod logs_subscribe;
pub mod logs_events;
pub mod types;
pub use types::*;
+2
View File
@@ -0,0 +1,2 @@
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
pub type AnyResult<T> = anyhow::Result<T>;
-197
View File
@@ -1,197 +0,0 @@
//! Error types for the Pump.fun SDK.
//!
//! This module defines the `ClientError` enum, which encompasses various error types that can occur when interacting with the Pump.fun program.
//! It includes specific error cases for bonding curve operations, metadata uploads, Solana client errors, and more.
//!
//! The `ClientError` enum provides a comprehensive set of error types to help developers handle and debug issues that may arise during interactions with the Pump.fun program.
//!
//! # Error Types
//!
//! - `BondingCurveNotFound`: The bonding curve account was not found.
//! - `BondingCurveError`: An error occurred while interacting with the bonding curve.
//! - `BorshError`: An error occurred while serializing or deserializing data using Borsh.
//! - `SolanaClientError`: An error occurred while interacting with the Solana RPC client.
//! - `UploadMetadataError`: An error occurred while uploading metadata to IPFS.
//! - `InvalidInput`: Invalid input parameters were provided.
//! - `InsufficientFunds`: Insufficient funds for a transaction.
//! - `SimulationError`: Transaction simulation failed.
//! - `RateLimitExceeded`: Rate limit exceeded.
use serde_json::Error;
use solana_client::{
client_error::ClientError as SolanaClientError,
pubsub_client::PubsubClientError
};
use solana_sdk::pubkey::ParsePubkeyError;
// #[derive(Debug)]
// #[allow(dead_code)]
// pub struct AppError(anyhow::Error);
// impl<E> From<E> for AppError
// where
// E: Into<anyhow::Error>,
// {
// fn from(err: E) -> Self {
// Self(err.into())
// }
// }
#[derive(Debug)]
pub enum ClientError {
/// Bonding curve account was not found
BondingCurveNotFound,
/// Error related to bonding curve operations
BondingCurveError(&'static str),
/// Error deserializing data using Borsh
BorshError(std::io::Error),
/// Error from Solana RPC client
SolanaClientError(solana_client::client_error::ClientError),
/// Error uploading metadata
UploadMetadataError(Box<dyn std::error::Error>),
/// Invalid input parameters
InvalidInput(&'static str),
/// Insufficient funds for transaction
InsufficientFunds,
/// Transaction simulation failed
SimulationError(String),
/// Rate limit exceeded
RateLimitExceeded,
OrderLimitExceeded,
ExternalService(String),
Redis(String, String),
Solana(String, String),
Parse(String, String),
Pubkey(String, String),
Jito(String, String),
Join(String),
Subscribe(String, String),
Send(String, String),
Other(String),
Anyhow(&'static str),
InvalidData(String),
PumpFunBuy(String),
PumpFunSell(String),
Timeout(String, String),
Duplicate(String),
InvalidEventType,
ChannelClosed,
}
impl std::fmt::Display for ClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BondingCurveNotFound => write!(f, "Bonding curve not found"),
Self::BondingCurveError(msg) => write!(f, "Bonding curve error: {}", msg),
Self::BorshError(err) => write!(f, "Borsh serialization error: {}", err),
Self::SolanaClientError(err) => write!(f, "Solana client error: {}", err),
Self::UploadMetadataError(err) => write!(f, "Metadata upload error: {}", err),
Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
Self::InsufficientFunds => write!(f, "Insufficient funds for transaction"),
Self::SimulationError(msg) => write!(f, "Transaction simulation failed: {}", msg),
Self::ExternalService(msg) => write!(f, "External service error: {}", msg),
Self::RateLimitExceeded => write!(f, "Rate limit exceeded"),
Self::OrderLimitExceeded => write!(f, "Order limit exceeded"),
Self::Anyhow(msg) => write!(f, "Anyhow error: {}", msg),
Self::Solana(msg, details) => write!(f, "Solana error: {}, details: {}", msg, details),
Self::Parse(msg, details) => write!(f, "Parse error: {}, details: {}", msg, details),
Self::Jito(msg, details) => write!(f, "Jito error: {}, details: {}", msg, details),
Self::Redis(msg, details) => write!(f, "Redis error: {}, details: {}", msg, details),
Self::Join(msg) => write!(f, "Task join error: {}", msg),
Self::Pubkey(msg, details) => write!(f, "Pubkey error: {}, details: {}", msg, details),
Self::Subscribe(msg, details) => write!(f, "Subscribe error: {}, details: {}", msg, details),
Self::Send(msg, details) => write!(f, "Send error: {}, details: {}", msg, details),
Self::Other(msg) => write!(f, "Other error: {}", msg),
Self::PumpFunBuy(msg) => write!(f, "PumpFun buy error: {}", msg),
Self::PumpFunSell(msg) => write!(f, "PumpFun sell error: {}", msg),
Self::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
Self::Timeout(msg, details) => write!(f, "Operation timed out: {}, details: {}", msg, details),
Self::Duplicate(msg) => write!(f, "Duplicate event: {}", msg),
Self::InvalidEventType => write!(f, "Invalid event type"),
Self::ChannelClosed => write!(f, "Channel closed"),
}
}
}
impl std::error::Error for ClientError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::BorshError(err) => Some(err),
Self::SolanaClientError(err) => Some(err),
Self::UploadMetadataError(err) => Some(err.as_ref()),
Self::ExternalService(_) => None,
Self::Redis(_, _) => None,
Self::Solana(_, _) => None,
Self::Parse(_, _) => None,
Self::Jito(_, _) => None,
Self::Join(_) => None,
Self::Pubkey(_, _) => None,
Self::Subscribe(_, _) => None,
Self::Send(_, _) => None,
Self::Other(_) => None,
Self::PumpFunBuy(_) => None,
Self::PumpFunSell(_) => None,
Self::Timeout(_, _) => None,
Self::Duplicate(_) => None,
Self::InvalidEventType => None,
Self::ChannelClosed => None,
_ => None,
}
}
}
impl From<SolanaClientError> for ClientError {
fn from(error: SolanaClientError) -> Self {
ClientError::Solana(
"Solana client error".to_string(),
error.to_string(),
)
}
}
impl From<PubsubClientError> for ClientError {
fn from(error: PubsubClientError) -> Self {
ClientError::Solana(
"PubSub client error".to_string(),
error.to_string(),
)
}
}
impl From<ParsePubkeyError> for ClientError {
fn from(error: ParsePubkeyError) -> Self {
ClientError::Pubkey(
"Pubkey error".to_string(),
error.to_string(),
)
}
}
impl From<Error> for ClientError {
fn from(err: Error) -> Self {
ClientError::Parse(
"JSON serialization error".to_string(),
err.to_string()
)
}
}
pub type ClientResult<T> = Result<T, ClientError>;
-3
View File
@@ -1,3 +0,0 @@
pub mod yellow_stone;
pub use yellow_stone::YellowstoneGrpc;
-352
View File
@@ -1,352 +0,0 @@
use std::{collections::HashMap, fmt, time::Duration};
use futures::{channel::mpsc, sink::Sink, Stream, StreamExt, SinkExt};
use rustls::crypto::{ring::default_provider, CryptoProvider};
use tonic::{transport::channel::ClientTlsConfig, Status};
use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor};
use yellowstone_grpc_proto::geyser::{
CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions, SubscribeUpdate,
SubscribeUpdateTransaction, subscribe_update::UpdateOneof, SubscribeRequestPing,
};
use log::{error, info};
use chrono::Local;
use solana_sdk::{pubkey, pubkey::Pubkey, signature::Signature};
use solana_transaction_status::{
option_serializer::OptionSerializer, EncodedTransactionWithStatusMeta, UiTransactionEncoding,
};
use crate::common::logs_data::{DexInstruction, TransferInfo};
use crate::common::logs_events::{PumpfunEvent, SystemEvent};
use crate::common::logs_filters::LogFilter;
pub type AnyResult<T> = anyhow::Result<T>;
type TransactionsFilterMap = HashMap<String, SubscribeRequestFilterTransactions>;
const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111");
const CONNECT_TIMEOUT: u64 = 10;
const REQUEST_TIMEOUT: u64 = 60;
const CHANNEL_SIZE: usize = 1000;
const MAX_DECODING_MESSAGE_SIZE: usize = 1024 * 1024 * 10;
#[derive(Clone)]
pub struct TransactionPretty {
pub slot: u64,
pub signature: Signature,
pub is_vote: bool,
pub tx: EncodedTransactionWithStatusMeta,
}
impl fmt::Debug for TransactionPretty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta);
impl<'a> fmt::Debug for TxWrap<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let serialized = serde_json::to_string(self.0).expect("failed to serialize");
fmt::Display::fmt(&serialized, f)
}
}
f.debug_struct("TransactionPretty")
.field("slot", &self.slot)
.field("signature", &self.signature)
.field("is_vote", &self.is_vote)
.field("tx", &TxWrap(&self.tx))
.finish()
}
}
impl From<SubscribeUpdateTransaction> for TransactionPretty {
fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self {
let tx = transaction.expect("should be defined");
Self {
slot,
signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"),
is_vote: tx.is_vote,
tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx)
.expect("valid tx with meta")
.encode(UiTransactionEncoding::Base64, Some(u8::MAX), true)
.expect("failed to encode"),
}
}
}
pub struct YellowstoneGrpc {
endpoint: String,
x_token: Option<String>,
}
impl YellowstoneGrpc {
pub fn new(endpoint: String, x_token: Option<String>) -> AnyResult<Self> {
if CryptoProvider::get_default().is_none() {
default_provider()
.install_default()
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?;
}
Ok(Self {
endpoint,
x_token,
})
}
pub async fn connect(
&self,
) -> AnyResult<GeyserGrpcClient<impl Interceptor>>
{
let builder = GeyserGrpcClient::build_from_shared(self.endpoint.clone())?
.x_token(self.x_token.clone())?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.max_decoding_message_size(MAX_DECODING_MESSAGE_SIZE)
.connect_timeout(Duration::from_secs(CONNECT_TIMEOUT))
.timeout(Duration::from_secs(REQUEST_TIMEOUT));
Ok(builder.connect().await?)
}
pub async fn subscribe_with_request(
&self,
transactions: TransactionsFilterMap,
) -> AnyResult<(
impl Sink<SubscribeRequest, Error = mpsc::SendError>,
impl Stream<Item = Result<SubscribeUpdate, Status>>,
)> {
let subscribe_request = SubscribeRequest {
transactions,
commitment: Some(CommitmentLevel::Processed.into()),
..Default::default()
};
let mut client = self.connect().await?;
let (sink, stream) = client.subscribe_with_request(Some(subscribe_request)).await?;
Ok((sink, stream))
}
pub fn get_subscribe_request_filter(
&self,
account_include: Vec<String>,
account_exclude: Vec<String>,
account_required: Vec<String>,
) -> TransactionsFilterMap {
let mut transactions = HashMap::new();
transactions.insert(
"client".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false),
failed: Some(false),
signature: None,
account_include,
account_exclude,
account_required,
},
);
transactions
}
async fn handle_stream_message(
msg: SubscribeUpdate,
tx: &mut mpsc::Sender<TransactionPretty>,
subscribe_tx: &mut (impl Sink<SubscribeRequest, Error = mpsc::SendError> + Unpin),
) -> AnyResult<()> {
match msg.update_oneof {
Some(UpdateOneof::Transaction(sut)) => {
let transaction_pretty = TransactionPretty::from(sut);
tx.try_send(transaction_pretty)?;
}
Some(UpdateOneof::Ping(_)) => {
subscribe_tx
.send(SubscribeRequest {
ping: Some(SubscribeRequestPing { id: 1 }),
..Default::default()
})
.await?;
info!("service is ping: {}", Local::now());
}
Some(UpdateOneof::Pong(_)) => {
info!("service is pong: {}", Local::now());
}
_ => {}
}
Ok(())
}
pub async fn subscribe_pumpfun<F>(&self, callback: F, bot_wallet: Option<Pubkey>) -> AnyResult<()>
where
F: Fn(PumpfunEvent) + Send + Sync + 'static,
{
let addrs = vec![PUMP_PROGRAM_ID.to_string()];
let transactions = self.get_subscribe_request_filter(addrs, vec![], vec![]);
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
let callback = Box::new(callback);
tokio::spawn(async move {
while let Some(message) = stream.next().await {
match message {
Ok(msg) => {
if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await {
error!("Error handling message: {:?}", e);
break;
}
}
Err(error) => {
error!("Stream error: {error:?}");
break;
}
}
}
});
while let Some(transaction_pretty) = rx.next().await {
if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await {
error!("Error processing transaction: {:?}", e);
}
}
Ok(())
}
pub async fn subscribe_pumpfun_with_filter<F>(&self, callback: F, bot_wallet: Option<Pubkey>, account_include: Option<Vec<String>>, account_exclude: Option<Vec<String>>) -> AnyResult<()>
where
F: Fn(PumpfunEvent) + Send + Sync + 'static,
{
let addrs = vec![PUMP_PROGRAM_ID.to_string()];
let account_include = account_include.unwrap_or_default();
let account_exclude = account_exclude.unwrap_or_default();
let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs);
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
let callback = Box::new(callback);
tokio::spawn(async move {
while let Some(message) = stream.next().await {
match message {
Ok(msg) => {
if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await {
error!("Error handling message: {:?}", e);
break;
}
}
Err(error) => {
error!("Stream error: {error:?}");
break;
}
}
}
});
while let Some(transaction_pretty) = rx.next().await {
if let Err(e) = Self::process_pumpfun_transaction(transaction_pretty, &*callback, bot_wallet).await {
error!("Error processing transaction: {:?}", e);
}
}
Ok(())
}
async fn process_pumpfun_transaction<F>(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option<Pubkey>) -> AnyResult<()>
where
F: Fn(PumpfunEvent) + Send + Sync,
{
let slot = transaction_pretty.slot;
let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx;
let meta = trade_raw.meta.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
if meta.err.is_some() {
return Ok(());
}
let logs = if let OptionSerializer::Some(logs) = &meta.log_messages {
logs
} else {
&vec![]
};
let mut dev_address: Option<Pubkey> = None;
let instructions = LogFilter::parse_instruction(logs, bot_wallet).unwrap();
for instruction in instructions {
match instruction {
DexInstruction::CreateToken(mut token_info) => {
token_info.slot = slot;
dev_address = Some(token_info.user);
callback(PumpfunEvent::NewToken(token_info));
}
DexInstruction::UserTrade(mut trade_info) => {
trade_info.slot = slot;
if Some(trade_info.user) == dev_address {
callback(PumpfunEvent::NewDevTrade(trade_info));
} else {
callback(PumpfunEvent::NewUserTrade(trade_info));
}
}
DexInstruction::BotTrade(mut trade_info) => {
trade_info.slot = slot;
callback(PumpfunEvent::NewBotTrade(trade_info));
}
_ => {}
}
}
Ok(())
}
pub async fn subscribe_system<F>(&self, callback: F, account_include: Option<Vec<String>>, account_exclude: Option<Vec<String>>) -> AnyResult<()>
where
F: Fn(SystemEvent) + Send + Sync + 'static,
{
let addrs = vec![SYSTEM_PROGRAM_ID.to_string()];
let account_include = account_include.unwrap_or_default();
let account_exclude = account_exclude.unwrap_or_default();
let transactions = self.get_subscribe_request_filter(account_include, account_exclude, addrs);
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
let callback = Box::new(callback);
tokio::spawn(async move {
while let Some(message) = stream.next().await {
match message {
Ok(msg) => {
if let Err(e) = Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await {
error!("Error handling message: {:?}", e);
break;
}
}
Err(error) => {
error!("Stream error: {error:?}");
break;
}
}
}
});
while let Some(transaction_pretty) = rx.next().await {
if let Err(e) = Self::process_system_transaction(transaction_pretty, &*callback).await {
error!("Error processing transaction: {:?}", e);
}
}
Ok(())
}
async fn process_system_transaction<F>(transaction_pretty: TransactionPretty, callback: &F) -> AnyResult<()>
where
F: Fn(SystemEvent) + Send + Sync,
{
let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx;
let meta = trade_raw.meta.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
if meta.err.is_some() {
return Ok(());
}
callback(SystemEvent::NewTransfer(TransferInfo {
slot: transaction_pretty.slot,
signature: transaction_pretty.signature.to_string(),
tx: trade_raw.transaction.decode(),
}));
Ok(())
}
}
+3 -3
View File
@@ -1,3 +1,3 @@
pub mod common;
pub mod grpc;
pub mod error;
pub mod streaming;
pub mod protos;
pub mod common;
+100 -24
View File
@@ -1,38 +1,114 @@
use grpc_parsed::{common::{
logs_events::PumpfunEvent,
}, grpc::YellowstoneGrpc};
use solana_streamer::{
match_event,
streaming::{
event_parser::{
protocols::{
bonk::{BonkPoolCreateEvent, BonkTradeEvent},
pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent},
pumpswap::{
PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
PumpSwapSellEvent, PumpSwapWithdrawEvent,
},
raydium_clmm::{RaydiumClmmSwapEvent, RaydiumClmmSwapV2Event},
raydium_cpmm::RaydiumCpmmSwapEvent,
},
Protocol, UnifiedEvent,
},
ShredStreamGrpc, YellowstoneGrpc,
},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
test_grpc().await?;
test_shreds().await?;
Ok(())
}
async fn test_grpc() -> Result<(), Box<dyn std::error::Error>> {
println!("正在订阅 GRPC 事件...");
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None,
)?;
println!("Connected to the network");
let callback = create_event_callback();
let protocols = vec![
Protocol::PumpFun,
Protocol::PumpSwap,
Protocol::Bonk,
Protocol::RaydiumCpmm,
Protocol::RaydiumClmm,
];
let callback = |event: PumpfunEvent| {
println!("开始监听事件,按 Ctrl+C 停止...");
grpc.subscribe_events(protocols, None, None, None, callback)
.await?;
match event {
PumpfunEvent::NewDevTrade(trade_info) => {
println!("Received new dev trade event: {:?}", trade_info);
Ok(())
}
async fn test_shreds() -> Result<(), Box<dyn std::error::Error>> {
println!("正在订阅 ShredStream 事件...");
let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?;
let callback = create_event_callback();
let protocols = vec![
Protocol::PumpFun,
Protocol::PumpSwap,
Protocol::Bonk,
Protocol::RaydiumCpmm,
Protocol::RaydiumClmm,
];
println!("开始监听事件,按 Ctrl+C 停止...");
shred_stream
.shredstream_subscribe(protocols, None, callback)
.await?;
Ok(())
}
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
BonkPoolCreateEvent => |e: BonkPoolCreateEvent| {
println!("BonkPoolCreateEvent: {:?}", e.base_mint_param.symbol);
},
PumpfunEvent::NewToken(token_info) => {
println!("Received new token event: {:?}", token_info);
BonkTradeEvent => |e: BonkTradeEvent| {
println!("BonkTradeEvent: {:?}", e);
},
PumpfunEvent::NewUserTrade(trade_info) => {
println!("Received new trade event: {:?}", trade_info);
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
println!("PumpFunTradeEvent: {:?}", e);
},
PumpfunEvent::NewBotTrade(trade_info) => {
println!("Received new bot trade event: {:?}", trade_info);
PumpFunCreateTokenEvent => |e: PumpFunCreateTokenEvent| {
println!("PumpFunCreateTokenEvent: {:?}", e);
},
PumpfunEvent::Error(err) => {
println!("Received error: {}", err);
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
println!("Buy event: {:?}", e);
},
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
println!("Sell event: {:?}", e);
},
PumpSwapCreatePoolEvent => |e: PumpSwapCreatePoolEvent| {
println!("CreatePool event: {:?}", e);
},
PumpSwapDepositEvent => |e: PumpSwapDepositEvent| {
println!("Deposit event: {:?}", e);
},
PumpSwapWithdrawEvent => |e: PumpSwapWithdrawEvent| {
println!("Withdraw event: {:?}", e);
},
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
println!("RaydiumCpmmSwapEvent: {:?}", e);
},
RaydiumClmmSwapEvent => |e: RaydiumClmmSwapEvent| {
println!("RaydiumClmmSwapEvent: {:?}", e);
},
RaydiumClmmSwapV2Event => |e: RaydiumClmmSwapV2Event| {
println!("RaydiumClmmSwapV2Event: {:?}", e);
}
}
};
grpc.subscribe_pumpfun(callback, None).await?;
Ok(())
}
});
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod shared;
pub mod shredstream;
+18
View File
@@ -0,0 +1,18 @@
// This file is @generated by prost-build.
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct Header {
#[prost(message, optional, tag = "1")]
pub ts: ::core::option::Option<::prost_types::Timestamp>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct Heartbeat {
#[prost(uint64, tag = "1")]
pub count: u64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Socket {
#[prost(string, tag = "1")]
pub ip: ::prost::alloc::string::String,
#[prost(int64, tag = "2")]
pub port: i64,
}
+279
View File
@@ -0,0 +1,279 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Heartbeat {
/// don't trust IP:PORT from tcp header since it can be tampered over the wire
/// `socket.ip` must match incoming packet's ip. this prevents spamming an unwitting destination
#[prost(message, optional, tag = "1")]
pub socket: ::core::option::Option<super::shared::Socket>,
/// regions for shredstream proxy to receive shreds from
/// list of valid regions: <https://docs.jito.wtf/lowlatencytxnsend/#api>
#[prost(string, repeated, tag = "2")]
pub regions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct HeartbeatResponse {
/// client must respond within `ttl_ms` to keep stream alive
#[prost(uint32, tag = "1")]
pub ttl_ms: u32,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TraceShred {
/// source region, one of: <https://docs.jito.wtf/lowlatencytxnsend/#api>
#[prost(string, tag = "1")]
pub region: ::prost::alloc::string::String,
/// timestamp of creation
#[prost(message, optional, tag = "2")]
pub created_at: ::core::option::Option<::prost_types::Timestamp>,
/// monotonically increases, resets upon service restart
#[prost(uint32, tag = "3")]
pub seq_num: u32,
}
/// tbd: we may want to add filters here
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct SubscribeEntriesRequest {}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Entry {
/// the slot that the entry is from
#[prost(uint64, tag = "1")]
pub slot: u64,
/// Serialized bytes of Vec<Entry>: <https://docs.rs/solana-entry/latest/solana_entry/entry/struct.Entry.html>
#[prost(bytes = "vec", tag = "2")]
pub entries: ::prost::alloc::vec::Vec<u8>,
}
/// Generated client implementations.
pub mod shredstream_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct ShredstreamClient<T> {
inner: tonic::client::Grpc<T>,
}
impl ShredstreamClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> ShredstreamClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> ShredstreamClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
ShredstreamClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
/// RPC endpoint to send heartbeats to keep shreds flowing
pub async fn send_heartbeat(
&mut self,
request: impl tonic::IntoRequest<super::Heartbeat>,
) -> std::result::Result<
tonic::Response<super::HeartbeatResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/shredstream.Shredstream/SendHeartbeat",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("shredstream.Shredstream", "SendHeartbeat"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated client implementations.
pub mod shredstream_proxy_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct ShredstreamProxyClient<T> {
inner: tonic::client::Grpc<T>,
}
impl ShredstreamProxyClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> ShredstreamProxyClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> ShredstreamProxyClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
ShredstreamProxyClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn subscribe_entries(
&mut self,
request: impl tonic::IntoRequest<super::SubscribeEntriesRequest>,
) -> std::result::Result<
tonic::Response<tonic::codec::Streaming<super::Entry>>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/shredstream.ShredstreamProxy/SubscribeEntries",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("shredstream.ShredstreamProxy", "SubscribeEntries"),
);
self.inner.server_streaming(req, path, codec).await
}
}
}
+54
View File
@@ -0,0 +1,54 @@
pub mod types;
pub mod utils;
/// 自动生成UnifiedEvent trait实现的宏
#[macro_export]
macro_rules! impl_unified_event {
// 带有自定义ID表达式的版本
($struct_name:ident, $($field:ident),*) => {
impl $crate::streaming::event_parser::core::traits::UnifiedEvent for $struct_name {
fn id(&self) -> &str {
&self.metadata.id
}
fn event_type(&self) -> $crate::streaming::event_parser::common::types::EventType {
self.metadata.event_type.clone()
}
fn signature(&self) -> &str {
&self.metadata.signature
}
fn slot(&self) -> u64 {
self.metadata.slot
}
fn program_received_time_ms(&self) -> i64 {
self.metadata.program_received_time_ms
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn clone_boxed(&self) -> Box<dyn $crate::streaming::event_parser::core::traits::UnifiedEvent> {
Box::new(self.clone())
}
fn merge(&mut self, other: Box<dyn $crate::streaming::event_parser::core::traits::UnifiedEvent>) {
if let Some(e) = other.as_any().downcast_ref::<$struct_name>() {
$(
self.$field = e.$field.clone();
)*
}
}
}
};
}
pub use types::*;
pub use utils::*;
+173
View File
@@ -0,0 +1,173 @@
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[derive(
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
pub enum ProtocolType {
#[default]
PumpSwap,
PumpFun,
Bonk,
RaydiumCpmm,
RaydiumClmm,
}
/// 事件类型枚举
#[derive(
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
pub enum EventType {
// PumpSwap 事件
#[default]
PumpSwapBuy,
PumpSwapSell,
PumpSwapCreatePool,
PumpSwapDeposit,
PumpSwapWithdraw,
// PumpFun 事件
PumpFunCreateToken,
PumpFunBuy,
PumpFunSell,
// Bonk 事件
BonkBuyExactIn,
BonkBuyExactOut,
BonkSellExactIn,
BonkSellExactOut,
BonkInitialize,
// Raydium CPMM 事件
RaydiumCpmmSwapBaseInput,
RaydiumCpmmSwapBaseOutput,
// Raydium CLMM 事件
RaydiumClmmSwap,
RaydiumClmmSwapV2,
// 通用事件
Unknown,
}
impl EventType {
pub fn to_string(&self) -> String {
match self {
EventType::PumpSwapBuy => "PumpSwapBuy".to_string(),
EventType::PumpSwapSell => "PumpSwapSell".to_string(),
EventType::PumpSwapCreatePool => "PumpSwapCreatePool".to_string(),
EventType::PumpSwapDeposit => "PumpSwapDeposit".to_string(),
EventType::PumpSwapWithdraw => "PumpSwapWithdraw".to_string(),
EventType::PumpFunCreateToken => "PumpFunCreateToken".to_string(),
EventType::PumpFunBuy => "PumpFunBuy".to_string(),
EventType::PumpFunSell => "PumpFunSell".to_string(),
EventType::BonkBuyExactIn => "BonkBuyExactIn".to_string(),
EventType::BonkBuyExactOut => "BonkBuyExactOut".to_string(),
EventType::BonkSellExactIn => "BonkSellExactIn".to_string(),
EventType::BonkSellExactOut => "BonkSellExactOut".to_string(),
EventType::BonkInitialize => "BonkInitialize".to_string(),
EventType::RaydiumCpmmSwapBaseInput => "RaydiumCpmmSwapBaseInput".to_string(),
EventType::RaydiumCpmmSwapBaseOutput => "RaydiumCpmmSwapBaseOutput".to_string(),
EventType::RaydiumClmmSwap => "RaydiumClmmSwap".to_string(),
EventType::RaydiumClmmSwapV2 => "RaydiumClmmSwapV2".to_string(),
EventType::Unknown => "Unknown".to_string(),
}
}
}
/// 解析结果
#[derive(Debug, Clone)]
pub struct ParseResult<T> {
pub success: bool,
pub data: Option<T>,
pub error: Option<String>,
}
impl<T> ParseResult<T> {
pub fn success(data: T) -> Self {
Self {
success: true,
data: Some(data),
error: None,
}
}
pub fn failure(error: String) -> Self {
Self {
success: false,
data: None,
error: Some(error),
}
}
pub fn is_success(&self) -> bool {
self.success
}
pub fn is_failure(&self) -> bool {
!self.success
}
}
/// 协议信息
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProtocolInfo {
pub name: String,
pub program_ids: Vec<Pubkey>,
}
impl ProtocolInfo {
pub fn new(name: String, program_ids: Vec<Pubkey>) -> Self {
Self { name, program_ids }
}
pub fn supports_program(&self, program_id: &Pubkey) -> bool {
self.program_ids.contains(program_id)
}
}
/// 事件元数据
#[derive(
Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
pub struct EventMetadata {
pub id: String,
pub signature: String,
pub slot: u64,
pub program_received_time_ms: i64,
pub protocol: ProtocolType,
pub event_type: EventType,
pub program_id: Pubkey,
}
impl EventMetadata {
pub fn new(
id: String,
signature: String,
slot: u64,
protocol: ProtocolType,
event_type: EventType,
program_id: Pubkey,
) -> Self {
Self {
id,
signature,
slot,
program_received_time_ms: chrono::Utc::now().timestamp_millis(),
protocol,
event_type,
program_id,
}
}
pub fn set_id(&mut self, id: String) {
let _id = format!("{}-{}-{}", self.signature, self.event_type.to_string(), id);
// 对传入的 id 进行哈希处理
let mut hasher = DefaultHasher::new();
_id.hash(&mut hasher);
let hash_value = hasher.finish();
self.id = format!("{:x}", hash_value);
}
}
+111
View File
@@ -0,0 +1,111 @@
use base64::engine::general_purpose;
use base64::Engine;
use std::time::{SystemTime, UNIX_EPOCH};
/// 获取当前时间戳
pub fn current_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64
}
/// 从base64字符串解码数据
pub fn decode_base64(data: &str) -> Result<Vec<u8>, base64::DecodeError> {
general_purpose::STANDARD.decode(data)
}
/// 将数据编码为base64字符串
pub fn encode_base64(data: &[u8]) -> String {
general_purpose::STANDARD.encode(data)
}
/// 从字节数组中提取鉴别器和剩余数据
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
if data.len() < length {
return None;
}
Some((&data[..length], &data[length..]))
}
/// 检查鉴别器是否匹配
pub fn discriminator_matches(data: &str, expected: &str) -> bool {
if data.len() < expected.len() {
return false;
}
&data[..expected.len()] == expected
}
/// 从日志中提取程序数据
pub fn extract_program_data(log: &str) -> Option<&str> {
const PROGRAM_DATA_PREFIX: &str = "Program data: ";
log.strip_prefix(PROGRAM_DATA_PREFIX)
}
/// 从日志中提取程序日志
pub fn extract_program_log<'a>(log: &'a str, prefix: &str) -> Option<&'a str> {
log.strip_prefix(prefix)
}
/// 安全地从字节数组中读取u64
pub fn read_u64_le(data: &[u8], offset: usize) -> Option<u64> {
if data.len() < offset + 8 {
return None;
}
let bytes: [u8; 8] = data[offset..offset + 8].try_into().ok()?;
Some(u64::from_le_bytes(bytes))
}
pub fn read_u128_le(data: &[u8], offset: usize) -> Option<u128> {
if data.len() < offset + 16 {
return None;
}
let bytes: [u8; 16] = data[offset..offset + 16].try_into().ok()?;
Some(u128::from_le_bytes(bytes))
}
pub fn read_u8_le(data: &[u8], offset: usize) -> Option<u8> {
if data.len() < offset + 1 {
return None;
}
let bytes: [u8; 1] = data[offset..offset + 1].try_into().ok()?;
Some(u8::from_le_bytes(bytes))
}
/// 安全地从字节数组中读取u32
pub fn read_u32_le(data: &[u8], offset: usize) -> Option<u32> {
if data.len() < offset + 4 {
return None;
}
let bytes: [u8; 4] = data[offset..offset + 4].try_into().ok()?;
Some(u32::from_le_bytes(bytes))
}
/// 安全地从字节数组中读取u16
pub fn read_u16_le(data: &[u8], offset: usize) -> Option<u16> {
if data.len() < offset + 2 {
return None;
}
let bytes: [u8; 2] = data[offset..offset + 2].try_into().ok()?;
Some(u16::from_le_bytes(bytes))
}
/// 安全地从字节数组中读取u8
pub fn read_u8(data: &[u8], offset: usize) -> Option<u8> {
data.get(offset).copied()
}
/// 验证账户索引的有效性
pub fn validate_account_indices(indices: &[u8], account_count: usize) -> bool {
indices.iter().all(|&idx| (idx as usize) < account_count)
}
/// 格式化公钥为短字符串
pub fn format_pubkey_short(pubkey: &solana_sdk::pubkey::Pubkey) -> String {
let s = pubkey.to_string();
if s.len() <= 8 {
s
} else {
format!("{}...{}", &s[..4], &s[s.len() - 4..])
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod traits;
pub use traits::{EventParser, UnifiedEvent};
+485
View File
@@ -0,0 +1,485 @@
use anyhow::Result;
use solana_sdk::{
instruction::CompiledInstruction, pubkey::Pubkey, transaction::VersionedTransaction,
};
use solana_transaction_status::{
EncodedTransactionWithStatusMeta, UiCompiledInstruction, UiInstruction,
};
use std::fmt::Debug;
use std::{collections::HashMap, str::FromStr};
use crate::streaming::event_parser::{
common::{utils::*, EventMetadata, EventType, ProtocolType},
protocols::{
bonk::{BonkPoolCreateEvent, BonkTradeEvent},
pumpfun::{PumpFunCreateTokenEvent, PumpFunTradeEvent},
},
};
/// 统一事件接口 - 所有协议的事件都需要实现此trait
pub trait UnifiedEvent: Debug + Send + Sync {
/// 获取事件ID
fn id(&self) -> &str;
/// 获取事件类型
fn event_type(&self) -> EventType;
/// 获取交易签名
fn signature(&self) -> &str;
/// 获取槽位号
fn slot(&self) -> u64;
/// 获取程序接收的时间戳(毫秒)
fn program_received_time_ms(&self) -> i64;
/// 将事件转换为Any以便向下转型
fn as_any(&self) -> &dyn std::any::Any;
/// 将事件转换为可变Any以便向下转型
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
/// 克隆事件
fn clone_boxed(&self) -> Box<dyn UnifiedEvent>;
/// 合并事件(可选实现)
fn merge(&mut self, _other: Box<dyn UnifiedEvent>) {
// 默认实现:不进行任何合并操作
}
}
/// 事件解析器trait - 定义了事件解析的核心方法
#[async_trait::async_trait]
pub trait EventParser: Send + Sync {
/// 从内联指令中解析事件数据
fn parse_events_from_inner_instruction(
&self,
instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>>;
/// 从指令中解析事件数据
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>>;
/// 从VersionedTransaction中解析指令事件的通用方法
async fn parse_instruction_events_from_versioned_transaction(
&self,
versioned_tx: &VersionedTransaction,
signature: &str,
slot: Option<u64>,
accounts: &[Pubkey],
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
let mut instruction_events = Vec::new();
// 获取交易的指令和账户
let compiled_instructions = versioned_tx.message.instructions();
let mut accounts: Vec<Pubkey> = accounts.to_vec();
// 检查交易中是否包含程序
let has_program = accounts.iter().any(|account| self.should_handle(account));
if has_program {
// 解析每个指令
for instruction in compiled_instructions {
if let Some(program_id) = accounts.get(instruction.program_id_index as usize) {
if self.should_handle(program_id) {
let max_idx = instruction.accounts.iter().max().unwrap_or(&0);
// 补齐accounts(使用Pubkey::default())
if *max_idx as usize > accounts.len() {
for _i in accounts.len()..*max_idx as usize {
accounts.push(Pubkey::default());
}
}
if let Ok(events) = self
.parse_instruction(instruction, &accounts, signature, slot)
.await
{
instruction_events.extend(events);
}
}
}
}
}
Ok(instruction_events)
}
async fn parse_versioned_transaction(
&self,
versioned_tx: &VersionedTransaction,
signature: &str,
slot: Option<u64>,
bot_wallet: Option<Pubkey>,
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
let accounts: Vec<Pubkey> = versioned_tx.message.static_account_keys().to_vec();
let events = self
.parse_instruction_events_from_versioned_transaction(
versioned_tx,
signature,
slot,
&accounts,
)
.await
.unwrap_or_else(|_e| vec![]);
Ok(self.process_events(events, bot_wallet))
}
async fn parse_transaction(
&self,
tx: EncodedTransactionWithStatusMeta,
signature: &str,
slot: Option<u64>,
bot_wallet: Option<Pubkey>,
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
let transaction = tx.transaction;
// 检查交易元数据
let meta = tx
.meta
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
let mut address_table_lookups: Vec<Pubkey> = vec![];
if meta.err.is_none() {
let loaded_addresses = meta.loaded_addresses.as_ref().unwrap();
for lookup in &loaded_addresses.writable {
address_table_lookups.push(Pubkey::from_str(lookup).unwrap());
}
for lookup in &loaded_addresses.readonly {
address_table_lookups.push(Pubkey::from_str(lookup).unwrap());
}
}
let mut accounts: Vec<Pubkey> = vec![];
let mut instruction_events = Vec::new();
// 解析指令事件
if let Some(versioned_tx) = transaction.decode() {
accounts = versioned_tx.message.static_account_keys().to_vec();
accounts.extend(address_table_lookups.clone());
instruction_events = self
.parse_instruction_events_from_versioned_transaction(
&versioned_tx,
signature,
slot,
&accounts,
)
.await
.unwrap_or_else(|_e| vec![]);
} else {
accounts.extend(address_table_lookups.clone());
}
// 解析内联指令事件
let mut inner_instruction_events = Vec::new();
// 检查交易是否成功
if meta.err.is_none() {
let inner_instructions = meta.inner_instructions.as_ref().unwrap();
for inner_instruction in inner_instructions {
for instruction in &inner_instruction.instructions {
match instruction {
UiInstruction::Compiled(compiled) => {
// 解析嵌套指令
let compiled_instruction = CompiledInstruction {
program_id_index: compiled.program_id_index,
accounts: compiled.accounts.clone(),
data: bs58::decode(compiled.data.clone()).into_vec().unwrap(),
};
if let Ok(events) = self
.parse_instruction(
&compiled_instruction,
&accounts,
signature,
slot,
)
.await
{
instruction_events.extend(events);
}
if let Ok(events) = self
.parse_inner_instruction(compiled, signature, slot)
.await
{
inner_instruction_events.extend(events);
}
}
_ => {}
}
}
}
}
if instruction_events.len() > 0 && inner_instruction_events.len() > 0 {
for instruction_event in &mut instruction_events {
for inner_instruction_event in &inner_instruction_events {
if instruction_event.id() == inner_instruction_event.id()
&& instruction_event.event_type() == inner_instruction_event.event_type()
{
instruction_event.merge(inner_instruction_event.clone_boxed());
break;
}
}
}
}
Ok(self.process_events(instruction_events, bot_wallet))
}
fn process_events(
&self,
mut events: Vec<Box<dyn UnifiedEvent>>,
bot_wallet: Option<Pubkey>,
) -> Vec<Box<dyn UnifiedEvent>> {
let mut dev_address = None;
let mut bonk_dev_address = None;
for event in &mut events {
if let Some(token_info) = event.as_any().downcast_ref::<PumpFunCreateTokenEvent>() {
dev_address = Some(token_info.user);
} else if let Some(trade_info) = event.as_any_mut().downcast_mut::<PumpFunTradeEvent>()
{
if Some(trade_info.user) == dev_address {
trade_info.is_dev_create_token_trade = true;
} else if Some(trade_info.user) == bot_wallet {
trade_info.is_bot = true;
} else {
trade_info.is_dev_create_token_trade = false;
}
}
if let Some(pool_info) = event.as_any().downcast_ref::<BonkPoolCreateEvent>() {
bonk_dev_address = Some(pool_info.creator);
} else if let Some(trade_info) = event.as_any_mut().downcast_mut::<BonkTradeEvent>() {
if Some(trade_info.payer) == bonk_dev_address {
trade_info.is_dev_create_token_trade = true;
} else if Some(trade_info.payer) == bot_wallet {
trade_info.is_bot = true;
} else {
trade_info.is_dev_create_token_trade = false;
}
}
}
events
}
async fn parse_inner_instruction(
&self,
instruction: &UiCompiledInstruction,
signature: &str,
slot: Option<u64>,
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
let slot = slot.unwrap_or(0);
let events = self.parse_events_from_inner_instruction(instruction, signature, slot);
Ok(events)
}
async fn parse_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: Option<u64>,
) -> Result<Vec<Box<dyn UnifiedEvent>>> {
let slot = slot.unwrap_or(0);
let events = self.parse_events_from_instruction(instruction, accounts, signature, slot);
Ok(events)
}
/// 检查是否应该处理此程序ID
fn should_handle(&self, program_id: &Pubkey) -> bool;
/// 获取支持的程序ID列表
fn supported_program_ids(&self) -> Vec<Pubkey>;
}
// 为Box<dyn UnifiedEvent>实现Clone
impl Clone for Box<dyn UnifiedEvent> {
fn clone(&self) -> Self {
self.clone_boxed()
}
}
/// 通用事件解析器配置
#[derive(Debug, Clone)]
pub struct GenericEventParseConfig {
pub inner_instruction_discriminator: &'static str,
pub instruction_discriminator: &'static [u8],
pub event_type: EventType,
pub inner_instruction_parser: InnerInstructionEventParser,
pub instruction_parser: InstructionEventParser,
}
/// 内联指令事件解析器
pub type InnerInstructionEventParser =
fn(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>>;
/// 指令事件解析器
pub type InstructionEventParser =
fn(data: &[u8], accounts: &[Pubkey], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>>;
/// 通用事件解析器基类
pub struct GenericEventParser {
program_id: Pubkey,
protocol_type: ProtocolType,
inner_instruction_configs: HashMap<&'static str, Vec<GenericEventParseConfig>>,
instruction_configs: HashMap<Vec<u8>, Vec<GenericEventParseConfig>>,
}
impl GenericEventParser {
/// 创建新的通用事件解析器
pub fn new(
program_id: Pubkey,
protocol_type: ProtocolType,
configs: Vec<GenericEventParseConfig>,
) -> Self {
let mut inner_instruction_configs = HashMap::new();
let mut instruction_configs = HashMap::new();
for config in configs {
inner_instruction_configs
.entry(config.inner_instruction_discriminator)
.or_insert(vec![])
.push(config.clone());
instruction_configs
.entry(config.instruction_discriminator.to_vec())
.or_insert(vec![])
.push(config);
}
Self {
program_id,
protocol_type,
inner_instruction_configs,
instruction_configs,
}
}
/// 通用的内联指令解析方法
fn parse_inner_instruction_event(
&self,
config: &GenericEventParseConfig,
data: &[u8],
signature: &str,
slot: u64,
) -> Option<Box<dyn UnifiedEvent>> {
let metadata = EventMetadata::new(
signature.to_string(),
signature.to_string(),
slot,
self.protocol_type.clone(),
config.event_type.clone(),
self.program_id,
);
(config.inner_instruction_parser)(data, metadata)
}
/// 通用的指令解析方法
fn parse_instruction_event(
&self,
config: &GenericEventParseConfig,
data: &[u8],
account_pubkeys: &[Pubkey],
signature: &str,
slot: u64,
) -> Option<Box<dyn UnifiedEvent>> {
let metadata = EventMetadata::new(
signature.to_string(),
signature.to_string(),
slot,
self.protocol_type.clone(),
config.event_type.clone(),
self.program_id,
);
(config.instruction_parser)(data, account_pubkeys, metadata)
}
}
#[async_trait::async_trait]
impl EventParser for GenericEventParser {
/// 从内联指令中解析事件数据
fn parse_events_from_inner_instruction(
&self,
inner_instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
let inner_instruction_data = inner_instruction.data.clone();
let inner_instruction_data_decoded =
bs58::decode(inner_instruction_data).into_vec().unwrap();
if inner_instruction_data_decoded.len() < 16 {
return Vec::new();
}
let inner_instruction_data_decoded_str =
format!("0x{}", hex::encode(&inner_instruction_data_decoded));
let data = &inner_instruction_data_decoded[16..];
let mut events = Vec::new();
for (disc, configs) in &self.inner_instruction_configs {
if discriminator_matches(&inner_instruction_data_decoded_str, disc) {
for config in configs {
if let Some(event) =
self.parse_inner_instruction_event(config, data, signature, slot)
{
events.push(event);
}
}
}
}
events
}
/// 从指令中解析事件
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
let program_id = accounts[instruction.program_id_index as usize];
if !self.should_handle(&program_id) {
return Vec::new();
}
let mut events = Vec::new();
for (disc, configs) in &self.instruction_configs {
if instruction.data.len() < disc.len() {
continue;
}
let discriminator = &instruction.data[..disc.len()];
let data = &instruction.data[disc.len()..];
if discriminator == disc {
// 验证账户索引
if !validate_account_indices(&instruction.accounts, accounts.len()) {
continue;
}
let account_pubkeys: Vec<Pubkey> = instruction
.accounts
.iter()
.map(|&idx| accounts[idx as usize])
.collect();
for config in configs {
if let Some(event) = self.parse_instruction_event(
config,
data,
&account_pubkeys,
signature,
slot,
) {
events.push(event);
}
}
}
}
events
}
fn should_handle(&self, program_id: &Pubkey) -> bool {
*program_id == self.program_id
}
fn supported_program_ids(&self) -> Vec<Pubkey> {
vec![self.program_id]
}
}
+98
View File
@@ -0,0 +1,98 @@
use anyhow::{anyhow, Result};
use solana_sdk::pubkey::Pubkey;
use std::sync::Arc;
use crate::streaming::event_parser::protocols::{
bonk::parser::BONK_PROGRAM_ID, pumpfun::parser::PUMPFUN_PROGRAM_ID,
pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID,
raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID, BonkEventParser, RaydiumCpmmEventParser,
RaydiumClmmEventParser,
};
use super::{
core::traits::EventParser,
protocols::{pumpfun::PumpFunEventParser, pumpswap::PumpSwapEventParser},
};
/// 支持的协议
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Protocol {
PumpSwap,
PumpFun,
Bonk,
RaydiumCpmm,
RaydiumClmm,
}
impl Protocol {
pub fn get_program_id(&self) -> Vec<Pubkey> {
match self {
Protocol::PumpSwap => vec![PUMPSWAP_PROGRAM_ID],
Protocol::PumpFun => vec![PUMPFUN_PROGRAM_ID],
Protocol::Bonk => vec![BONK_PROGRAM_ID],
Protocol::RaydiumCpmm => vec![RAYDIUM_CPMM_PROGRAM_ID],
Protocol::RaydiumClmm => vec![RAYDIUM_CLMM_PROGRAM_ID],
}
}
}
impl std::fmt::Display for Protocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Protocol::PumpSwap => write!(f, "PumpSwap"),
Protocol::PumpFun => write!(f, "PumpFun"),
Protocol::Bonk => write!(f, "Bonk"),
Protocol::RaydiumCpmm => write!(f, "RaydiumCpmm"),
Protocol::RaydiumClmm => write!(f, "RaydiumClmm"),
}
}
}
impl std::str::FromStr for Protocol {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"pumpswap" => Ok(Protocol::PumpSwap),
"pumpfun" => Ok(Protocol::PumpFun),
"bonk" => Ok(Protocol::Bonk),
"raydiumcpmm" => Ok(Protocol::RaydiumCpmm),
"raydiumclmm" => Ok(Protocol::RaydiumClmm),
_ => Err(anyhow!("Unsupported protocol: {}", s)),
}
}
}
/// 事件解析器工厂 - 用于创建不同协议的事件解析器
pub struct EventParserFactory;
impl EventParserFactory {
/// 创建指定协议的事件解析器
pub fn create_parser(protocol: Protocol) -> Arc<dyn EventParser> {
match protocol {
Protocol::PumpSwap => Arc::new(PumpSwapEventParser::new()),
Protocol::PumpFun => Arc::new(PumpFunEventParser::new()),
Protocol::Bonk => Arc::new(BonkEventParser::new()),
Protocol::RaydiumCpmm => Arc::new(RaydiumCpmmEventParser::new()),
Protocol::RaydiumClmm => Arc::new(RaydiumClmmEventParser::new()),
}
}
/// 创建所有协议的事件解析器
pub fn create_all_parsers() -> Vec<Arc<dyn EventParser>> {
Self::supported_protocols()
.into_iter()
.map(Self::create_parser)
.collect()
}
/// 获取所有支持的协议
pub fn supported_protocols() -> Vec<Protocol> {
vec![Protocol::PumpSwap]
}
/// 检查协议是否支持
pub fn is_supported(protocol: &Protocol) -> bool {
Self::supported_protocols().contains(protocol)
}
}
+41
View File
@@ -0,0 +1,41 @@
pub mod common;
pub mod core;
pub mod factory;
pub mod protocols;
pub use core::traits::{EventParser, UnifiedEvent};
pub use factory::{EventParserFactory, Protocol};
/// 宏:简化 downcast_ref 模式匹配
///
/// # 使用示例
/// ```
/// use sol_trade_sdk::event_parser::match_event;
///
/// match_event!(event, {
/// PumpSwapCreatePoolEvent => |typed_event| {
/// println!("CreatePool event: {:?}", typed_event);
/// },
/// PumpSwapDepositEvent => |typed_event| {
/// // 处理存款事件
/// },
/// });
/// ```
#[macro_export]
macro_rules! match_event {
($event:expr, {
$($event_type:ty => $handler:expr),* $(,)?
}) => {
$(
if let Some(typed_event) = $event.as_any().downcast_ref::<$event_type>() {
$handler(typed_event.clone());
} else
)*
{
// 默认情况:什么都不做
}
};
}
// 重新导出宏以便于使用
pub use match_event;
+126
View File
@@ -0,0 +1,126 @@
use crate::streaming::event_parser::protocols::bonk::types::{
CurveParams, MintParams, PoolStatus, TradeDirection, VestingParams,
};
use crate::streaming::event_parser::common::EventMetadata;
use crate::impl_unified_event;
use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
/// 买入事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct BonkTradeEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub pool_state: Pubkey,
pub total_base_sell: u64,
pub virtual_base: u64,
pub virtual_quote: u64,
pub real_base_before: u64,
pub real_quote_before: u64,
pub real_base_after: u64,
pub real_quote_after: u64,
pub amount_in: u64,
pub amount_out: u64,
pub protocol_fee: u64,
pub platform_fee: u64,
pub share_fee: u64,
pub trade_direction: TradeDirection,
pub pool_status: PoolStatus,
#[borsh(skip)]
pub minimum_amount_out: u64,
#[borsh(skip)]
pub maximum_amount_in: u64,
#[borsh(skip)]
pub share_fee_rate: u64,
#[borsh(skip)]
pub payer: Pubkey,
#[borsh(skip)]
pub user_base_token: Pubkey,
#[borsh(skip)]
pub user_quote_token: Pubkey,
#[borsh(skip)]
pub base_vault: Pubkey,
#[borsh(skip)]
pub quote_vault: Pubkey,
#[borsh(skip)]
pub base_token_mint: Pubkey,
#[borsh(skip)]
pub quote_token_mint: Pubkey,
#[borsh(skip)]
pub is_dev_create_token_trade: bool,
#[borsh(skip)]
pub is_bot: bool,
}
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
impl_unified_event!(
BonkTradeEvent,
pool_state,
total_base_sell,
virtual_base,
virtual_quote,
real_base_before,
real_quote_before,
real_base_after,
real_quote_after,
amount_in,
amount_out,
protocol_fee,
platform_fee,
share_fee,
trade_direction,
pool_status
);
/// 创建池事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct BonkPoolCreateEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub pool_state: Pubkey,
pub creator: Pubkey,
pub config: Pubkey,
pub base_mint_param: MintParams,
pub curve_param: CurveParams,
pub vesting_param: VestingParams,
#[borsh(skip)]
pub payer: Pubkey,
#[borsh(skip)]
pub base_mint: Pubkey,
#[borsh(skip)]
pub quote_mint: Pubkey,
#[borsh(skip)]
pub base_vault: Pubkey,
#[borsh(skip)]
pub quote_vault: Pubkey,
#[borsh(skip)]
pub global_config: Pubkey,
#[borsh(skip)]
pub platform_config: Pubkey,
}
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
impl_unified_event!(
BonkPoolCreateEvent,
pool_state,
creator,
config,
base_mint_param,
curve_param,
vesting_param
);
/// 事件鉴别器常量
pub mod discriminators {
// 事件鉴别器
pub const TRADE_EVENT: &str = "0xe445a52e51cb9a1dbddb7fd34ee661ee";
pub const POOL_CREATE_EVENT: &str = "0xe445a52e51cb9a1d97d7e20976a173ae";
// 指令鉴别器
pub const BUY_EXACT_IN: &[u8] = &[250, 234, 13, 123, 213, 156, 19, 236];
pub const BUY_EXACT_OUT: &[u8] = &[24, 211, 116, 40, 105, 3, 153, 56];
pub const SELL_EXACT_IN: &[u8] = &[149, 39, 222, 155, 211, 124, 152, 26];
pub const SELL_EXACT_OUT: &[u8] = &[95, 200, 71, 34, 8, 9, 11, 166];
pub const INITIALIZE: &[u8] = &[175, 175, 109, 31, 13, 152, 155, 237];
}
+7
View File
@@ -0,0 +1,7 @@
pub mod events;
pub mod parser;
pub mod types;
pub use events::*;
pub use parser::BonkEventParser;
pub use types::*;
+445
View File
@@ -0,0 +1,445 @@
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
use solana_transaction_status::UiCompiledInstruction;
use crate::streaming::event_parser::{
common::{utils::*, EventMetadata, EventType, ProtocolType},
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
protocols::bonk::{
discriminators, BonkPoolCreateEvent, BonkTradeEvent, ConstantCurve, CurveParams,
FixedCurve, LinearCurve, MintParams, TradeDirection, VestingParams,
},
};
/// Bonk程序ID
pub const BONK_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj");
/// Bonk事件解析器
pub struct BonkEventParser {
inner: GenericEventParser,
}
impl BonkEventParser {
pub fn new() -> Self {
// 配置所有事件类型
let configs = vec![
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::TRADE_EVENT,
instruction_discriminator: discriminators::BUY_EXACT_IN,
event_type: EventType::BonkBuyExactIn,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_buy_exact_in_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::TRADE_EVENT,
instruction_discriminator: discriminators::BUY_EXACT_OUT,
event_type: EventType::BonkBuyExactOut,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_buy_exact_out_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::TRADE_EVENT,
instruction_discriminator: discriminators::SELL_EXACT_IN,
event_type: EventType::BonkSellExactIn,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_sell_exact_in_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::TRADE_EVENT,
instruction_discriminator: discriminators::SELL_EXACT_OUT,
event_type: EventType::BonkSellExactOut,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_sell_exact_out_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::POOL_CREATE_EVENT,
instruction_discriminator: discriminators::INITIALIZE,
event_type: EventType::BonkInitialize,
inner_instruction_parser: Self::parse_pool_create_inner_instruction,
instruction_parser: Self::parse_initialize_instruction,
},
];
let inner = GenericEventParser::new(BONK_PROGRAM_ID, ProtocolType::Bonk, configs);
Self { inner }
}
/// 解析创建池事件
fn parse_pool_create_inner_instruction(
data: &[u8],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<BonkPoolCreateEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!("{}", metadata.signature,));
Some(Box::new(BonkPoolCreateEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析交易事件
fn parse_trade_inner_instruction(
data: &[u8],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<BonkTradeEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}",
metadata.signature,
event.pool_state.to_string()
));
if metadata.event_type == EventType::BonkBuyExactIn
|| metadata.event_type == EventType::BonkBuyExactOut
{
if event.trade_direction != TradeDirection::Buy {
return None;
}
} else if metadata.event_type == EventType::BonkSellExactIn
|| metadata.event_type == EventType::BonkSellExactOut
{
if event.trade_direction != TradeDirection::Sell {
return None;
}
}
Some(Box::new(BonkTradeEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析买入指令事件
fn parse_buy_exact_in_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
let mut metadata = metadata;
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
Some(Box::new(BonkTradeEvent {
metadata,
amount_in,
minimum_amount_out,
share_fee_rate,
payer: accounts[0],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
trade_direction: TradeDirection::Buy,
..Default::default()
}))
}
fn parse_buy_exact_out_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let amount_out = read_u64_le(data, 0)?;
let maximum_amount_in = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
let mut metadata = metadata;
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
Some(Box::new(BonkTradeEvent {
metadata,
amount_out,
maximum_amount_in,
share_fee_rate,
payer: accounts[0],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
trade_direction: TradeDirection::Buy,
..Default::default()
}))
}
fn parse_sell_exact_in_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
let mut metadata = metadata;
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
Some(Box::new(BonkTradeEvent {
metadata,
amount_in,
minimum_amount_out,
share_fee_rate,
payer: accounts[0],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
trade_direction: TradeDirection::Sell,
..Default::default()
}))
}
fn parse_sell_exact_out_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let amount_out = read_u64_le(data, 0)?;
let maximum_amount_in = read_u64_le(data, 8)?;
let share_fee_rate = read_u64_le(data, 16)?;
let mut metadata = metadata;
metadata.set_id(format!("{}-{}", metadata.signature, accounts[4]));
Some(Box::new(BonkTradeEvent {
metadata,
amount_out,
maximum_amount_in,
share_fee_rate,
payer: accounts[0],
pool_state: accounts[4],
user_base_token: accounts[5],
user_quote_token: accounts[6],
base_vault: accounts[7],
quote_vault: accounts[8],
base_token_mint: accounts[9],
quote_token_mint: accounts[10],
trade_direction: TradeDirection::Sell,
..Default::default()
}))
}
/// 解析初始化事件
fn parse_initialize_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 24 {
return None;
}
let mut offset = 0;
let base_mint_param = Self::parse_mint_params(data, &mut offset)?;
let curve_param = Self::parse_curve_params(data, &mut offset)?;
let vesting_param = Self::parse_vesting_params(data, &mut offset)?;
let mut metadata = metadata;
metadata.set_id(format!("{}", metadata.signature));
Some(Box::new(BonkPoolCreateEvent {
metadata,
payer: accounts[0],
creator: accounts[1],
global_config: accounts[2],
platform_config: accounts[3],
pool_state: accounts[5],
base_mint: accounts[6],
quote_mint: accounts[7],
base_vault: accounts[8],
quote_vault: accounts[9],
base_mint_param,
curve_param,
vesting_param,
..Default::default()
}))
}
/// 解析 MintParams 结构
fn parse_mint_params(data: &[u8], offset: &mut usize) -> Option<MintParams> {
// 读取decimals (1字节)
let decimals = read_u8(data, *offset)?;
*offset += 1;
// 读取name字符串长度和内容
let name_len = read_u32_le(data, *offset)? as usize;
*offset += 4;
if data.len() < *offset + name_len {
return None;
}
let name = String::from_utf8(data[*offset..*offset + name_len].to_vec()).ok()?;
*offset += name_len;
// 读取symbol字符串长度和内容
let symbol_len = read_u32_le(data, *offset)? as usize;
*offset += 4;
if data.len() < *offset + symbol_len {
return None;
}
let symbol = String::from_utf8(data[*offset..*offset + symbol_len].to_vec()).ok()?;
*offset += symbol_len;
// 读取uri字符串长度和内容
let uri_len = read_u32_le(data, *offset)? as usize;
*offset += 4;
if data.len() < *offset + uri_len {
return None;
}
let uri = String::from_utf8(data[*offset..*offset + uri_len].to_vec()).ok()?;
*offset += uri_len;
Some(MintParams {
decimals,
name,
symbol,
uri,
})
}
/// 解析 CurveParams 结构
fn parse_curve_params(data: &[u8], offset: &mut usize) -> Option<CurveParams> {
// 读取curve类型标识符 (1字节)
let curve_type = read_u8(data, *offset)?;
*offset += 1;
match curve_type {
0 => {
// Constant curve
let supply = read_u64_le(data, *offset)?;
*offset += 8;
let total_base_sell = read_u64_le(data, *offset)?;
*offset += 8;
let total_quote_fund_raising = read_u64_le(data, *offset)?;
*offset += 8;
let migrate_type = read_u8(data, *offset)?;
*offset += 1;
Some(CurveParams::Constant {
data: ConstantCurve {
supply,
total_base_sell,
total_quote_fund_raising,
migrate_type,
},
})
}
1 => {
// Fixed curve
let supply = read_u64_le(data, *offset)?;
*offset += 8;
let total_quote_fund_raising = read_u64_le(data, *offset)?;
*offset += 8;
let migrate_type = read_u8(data, *offset)?;
*offset += 1;
Some(CurveParams::Fixed {
data: FixedCurve {
supply,
total_quote_fund_raising,
migrate_type,
},
})
}
2 => {
// Linear curve
let supply = read_u64_le(data, *offset)?;
*offset += 8;
let total_quote_fund_raising = read_u64_le(data, *offset)?;
*offset += 8;
let migrate_type = read_u8(data, *offset)?;
*offset += 1;
Some(CurveParams::Linear {
data: LinearCurve {
supply,
total_quote_fund_raising,
migrate_type,
},
})
}
_ => None,
}
}
/// 解析 VestingParams 结构
fn parse_vesting_params(data: &[u8], offset: &mut usize) -> Option<VestingParams> {
let total_locked_amount = read_u64_le(data, *offset)?;
*offset += 8;
let cliff_period = read_u64_le(data, *offset)?;
*offset += 8;
let unlock_period = read_u64_le(data, *offset)?;
*offset += 8;
Some(VestingParams {
total_locked_amount,
cliff_period,
unlock_period,
})
}
}
#[async_trait::async_trait]
impl EventParser for BonkEventParser {
fn parse_events_from_inner_instruction(
&self,
inner_instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
}
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_instruction(instruction, accounts, signature, slot)
}
fn should_handle(&self, program_id: &Pubkey) -> bool {
self.inner.should_handle(program_id)
}
fn supported_program_ids(&self) -> Vec<Pubkey> {
self.inner.supported_program_ids()
}
}
+69
View File
@@ -0,0 +1,69 @@
use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub enum TradeDirection {
#[default]
Buy,
Sell,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub enum PoolStatus {
#[default]
Fund,
Migrate,
Trade,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct MintParams {
pub decimals: u8,
pub name: String,
pub symbol: String,
pub uri: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct VestingParams {
pub total_locked_amount: u64,
pub cliff_period: u64,
pub unlock_period: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct ConstantCurve {
pub supply: u64,
pub total_base_sell: u64,
pub total_quote_fund_raising: u64,
pub migrate_type: u8,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct FixedCurve {
pub supply: u64,
pub total_quote_fund_raising: u64,
pub migrate_type: u8,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct LinearCurve {
pub supply: u64,
pub total_quote_fund_raising: u64,
pub migrate_type: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub enum CurveParams {
Constant { data: ConstantCurve },
Fixed { data: FixedCurve },
Linear { data: LinearCurve },
}
impl Default for CurveParams {
fn default() -> Self {
Self::Constant {
data: ConstantCurve::default(),
}
}
}
+11
View File
@@ -0,0 +1,11 @@
pub mod pumpfun;
pub mod pumpswap;
pub mod bonk;
pub mod raydium_cpmm;
pub mod raydium_clmm;
pub use pumpfun::PumpFunEventParser;
pub use pumpswap::PumpSwapEventParser;
pub use bonk::BonkEventParser;
pub use raydium_cpmm::RaydiumCpmmEventParser;
pub use raydium_clmm::RaydiumClmmEventParser;
+113
View File
@@ -0,0 +1,113 @@
use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::event_parser::common::EventMetadata;
use crate::impl_unified_event;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpFunCreateTokenEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub name: String,
pub symbol: String,
pub uri: String,
pub mint: Pubkey,
pub bonding_curve: Pubkey,
pub user: Pubkey,
pub creator: Pubkey,
pub timestamp: i64,
pub virtual_token_reserves: u64,
pub virtual_sol_reserves: u64,
pub real_token_reserves: u64,
pub token_total_supply: u64,
#[borsh(skip)]
pub mint_authority: Pubkey,
#[borsh(skip)]
pub associated_bonding_curve: Pubkey,
}
impl_unified_event!(
PumpFunCreateTokenEvent,
mint,
bonding_curve,
user,
creator,
timestamp,
virtual_token_reserves,
virtual_sol_reserves,
real_token_reserves,
token_total_supply
);
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpFunTradeEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub mint: Pubkey,
pub sol_amount: u64,
pub token_amount: u64,
pub is_buy: bool,
pub user: Pubkey,
pub timestamp: i64,
pub virtual_sol_reserves: u64,
pub virtual_token_reserves: u64,
pub real_sol_reserves: u64,
pub real_token_reserves: u64,
pub fee_recipient: Pubkey,
pub fee_basis_points: u64,
pub fee: u64,
pub creator: Pubkey,
pub creator_fee_basis_points: u64,
pub creator_fee: u64,
#[borsh(skip)]
pub bonding_curve: Pubkey,
#[borsh(skip)]
pub associated_bonding_curve: Pubkey,
#[borsh(skip)]
pub associated_user: Pubkey,
#[borsh(skip)]
pub creator_vault: Pubkey,
#[borsh(skip)]
pub max_sol_cost: u64,
#[borsh(skip)]
pub min_sol_output: u64,
#[borsh(skip)]
pub amount: u64,
#[borsh(skip)]
pub is_bot: bool,
#[borsh(skip)]
pub is_dev_create_token_trade: bool, // 是否是dev创建token的交易
}
impl_unified_event!(
PumpFunTradeEvent,
mint,
sol_amount,
token_amount,
is_buy,
user,
timestamp,
virtual_sol_reserves,
virtual_token_reserves,
real_sol_reserves,
real_token_reserves,
fee_recipient,
fee_basis_points,
fee,
creator,
creator_fee_basis_points,
creator_fee
);
/// 事件鉴别器常量
pub mod discriminators {
// 事件鉴别器
pub const CREATE_TOKEN_EVENT: &str = "0xe445a52e51cb9a1d1b72a94ddeeb6376";
pub const TRADE_EVENT: &str = "0xe445a52e51cb9a1dbddb7fd34ee661ee";
// 指令鉴别器
pub const CREATE_TOKEN_IX: &[u8] = &[24, 30, 200, 40, 5, 28, 7, 119];
pub const BUY_IX: &[u8] = &[102, 6, 61, 18, 1, 218, 235, 234];
pub const SELL_IX: &[u8] = &[51, 230, 133, 164, 1, 127, 131, 173];
}
+5
View File
@@ -0,0 +1,5 @@
pub mod events;
pub mod parser;
pub use events::*;
pub use parser::PumpFunEventParser;
+250
View File
@@ -0,0 +1,250 @@
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
use solana_transaction_status::UiCompiledInstruction;
use crate::streaming::event_parser::{
common::{EventMetadata, EventType, ProtocolType},
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
protocols::pumpfun::{discriminators, PumpFunCreateTokenEvent, PumpFunTradeEvent},
};
/// PumpFun程序ID
pub const PUMPFUN_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
/// PumpFun事件解析器
pub struct PumpFunEventParser {
inner: GenericEventParser,
}
impl PumpFunEventParser {
pub fn new() -> Self {
// 配置所有事件类型
let configs = vec![
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::CREATE_TOKEN_EVENT,
instruction_discriminator: discriminators::CREATE_TOKEN_IX,
event_type: EventType::PumpFunCreateToken,
inner_instruction_parser: Self::parse_create_token_inner_instruction,
instruction_parser: Self::parse_create_token_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::TRADE_EVENT,
instruction_discriminator: discriminators::BUY_IX,
event_type: EventType::PumpFunBuy,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_buy_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::TRADE_EVENT,
instruction_discriminator: discriminators::SELL_IX,
event_type: EventType::PumpFunSell,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_sell_instruction,
},
];
let inner = GenericEventParser::new(PUMPFUN_PROGRAM_ID, ProtocolType::PumpFun, configs);
Self { inner }
}
/// 解析创建代币日志事件
fn parse_create_token_inner_instruction(
data: &[u8],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<PumpFunCreateTokenEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature,
event.name,
event.symbol,
event.mint.to_string()
));
Some(Box::new(PumpFunCreateTokenEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析交易事件
fn parse_trade_inner_instruction(
data: &[u8],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<PumpFunTradeEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature,
event.mint.to_string(),
event.user.to_string(),
event.is_buy.to_string()
));
Some(Box::new(PumpFunTradeEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析创建代币指令事件
fn parse_create_token_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let mut offset = 0;
let name_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
let name = String::from_utf8_lossy(&data[offset..offset + name_len]);
offset += name_len;
let symbol_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
let symbol = String::from_utf8_lossy(&data[offset..offset + symbol_len]);
offset += symbol_len;
let uri_len = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
offset += 4;
let uri = String::from_utf8_lossy(&data[offset..offset + uri_len]);
offset += uri_len;
let creator = if offset + 32 <= data.len() {
Pubkey::new_from_array(data[offset..offset + 32].try_into().ok()?)
} else {
Pubkey::default()
};
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature,
name,
symbol,
accounts[0].to_string()
));
Some(Box::new(PumpFunCreateTokenEvent {
metadata,
name: name.to_string(),
symbol: symbol.to_string(),
uri: uri.to_string(),
creator,
mint: accounts[0],
mint_authority: accounts[1],
bonding_curve: accounts[2],
associated_bonding_curve: accounts[3],
user: accounts[7],
..Default::default()
}))
}
// 解析买入指令事件
fn parse_buy_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
let max_sol_cost = u64::from_le_bytes(data[8..16].try_into().unwrap());
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature,
accounts[2].to_string(),
accounts[6].to_string(),
true.to_string()
));
Some(Box::new(PumpFunTradeEvent {
metadata,
fee_recipient: accounts[1],
mint: accounts[2],
bonding_curve: accounts[3],
associated_bonding_curve: accounts[4],
associated_user: accounts[5],
user: accounts[6],
creator_vault: accounts[8],
max_sol_cost,
amount,
is_buy: true,
..Default::default()
}))
}
// 解析卖出指令事件
fn parse_sell_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
let min_sol_output = u64::from_le_bytes(data[8..16].try_into().unwrap());
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature,
accounts[2].to_string(),
accounts[6].to_string(),
false.to_string()
));
Some(Box::new(PumpFunTradeEvent {
metadata,
fee_recipient: accounts[1],
mint: accounts[2],
bonding_curve: accounts[3],
associated_bonding_curve: accounts[4],
associated_user: accounts[5],
user: accounts[6],
creator_vault: accounts[8],
min_sol_output,
amount,
is_buy: false,
..Default::default()
}))
}
}
#[async_trait::async_trait]
impl EventParser for PumpFunEventParser {
fn parse_events_from_inner_instruction(
&self,
inner_instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
}
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_instruction(instruction, accounts, signature, slot)
}
fn should_handle(&self, program_id: &Pubkey) -> bool {
self.inner.should_handle(program_id)
}
fn supported_program_ids(&self) -> Vec<Pubkey> {
self.inner.supported_program_ids()
}
}
+322
View File
@@ -0,0 +1,322 @@
use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
use crate::streaming::event_parser::common::EventMetadata;
use crate::impl_unified_event;
/// 买入事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapBuyEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub timestamp: i64,
pub base_amount_out: u64,
pub max_quote_amount_in: u64,
pub user_base_token_reserves: u64,
pub user_quote_token_reserves: u64,
pub pool_base_token_reserves: u64,
pub pool_quote_token_reserves: u64,
pub quote_amount_in: u64,
pub lp_fee_basis_points: u64,
pub lp_fee: u64,
pub protocol_fee_basis_points: u64,
pub protocol_fee: u64,
pub quote_amount_in_with_lp_fee: u64,
pub user_quote_amount_in: u64,
pub pool: Pubkey,
pub user: Pubkey,
pub user_base_token_account: Pubkey,
pub user_quote_token_account: Pubkey,
pub protocol_fee_recipient: Pubkey,
pub protocol_fee_recipient_token_account: Pubkey,
pub coin_creator: Pubkey,
pub coin_creator_fee_basis_points: u64,
pub coin_creator_fee: u64,
#[borsh(skip)]
pub base_mint: Pubkey,
#[borsh(skip)]
pub quote_mint: Pubkey,
#[borsh(skip)]
pub pool_base_token_account: Pubkey,
#[borsh(skip)]
pub pool_quote_token_account: Pubkey,
#[borsh(skip)]
pub coin_creator_vault_ata: Pubkey,
#[borsh(skip)]
pub coin_creator_vault_authority: Pubkey,
}
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
impl_unified_event!(
PumpSwapBuyEvent,
timestamp,
base_amount_out,
max_quote_amount_in,
user_base_token_reserves,
user_quote_token_reserves,
pool_base_token_reserves,
pool_quote_token_reserves,
quote_amount_in,
lp_fee_basis_points,
lp_fee,
protocol_fee_basis_points,
protocol_fee,
quote_amount_in_with_lp_fee,
user_quote_amount_in,
pool,
user,
user_base_token_account,
user_quote_token_account,
protocol_fee_recipient,
protocol_fee_recipient_token_account,
coin_creator,
coin_creator_fee_basis_points,
coin_creator_fee
);
/// 卖出事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapSellEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub timestamp: i64,
pub base_amount_in: u64,
pub min_quote_amount_out: u64,
pub user_base_token_reserves: u64,
pub user_quote_token_reserves: u64,
pub pool_base_token_reserves: u64,
pub pool_quote_token_reserves: u64,
pub quote_amount_out: u64,
pub lp_fee_basis_points: u64,
pub lp_fee: u64,
pub protocol_fee_basis_points: u64,
pub protocol_fee: u64,
pub quote_amount_out_without_lp_fee: u64,
pub user_quote_amount_out: u64,
pub pool: Pubkey,
pub user: Pubkey,
pub user_base_token_account: Pubkey,
pub user_quote_token_account: Pubkey,
pub protocol_fee_recipient: Pubkey,
pub protocol_fee_recipient_token_account: Pubkey,
pub coin_creator: Pubkey,
pub coin_creator_fee_basis_points: u64,
pub coin_creator_fee: u64,
#[borsh(skip)]
pub base_mint: Pubkey,
#[borsh(skip)]
pub quote_mint: Pubkey,
#[borsh(skip)]
pub pool_base_token_account: Pubkey,
#[borsh(skip)]
pub pool_quote_token_account: Pubkey,
#[borsh(skip)]
pub coin_creator_vault_ata: Pubkey,
#[borsh(skip)]
pub coin_creator_vault_authority: Pubkey,
}
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
impl_unified_event!(
PumpSwapSellEvent,
timestamp,
base_amount_in,
min_quote_amount_out,
user_base_token_reserves,
user_quote_token_reserves,
pool_base_token_reserves,
pool_quote_token_reserves,
quote_amount_out,
lp_fee_basis_points,
lp_fee,
protocol_fee_basis_points,
protocol_fee,
quote_amount_out_without_lp_fee,
user_quote_amount_out,
pool,
user,
user_base_token_account,
user_quote_token_account,
protocol_fee_recipient,
protocol_fee_recipient_token_account,
coin_creator,
coin_creator_fee_basis_points,
coin_creator_fee
);
/// 创建池子事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapCreatePoolEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub timestamp: i64,
pub index: u16,
pub creator: Pubkey,
pub base_mint: Pubkey,
pub quote_mint: Pubkey,
pub base_mint_decimals: u8,
pub quote_mint_decimals: u8,
pub base_amount_in: u64,
pub quote_amount_in: u64,
pub pool_base_amount: u64,
pub pool_quote_amount: u64,
pub minimum_liquidity: u64,
pub initial_liquidity: u64,
pub lp_token_amount_out: u64,
pub pool_bump: u8,
pub pool: Pubkey,
pub lp_mint: Pubkey,
pub user_base_token_account: Pubkey,
pub user_quote_token_account: Pubkey,
pub coin_creator: Pubkey,
#[borsh(skip)]
pub user_pool_token_account: Pubkey,
#[borsh(skip)]
pub pool_base_token_account: Pubkey,
#[borsh(skip)]
pub pool_quote_token_account: Pubkey,
}
impl_unified_event!(
PumpSwapCreatePoolEvent,
timestamp,
index,
creator,
base_mint,
quote_mint,
base_mint_decimals,
quote_mint_decimals,
base_amount_in,
quote_amount_in,
pool_base_amount,
pool_quote_amount,
minimum_liquidity,
initial_liquidity,
lp_token_amount_out,
pool_bump,
pool,
lp_mint,
user_base_token_account,
user_quote_token_account,
coin_creator
);
/// 存款事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapDepositEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub timestamp: i64,
pub lp_token_amount_out: u64,
pub max_base_amount_in: u64,
pub max_quote_amount_in: u64,
pub user_base_token_reserves: u64,
pub user_quote_token_reserves: u64,
pub pool_base_token_reserves: u64,
pub pool_quote_token_reserves: u64,
pub base_amount_in: u64,
pub quote_amount_in: u64,
pub lp_mint_supply: u64,
pub pool: Pubkey,
pub user: Pubkey,
pub user_base_token_account: Pubkey,
pub user_quote_token_account: Pubkey,
pub user_pool_token_account: Pubkey,
#[borsh(skip)]
pub base_mint: Pubkey,
#[borsh(skip)]
pub quote_mint: Pubkey,
#[borsh(skip)]
pub pool_base_token_account: Pubkey,
#[borsh(skip)]
pub pool_quote_token_account: Pubkey,
}
impl_unified_event!(
PumpSwapDepositEvent,
timestamp,
lp_token_amount_out,
max_base_amount_in,
max_quote_amount_in,
user_base_token_reserves,
user_quote_token_reserves,
pool_base_token_reserves,
pool_quote_token_reserves,
base_amount_in,
quote_amount_in,
lp_mint_supply,
pool,
user,
user_base_token_account,
user_quote_token_account,
user_pool_token_account
);
/// 提款事件
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct PumpSwapWithdrawEvent {
#[borsh(skip)]
pub metadata: EventMetadata,
pub timestamp: i64,
pub lp_token_amount_in: u64,
pub min_base_amount_out: u64,
pub min_quote_amount_out: u64,
pub user_base_token_reserves: u64,
pub user_quote_token_reserves: u64,
pub pool_base_token_reserves: u64,
pub pool_quote_token_reserves: u64,
pub base_amount_out: u64,
pub quote_amount_out: u64,
pub lp_mint_supply: u64,
pub pool: Pubkey,
pub user: Pubkey,
pub user_base_token_account: Pubkey,
pub user_quote_token_account: Pubkey,
pub user_pool_token_account: Pubkey,
#[borsh(skip)]
pub base_mint: Pubkey,
#[borsh(skip)]
pub quote_mint: Pubkey,
#[borsh(skip)]
pub pool_base_token_account: Pubkey,
#[borsh(skip)]
pub pool_quote_token_account: Pubkey,
}
impl_unified_event!(
PumpSwapWithdrawEvent,
timestamp,
lp_token_amount_in,
min_base_amount_out,
min_quote_amount_out,
user_base_token_reserves,
user_quote_token_reserves,
pool_base_token_reserves,
pool_quote_token_reserves,
base_amount_out,
quote_amount_out,
lp_mint_supply,
pool,
user,
user_base_token_account,
user_quote_token_account,
user_pool_token_account
);
/// 事件鉴别器常量
pub mod discriminators {
// 事件鉴别器
pub const BUY_EVENT: &str = "0xe445a52e51cb9a1d67f4521f2cf57777";
pub const SELL_EVENT: &str = "0xe445a52e51cb9a1d3e2f370aa503dc2a";
pub const CREATE_POOL_EVENT: &str = "0xe445a52e51cb9a1db1310cd2a076a774";
pub const DEPOSIT_EVENT: &str = "0xe445a52e51cb9a1d78f83d531f8e6b90";
pub const WITHDRAW_EVENT: &str = "0xe445a52e51cb9a1d1609851aa02c47c0";
// 指令鉴别器
pub const BUY_IX: &[u8] = &[102, 6, 61, 18, 1, 218, 235, 234];
pub const SELL_IX: &[u8] = &[51, 230, 133, 164, 1, 127, 131, 173];
pub const CREATE_POOL_IX: &[u8] = &[233, 146, 209, 142, 207, 104, 64, 188];
pub const DEPOSIT_IX: &[u8] = &[242, 35, 198, 137, 82, 225, 242, 182];
pub const WITHDRAW_IX: &[u8] = &[183, 18, 70, 156, 148, 109, 161, 34];
}
+5
View File
@@ -0,0 +1,5 @@
pub mod events;
pub mod parser;
pub use events::*;
pub use parser::PumpSwapEventParser;
+386
View File
@@ -0,0 +1,386 @@
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
use solana_transaction_status::UiCompiledInstruction;
use crate::streaming::event_parser::{
common::{EventMetadata, EventType, ProtocolType, read_u64_le},
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
protocols::pumpswap::{
discriminators, PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent,
PumpSwapSellEvent, PumpSwapWithdrawEvent,
},
};
/// PumpSwap程序ID
pub const PUMPSWAP_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
/// PumpSwap事件解析器
pub struct PumpSwapEventParser {
inner: GenericEventParser,
}
impl PumpSwapEventParser {
pub fn new() -> Self {
// 配置所有事件类型
let configs = vec![
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::BUY_EVENT,
instruction_discriminator: discriminators::BUY_IX,
event_type: EventType::PumpSwapBuy,
inner_instruction_parser: Self::parse_buy_inner_instruction,
instruction_parser: Self::parse_buy_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::SELL_EVENT,
instruction_discriminator: discriminators::SELL_IX,
event_type: EventType::PumpSwapSell,
inner_instruction_parser: Self::parse_sell_inner_instruction,
instruction_parser: Self::parse_sell_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::CREATE_POOL_EVENT,
instruction_discriminator: discriminators::CREATE_POOL_IX,
event_type: EventType::PumpSwapCreatePool,
inner_instruction_parser: Self::parse_create_pool_inner_instruction,
instruction_parser: Self::parse_create_pool_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::DEPOSIT_EVENT,
instruction_discriminator: discriminators::DEPOSIT_IX,
event_type: EventType::PumpSwapDeposit,
inner_instruction_parser: Self::parse_deposit_inner_instruction,
instruction_parser: Self::parse_deposit_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: discriminators::WITHDRAW_EVENT,
instruction_discriminator: discriminators::WITHDRAW_IX,
event_type: EventType::PumpSwapWithdraw,
inner_instruction_parser: Self::parse_withdraw_inner_instruction,
instruction_parser: Self::parse_withdraw_instruction,
},
];
let inner = GenericEventParser::new(PUMPSWAP_PROGRAM_ID, ProtocolType::PumpSwap, configs);
Self { inner }
}
/// 解析买入日志事件
fn parse_buy_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<PumpSwapBuyEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, event.user, event.pool, event.base_amount_out
));
Some(Box::new(PumpSwapBuyEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析卖出日志事件
fn parse_sell_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<PumpSwapSellEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, event.user, event.pool, event.base_amount_in
));
Some(Box::new(PumpSwapSellEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析创建池子日志事件
fn parse_create_pool_inner_instruction(
data: &[u8],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<PumpSwapCreatePoolEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, event.pool, event.creator, event.base_amount_in
));
Some(Box::new(PumpSwapCreatePoolEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析存款日志事件
fn parse_deposit_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<PumpSwapDepositEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, event.pool, event.user, event.lp_token_amount_out
));
Some(Box::new(PumpSwapDepositEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析提款日志事件
fn parse_withdraw_inner_instruction(data: &[u8], metadata: EventMetadata) -> Option<Box<dyn UnifiedEvent>> {
if let Ok(event) = borsh::from_slice::<PumpSwapWithdrawEvent>(data) {
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, event.pool, event.user, event.lp_token_amount_in
));
Some(Box::new(PumpSwapWithdrawEvent {
metadata: metadata,
..event
}))
} else {
None
}
}
/// 解析买入指令事件
fn parse_buy_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let base_amount_out = read_u64_le(data, 0)?;
let max_quote_amount_in = read_u64_le(data, 8)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[1], accounts[0], base_amount_out
));
Some(Box::new(PumpSwapBuyEvent {
metadata,
base_amount_out,
max_quote_amount_in,
pool: accounts[0],
user: accounts[1],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[5],
user_quote_token_account: accounts[6],
pool_base_token_account: accounts[7],
pool_quote_token_account: accounts[8],
protocol_fee_recipient: accounts[9],
protocol_fee_recipient_token_account: accounts[10],
coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(),
coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(),
..Default::default()
}))
}
/// 解析卖出指令事件
fn parse_sell_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 11 {
return None;
}
let base_amount_in = read_u64_le(data, 0)?;
let min_quote_amount_out = read_u64_le(data, 8)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[1], accounts[0], base_amount_in
));
Some(Box::new(PumpSwapSellEvent {
metadata,
base_amount_in,
min_quote_amount_out,
pool: accounts[0],
user: accounts[1],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[5],
user_quote_token_account: accounts[6],
pool_base_token_account: accounts[7],
pool_quote_token_account: accounts[8],
protocol_fee_recipient: accounts[9],
protocol_fee_recipient_token_account: accounts[10],
coin_creator_vault_ata: accounts.get(17).copied().unwrap_or_default(),
coin_creator_vault_authority: accounts.get(18).copied().unwrap_or_default(),
..Default::default()
}))
}
/// 解析创建池子指令事件
fn parse_create_pool_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 18 || accounts.len() < 11 {
return None;
}
let index = u16::from_le_bytes(data[0..2].try_into().ok()?);
let base_amount_in = u64::from_le_bytes(data[2..10].try_into().ok()?);
let quote_amount_in = u64::from_le_bytes(data[10..18].try_into().ok()?);
let coin_creator = if data.len() >= 50 {
Pubkey::new_from_array(data[18..50].try_into().ok()?)
} else {
Pubkey::default()
};
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[0], accounts[2], base_amount_in
));
Some(Box::new(PumpSwapCreatePoolEvent {
metadata,
index,
base_amount_in,
quote_amount_in,
pool: accounts[0],
creator: accounts[2],
base_mint: accounts[3],
quote_mint: accounts[4],
lp_mint: accounts[5],
user_base_token_account: accounts[6],
user_quote_token_account: accounts[7],
user_pool_token_account: accounts[8],
pool_base_token_account: accounts[9],
pool_quote_token_account: accounts[10],
coin_creator,
..Default::default()
}))
}
/// 解析存款指令事件
fn parse_deposit_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 24 || accounts.len() < 11 {
return None;
}
let lp_token_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
let max_base_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
let max_quote_amount_in = u64::from_le_bytes(data[16..24].try_into().ok()?);
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[0], accounts[2], lp_token_amount_out
));
Some(Box::new(PumpSwapDepositEvent {
metadata,
lp_token_amount_out,
max_base_amount_in,
max_quote_amount_in,
pool: accounts[0],
user: accounts[2],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[6],
user_quote_token_account: accounts[7],
user_pool_token_account: accounts[8],
pool_base_token_account: accounts[9],
pool_quote_token_account: accounts[10],
..Default::default()
}))
}
/// 解析提款指令事件
fn parse_withdraw_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 24 || accounts.len() < 11 {
return None;
}
let lp_token_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
let min_base_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
let min_quote_amount_out = u64::from_le_bytes(data[16..24].try_into().ok()?);
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[0], accounts[2], lp_token_amount_in
));
Some(Box::new(PumpSwapWithdrawEvent {
metadata,
lp_token_amount_in,
min_base_amount_out,
min_quote_amount_out,
pool: accounts[0],
user: accounts[2],
base_mint: accounts[3],
quote_mint: accounts[4],
user_base_token_account: accounts[6],
user_quote_token_account: accounts[7],
user_pool_token_account: accounts[8],
pool_base_token_account: accounts[9],
pool_quote_token_account: accounts[10],
..Default::default()
}))
}
}
#[async_trait::async_trait]
impl EventParser for PumpSwapEventParser {
fn parse_events_from_inner_instruction(
&self,
inner_instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
}
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_instruction(instruction, accounts, signature, slot)
}
fn should_handle(&self, program_id: &Pubkey) -> bool {
self.inner.should_handle(program_id)
}
fn supported_program_ids(&self) -> Vec<Pubkey> {
self.inner.supported_program_ids()
}
}
@@ -0,0 +1,59 @@
use crate::impl_unified_event;
use crate::streaming::event_parser::common::EventMetadata;
// use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
/// 交易
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RaydiumClmmSwapEvent {
pub metadata: EventMetadata,
pub amount: u64,
pub other_amount_threshold: u64,
pub sqrt_price_limit_x64: u128,
pub is_base_input: bool,
pub payer: Pubkey,
pub amm_config: Pubkey,
pub pool_state: Pubkey,
pub input_token_account: Pubkey,
pub output_token_account: Pubkey,
pub input_vault: Pubkey,
pub output_vault: Pubkey,
pub observation_state: Pubkey,
pub token_program: Pubkey,
pub tick_array: Pubkey,
pub remaining_accounts: Vec<Pubkey>,
}
impl_unified_event!(RaydiumClmmSwapEvent,);
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RaydiumClmmSwapV2Event {
pub metadata: EventMetadata,
pub amount: u64,
pub other_amount_threshold: u64,
pub sqrt_price_limit_x64: u128,
pub is_base_input: bool,
pub payer: Pubkey,
pub amm_config: Pubkey,
pub pool_state: Pubkey,
pub input_token_account: Pubkey,
pub output_token_account: Pubkey,
pub input_vault: Pubkey,
pub output_vault: Pubkey,
pub observation_state: Pubkey,
pub token_program: Pubkey,
pub token_program2022: Pubkey,
pub memo_program: Pubkey,
pub input_vault_mint: Pubkey,
pub output_vault_mint: Pubkey,
pub remaining_accounts: Vec<Pubkey>,
}
impl_unified_event!(RaydiumClmmSwapV2Event,);
/// 事件鉴别器常量
pub mod discriminators {
// 指令鉴别器
pub const SWAP: &[u8] = &[248, 198, 158, 145, 225, 117, 135, 200];
pub const SWAP_V2: &[u8] = &[43, 4, 237, 11, 26, 201, 30, 98];
}
+5
View File
@@ -0,0 +1,5 @@
pub mod events;
pub mod parser;
pub use events::*;
pub use parser::RaydiumClmmEventParser;
+170
View File
@@ -0,0 +1,170 @@
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
use solana_transaction_status::UiCompiledInstruction;
use crate::streaming::event_parser::{
common::{read_u128_le, read_u64_le, read_u8_le, EventMetadata, EventType, ProtocolType},
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
protocols::raydium_clmm::{discriminators, RaydiumClmmSwapEvent, RaydiumClmmSwapV2Event},
};
/// Raydium CLMM程序ID
pub const RAYDIUM_CLMM_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK");
/// Raydium CLMM事件解析器
pub struct RaydiumClmmEventParser {
inner: GenericEventParser,
}
impl RaydiumClmmEventParser {
pub fn new() -> Self {
// 配置所有事件类型
let configs = vec![
GenericEventParseConfig {
inner_instruction_discriminator: "",
instruction_discriminator: discriminators::SWAP,
event_type: EventType::RaydiumClmmSwap,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_swap_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: "",
instruction_discriminator: discriminators::SWAP_V2,
event_type: EventType::RaydiumClmmSwapV2,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_swap_v2_instruction,
},
];
let inner =
GenericEventParser::new(RAYDIUM_CLMM_PROGRAM_ID, ProtocolType::RaydiumClmm, configs);
Self { inner }
}
/// 解析交易事件
fn parse_trade_inner_instruction(
_data: &[u8],
_metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
None
}
/// 解析交易指令事件
fn parse_swap_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 10 {
return None;
}
let amount = read_u64_le(data, 0)?;
let other_amount_threshold = read_u64_le(data, 8)?;
let sqrt_price_limit_x64 = read_u128_le(data, 16)?;
let is_base_input = read_u8_le(data, 32)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[2], accounts[3], accounts[4]
));
Some(Box::new(RaydiumClmmSwapEvent {
metadata,
amount,
other_amount_threshold,
sqrt_price_limit_x64,
is_base_input: is_base_input == 1,
payer: accounts[0],
amm_config: accounts[1],
pool_state: accounts[2],
input_token_account: accounts[3],
output_token_account: accounts[4],
input_vault: accounts[5],
output_vault: accounts[6],
observation_state: accounts[7],
token_program: accounts[8],
tick_array: accounts[9],
remaining_accounts: accounts[10..].to_vec(),
..Default::default()
}))
}
fn parse_swap_v2_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let amount = read_u64_le(data, 0)?;
let other_amount_threshold = read_u64_le(data, 8)?;
let sqrt_price_limit_x64 = read_u128_le(data, 16)?;
let is_base_input = read_u8_le(data, 32)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[2], accounts[3], accounts[4]
));
Some(Box::new(RaydiumClmmSwapV2Event {
metadata,
amount,
other_amount_threshold,
sqrt_price_limit_x64,
is_base_input: is_base_input == 1,
payer: accounts[0],
amm_config: accounts[1],
pool_state: accounts[2],
input_token_account: accounts[3],
output_token_account: accounts[4],
input_vault: accounts[5],
output_vault: accounts[6],
observation_state: accounts[7],
token_program: accounts[8],
token_program2022: accounts[9],
memo_program: accounts[10],
input_vault_mint: accounts[11],
output_vault_mint: accounts[12],
remaining_accounts: accounts[13..].to_vec(),
..Default::default()
}))
}
}
#[async_trait::async_trait]
impl EventParser for RaydiumClmmEventParser {
fn parse_events_from_inner_instruction(
&self,
inner_instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
}
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_instruction(instruction, accounts, signature, slot)
}
fn should_handle(&self, program_id: &Pubkey) -> bool {
self.inner.should_handle(program_id)
}
fn supported_program_ids(&self) -> Vec<Pubkey> {
self.inner.supported_program_ids()
}
}
@@ -0,0 +1,35 @@
use crate::impl_unified_event;
use crate::streaming::event_parser::common::EventMetadata;
use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
/// 交易
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct RaydiumCpmmSwapEvent {
pub metadata: EventMetadata,
pub amount_in: u64,
pub minimum_amount_out: u64,
pub max_amount_in: u64,
pub amount_out: u64,
pub payer: Pubkey,
pub authority: Pubkey,
pub amm_config: Pubkey,
pub pool_state: Pubkey,
pub input_token_account: Pubkey,
pub output_token_account: Pubkey,
pub input_vault: Pubkey,
pub output_vault: Pubkey,
pub input_token_mint: Pubkey,
pub output_token_mint: Pubkey,
pub observation_state: Pubkey,
}
impl_unified_event!(RaydiumCpmmSwapEvent,);
/// 事件鉴别器常量
pub mod discriminators {
// 指令鉴别器
pub const SWAP_BASE_IN: &[u8] = &[143, 190, 90, 218, 196, 30, 51, 222];
pub const SWAP_BASE_OUT: &[u8] = &[55, 217, 98, 86, 163, 74, 180, 173];
}
+5
View File
@@ -0,0 +1,5 @@
pub mod events;
pub mod parser;
pub use events::*;
pub use parser::RaydiumCpmmEventParser;
+159
View File
@@ -0,0 +1,159 @@
use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
use solana_transaction_status::UiCompiledInstruction;
use crate::streaming::event_parser::{
common::{read_u64_le, EventMetadata, EventType, ProtocolType},
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
protocols::raydium_cpmm::{discriminators, RaydiumCpmmSwapEvent},
};
/// Raydium CPMM程序ID
pub const RAYDIUM_CPMM_PROGRAM_ID: Pubkey =
solana_sdk::pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C");
/// Raydium CPMM事件解析器
pub struct RaydiumCpmmEventParser {
inner: GenericEventParser,
}
impl RaydiumCpmmEventParser {
pub fn new() -> Self {
// 配置所有事件类型
let configs = vec![
GenericEventParseConfig {
inner_instruction_discriminator: "",
instruction_discriminator: discriminators::SWAP_BASE_IN,
event_type: EventType::RaydiumCpmmSwapBaseInput,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_swap_base_input_instruction,
},
GenericEventParseConfig {
inner_instruction_discriminator: "",
instruction_discriminator: discriminators::SWAP_BASE_OUT,
event_type: EventType::RaydiumCpmmSwapBaseOutput,
inner_instruction_parser: Self::parse_trade_inner_instruction,
instruction_parser: Self::parse_swap_base_output_instruction,
},
];
let inner =
GenericEventParser::new(RAYDIUM_CPMM_PROGRAM_ID, ProtocolType::RaydiumCpmm, configs);
Self { inner }
}
/// 解析交易事件
fn parse_trade_inner_instruction(
_data: &[u8],
_metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
None
}
/// 解析买入指令事件
fn parse_swap_base_input_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let amount_in = read_u64_le(data, 0)?;
let minimum_amount_out = read_u64_le(data, 8)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[3], accounts[10], accounts[11]
));
Some(Box::new(RaydiumCpmmSwapEvent {
metadata,
amount_in,
minimum_amount_out,
payer: accounts[0],
authority: accounts[1],
amm_config: accounts[2],
pool_state: accounts[3],
input_token_account: accounts[4],
output_token_account: accounts[5],
input_vault: accounts[6],
output_vault: accounts[7],
input_token_mint: accounts[10],
output_token_mint: accounts[11],
observation_state: accounts[12],
..Default::default()
}))
}
fn parse_swap_base_output_instruction(
data: &[u8],
accounts: &[Pubkey],
metadata: EventMetadata,
) -> Option<Box<dyn UnifiedEvent>> {
if data.len() < 16 || accounts.len() < 13 {
return None;
}
let max_amount_in = read_u64_le(data, 0)?;
let amount_out = read_u64_le(data, 8)?;
let mut metadata = metadata;
metadata.set_id(format!(
"{}-{}-{}-{}",
metadata.signature, accounts[3], accounts[10], accounts[11]
));
Some(Box::new(RaydiumCpmmSwapEvent {
metadata,
max_amount_in,
amount_out,
payer: accounts[0],
authority: accounts[1],
amm_config: accounts[2],
pool_state: accounts[3],
input_token_account: accounts[4],
output_token_account: accounts[5],
input_vault: accounts[6],
output_vault: accounts[7],
input_token_mint: accounts[10],
output_token_mint: accounts[11],
observation_state: accounts[12],
..Default::default()
}))
}
}
#[async_trait::async_trait]
impl EventParser for RaydiumCpmmEventParser {
fn parse_events_from_inner_instruction(
&self,
inner_instruction: &UiCompiledInstruction,
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_inner_instruction(inner_instruction, signature, slot)
}
fn parse_events_from_instruction(
&self,
instruction: &CompiledInstruction,
accounts: &[Pubkey],
signature: &str,
slot: u64,
) -> Vec<Box<dyn UnifiedEvent>> {
self.inner
.parse_events_from_instruction(instruction, accounts, signature, slot)
}
fn should_handle(&self, program_id: &Pubkey) -> bool {
self.inner.should_handle(program_id)
}
fn supported_program_ids(&self) -> Vec<Pubkey> {
self.inner.supported_program_ids()
}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod yellowstone_grpc;
pub mod yellowstone_sub_system;
pub mod shred_stream;
pub mod event_parser;
pub use yellowstone_grpc::YellowstoneGrpc;
pub use yellowstone_sub_system::{SystemEvent, TransferInfo};
pub use shred_stream::ShredStreamGrpc;
+120
View File
@@ -0,0 +1,120 @@
use std::sync::Arc;
use futures::{channel::mpsc, StreamExt};
use solana_entry::entry::Entry;
use tonic::transport::Channel;
use log::error;
use solana_sdk::transaction::VersionedTransaction;
use crate::common::AnyResult;
use crate::streaming::event_parser::{EventParserFactory, Protocol, UnifiedEvent};
use crate::protos::shredstream::shredstream_proxy_client::ShredstreamProxyClient;
use crate::protos::shredstream::SubscribeEntriesRequest;
use solana_sdk::pubkey::Pubkey;
const CHANNEL_SIZE: usize = 1000;
pub struct ShredStreamGrpc {
shredstream_client: Arc<ShredstreamProxyClient<Channel>>,
}
struct TransactionWithSlot {
transaction: VersionedTransaction,
slot: u64,
}
impl ShredStreamGrpc {
pub async fn new(endpoint: String) -> AnyResult<Self> {
let shredstream_client = ShredstreamProxyClient::connect(endpoint.clone()).await?;
Ok(Self {
shredstream_client: Arc::new(shredstream_client),
})
}
pub async fn shredstream_subscribe<F>(
&self,
protocols: Vec<Protocol>,
bot_wallet: Option<Pubkey>,
callback: F,
) -> AnyResult<()>
where
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync + 'static,
{
let request = tonic::Request::new(SubscribeEntriesRequest {});
let mut client = (*self.shredstream_client).clone();
let mut stream = client.subscribe_entries(request).await?.into_inner();
let (mut tx, mut rx) = mpsc::channel::<TransactionWithSlot>(CHANNEL_SIZE);
let callback = Box::new(callback);
tokio::spawn(async move {
while let Some(message) = stream.next().await {
match message {
Ok(msg) => {
if let Ok(entries) = bincode::deserialize::<Vec<Entry>>(&msg.entries) {
for entry in entries {
for transaction in entry.transactions {
let _ = tx.try_send(TransactionWithSlot {
transaction: transaction.clone(),
slot: msg.slot,
});
}
}
}
}
Err(error) => {
error!("Stream error: {error:?}");
break;
}
}
}
});
while let Some(transaction_with_slot) = rx.next().await {
if let Err(e) = Self::process_transaction(
transaction_with_slot,
protocols.clone(),
bot_wallet,
&*callback,
)
.await
{
error!("Error processing transaction: {:?}", e);
}
}
Ok(())
}
async fn process_transaction<F>(
transaction_with_slot: TransactionWithSlot,
protocols: Vec<Protocol>,
bot_wallet: Option<Pubkey>,
callback: &F,
) -> AnyResult<()>
where
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync,
{
let slot = transaction_with_slot.slot;
let versioned_tx = transaction_with_slot.transaction;
let signature = versioned_tx.signatures[0];
for protocol in protocols {
let parser = EventParserFactory::create_parser(protocol.clone());
let events = parser
.parse_versioned_transaction(
&versioned_tx,
&signature.to_string(),
Some(slot),
bot_wallet.clone(),
)
.await
.unwrap_or_else(|_e| vec![]);
for event in events {
callback(event);
}
}
Ok(())
}
}
+266
View File
@@ -0,0 +1,266 @@
use std::{collections::HashMap, fmt, time::Duration};
use chrono::Local;
use futures::{channel::mpsc, sink::Sink, SinkExt, Stream, StreamExt};
use log::{error, info};
use rustls::crypto::{ring::default_provider, CryptoProvider};
use solana_sdk::{pubkey::Pubkey, signature::Signature};
use solana_transaction_status::{EncodedTransactionWithStatusMeta, UiTransactionEncoding};
use tonic::{transport::channel::ClientTlsConfig, Status};
use yellowstone_grpc_client::{GeyserGrpcClient, Interceptor};
use yellowstone_grpc_proto::geyser::{
subscribe_update::UpdateOneof, CommitmentLevel, SubscribeRequest,
SubscribeRequestFilterTransactions, SubscribeRequestPing, SubscribeUpdate,
SubscribeUpdateTransaction,
};
use crate::common::AnyResult;
use crate::streaming::event_parser::{EventParserFactory, Protocol, UnifiedEvent};
type TransactionsFilterMap = HashMap<String, SubscribeRequestFilterTransactions>;
const CONNECT_TIMEOUT: u64 = 10;
const REQUEST_TIMEOUT: u64 = 60;
const CHANNEL_SIZE: usize = 1000;
const MAX_DECODING_MESSAGE_SIZE: usize = 1024 * 1024 * 10;
#[derive(Clone)]
pub struct TransactionPretty {
pub slot: u64,
pub signature: Signature,
pub is_vote: bool,
pub tx: EncodedTransactionWithStatusMeta,
}
impl fmt::Debug for TransactionPretty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct TxWrap<'a>(&'a EncodedTransactionWithStatusMeta);
impl<'a> fmt::Debug for TxWrap<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let serialized = serde_json::to_string(self.0).expect("failed to serialize");
fmt::Display::fmt(&serialized, f)
}
}
f.debug_struct("TransactionPretty")
.field("slot", &self.slot)
.field("signature", &self.signature)
.field("is_vote", &self.is_vote)
.field("tx", &TxWrap(&self.tx))
.finish()
}
}
impl From<SubscribeUpdateTransaction> for TransactionPretty {
fn from(SubscribeUpdateTransaction { transaction, slot }: SubscribeUpdateTransaction) -> Self {
let tx = transaction.expect("should be defined");
Self {
slot,
signature: Signature::try_from(tx.signature.as_slice()).expect("valid signature"),
is_vote: tx.is_vote,
tx: yellowstone_grpc_proto::convert_from::create_tx_with_meta(tx)
.expect("valid tx with meta")
.encode(UiTransactionEncoding::Base64, Some(u8::MAX), true)
.expect("failed to encode"),
}
}
}
pub struct YellowstoneGrpc {
endpoint: String,
x_token: Option<String>,
}
impl YellowstoneGrpc {
pub fn new(endpoint: String, x_token: Option<String>) -> AnyResult<Self> {
if CryptoProvider::get_default().is_none() {
default_provider()
.install_default()
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e))?;
}
Ok(Self { endpoint, x_token })
}
pub async fn connect(&self) -> AnyResult<GeyserGrpcClient<impl Interceptor>> {
let builder = GeyserGrpcClient::build_from_shared(self.endpoint.clone())?
.x_token(self.x_token.clone())?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.max_decoding_message_size(MAX_DECODING_MESSAGE_SIZE)
.connect_timeout(Duration::from_secs(CONNECT_TIMEOUT))
.timeout(Duration::from_secs(REQUEST_TIMEOUT));
Ok(builder.connect().await?)
}
pub async fn subscribe_with_request(
&self,
transactions: TransactionsFilterMap,
) -> AnyResult<(
impl Sink<SubscribeRequest, Error = mpsc::SendError>,
impl Stream<Item = Result<SubscribeUpdate, Status>>,
)> {
let subscribe_request = SubscribeRequest {
transactions,
commitment: Some(CommitmentLevel::Processed.into()),
..Default::default()
};
let mut client = self.connect().await?;
let (sink, stream) = client
.subscribe_with_request(Some(subscribe_request))
.await?;
Ok((sink, stream))
}
pub fn get_subscribe_request_filter(
&self,
account_include: Vec<String>,
account_exclude: Vec<String>,
account_required: Vec<String>,
) -> TransactionsFilterMap {
let mut transactions = HashMap::new();
transactions.insert(
"client".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false),
failed: Some(false),
signature: None,
account_include,
account_exclude,
account_required,
},
);
transactions
}
pub async fn handle_stream_message(
msg: SubscribeUpdate,
tx: &mut mpsc::Sender<TransactionPretty>,
subscribe_tx: &mut (impl Sink<SubscribeRequest, Error = mpsc::SendError> + Unpin),
) -> AnyResult<()> {
match msg.update_oneof {
Some(UpdateOneof::Transaction(sut)) => {
let transaction_pretty = TransactionPretty::from(sut);
tx.try_send(transaction_pretty)?;
}
Some(UpdateOneof::Ping(_)) => {
subscribe_tx
.send(SubscribeRequest {
ping: Some(SubscribeRequestPing { id: 1 }),
..Default::default()
})
.await?;
info!("service is ping: {}", Local::now());
}
Some(UpdateOneof::Pong(_)) => {
info!("service is pong: {}", Local::now());
}
_ => {}
}
Ok(())
}
/// 订阅事件
pub async fn subscribe_events<F>(
&self,
protocols: Vec<Protocol>,
bot_wallet: Option<Pubkey>,
account_include: Option<Vec<String>>,
account_exclude: Option<Vec<String>>,
callback: F,
) -> AnyResult<()>
where
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync + 'static,
{
// 创建过滤器
let protocol_accounts = protocols
.iter()
.map(|p| p.get_program_id())
.flatten()
.map(|p| p.to_string())
.collect::<Vec<String>>();
let mut account_include = account_include.unwrap_or_default();
let account_exclude = account_exclude.unwrap_or_default();
account_include.extend(protocol_accounts.clone());
let transactions =
self.get_subscribe_request_filter(account_include, account_exclude, vec![]);
// 订阅事件
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
// 创建通道
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
// 创建回调函数
let callback = Box::new(callback);
// 启动处理流的任务
tokio::spawn(async move {
while let Some(message) = stream.next().await {
match message {
Ok(msg) => {
if let Err(e) =
Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await
{
error!("Error handling message: {:?}", e);
break;
}
}
Err(error) => {
error!("Stream error: {error:?}");
break;
}
}
}
});
// 处理交易
while let Some(transaction_pretty) = rx.next().await {
if let Err(e) = Self::process_event_transaction(
transaction_pretty,
&*callback,
bot_wallet,
protocols.clone(),
)
.await
{
error!("Error processing transaction: {:?}", e);
}
}
Ok(())
}
/// 处理事件交易
async fn process_event_transaction<F>(
transaction_pretty: TransactionPretty,
callback: &F,
bot_wallet: Option<Pubkey>,
protocols: Vec<Protocol>,
) -> AnyResult<()>
where
F: Fn(Box<dyn UnifiedEvent>) + Send + Sync,
{
let slot = transaction_pretty.slot;
let signature = transaction_pretty.signature.to_string();
for protocol in protocols {
let parser = EventParserFactory::create_parser(protocol);
let events = parser
.parse_transaction(
transaction_pretty.tx.clone(),
&signature,
Some(slot),
bot_wallet.clone(),
)
.await
.unwrap_or_else(|_e| vec![]);
for event in events {
callback(event);
}
}
Ok(())
}
}
+96
View File
@@ -0,0 +1,96 @@
use crate::{common::AnyResult, streaming::yellowstone_grpc::{TransactionPretty, YellowstoneGrpc}};
use solana_program::pubkey;
use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction};
use futures::{channel::mpsc, StreamExt};
use log::error;
use solana_transaction_status::EncodedTransactionWithStatusMeta;
const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111");
const CHANNEL_SIZE: usize = 1000;
#[derive(Debug)]
pub enum SystemEvent {
NewTransfer(TransferInfo),
Error(String),
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TransferInfo {
pub slot: u64,
pub signature: String,
pub tx: Option<VersionedTransaction>,
}
impl YellowstoneGrpc {
pub async fn subscribe_system<F>(
&self,
callback: F,
account_include: Option<Vec<String>>,
account_exclude: Option<Vec<String>>,
) -> AnyResult<()>
where
F: Fn(SystemEvent) + Send + Sync + 'static,
{
let addrs = vec![SYSTEM_PROGRAM_ID.to_string()];
let account_include = account_include.unwrap_or_default();
let account_exclude = account_exclude.unwrap_or_default();
let transactions =
self.get_subscribe_request_filter(account_include, account_exclude, addrs);
let (mut subscribe_tx, mut stream) = self.subscribe_with_request(transactions).await?;
let (mut tx, mut rx) = mpsc::channel::<TransactionPretty>(CHANNEL_SIZE);
let callback = Box::new(callback);
tokio::spawn(async move {
while let Some(message) = stream.next().await {
match message {
Ok(msg) => {
if let Err(e) =
Self::handle_stream_message(msg, &mut tx, &mut subscribe_tx).await
{
error!("Error handling message: {:?}", e);
break;
}
}
Err(error) => {
error!("Stream error: {error:?}");
break;
}
}
}
});
while let Some(transaction_pretty) = rx.next().await {
if let Err(e) = Self::process_system_transaction(transaction_pretty, &*callback).await {
error!("Error processing transaction: {:?}", e);
}
}
Ok(())
}
async fn process_system_transaction<F>(
transaction_pretty: TransactionPretty,
callback: &F,
) -> AnyResult<()>
where
F: Fn(SystemEvent) + Send + Sync,
{
let trade_raw: EncodedTransactionWithStatusMeta = transaction_pretty.tx;
let meta = trade_raw
.meta
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?;
if meta.err.is_some() {
return Ok(());
}
callback(SystemEvent::NewTransfer(TransferInfo {
slot: transaction_pretty.slot,
signature: transaction_pretty.signature.to_string(),
tx: trade_raw.transaction.decode(),
}));
Ok(())
}
}