From 119f4f18cbdad807b58513f931ab8fd166bf70f5 Mon Sep 17 00:00:00 2001 From: William Date: Tue, 31 Dec 2024 17:46:05 +0800 Subject: [PATCH] add logs process --- README.md | 4 +- crates/pumpfun/Cargo.toml | 4 +- crates/pumpfun/src/error/mod.rs | 64 ++++++++ crates/pumpfun/src/instruction/logs_data.rs | 28 ++++ crates/pumpfun/src/instruction/logs_filter.rs | 88 ++++++++++ crates/pumpfun/src/instruction/logs_paser.rs | 152 ++++++++++++++++++ crates/pumpfun/src/instruction/mod.rs | 8 + crates/pumpfun/src/lib.rs | 2 + 8 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 crates/pumpfun/src/instruction/logs_data.rs create mode 100644 crates/pumpfun/src/instruction/logs_filter.rs create mode 100644 crates/pumpfun/src/instruction/logs_paser.rs diff --git a/README.md b/README.md index 0115f4b..c8a4ce2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ A comprehensive Rust SDK for seamless interaction with the PumpFun Solana progra # Explanation This repository is forked from [https://github.com/nhuxhr/pumpfun-rs](https://github.com/nhuxhr/pumpfun-rs). -Change `PumpFun<'a>` to `PumpFun`, and `payer: &'a Keypair` to `payer: Arc`. +1. Change `PumpFun<'a>` to `PumpFun`, and `payer: &'a Keypair` to `payer: Arc`. +2. Add `logs_filter` and `logs_paser` to parse the logs of the PumpFun program. +3. Add `logs_data` to define the data structure of the logs. ## Table of Contents diff --git a/crates/pumpfun/Cargo.toml b/crates/pumpfun/Cargo.toml index 93eb18b..1ae7024 100644 --- a/crates/pumpfun/Cargo.toml +++ b/crates/pumpfun/Cargo.toml @@ -22,4 +22,6 @@ pumpfun-cpi = { path = "../pumpfun-cpi", version = "1.1.0" } serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.132" solana-sdk = "1.18.26" -tokio = "1.41.1" +tokio = "1.42.0" +base64 = "0.22.1" +bs58 = "0.5.1" diff --git a/crates/pumpfun/src/error/mod.rs b/crates/pumpfun/src/error/mod.rs index ce65e1d..09d1f12 100644 --- a/crates/pumpfun/src/error/mod.rs +++ b/crates/pumpfun/src/error/mod.rs @@ -19,6 +19,11 @@ //! - `RateLimitExceeded`: Rate limit exceeded. use anchor_client::solana_client; +use serde_json::Error; +use anchor_client::solana_client::{ + client_error::ClientError as SolanaClientError, + pubsub_client::PubsubClientError +}; #[derive(Debug)] pub enum ClientError { @@ -42,6 +47,26 @@ pub enum ClientError { SimulationError(String), /// Rate limit exceeded RateLimitExceeded, + + Solana(String, String), + + Parse(String, String), + + Join(String), + + Subscribe(String, String), + + Send(String, String), + + Other(String), + + Timeout(String, String), + + Duplicate(String), + + InvalidEventType, + + ChannelClosed, } impl std::fmt::Display for ClientError { @@ -57,6 +82,16 @@ impl std::fmt::Display for ClientError { Self::InsufficientFunds => write!(f, "Insufficient funds for transaction"), Self::SimulationError(msg) => write!(f, "Transaction simulation failed: {}", msg), Self::RateLimitExceeded => write!(f, "Rate limit exceeded"), + Self::Solana(msg, details) => write!(f, "Solana error: {}, details: {}", msg, details), + Self::Parse(msg, details) => write!(f, "Parse error: {}, details: {}", msg, details), + Self::Join(msg) => write!(f, "Task join error: {}", msg), + 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::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"), } } } @@ -72,3 +107,32 @@ impl std::error::Error for ClientError { } } } + +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(err: Error) -> Self { + ClientError::Parse( + "JSON serialization error".to_string(), + err.to_string() + ) + } +} + +pub type ClientResult = Result; diff --git a/crates/pumpfun/src/instruction/logs_data.rs b/crates/pumpfun/src/instruction/logs_data.rs new file mode 100644 index 0000000..c52d2b1 --- /dev/null +++ b/crates/pumpfun/src/instruction/logs_data.rs @@ -0,0 +1,28 @@ +use serde::{Serialize, Deserialize}; +// 添加新的数据结构 +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CreateTokenInfo { + pub signature: String, + pub name: String, + pub symbol: String, + pub uri: String, + pub mint: String, + pub bonding_curve: String, + pub user: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct TradeInfo { + pub signature: String, + pub mint: String, + pub bonding_curve: String, + pub sol_amount: u64, + pub token_amount: u64, + pub is_buy: bool, + pub user: String, + pub timestamp: i64, + pub virtual_sol_reserves: u64, + pub virtual_token_reserves: u64, + pub real_sol_reserves: u64, + pub real_token_reserves: u64, +} \ No newline at end of file diff --git a/crates/pumpfun/src/instruction/logs_filter.rs b/crates/pumpfun/src/instruction/logs_filter.rs new file mode 100644 index 0000000..e5a19a5 --- /dev/null +++ b/crates/pumpfun/src/instruction/logs_filter.rs @@ -0,0 +1,88 @@ +use crate::instruction::logs_data::{CreateTokenInfo, TradeInfo}; +use crate::instruction::logs_paser::{parse_create_token_data, parse_trade_data}; +use crate::error::ClientResult; + +pub struct LogFilter; + +#[derive(Debug)] +pub enum DexInstruction { + CreateToken(CreateTokenInfo), + Trade(TradeInfo), + Other, +} + +impl LogFilter { + const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"; + + /// 解析交易日志并返回具体的指令类型和数据 + pub fn parse_instruction(logs: &[String]) -> 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 { + // println!("log: {:?}", log); + // 检查程序调用 + if log.contains(&format!("Program {} invoke", Self::PROGRAM_ID)) { + invoke_depth += 1; + if invoke_depth == 1 { // 只在顶层调用时重置状态 + current_instruction = None; + program_data.clear(); + last_data_len = 0; + } + continue; + } + + // 如果不在我们的程序中,跳过 + if invoke_depth == 0 { + continue; + } + + // 识别指令类型(只在顶层调用时) + 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; + } + + // 收集 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(); + } + } + + // 检查程序是否结束 + if log.contains(&format!("Program {} success", Self::PROGRAM_ID)) { + invoke_depth -= 1; + if invoke_depth == 0 { // 只在顶层程序结束时处理数据 + 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) { + instructions.push(DexInstruction::Trade(trade_info)); + } + }, + _ => {} + } + } + } + } + } + } + + Ok(instructions) + } +} \ No newline at end of file diff --git a/crates/pumpfun/src/instruction/logs_paser.rs b/crates/pumpfun/src/instruction/logs_paser.rs new file mode 100644 index 0000000..f26461f --- /dev/null +++ b/crates/pumpfun/src/instruction/logs_paser.rs @@ -0,0 +1,152 @@ +use crate::error::{ClientError, ClientResult}; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use serde::{Serialize, Deserialize}; + +use crate::instruction::logs_data::*; +// 添加解析函数 +pub fn parse_create_token_data(data: &str) -> ClientResult { + // 首先进行 base64 解码 + let decoded = BASE64.decode(data) + .map_err(|e| ClientError::Other(format!("Failed to decode base64: {}", e)))?; + + // 跳过前缀字节(如果有的话) + let mut cursor = if decoded.len() > 8 { 8 } else { 0 }; + + // 读取名称长度和名称 + 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; + + // 读取符号长度和符号 + 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; + + // 读取 URI 长度和 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; + + // 确保还有足够的数据来读取公钥 + if cursor + 32 * 3 > decoded.len() { + return Err(ClientError::Other("Data too short for public keys".to_string())); + } + + // 解析 Mint Public Key + let mint = bs58::encode(&decoded[cursor..cursor+32]).into_string(); + cursor += 32; + + // 解析 Bonding Curve Public Key + let bonding_curve = bs58::encode(&decoded[cursor..cursor+32]).into_string(); + cursor += 32; + + // 解析 User Public Key + let user = bs58::encode(&decoded[cursor..cursor+32]).into_string(); + + Ok(CreateTokenInfo { + signature: String::new(), + name, + symbol, + uri, + mint, + bonding_curve, + user, + }) +} + +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; // 跳过前缀 + + // 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 { + signature: String::new(), + mint, + bonding_curve: String::new(), + sol_amount, + token_amount, + is_buy, + user, + timestamp, + virtual_sol_reserves, + virtual_token_reserves, + real_sol_reserves, + real_token_reserves, + }) +} \ No newline at end of file diff --git a/crates/pumpfun/src/instruction/mod.rs b/crates/pumpfun/src/instruction/mod.rs index 596188c..0e1f695 100644 --- a/crates/pumpfun/src/instruction/mod.rs +++ b/crates/pumpfun/src/instruction/mod.rs @@ -10,6 +10,14 @@ //! - `buy`: Instruction to buy tokens from a bonding curve by providing SOL. //! - `sell`: Instruction to sell tokens back to the bonding curve in exchange for SOL. +pub mod logs_data; +pub mod logs_paser; +pub mod logs_filter; + +pub use logs_data::*; +pub use logs_paser::*; +pub use logs_filter::*; + use crate::{constants, PumpFun}; use anchor_client::anchor_lang::InstructionData; use anchor_spl::associated_token::get_associated_token_address; diff --git a/crates/pumpfun/src/lib.rs b/crates/pumpfun/src/lib.rs index 42bf38b..a179cd6 100644 --- a/crates/pumpfun/src/lib.rs +++ b/crates/pumpfun/src/lib.rs @@ -6,6 +6,8 @@ pub mod error; pub mod instruction; pub mod utils; +use crate::error::ClientError::*; + use anchor_client::{ solana_client::rpc_client::RpcClient, solana_sdk::{