From 9e34a01874d95ea3632872bf0d22d5e475e80ea0 Mon Sep 17 00:00:00 2001 From: ysq Date: Sat, 19 Jul 2025 23:46:42 +0800 Subject: [PATCH] feat: refactor to multi-protocol event streaming system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.toml | 47 +- README.md | 291 ++++++++--- README_CN.md | 253 +++++++++ src/common/logs_data.rs | 97 ---- src/common/logs_events.rs | 94 ---- src/common/logs_filters.rs | 152 ------ src/common/logs_parser.rs | 236 --------- src/common/logs_subscribe.rs | 105 ---- src/common/mod.rs | 8 +- src/common/types.rs | 2 + src/error/mod.rs | 197 ------- src/grpc/mod.rs | 3 - src/grpc/yellow_stone.rs | 352 ------------- src/lib.rs | 6 +- src/main.rs | 124 ++++- src/protos/mod.rs | 2 + src/protos/shared.rs | 18 + src/protos/shredstream.rs | 279 ++++++++++ src/streaming/event_parser/common/mod.rs | 54 ++ src/streaming/event_parser/common/types.rs | 173 +++++++ src/streaming/event_parser/common/utils.rs | 111 ++++ src/streaming/event_parser/core/mod.rs | 2 + src/streaming/event_parser/core/traits.rs | 485 ++++++++++++++++++ src/streaming/event_parser/factory.rs | 98 ++++ src/streaming/event_parser/mod.rs | 41 ++ .../event_parser/protocols/bonk/events.rs | 126 +++++ .../event_parser/protocols/bonk/mod.rs | 7 + .../event_parser/protocols/bonk/parser.rs | 445 ++++++++++++++++ .../event_parser/protocols/bonk/types.rs | 69 +++ src/streaming/event_parser/protocols/mod.rs | 11 + .../event_parser/protocols/pumpfun/events.rs | 113 ++++ .../event_parser/protocols/pumpfun/mod.rs | 5 + .../event_parser/protocols/pumpfun/parser.rs | 250 +++++++++ .../event_parser/protocols/pumpswap/events.rs | 322 ++++++++++++ .../event_parser/protocols/pumpswap/mod.rs | 5 + .../event_parser/protocols/pumpswap/parser.rs | 386 ++++++++++++++ .../protocols/raydium_clmm/events.rs | 59 +++ .../protocols/raydium_clmm/mod.rs | 5 + .../protocols/raydium_clmm/parser.rs | 170 ++++++ .../protocols/raydium_cpmm/events.rs | 35 ++ .../protocols/raydium_cpmm/mod.rs | 5 + .../protocols/raydium_cpmm/parser.rs | 159 ++++++ src/streaming/mod.rs | 8 + src/streaming/shred_stream.rs | 120 +++++ src/streaming/yellowstone_grpc.rs | 266 ++++++++++ src/streaming/yellowstone_sub_system.rs | 96 ++++ 46 files changed, 4511 insertions(+), 1381 deletions(-) create mode 100644 README_CN.md delete mode 100755 src/common/logs_data.rs delete mode 100755 src/common/logs_events.rs delete mode 100755 src/common/logs_filters.rs delete mode 100755 src/common/logs_parser.rs delete mode 100755 src/common/logs_subscribe.rs mode change 100755 => 100644 src/common/mod.rs create mode 100644 src/common/types.rs delete mode 100755 src/error/mod.rs delete mode 100755 src/grpc/mod.rs delete mode 100755 src/grpc/yellow_stone.rs create mode 100755 src/protos/mod.rs create mode 100755 src/protos/shared.rs create mode 100755 src/protos/shredstream.rs create mode 100755 src/streaming/event_parser/common/mod.rs create mode 100755 src/streaming/event_parser/common/types.rs create mode 100755 src/streaming/event_parser/common/utils.rs create mode 100755 src/streaming/event_parser/core/mod.rs create mode 100755 src/streaming/event_parser/core/traits.rs create mode 100755 src/streaming/event_parser/factory.rs create mode 100755 src/streaming/event_parser/mod.rs create mode 100755 src/streaming/event_parser/protocols/bonk/events.rs create mode 100755 src/streaming/event_parser/protocols/bonk/mod.rs create mode 100755 src/streaming/event_parser/protocols/bonk/parser.rs create mode 100755 src/streaming/event_parser/protocols/bonk/types.rs create mode 100755 src/streaming/event_parser/protocols/mod.rs create mode 100755 src/streaming/event_parser/protocols/pumpfun/events.rs create mode 100755 src/streaming/event_parser/protocols/pumpfun/mod.rs create mode 100755 src/streaming/event_parser/protocols/pumpfun/parser.rs create mode 100755 src/streaming/event_parser/protocols/pumpswap/events.rs create mode 100755 src/streaming/event_parser/protocols/pumpswap/mod.rs create mode 100755 src/streaming/event_parser/protocols/pumpswap/parser.rs create mode 100755 src/streaming/event_parser/protocols/raydium_clmm/events.rs create mode 100755 src/streaming/event_parser/protocols/raydium_clmm/mod.rs create mode 100755 src/streaming/event_parser/protocols/raydium_clmm/parser.rs create mode 100755 src/streaming/event_parser/protocols/raydium_cpmm/events.rs create mode 100755 src/streaming/event_parser/protocols/raydium_cpmm/mod.rs create mode 100755 src/streaming/event_parser/protocols/raydium_cpmm/parser.rs create mode 100755 src/streaming/mod.rs create mode 100755 src/streaming/shred_stream.rs create mode 100755 src/streaming/yellowstone_grpc.rs create mode 100755 src/streaming/yellowstone_sub_system.rs diff --git a/Cargo.toml b/Cargo.toml index 1f57d60..31da065 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 ", "sgxiang ", "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" \ No newline at end of file diff --git a/README.md b/README.md index 5cd7690..42f2001 100755 --- a/README.md +++ b/README.md @@ -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> { - 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> { + test_grpc().await?; + test_shreds().await?; + Ok(()) } -``` -### 2. Subscribing to Transaction Data +async fn test_grpc() -> Result<(), Box> { + 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> { - 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> { + 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) { + |event: Box| { + 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) diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..df373fa --- /dev/null +++ b/README_CN.md @@ -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> { + test_grpc().await?; + test_shreds().await?; + Ok(()) +} + +async fn test_grpc() -> Result<(), Box> { + 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> { + 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) { + |event: Box| { + 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) \ No newline at end of file diff --git a/src/common/logs_data.rs b/src/common/logs_data.rs deleted file mode 100755 index c8f5c5b..0000000 --- a/src/common/logs_data.rs +++ /dev/null @@ -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, -} - -pub trait EventTrait: Sized + std::fmt::Debug { - fn from_bytes(bytes: &[u8]) -> ClientResult; -} - -impl EventTrait for CreateTokenInfo { - fn from_bytes(bytes: &[u8]) -> ClientResult { - CreateTokenInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} - -impl EventTrait for TradeInfo { - fn from_bytes(bytes: &[u8]) -> ClientResult { - TradeInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} - -impl EventTrait for CompleteInfo { - fn from_bytes(bytes: &[u8]) -> ClientResult { - CompleteInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} - -impl EventTrait for SwapBaseInLog { - fn from_bytes(bytes: &[u8]) -> ClientResult { - SwapBaseInLog::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string())) - } -} \ No newline at end of file diff --git a/src/common/logs_events.rs b/src/common/logs_events.rs deleted file mode 100755 index 5477b66..0000000 --- a/src/common/logs_events.rs +++ /dev/null @@ -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) -> (Option, Option) { - let mut create_info: Option = None; - let mut trade_info: Option = 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(logs: &Vec) -> Option { - let mut event: Option = 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[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 - } -} \ No newline at end of file diff --git a/src/common/logs_filters.rs b/src/common/logs_filters.rs deleted file mode 100755 index d0b6c96..0000000 --- a/src/common/logs_filters.rs +++ /dev/null @@ -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) -> ClientResult> { - 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 = 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) -> ClientResult> { - 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) - } -} \ No newline at end of file diff --git a/src/common/logs_parser.rs b/src/common/logs_parser.rs deleted file mode 100755 index 7832f74..0000000 --- a/src/common/logs_parser.rs +++ /dev/null @@ -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( - signature: &str, - logs: Vec, - callback: F, - payer: Option, -) -> 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 { - // 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 { - 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 { - 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 { - 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, - }) -} \ No newline at end of file diff --git a/src/common/logs_subscribe.rs b/src/common/logs_subscribe.rs deleted file mode 100755 index 2b0d97a..0000000 --- a/src/common/logs_subscribe.rs +++ /dev/null @@ -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, -} - -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( - ws_url: &str, - commitment: CommitmentConfig, - callback: F, - bot_wallet: Option, -) -> Result> -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; -} diff --git a/src/common/mod.rs b/src/common/mod.rs old mode 100755 new mode 100644 index 7b13f32..258fad5 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -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::*; diff --git a/src/common/types.rs b/src/common/types.rs new file mode 100644 index 0000000..8bb08c7 --- /dev/null +++ b/src/common/types.rs @@ -0,0 +1,2 @@ +pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient; +pub type AnyResult = anyhow::Result; diff --git a/src/error/mod.rs b/src/error/mod.rs deleted file mode 100755 index 24f4b57..0000000 --- a/src/error/mod.rs +++ /dev/null @@ -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 From for AppError -// where -// E: Into, -// { -// 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), - /// 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 for ClientError { - fn from(error: SolanaClientError) -> Self { - ClientError::Solana( - "Solana client error".to_string(), - error.to_string(), - ) - } -} - -impl From for ClientError { - fn from(error: PubsubClientError) -> Self { - ClientError::Solana( - "PubSub client error".to_string(), - error.to_string(), - ) - } -} - -impl From for ClientError { - fn from(error: ParsePubkeyError) -> Self { - ClientError::Pubkey( - "Pubkey error".to_string(), - error.to_string(), - ) - } -} - -impl From for ClientError { - fn from(err: Error) -> Self { - ClientError::Parse( - "JSON serialization error".to_string(), - err.to_string() - ) - } -} - -pub type ClientResult = Result; diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs deleted file mode 100755 index 228048f..0000000 --- a/src/grpc/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod yellow_stone; - -pub use yellow_stone::YellowstoneGrpc; \ No newline at end of file diff --git a/src/grpc/yellow_stone.rs b/src/grpc/yellow_stone.rs deleted file mode 100755 index 4423092..0000000 --- a/src/grpc/yellow_stone.rs +++ /dev/null @@ -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 = anyhow::Result; - -type TransactionsFilterMap = HashMap; - -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 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, -} - -impl YellowstoneGrpc { - pub fn new(endpoint: String, x_token: Option) -> AnyResult { - 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> - { - 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, - impl Stream>, - )> { - 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, - account_exclude: Vec, - account_required: Vec, - ) -> 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, - subscribe_tx: &mut (impl Sink + 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(&self, callback: F, bot_wallet: Option) -> 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::(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(&self, callback: F, bot_wallet: Option, account_include: Option>, account_exclude: Option>) -> 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::(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(transaction_pretty: TransactionPretty, callback: &F, bot_wallet: Option) -> 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 = 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(&self, callback: F, account_include: Option>, account_exclude: Option>) -> 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::(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(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(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index db18ab9..8416f5b 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ -pub mod common; -pub mod grpc; -pub mod error; +pub mod streaming; +pub mod protos; +pub mod common; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d571856..101a7fc 100755 --- a/src/main.rs +++ b/src/main.rs @@ -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> { + test_grpc().await?; + test_shreds().await?; + Ok(()) +} + +async fn test_grpc() -> Result<(), Box> { + 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> { + 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) { + |event: Box| { + 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(()) -} \ No newline at end of file + }); + } +} diff --git a/src/protos/mod.rs b/src/protos/mod.rs new file mode 100755 index 0000000..f0dd746 --- /dev/null +++ b/src/protos/mod.rs @@ -0,0 +1,2 @@ +pub mod shared; +pub mod shredstream; diff --git a/src/protos/shared.rs b/src/protos/shared.rs new file mode 100755 index 0000000..b2b0680 --- /dev/null +++ b/src/protos/shared.rs @@ -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, +} diff --git a/src/protos/shredstream.rs b/src/protos/shredstream.rs new file mode 100755 index 0000000..95a36f5 --- /dev/null +++ b/src/protos/shredstream.rs @@ -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, + /// regions for shredstream proxy to receive shreds from + /// list of valid regions: + #[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: + #[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: + #[prost(bytes = "vec", tag = "2")] + pub entries: ::prost::alloc::vec::Vec, +} +/// 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 { + inner: tonic::client::Grpc, + } + impl ShredstreamClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ShredstreamClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + 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( + inner: T, + interceptor: F, + ) -> ShredstreamClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + 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, + ) -> std::result::Result< + tonic::Response, + 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 { + inner: tonic::client::Grpc, + } + impl ShredstreamProxyClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ShredstreamProxyClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + 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( + inner: T, + interceptor: F, + ) -> ShredstreamProxyClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + 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, + ) -> std::result::Result< + tonic::Response>, + 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 + } + } +} diff --git a/src/streaming/event_parser/common/mod.rs b/src/streaming/event_parser/common/mod.rs new file mode 100755 index 0000000..a070106 --- /dev/null +++ b/src/streaming/event_parser/common/mod.rs @@ -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 { + Box::new(self.clone()) + } + + fn merge(&mut self, other: Box) { + if let Some(e) = other.as_any().downcast_ref::<$struct_name>() { + $( + self.$field = e.$field.clone(); + )* + } + } + } + }; +} + +pub use types::*; +pub use utils::*; diff --git a/src/streaming/event_parser/common/types.rs b/src/streaming/event_parser/common/types.rs new file mode 100755 index 0000000..0ad95f9 --- /dev/null +++ b/src/streaming/event_parser/common/types.rs @@ -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 { + pub success: bool, + pub data: Option, + pub error: Option, +} + +impl ParseResult { + 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, +} + +impl ProtocolInfo { + pub fn new(name: String, program_ids: Vec) -> 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); + } +} diff --git a/src/streaming/event_parser/common/utils.rs b/src/streaming/event_parser/common/utils.rs new file mode 100755 index 0000000..5452cfc --- /dev/null +++ b/src/streaming/event_parser/common/utils.rs @@ -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, 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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..]) + } +} diff --git a/src/streaming/event_parser/core/mod.rs b/src/streaming/event_parser/core/mod.rs new file mode 100755 index 0000000..663ef85 --- /dev/null +++ b/src/streaming/event_parser/core/mod.rs @@ -0,0 +1,2 @@ +pub mod traits; +pub use traits::{EventParser, UnifiedEvent}; diff --git a/src/streaming/event_parser/core/traits.rs b/src/streaming/event_parser/core/traits.rs new file mode 100755 index 0000000..331fe43 --- /dev/null +++ b/src/streaming/event_parser/core/traits.rs @@ -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; + + /// 合并事件(可选实现) + fn merge(&mut self, _other: Box) { + // 默认实现:不进行任何合并操作 + } +} + +/// 事件解析器trait - 定义了事件解析的核心方法 +#[async_trait::async_trait] +pub trait EventParser: Send + Sync { + /// 从内联指令中解析事件数据 + fn parse_events_from_inner_instruction( + &self, + instruction: &UiCompiledInstruction, + signature: &str, + slot: u64, + ) -> Vec>; + + /// 从指令中解析事件数据 + fn parse_events_from_instruction( + &self, + instruction: &CompiledInstruction, + accounts: &[Pubkey], + signature: &str, + slot: u64, + ) -> Vec>; + + /// 从VersionedTransaction中解析指令事件的通用方法 + async fn parse_instruction_events_from_versioned_transaction( + &self, + versioned_tx: &VersionedTransaction, + signature: &str, + slot: Option, + accounts: &[Pubkey], + ) -> Result>> { + let mut instruction_events = Vec::new(); + // 获取交易的指令和账户 + let compiled_instructions = versioned_tx.message.instructions(); + let mut accounts: Vec = 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, + bot_wallet: Option, + ) -> Result>> { + let accounts: Vec = 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, + bot_wallet: Option, + ) -> Result>> { + let transaction = tx.transaction; + // 检查交易元数据 + let meta = tx + .meta + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing transaction metadata"))?; + + let mut address_table_lookups: Vec = 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 = 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>, + bot_wallet: Option, + ) -> Vec> { + 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::() { + dev_address = Some(token_info.user); + } else if let Some(trade_info) = event.as_any_mut().downcast_mut::() + { + 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::() { + bonk_dev_address = Some(pool_info.creator); + } else if let Some(trade_info) = event.as_any_mut().downcast_mut::() { + 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, + ) -> Result>> { + 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, + ) -> Result>> { + 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; +} + +// 为Box实现Clone +impl Clone for Box { + 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>; + +/// 指令事件解析器 +pub type InstructionEventParser = + fn(data: &[u8], accounts: &[Pubkey], metadata: EventMetadata) -> Option>; + +/// 通用事件解析器基类 +pub struct GenericEventParser { + program_id: Pubkey, + protocol_type: ProtocolType, + inner_instruction_configs: HashMap<&'static str, Vec>, + instruction_configs: HashMap, Vec>, +} + +impl GenericEventParser { + /// 创建新的通用事件解析器 + pub fn new( + program_id: Pubkey, + protocol_type: ProtocolType, + configs: Vec, + ) -> 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> { + 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> { + 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> { + 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> { + 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 = 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 { + vec![self.program_id] + } +} diff --git a/src/streaming/event_parser/factory.rs b/src/streaming/event_parser/factory.rs new file mode 100755 index 0000000..b7830d5 --- /dev/null +++ b/src/streaming/event_parser/factory.rs @@ -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 { + 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 { + 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 { + 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> { + Self::supported_protocols() + .into_iter() + .map(Self::create_parser) + .collect() + } + + /// 获取所有支持的协议 + pub fn supported_protocols() -> Vec { + vec![Protocol::PumpSwap] + } + + /// 检查协议是否支持 + pub fn is_supported(protocol: &Protocol) -> bool { + Self::supported_protocols().contains(protocol) + } +} diff --git a/src/streaming/event_parser/mod.rs b/src/streaming/event_parser/mod.rs new file mode 100755 index 0000000..99d71eb --- /dev/null +++ b/src/streaming/event_parser/mod.rs @@ -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; diff --git a/src/streaming/event_parser/protocols/bonk/events.rs b/src/streaming/event_parser/protocols/bonk/events.rs new file mode 100755 index 0000000..93cea47 --- /dev/null +++ b/src/streaming/event_parser/protocols/bonk/events.rs @@ -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]; +} diff --git a/src/streaming/event_parser/protocols/bonk/mod.rs b/src/streaming/event_parser/protocols/bonk/mod.rs new file mode 100755 index 0000000..8c7a629 --- /dev/null +++ b/src/streaming/event_parser/protocols/bonk/mod.rs @@ -0,0 +1,7 @@ +pub mod events; +pub mod parser; +pub mod types; + +pub use events::*; +pub use parser::BonkEventParser; +pub use types::*; \ No newline at end of file diff --git a/src/streaming/event_parser/protocols/bonk/parser.rs b/src/streaming/event_parser/protocols/bonk/parser.rs new file mode 100755 index 0000000..4e20a6f --- /dev/null +++ b/src/streaming/event_parser/protocols/bonk/parser.rs @@ -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> { + if let Ok(event) = borsh::from_slice::(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> { + if let Ok(event) = borsh::from_slice::(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> { + 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> { + 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> { + 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> { + 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> { + 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 { + // 读取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 { + // 读取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 { + 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> { + 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> { + 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 { + self.inner.supported_program_ids() + } +} diff --git a/src/streaming/event_parser/protocols/bonk/types.rs b/src/streaming/event_parser/protocols/bonk/types.rs new file mode 100755 index 0000000..5751f8c --- /dev/null +++ b/src/streaming/event_parser/protocols/bonk/types.rs @@ -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(), + } + } +} diff --git a/src/streaming/event_parser/protocols/mod.rs b/src/streaming/event_parser/protocols/mod.rs new file mode 100755 index 0000000..e0d8aa5 --- /dev/null +++ b/src/streaming/event_parser/protocols/mod.rs @@ -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; \ No newline at end of file diff --git a/src/streaming/event_parser/protocols/pumpfun/events.rs b/src/streaming/event_parser/protocols/pumpfun/events.rs new file mode 100755 index 0000000..99c04ac --- /dev/null +++ b/src/streaming/event_parser/protocols/pumpfun/events.rs @@ -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]; +} diff --git a/src/streaming/event_parser/protocols/pumpfun/mod.rs b/src/streaming/event_parser/protocols/pumpfun/mod.rs new file mode 100755 index 0000000..eee7acf --- /dev/null +++ b/src/streaming/event_parser/protocols/pumpfun/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::PumpFunEventParser; \ No newline at end of file diff --git a/src/streaming/event_parser/protocols/pumpfun/parser.rs b/src/streaming/event_parser/protocols/pumpfun/parser.rs new file mode 100755 index 0000000..4a19b7e --- /dev/null +++ b/src/streaming/event_parser/protocols/pumpfun/parser.rs @@ -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> { + if let Ok(event) = borsh::from_slice::(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> { + if let Ok(event) = borsh::from_slice::(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> { + 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> { + 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> { + 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> { + 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> { + 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 { + self.inner.supported_program_ids() + } +} diff --git a/src/streaming/event_parser/protocols/pumpswap/events.rs b/src/streaming/event_parser/protocols/pumpswap/events.rs new file mode 100755 index 0000000..f1447df --- /dev/null +++ b/src/streaming/event_parser/protocols/pumpswap/events.rs @@ -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]; +} diff --git a/src/streaming/event_parser/protocols/pumpswap/mod.rs b/src/streaming/event_parser/protocols/pumpswap/mod.rs new file mode 100755 index 0000000..cfa440f --- /dev/null +++ b/src/streaming/event_parser/protocols/pumpswap/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::PumpSwapEventParser; \ No newline at end of file diff --git a/src/streaming/event_parser/protocols/pumpswap/parser.rs b/src/streaming/event_parser/protocols/pumpswap/parser.rs new file mode 100755 index 0000000..52e56a2 --- /dev/null +++ b/src/streaming/event_parser/protocols/pumpswap/parser.rs @@ -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> { + if let Ok(event) = borsh::from_slice::(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> { + if let Ok(event) = borsh::from_slice::(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> { + if let Ok(event) = borsh::from_slice::(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> { + if let Ok(event) = borsh::from_slice::(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> { + if let Ok(event) = borsh::from_slice::(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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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> { + 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 { + self.inner.supported_program_ids() + } +} diff --git a/src/streaming/event_parser/protocols/raydium_clmm/events.rs b/src/streaming/event_parser/protocols/raydium_clmm/events.rs new file mode 100755 index 0000000..cd0c0b1 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_clmm/events.rs @@ -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, +} + +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, +} +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]; +} diff --git a/src/streaming/event_parser/protocols/raydium_clmm/mod.rs b/src/streaming/event_parser/protocols/raydium_clmm/mod.rs new file mode 100755 index 0000000..89b867a --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_clmm/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::RaydiumClmmEventParser; diff --git a/src/streaming/event_parser/protocols/raydium_clmm/parser.rs b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs new file mode 100755 index 0000000..09bc0a6 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_clmm/parser.rs @@ -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> { + None + } + + /// 解析交易指令事件 + fn parse_swap_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + 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> { + 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> { + 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> { + 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 { + self.inner.supported_program_ids() + } +} diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/events.rs b/src/streaming/event_parser/protocols/raydium_cpmm/events.rs new file mode 100755 index 0000000..2c3f770 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_cpmm/events.rs @@ -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]; +} diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/mod.rs b/src/streaming/event_parser/protocols/raydium_cpmm/mod.rs new file mode 100755 index 0000000..e5896a6 --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_cpmm/mod.rs @@ -0,0 +1,5 @@ +pub mod events; +pub mod parser; + +pub use events::*; +pub use parser::RaydiumCpmmEventParser; diff --git a/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs b/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs new file mode 100755 index 0000000..8b6984a --- /dev/null +++ b/src/streaming/event_parser/protocols/raydium_cpmm/parser.rs @@ -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> { + None + } + + /// 解析买入指令事件 + fn parse_swap_base_input_instruction( + data: &[u8], + accounts: &[Pubkey], + metadata: EventMetadata, + ) -> Option> { + 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> { + 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> { + 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> { + 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 { + self.inner.supported_program_ids() + } +} diff --git a/src/streaming/mod.rs b/src/streaming/mod.rs new file mode 100755 index 0000000..019ab5f --- /dev/null +++ b/src/streaming/mod.rs @@ -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; \ No newline at end of file diff --git a/src/streaming/shred_stream.rs b/src/streaming/shred_stream.rs new file mode 100755 index 0000000..471cfdb --- /dev/null +++ b/src/streaming/shred_stream.rs @@ -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>, +} + +struct TransactionWithSlot { + transaction: VersionedTransaction, + slot: u64, +} + +impl ShredStreamGrpc { + pub async fn new(endpoint: String) -> AnyResult { + let shredstream_client = ShredstreamProxyClient::connect(endpoint.clone()).await?; + Ok(Self { + shredstream_client: Arc::new(shredstream_client), + }) + } + + pub async fn shredstream_subscribe( + &self, + protocols: Vec, + bot_wallet: Option, + callback: F, + ) -> AnyResult<()> + where + F: Fn(Box) + 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::(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::>(&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( + transaction_with_slot: TransactionWithSlot, + protocols: Vec, + bot_wallet: Option, + callback: &F, + ) -> AnyResult<()> + where + F: Fn(Box) + 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(()) + } +} diff --git a/src/streaming/yellowstone_grpc.rs b/src/streaming/yellowstone_grpc.rs new file mode 100755 index 0000000..909aebc --- /dev/null +++ b/src/streaming/yellowstone_grpc.rs @@ -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; + +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 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, +} + +impl YellowstoneGrpc { + pub fn new(endpoint: String, x_token: Option) -> AnyResult { + 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> { + 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, + impl Stream>, + )> { + 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, + account_exclude: Vec, + account_required: Vec, + ) -> 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, + subscribe_tx: &mut (impl Sink + 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( + &self, + protocols: Vec, + bot_wallet: Option, + account_include: Option>, + account_exclude: Option>, + callback: F, + ) -> AnyResult<()> + where + F: Fn(Box) + Send + Sync + 'static, + { + // 创建过滤器 + let protocol_accounts = protocols + .iter() + .map(|p| p.get_program_id()) + .flatten() + .map(|p| p.to_string()) + .collect::>(); + 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::(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( + transaction_pretty: TransactionPretty, + callback: &F, + bot_wallet: Option, + protocols: Vec, + ) -> AnyResult<()> + where + F: Fn(Box) + 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(()) + } +} diff --git a/src/streaming/yellowstone_sub_system.rs b/src/streaming/yellowstone_sub_system.rs new file mode 100755 index 0000000..f673e25 --- /dev/null +++ b/src/streaming/yellowstone_sub_system.rs @@ -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, +} + +impl YellowstoneGrpc { + pub async fn subscribe_system( + &self, + callback: F, + account_include: Option>, + account_exclude: Option>, + ) -> 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::(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( + 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(()) + } +}