feat: refactor event parsing architecture and add Raydium Launchpad support
Major changes: - Refactor event parsing system: migrate scattered logs_* modules to unified event_parser architecture - Add unified UnifiedEvent trait and EventParser trait for standardized event parsing interface - Introduce GenericEventParser and EventParserFactory for plugin-style protocol extension - Add match_event! macro to simplify event type matching and handling - Add Raydium Launchpad (Bonk.fun) protocol support: - Complete buy/sell trading functionality - Pool state management and querying - Event parsing and subscription - Update trading system to support new protocol architecture - Refactor constants organization, rename raydium to raydium_launchpad - Update documentation and example code Technical improvements: - Unified event interface design for better code maintainability - Factory pattern implementation for dynamic protocol loading - Generic event parser to reduce code duplication - Improved error handling and type safety Breaking Changes: - Remove old logs_* modules, use new event_parser system - Protocol constants path change: raydium -> raydium_launchpad - Event subscription API updated to unified interface
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod raydium;
|
||||
pub mod address_lookup;
|
||||
pub mod nonce_cache;
|
||||
pub mod tip_cache;
|
||||
|
||||
@@ -1,104 +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),
|
||||
Tip(TipInfo),
|
||||
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 TipInfo {
|
||||
pub slot: u64,
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct CompleteInfo {
|
||||
pub user: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub bonding_curve: Pubkey,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize)]
|
||||
pub struct SwapBaseInLog {
|
||||
pub log_type: u8,
|
||||
// input
|
||||
pub amount_in: u64,
|
||||
pub minimum_out: u64,
|
||||
pub direction: u64,
|
||||
// user info
|
||||
pub user_source: u64,
|
||||
// pool info
|
||||
pub pool_coin: u64,
|
||||
pub pool_pc: u64,
|
||||
// calc result
|
||||
pub out_amount: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct TransferInfo {
|
||||
pub slot: u64,
|
||||
pub signature: String,
|
||||
pub tx: Option<VersionedTransaction>,
|
||||
}
|
||||
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||
}
|
||||
|
||||
impl EventTrait for CreateTokenInfo {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
CreateTokenInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for TradeInfo {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
TradeInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for CompleteInfo {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
CompleteInfo::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventTrait for SwapBaseInLog {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self> {
|
||||
SwapBaseInLog::try_from_slice(bytes).map_err(|e| ClientError::Other(e.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use regex::Regex;
|
||||
use crate::common::pumpfun::logs_data::{CreateTokenInfo, TradeInfo, EventTrait, TransferInfo, TipInfo};
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PumpfunEvent {
|
||||
NewToken(CreateTokenInfo),
|
||||
NewDevTrade(TradeInfo),
|
||||
NewUserTrade(TradeInfo),
|
||||
NewBotTrade(TradeInfo),
|
||||
// NewTip(TipInfo),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DexEvent {
|
||||
NewToken(CreateTokenInfo),
|
||||
NewUserTrade(TradeInfo),
|
||||
NewBotTrade(TradeInfo),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SystemEvent {
|
||||
NewTransfer(TransferInfo),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
// #[derive(Debug, Clone, Copy)]
|
||||
// pub struct PumpEvent {}
|
||||
|
||||
impl PumpfunEvent {
|
||||
pub fn parse_logs(logs: &Vec<String>) -> (Option<CreateTokenInfo>, Option<TradeInfo>) {
|
||||
let mut create_info: Option<CreateTokenInfo> = None;
|
||||
let mut trade_info: Option<TradeInfo> = None;
|
||||
|
||||
if !logs.is_empty() {
|
||||
let logs_iter = logs.iter().peekable();
|
||||
|
||||
for l in logs_iter.rev() {
|
||||
if let Some(log) = l.strip_prefix(PROGRAM_DATA) {
|
||||
let borsh_bytes = general_purpose::STANDARD.decode(log).unwrap();
|
||||
let slice: &[u8] = &borsh_bytes[8..];
|
||||
|
||||
if create_info.is_none() {
|
||||
if let Ok(e) = CreateTokenInfo::from_bytes(slice) {
|
||||
create_info = Some(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if trade_info.is_none() {
|
||||
if let Ok(e) = TradeInfo::from_bytes(slice) {
|
||||
trade_info = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(create_info, trade_info)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RaydiumEvent {}
|
||||
|
||||
impl RaydiumEvent {
|
||||
pub fn parse_logs<T: EventTrait + Clone>(logs: &Vec<String>) -> Option<T> {
|
||||
let mut event: Option<T> = None;
|
||||
|
||||
if !logs.is_empty() {
|
||||
let logs_iter = logs.iter().peekable();
|
||||
|
||||
for l in logs_iter.rev() {
|
||||
let re = Regex::new(r"ray_log: (?P<base64>[A-Za-z0-9+/=]+)").unwrap();
|
||||
|
||||
if let Some(caps) = re.captures(l) {
|
||||
if let Some(base64) = caps.name("base64") {
|
||||
let bytes = general_purpose::STANDARD.decode(base64.as_str()).unwrap();
|
||||
|
||||
if let Ok(e) = T::from_bytes(&bytes) {
|
||||
event = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
use crate::common::pumpfun::logs_data::DexInstruction;
|
||||
use crate::common::pumpfun::logs_parser::{parse_create_token_data, parse_trade_data, parse_instruction_create_token_data, parse_instruction_trade_data};
|
||||
use crate::error::ClientResult;
|
||||
pub struct LogFilter;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::str::FromStr;
|
||||
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
impl LogFilter {
|
||||
const PROGRAM_ID: &'static str = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
|
||||
|
||||
/// Parse transaction logs and return instruction type and data
|
||||
pub fn parse_compiled_instruction(
|
||||
versioned_tx: VersionedTransaction,
|
||||
bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
let program_id = Pubkey::from_str(Self::PROGRAM_ID).unwrap_or_default();
|
||||
let pump_index = accounts.iter().position(|key| key == &program_id);
|
||||
let mut instructions: Vec<DexInstruction> = Vec::new();
|
||||
if let Some(index) = pump_index {
|
||||
for instruction in compiled_instructions {
|
||||
if instruction.program_id_index as usize == index {
|
||||
let all_accounts_valid = instruction.accounts.iter()
|
||||
.all(|&acc_idx| (acc_idx as usize) < accounts.len());
|
||||
if !all_accounts_valid {
|
||||
continue;
|
||||
}
|
||||
match instruction.data.first() {
|
||||
// create
|
||||
Some(&24) => {
|
||||
if let Ok(token_info) = parse_instruction_create_token_data(instruction, accounts) {
|
||||
instructions.push(DexInstruction::CreateToken(token_info));
|
||||
};
|
||||
}
|
||||
// buy
|
||||
Some(&102) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
|
||||
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, true) {
|
||||
if let Some(bot_wallet_pubkey) = bot_wallet {
|
||||
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
|
||||
instructions.push(DexInstruction::BotTrade(trade_info));
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
};
|
||||
}
|
||||
// sell
|
||||
Some(&51) if instruction.data.len() == 24 && instruction.accounts.len() >= 12 => {
|
||||
if let Ok(trade_info) = parse_instruction_trade_data(instruction, accounts, false) {
|
||||
if let Some(bot_wallet_pubkey) = bot_wallet {
|
||||
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
|
||||
instructions.push(DexInstruction::BotTrade(trade_info));
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
|
||||
/// Parse transaction logs and return instruction type and data
|
||||
pub fn parse_instruction(logs: &[String], bot_wallet: Option<Pubkey>) -> ClientResult<Vec<DexInstruction>> {
|
||||
let mut current_instruction = None;
|
||||
let mut program_data = String::new();
|
||||
let mut invoke_depth = 0;
|
||||
let mut last_data_len = 0;
|
||||
let mut instructions = Vec::new();
|
||||
for log in logs {
|
||||
// Check program invocation
|
||||
if log.contains(&format!("Program {} invoke", Self::PROGRAM_ID)) {
|
||||
invoke_depth += 1;
|
||||
if invoke_depth == 1 { // Only reset state at top level call
|
||||
current_instruction = None;
|
||||
program_data.clear();
|
||||
last_data_len = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not in our program
|
||||
if invoke_depth == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Identify instruction type (only at top level)
|
||||
if invoke_depth == 1 && log.contains("Program log: Instruction:") {
|
||||
if log.contains("Create") {
|
||||
current_instruction = Some("create");
|
||||
} else if log.contains("Buy") || log.contains("Sell") {
|
||||
current_instruction = Some("trade");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect Program data
|
||||
if log.starts_with("Program data: ") {
|
||||
let data = log.trim_start_matches("Program data: ");
|
||||
if data.len() > last_data_len {
|
||||
program_data = data.to_string();
|
||||
last_data_len = data.len();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if program ends
|
||||
if log.contains(&format!("Program {} success", Self::PROGRAM_ID)) {
|
||||
invoke_depth -= 1;
|
||||
if invoke_depth == 0 { // Only process data when top level program ends
|
||||
if let Some(instruction_type) = current_instruction {
|
||||
if !program_data.is_empty() {
|
||||
match instruction_type {
|
||||
"create" => {
|
||||
if let Ok(token_info) = parse_create_token_data(&program_data) {
|
||||
instructions.push(DexInstruction::CreateToken(token_info));
|
||||
}
|
||||
},
|
||||
"trade" => {
|
||||
if let Ok(trade_info) = parse_trade_data(&program_data) {
|
||||
if let Some(bot_wallet_pubkey) = bot_wallet {
|
||||
if trade_info.user.to_string() == bot_wallet_pubkey.to_string() {
|
||||
instructions.push(DexInstruction::BotTrade(trade_info));
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
} else {
|
||||
instructions.push(DexInstruction::UserTrade(trade_info));
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
@@ -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::pumpfun::{
|
||||
logs_data::{DexInstruction, CreateTokenInfo, TradeInfo},
|
||||
logs_filters::LogFilter
|
||||
};
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::instruction::CompiledInstruction;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub async fn process_logs<F>(
|
||||
signature: &str,
|
||||
logs: Vec<String>,
|
||||
callback: F,
|
||||
payer: Option<Pubkey>,
|
||||
) -> ClientResult<()>
|
||||
where
|
||||
F: Fn(&str, DexInstruction) + Send + Sync,
|
||||
{
|
||||
let instructions = LogFilter::parse_instruction(&logs, payer)?;
|
||||
for instruction in instructions {
|
||||
callback(signature, instruction);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Add parsing function
|
||||
pub fn parse_create_token_data(data: &str) -> ClientResult<CreateTokenInfo> {
|
||||
// First do base64 decoding
|
||||
let decoded = BASE64.decode(data)
|
||||
.map_err(|e| ClientError::Other(format!("Failed to decode base64: {}", e)))?;
|
||||
|
||||
// Skip prefix bytes (if any)
|
||||
let mut cursor = if decoded.len() > 8 { 8 } else { 0 };
|
||||
|
||||
// Read name length and name
|
||||
if cursor + 4 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for name length".to_string()));
|
||||
}
|
||||
let name_len = read_u32(&decoded[cursor..]) as usize;
|
||||
cursor += 4;
|
||||
|
||||
if cursor + name_len > decoded.len() {
|
||||
return Err(ClientError::Other(format!("Data too short for name: need {} bytes", name_len)));
|
||||
}
|
||||
let name = String::from_utf8(decoded[cursor..cursor + name_len].to_vec())
|
||||
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in name: {}", e)))?;
|
||||
cursor += name_len;
|
||||
|
||||
// Read symbol length and symbol
|
||||
if cursor + 4 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for symbol length".to_string()));
|
||||
}
|
||||
let symbol_len = read_u32(&decoded[cursor..]) as usize;
|
||||
cursor += 4;
|
||||
|
||||
if cursor + symbol_len > decoded.len() {
|
||||
return Err(ClientError::Other(format!("Data too short for symbol: need {} bytes", symbol_len)));
|
||||
}
|
||||
let symbol = String::from_utf8(decoded[cursor..cursor + symbol_len].to_vec())
|
||||
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in symbol: {}", e)))?;
|
||||
cursor += symbol_len;
|
||||
|
||||
// Read URI length and URI
|
||||
if cursor + 4 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for URI length".to_string()));
|
||||
}
|
||||
let uri_len = read_u32(&decoded[cursor..]) as usize;
|
||||
cursor += 4;
|
||||
|
||||
if cursor + uri_len > decoded.len() {
|
||||
return Err(ClientError::Other(format!("Data too short for URI: need {} bytes", uri_len)));
|
||||
}
|
||||
let uri = String::from_utf8(decoded[cursor..cursor + uri_len].to_vec())
|
||||
.map_err(|e| ClientError::Other(format!("Invalid UTF-8 in uri: {}", e)))?;
|
||||
cursor += uri_len;
|
||||
|
||||
// Make sure there is enough data to read public keys
|
||||
if cursor + 32 * 3 > decoded.len() {
|
||||
return Err(ClientError::Other("Data too short for public keys".to_string()));
|
||||
}
|
||||
|
||||
// Parse Mint Public Key
|
||||
let mint = bs58::encode(&decoded[cursor..cursor+32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// Parse Bonding Curve Public Key
|
||||
let bonding_curve = bs58::encode(&decoded[cursor..cursor+32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// Parse User Public Key
|
||||
let user = bs58::encode(&decoded[cursor..cursor+32]).into_string();
|
||||
|
||||
Ok(CreateTokenInfo {
|
||||
slot: 0,
|
||||
name,
|
||||
symbol,
|
||||
uri,
|
||||
mint: Pubkey::from_str(&mint).unwrap(),
|
||||
bonding_curve: Pubkey::from_str(&bonding_curve).unwrap(),
|
||||
user: Pubkey::from_str(&user).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_u32(data: &[u8]) -> u32 {
|
||||
let mut bytes = [0u8; 4];
|
||||
bytes.copy_from_slice(&data[..4]);
|
||||
u32::from_le_bytes(bytes)
|
||||
}
|
||||
|
||||
pub fn parse_trade_data(data: &str) -> ClientResult<TradeInfo> {
|
||||
let engine = base64::engine::general_purpose::STANDARD;
|
||||
let decoded = engine.decode(data).map_err(|e|
|
||||
ClientError::Parse(
|
||||
"Failed to decode base64".to_string(),
|
||||
e.to_string()
|
||||
)
|
||||
)?;
|
||||
|
||||
let mut cursor = 8; // Skip prefix
|
||||
|
||||
// 1. Mint (32 bytes)
|
||||
let mint = bs58::encode(&decoded[cursor..cursor + 32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// 2. Sol Amount (8 bytes)
|
||||
let sol_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 3. Token Amount (8 bytes)
|
||||
let token_amount = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 4. Is Buy (1 byte)
|
||||
let is_buy = decoded[cursor] != 0;
|
||||
cursor += 1;
|
||||
|
||||
// 5. User (32 bytes)
|
||||
let user = bs58::encode(&decoded[cursor..cursor + 32]).into_string();
|
||||
cursor += 32;
|
||||
|
||||
// 6. Timestamp (8 bytes)
|
||||
let timestamp = i64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 7. Virtual Sol Reserves (8 bytes)
|
||||
let virtual_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
// 8. Virtual Token Reserves (8 bytes)
|
||||
let virtual_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
let real_sol_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
cursor += 8;
|
||||
|
||||
let real_token_reserves = u64::from_le_bytes(decoded[cursor..cursor + 8].try_into().unwrap());
|
||||
|
||||
Ok(TradeInfo {
|
||||
slot: 0,
|
||||
mint: Pubkey::from_str(&mint).unwrap(),
|
||||
sol_amount,
|
||||
token_amount,
|
||||
is_buy,
|
||||
user: Pubkey::from_str(&user).unwrap(),
|
||||
timestamp,
|
||||
virtual_sol_reserves,
|
||||
virtual_token_reserves,
|
||||
real_sol_reserves,
|
||||
real_token_reserves,
|
||||
})
|
||||
}
|
||||
|
||||
fn current_timestamp_millis() -> i64 {
|
||||
let duration = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
|
||||
duration.as_millis() as i64
|
||||
}
|
||||
|
||||
pub fn parse_instruction_create_token_data(instruction: &CompiledInstruction, accounts: &[Pubkey]) -> ClientResult<CreateTokenInfo> {
|
||||
let data = instruction.data.clone();
|
||||
let mut offset = 0;
|
||||
offset += 8;
|
||||
let len1 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
|
||||
offset += 4;
|
||||
let name = String::from_utf8_lossy(&data[offset..offset + len1]);
|
||||
offset += len1;
|
||||
let len2 = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
|
||||
offset += 4;
|
||||
let symbol = String::from_utf8_lossy(&data[offset..offset + len2]);
|
||||
offset += len2;
|
||||
let _flag = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
|
||||
offset += 4;
|
||||
let hash_start = data.len() - 32;
|
||||
let ipfs_bytes = &data[offset..hash_start];
|
||||
let uri = String::from_utf8_lossy(ipfs_bytes);
|
||||
let mint = accounts[instruction.accounts[0] as usize];
|
||||
let user = accounts[instruction.accounts[7] as usize];
|
||||
let bonding_curve= accounts[instruction.accounts[2] as usize];
|
||||
Ok(CreateTokenInfo {
|
||||
slot: 0,
|
||||
name: name.to_string(),
|
||||
symbol: symbol.to_string(),
|
||||
uri: uri.to_string(),
|
||||
mint,
|
||||
bonding_curve,
|
||||
user,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_instruction_trade_data(instruction: &CompiledInstruction, accounts: &[Pubkey], is_buy: bool) -> ClientResult<TradeInfo> {
|
||||
let data = instruction.data.clone();
|
||||
let amount = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
let max_sol_cost_or_min_sol_output = u64::from_le_bytes(data[16..24].try_into().unwrap());
|
||||
let user = accounts[instruction.accounts[6] as usize];
|
||||
let mint = accounts[instruction.accounts[2] as usize];
|
||||
Ok(TradeInfo {
|
||||
slot: 0,
|
||||
mint,
|
||||
sol_amount: max_sol_cost_or_min_sol_output,
|
||||
token_amount: amount,
|
||||
is_buy,
|
||||
user,
|
||||
timestamp: current_timestamp_millis(),
|
||||
virtual_sol_reserves: 0,
|
||||
virtual_token_reserves: 0,
|
||||
real_sol_reserves: 0,
|
||||
real_token_reserves: 0,
|
||||
})
|
||||
}
|
||||
@@ -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::{constants, common::pumpfun::{
|
||||
logs_data::DexInstruction, logs_events::DexEvent, logs_filters::LogFilter
|
||||
}};
|
||||
|
||||
use super::logs_events::PumpfunEvent;
|
||||
|
||||
/// Subscription handle containing task and unsubscribe logic
|
||||
pub struct SubscriptionHandle {
|
||||
pub task: JoinHandle<()>,
|
||||
pub unsub_fn: Box<dyn Fn() + Send>,
|
||||
}
|
||||
|
||||
impl SubscriptionHandle {
|
||||
pub async fn shutdown(self) {
|
||||
(self.unsub_fn)();
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient {
|
||||
PubsubClient::new(ws_url).await.unwrap()
|
||||
}
|
||||
|
||||
/// 启动订阅
|
||||
pub async fn tokens_subscription<F>(
|
||||
ws_url: &str,
|
||||
commitment: CommitmentConfig,
|
||||
callback: F,
|
||||
bot_wallet: Option<Pubkey>,
|
||||
) -> Result<SubscriptionHandle, Box<dyn std::error::Error>>
|
||||
where
|
||||
F: Fn(PumpfunEvent) + Send + Sync + 'static,
|
||||
{
|
||||
let program_address = constants::pumpfun::accounts::PUMPFUN.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;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_subscribe;
|
||||
pub mod logs_events;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_subscribe::*;
|
||||
pub use logs_events::*;
|
||||
@@ -1,353 +0,0 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
/// PumpSwap指令类型
|
||||
#[derive(Debug)]
|
||||
pub enum PumpSwapInstruction {
|
||||
Buy(BuyEvent),
|
||||
Sell(SellEvent),
|
||||
CreatePool(CreatePoolEvent),
|
||||
Deposit(DepositEvent),
|
||||
Withdraw(WithdrawEvent),
|
||||
Disable(DisableEvent),
|
||||
UpdateAdmin(UpdateAdminEvent),
|
||||
UpdateFeeConfig(UpdateFeeConfigEvent),
|
||||
Other,
|
||||
}
|
||||
|
||||
/// 买入事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct BuyEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
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 signature: String,
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
/// 卖出事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct SellEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
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 signature: String,
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
/// 创建池子事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct CreatePoolEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
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,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 存款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct DepositEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
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 signature: String,
|
||||
}
|
||||
|
||||
/// 提款事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct WithdrawEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
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 signature: String,
|
||||
}
|
||||
|
||||
/// 禁用事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct DisableEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub admin: Pubkey,
|
||||
pub disable_create_pool: bool,
|
||||
pub disable_deposit: bool,
|
||||
pub disable_withdraw: bool,
|
||||
pub disable_buy: bool,
|
||||
pub disable_sell: bool,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 更新管理员事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct UpdateAdminEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub old_admin: Pubkey,
|
||||
pub new_admin: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
/// 更新费用配置事件
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct UpdateFeeConfigEvent {
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
pub timestamp: i64,
|
||||
pub admin: Pubkey,
|
||||
pub old_lp_fee_basis_points: u64,
|
||||
pub new_lp_fee_basis_points: u64,
|
||||
pub old_protocol_fee_basis_points: u64,
|
||||
pub new_protocol_fee_basis_points: u64,
|
||||
pub old_protocol_fee_recipients: [Pubkey; 8],
|
||||
pub new_protocol_fee_recipients: [Pubkey; 8],
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 全局配置
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct GlobalConfig {
|
||||
pub admin: Pubkey,
|
||||
pub lp_fee_basis_points: u64,
|
||||
pub protocol_fee_basis_points: u64,
|
||||
pub disable_flags: u8,
|
||||
pub protocol_fee_recipients: [Pubkey; 8],
|
||||
}
|
||||
|
||||
/// 池子信息
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct Pool {
|
||||
pub index: u16,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub lp_mint: Pubkey,
|
||||
pub base_mint_decimals: u8,
|
||||
pub quote_mint_decimals: u8,
|
||||
pub lp_mint_decimals: u8,
|
||||
pub base_token_account: Pubkey,
|
||||
pub quote_token_account: Pubkey,
|
||||
pub bump: u8,
|
||||
pub is_disabled: bool,
|
||||
}
|
||||
|
||||
/// 事件特性
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||
fn discriminator() -> &'static [u8];
|
||||
}
|
||||
|
||||
/// 从字节中提取鉴别器
|
||||
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
if data.len() < length {
|
||||
return None;
|
||||
}
|
||||
Some((&data[..length], &data[length..]))
|
||||
}
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
// 事件鉴别器
|
||||
pub const BUY_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x67, 0xf4, 0x52, 0x1f, 0x2c, 0xf5, 0x77, 0x77];
|
||||
pub const SELL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x3e, 0x2f, 0x37, 0x0a, 0xa5, 0x03, 0xdc, 0x2a];
|
||||
pub const CREATE_POOL_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xb1, 0x31, 0x0c, 0xd2, 0xa0, 0x76, 0xa7, 0x74];
|
||||
pub const DEPOSIT_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x78, 0xf8, 0x3d, 0x53, 0x1f, 0x8e, 0x6b, 0x90];
|
||||
pub const WITHDRAW_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x16, 0x09, 0x85, 0x1a, 0xa0, 0x2c, 0x47, 0xc0];
|
||||
pub const DISABLE_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x6b, 0xfd, 0xc1, 0x4c, 0xe4, 0xca, 0x1b, 0x68];
|
||||
pub const UPDATE_ADMIN_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0xe1, 0x98, 0xab, 0x57, 0xf6, 0x3f, 0x42, 0xea];
|
||||
pub const UPDATE_FEE_CONFIG_EVENT: &[u8] = &[0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d, 0x5a, 0x17, 0x41, 0x23, 0x3e, 0xf4, 0xbc, 0xd0];
|
||||
|
||||
// 指令鉴别器
|
||||
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];
|
||||
pub const DISABLE_IX: &[u8] = &[107,
|
||||
253,
|
||||
193,
|
||||
76,
|
||||
228,
|
||||
202,
|
||||
27,
|
||||
104];
|
||||
pub const UPDATE_ADMIN_IX: &[u8] = &[225,
|
||||
152,
|
||||
171,
|
||||
87,
|
||||
246,
|
||||
63,
|
||||
66,
|
||||
234];
|
||||
pub const UPDATE_FEE_CONFIG_IX: &[u8] = &[90,
|
||||
23,
|
||||
65,
|
||||
35,
|
||||
62,
|
||||
244,
|
||||
188,
|
||||
208];
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use crate::common::pumpswap::logs_data::{
|
||||
BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent,
|
||||
DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators
|
||||
};
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
pub const PROGRAM_LOG_PREFIX: &str = "Program log: PumpSwap: ";
|
||||
|
||||
/// PumpSwap事件枚举
|
||||
#[derive(Debug)]
|
||||
pub enum PumpSwapEvent {
|
||||
Buy(BuyEvent),
|
||||
Sell(SellEvent),
|
||||
CreatePool(CreatePoolEvent),
|
||||
Deposit(DepositEvent),
|
||||
Withdraw(WithdrawEvent),
|
||||
Disable(DisableEvent),
|
||||
UpdateAdmin(UpdateAdminEvent),
|
||||
UpdateFeeConfig(UpdateFeeConfigEvent),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl PumpSwapEvent {
|
||||
/// 解析日志并提取PumpSwap事件
|
||||
pub fn parse_logs(logs: &[String]) -> Vec<PumpSwapEvent> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
if logs.is_empty() {
|
||||
return events;
|
||||
}
|
||||
|
||||
for log in logs {
|
||||
// 检查是否是事件日志
|
||||
if let Some(event_data) = log.strip_prefix(PROGRAM_DATA) {
|
||||
let borsh_bytes = match general_purpose::STANDARD.decode(event_data) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
// 检查鉴别器
|
||||
if borsh_bytes.len() < 16 {
|
||||
continue;
|
||||
}
|
||||
let prefix = [0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d];
|
||||
let discriminator = &[&prefix[..], &borsh_bytes[..8]].concat();
|
||||
let data = &borsh_bytes[8..];
|
||||
// 根据鉴别器解析不同类型的事件
|
||||
if discriminator == discriminators::BUY_EVENT {
|
||||
if let Ok(mut event) = BuyEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Buy(event));
|
||||
}
|
||||
} else if discriminator == discriminators::SELL_EVENT {
|
||||
if let Ok(mut event) = SellEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Sell(event));
|
||||
}
|
||||
} else if discriminator == discriminators::CREATE_POOL_EVENT {
|
||||
if let Ok(mut event) = CreatePoolEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::CreatePool(event));
|
||||
}
|
||||
} else if discriminator == discriminators::DEPOSIT_EVENT {
|
||||
if let Ok(mut event) = DepositEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Deposit(event));
|
||||
}
|
||||
} else if discriminator == discriminators::WITHDRAW_EVENT {
|
||||
if let Ok(mut event) = WithdrawEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Withdraw(event));
|
||||
}
|
||||
} else if discriminator == discriminators::DISABLE_EVENT {
|
||||
if let Ok(mut event) = DisableEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::Disable(event));
|
||||
}
|
||||
} else if discriminator == discriminators::UPDATE_ADMIN_EVENT {
|
||||
if let Ok(mut event) = UpdateAdminEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::UpdateAdmin(event));
|
||||
}
|
||||
} else if discriminator == discriminators::UPDATE_FEE_CONFIG_EVENT {
|
||||
if let Ok(mut event) = UpdateFeeConfigEvent::deserialize(&mut &data[..]) {
|
||||
event.signature = String::new(); // 在外部设置
|
||||
events.push(PumpSwapEvent::UpdateFeeConfig(event));
|
||||
}
|
||||
}
|
||||
} else if let Some(event_log) = log.strip_prefix(PROGRAM_LOG_PREFIX) {
|
||||
// 处理程序日志中的事件信息
|
||||
if event_log.contains("BuyEvent") {
|
||||
// 这里可以添加从日志文本中解析事件的逻辑
|
||||
// 例如使用正则表达式提取关键信息
|
||||
} else if event_log.contains("SellEvent") {
|
||||
// 同上
|
||||
}
|
||||
// 其他事件类型...
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use crate::common::pumpswap::logs_data::PumpSwapInstruction;
|
||||
use crate::common::pumpswap::logs_parser::parse_pumpswap_instruction;
|
||||
use crate::common::pumpswap::logs_events::PumpSwapEvent;
|
||||
use crate::constants::pumpswap::accounts;
|
||||
use crate::error::ClientResult;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
pub struct LogFilter;
|
||||
|
||||
impl LogFilter {
|
||||
/// 解析PumpSwap编译后的指令并返回指令类型和数据
|
||||
pub fn parse_pumpswap_compiled_instruction(
|
||||
versioned_tx: VersionedTransaction) -> ClientResult<Vec<PumpSwapInstruction>> {
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
let pump_index = accounts.iter().position(|key| key == &program_id);
|
||||
let mut instructions: Vec<PumpSwapInstruction> = 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;
|
||||
}
|
||||
|
||||
if let Some(parsed_instruction) = parse_pumpswap_instruction(instruction, accounts) {
|
||||
instructions.push(parsed_instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
/// 解析PumpSwap交易日志并返回事件
|
||||
pub fn parse_pumpswap_logs(logs: &[String]) -> Vec<PumpSwapEvent> {
|
||||
PumpSwapEvent::parse_logs(logs)
|
||||
}
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
use crate::error::ClientResult;
|
||||
use crate::common::pumpswap::{
|
||||
logs_data::{
|
||||
PumpSwapInstruction,
|
||||
BuyEvent, SellEvent, CreatePoolEvent, DepositEvent, WithdrawEvent,
|
||||
DisableEvent, UpdateAdminEvent, UpdateFeeConfigEvent, discriminators
|
||||
},
|
||||
logs_events::PumpSwapEvent
|
||||
};
|
||||
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::instruction::CompiledInstruction;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 处理PumpSwap日志并调用回调函数
|
||||
pub async fn process_pumpswap_logs<F>(
|
||||
signature: &str,
|
||||
logs: Vec<String>,
|
||||
slot: Option<u64>,
|
||||
callback: F,
|
||||
) -> ClientResult<()>
|
||||
where
|
||||
F: Fn(&str, PumpSwapEvent) + Send + Sync,
|
||||
{
|
||||
let events = PumpSwapEvent::parse_logs(&logs);
|
||||
for mut event in events {
|
||||
// 设置签名和slot
|
||||
match &mut event {
|
||||
PumpSwapEvent::Buy(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Sell(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::CreatePool(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Deposit(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Withdraw(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::Disable(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::UpdateAdmin(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
PumpSwapEvent::UpdateFeeConfig(e) => {
|
||||
e.signature = signature.to_string();
|
||||
if let Some(s) = slot {
|
||||
e.slot = s;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
callback(signature, event);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前时间戳
|
||||
fn current_timestamp() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// 从指令中解析PumpSwap指令
|
||||
pub fn parse_pumpswap_instruction(instruction: &CompiledInstruction, _accounts: &[Pubkey]) -> Option<PumpSwapInstruction> {
|
||||
if instruction.data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let discriminator = &instruction.data[..8];
|
||||
let data = &instruction.data[8..];
|
||||
|
||||
let accounts: Vec<Pubkey> = instruction.accounts.iter()
|
||||
.map(|&idx| _accounts[idx as usize])
|
||||
.collect();
|
||||
|
||||
match discriminator {
|
||||
d if d == discriminators::BUY_IX => {
|
||||
// buy指令参数: base_amount_out: u64, max_quote_amount_in: u64
|
||||
// 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account,
|
||||
// user_quote_token_account, pool_base_token_account, pool_quote_token_account,
|
||||
// protocol_fee_recipient, protocol_fee_recipient_token_account, ...
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let base_amount_out = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let max_quote_amount_in = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Buy(BuyEvent {
|
||||
base_amount_out,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
timestamp: current_timestamp(),
|
||||
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
pool_base_token_account: accounts[7],
|
||||
pool_quote_token_account: accounts[8],
|
||||
coin_creator_vault_ata: if accounts.len() > 17 { accounts[17] } else { Pubkey::default() },
|
||||
coin_creator_vault_authority: if accounts.len() > 18 { accounts[18] } else { Pubkey::default() },
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::SELL_IX => {
|
||||
// sell指令参数: base_amount_in: u64, min_quote_amount_out: u64
|
||||
// 账户顺序:pool, user, global_config, base_mint, quote_mint, user_base_token_account,
|
||||
// user_quote_token_account, pool_base_token_account, pool_quote_token_account,
|
||||
// protocol_fee_recipient, protocol_fee_recipient_token_account, ...
|
||||
if data.len() < 16 || accounts.len() < 11 {
|
||||
return None;
|
||||
}
|
||||
let base_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let min_quote_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(PumpSwapInstruction::Sell(SellEvent {
|
||||
base_amount_in,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[1],
|
||||
user_base_token_account: accounts[5],
|
||||
user_quote_token_account: accounts[6],
|
||||
protocol_fee_recipient: accounts[9],
|
||||
protocol_fee_recipient_token_account: accounts[10],
|
||||
timestamp: current_timestamp(),
|
||||
|
||||
base_mint: accounts[3],
|
||||
quote_mint: accounts[4],
|
||||
pool_base_token_account: accounts[7],
|
||||
pool_quote_token_account: accounts[8],
|
||||
coin_creator_vault_ata: if accounts.len() > 17 { accounts[17] } else { Pubkey::default() },
|
||||
coin_creator_vault_authority: if accounts.len() > 18 { accounts[18] } else { Pubkey::default() },
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::CREATE_POOL_IX => {
|
||||
// create_pool指令参数: index: u16, base_amount_in: u64, quote_amount_in: u64
|
||||
// 账户顺序:pool, global_config, creator, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
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()?);
|
||||
|
||||
Some(PumpSwapInstruction::CreatePool(CreatePoolEvent {
|
||||
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],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::DEPOSIT_IX => {
|
||||
// deposit指令参数: lp_token_amount_out: u64, max_base_amount_in: u64, max_quote_amount_in: u64
|
||||
// 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
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()?);
|
||||
|
||||
Some(PumpSwapInstruction::Deposit(DepositEvent {
|
||||
lp_token_amount_out,
|
||||
max_base_amount_in,
|
||||
max_quote_amount_in,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::WITHDRAW_IX => {
|
||||
// withdraw指令参数: lp_token_amount_in: u64, min_base_amount_out: u64, min_quote_amount_out: u64
|
||||
// 账户顺序:pool, global_config, user, base_mint, quote_mint, lp_mint,
|
||||
// user_base_token_account, user_quote_token_account, user_pool_token_account,
|
||||
// pool_base_token_account, pool_quote_token_account, ...
|
||||
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()?);
|
||||
|
||||
Some(PumpSwapInstruction::Withdraw(WithdrawEvent {
|
||||
lp_token_amount_in,
|
||||
min_base_amount_out,
|
||||
min_quote_amount_out,
|
||||
pool: accounts[0],
|
||||
user: accounts[2],
|
||||
user_base_token_account: accounts[6],
|
||||
user_quote_token_account: accounts[7],
|
||||
user_pool_token_account: accounts[8],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::DISABLE_IX => {
|
||||
// disable指令参数: disable_create_pool: bool, disable_deposit: bool, disable_withdraw: bool, disable_buy: bool, disable_sell: bool
|
||||
// 账户顺序:admin, global_config, event_authority, program
|
||||
if data.len() < 5 || accounts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let disable_create_pool = data[0] != 0;
|
||||
let disable_deposit = data[1] != 0;
|
||||
let disable_withdraw = data[2] != 0;
|
||||
let disable_buy = data[3] != 0;
|
||||
let disable_sell = data[4] != 0;
|
||||
|
||||
Some(PumpSwapInstruction::Disable(DisableEvent {
|
||||
disable_create_pool,
|
||||
disable_deposit,
|
||||
disable_withdraw,
|
||||
disable_buy,
|
||||
disable_sell,
|
||||
admin: accounts[0],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::UPDATE_ADMIN_IX => {
|
||||
// update_admin指令参数: 无
|
||||
// 账户顺序:admin, global_config, new_admin, event_authority, program
|
||||
if accounts.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
Some(PumpSwapInstruction::UpdateAdmin(UpdateAdminEvent {
|
||||
old_admin: accounts[0],
|
||||
new_admin: accounts[2],
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
d if d == discriminators::UPDATE_FEE_CONFIG_IX => {
|
||||
// update_fee_config指令参数: lp_fee_basis_points: u64, protocol_fee_basis_points: u64, protocol_fee_recipients: [pubkey; 8]
|
||||
// 账户顺序:admin, global_config, event_authority, program
|
||||
if data.len() < 272 || accounts.len() < 2 { // 8 + 8 + 32*8 = 272 bytes
|
||||
return None;
|
||||
}
|
||||
let lp_fee_basis_points = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let protocol_fee_basis_points = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
let mut protocol_fee_recipients = [Pubkey::default(); 8];
|
||||
for i in 0..8 {
|
||||
let start = 16 + i * 32;
|
||||
let end = start + 32;
|
||||
if let Ok(pubkey_bytes) = data[start..end].try_into() {
|
||||
protocol_fee_recipients[i] = Pubkey::new_from_array(pubkey_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PumpSwapInstruction::UpdateFeeConfig(UpdateFeeConfigEvent {
|
||||
admin: accounts[0],
|
||||
new_lp_fee_basis_points: lp_fee_basis_points,
|
||||
new_protocol_fee_basis_points: protocol_fee_basis_points,
|
||||
new_protocol_fee_recipients: protocol_fee_recipients,
|
||||
timestamp: current_timestamp(),
|
||||
..Default::default()
|
||||
}))
|
||||
},
|
||||
_ => Some(PumpSwapInstruction::Other),
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_events;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_events::*;
|
||||
@@ -1,161 +0,0 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use crate::error::{ClientError, ClientResult};
|
||||
|
||||
/// Raydium指令类型
|
||||
#[derive(Debug)]
|
||||
pub enum RaydiumInstruction {
|
||||
V4Swap(V4SwapEvent),
|
||||
SwapBaseInput(SwapBaseInputEvent),
|
||||
SwapBaseOutput(SwapBaseOutputEvent),
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct SwapBaseOutputEvent {
|
||||
#[borsh(skip)]
|
||||
pub timestamp: i64,
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
pub max_amount_in: u64,
|
||||
pub amount_out: u64,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub payer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_config: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_state: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct SwapBaseInputEvent {
|
||||
#[borsh(skip)]
|
||||
pub timestamp: i64,
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
pub amount_in: u64,
|
||||
pub minimum_amount_out: u64,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub payer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_config: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_state: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub input_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub output_token_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct V4SwapEvent {
|
||||
#[borsh(skip)]
|
||||
pub timestamp: i64,
|
||||
#[borsh(skip)]
|
||||
pub slot: u64,
|
||||
#[borsh(skip)]
|
||||
pub signature: String,
|
||||
pub amount_in: u64,
|
||||
pub minimum_amount_out: u64,
|
||||
|
||||
#[borsh(skip)]
|
||||
pub amm: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_open_orders: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_target_orders: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_coin_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_pc_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_market: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_bids: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_asks: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_event_queue: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_coin_vault_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_pc_vault_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub serum_vault_signer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_source_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_destination_token_account: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub user_source_owner: Pubkey,
|
||||
}
|
||||
|
||||
/// 事件特性
|
||||
pub trait EventTrait: Sized + std::fmt::Debug {
|
||||
fn from_bytes(bytes: &[u8]) -> ClientResult<Self>;
|
||||
fn discriminator() -> &'static [u8];
|
||||
}
|
||||
|
||||
/// 从字节中提取鉴别器
|
||||
pub fn extract_discriminator(length: usize, data: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
if data.len() < length {
|
||||
return None;
|
||||
}
|
||||
Some((&data[..length], &data[length..]))
|
||||
}
|
||||
|
||||
/// 事件鉴别器常量
|
||||
pub mod discriminators {
|
||||
pub const V4_SWAP_IX: &u8 = &9;
|
||||
|
||||
pub const SWAP_BASE_INPUT_IX: &[u8] = &[143, 190, 90, 218, 196, 30, 51, 222];
|
||||
pub const SWAP_BASE_OUTPUT_IX: &[u8] = &[55, 217, 98, 86, 163, 74, 180, 173];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use crate::common::raydium::logs_data::{discriminators, SwapBaseInputEvent, V4SwapEvent};
|
||||
use crate::common::raydium::SwapBaseOutputEvent;
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use borsh::BorshDeserialize;
|
||||
|
||||
pub const PROGRAM_DATA: &str = "Program data: ";
|
||||
pub const PROGRAM_LOG_PREFIX: &str = "Program log: ray_log: ";
|
||||
|
||||
/// Raydium事件枚举
|
||||
#[derive(Debug)]
|
||||
pub enum RaydiumEvent {
|
||||
V4Swap(V4SwapEvent),
|
||||
SwapBaseInput(SwapBaseInputEvent),
|
||||
SwapBaseOutput(SwapBaseOutputEvent),
|
||||
Error(String),
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use crate::common::raydium::logs_data::RaydiumInstruction;
|
||||
use crate::common::raydium::logs_parser::parse_raydium_instruction;
|
||||
use crate::constants::raydium::accounts;
|
||||
use crate::error::ClientResult;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
|
||||
pub struct LogFilter;
|
||||
|
||||
impl LogFilter {
|
||||
/// 解析Raydium编译后的指令并返回指令类型和数据
|
||||
pub fn parse_raydium_compiled_instruction(
|
||||
versioned_tx: VersionedTransaction,
|
||||
) -> ClientResult<Vec<RaydiumInstruction>> {
|
||||
let compiled_instructions = versioned_tx.message.instructions();
|
||||
let accounts = versioned_tx.message.static_account_keys();
|
||||
let ammv4_program_id = accounts::AMMV4_PROGRAM;
|
||||
let cpmm_program_id = accounts::CPMM_PROGRAM;
|
||||
let raydium_index = accounts.iter().position(|key| key == &ammv4_program_id);
|
||||
let cpmm_index = accounts.iter().position(|key| key == &cpmm_program_id);
|
||||
let mut instructions: Vec<RaydiumInstruction> = Vec::new();
|
||||
if let Some(index) = raydium_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;
|
||||
}
|
||||
|
||||
if let Some(parsed_instruction) =
|
||||
parse_raydium_instruction(instruction, accounts, &ammv4_program_id)
|
||||
{
|
||||
instructions.push(parsed_instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(index) = cpmm_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;
|
||||
}
|
||||
|
||||
if let Some(parsed_instruction) =
|
||||
parse_raydium_instruction(instruction, accounts, &cpmm_program_id)
|
||||
{
|
||||
instructions.push(parsed_instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
use crate::common::raydium::{
|
||||
logs_data::{discriminators, RaydiumInstruction, V4SwapEvent},
|
||||
logs_events::RaydiumEvent,
|
||||
};
|
||||
use crate::error::ClientResult;
|
||||
|
||||
use solana_sdk::instruction::CompiledInstruction;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// 获取当前时间戳
|
||||
fn current_timestamp() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
/// 从指令中解析Raydium指令
|
||||
pub fn parse_raydium_instruction(
|
||||
instruction: &CompiledInstruction,
|
||||
_accounts: &[Pubkey],
|
||||
program_id: &Pubkey,
|
||||
) -> Option<RaydiumInstruction> {
|
||||
if instruction.data.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if program_id == &crate::constants::raydium::accounts::CPMM_PROGRAM {
|
||||
let discriminator = &instruction.data[..8];
|
||||
let data = &instruction.data[8..];
|
||||
|
||||
let accounts: Vec<Pubkey> = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|&idx| _accounts[idx as usize])
|
||||
.collect();
|
||||
|
||||
match discriminator {
|
||||
d if d == discriminators::SWAP_BASE_INPUT_IX => {
|
||||
if data.len() < 16 || accounts.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
let amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let minimum_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(RaydiumInstruction::SwapBaseInput(
|
||||
crate::common::raydium::SwapBaseInputEvent {
|
||||
timestamp: current_timestamp(),
|
||||
amount_in: amount_in,
|
||||
minimum_amount_out: 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_program: accounts[8],
|
||||
output_token_program: accounts[9],
|
||||
input_token_mint: accounts[10],
|
||||
output_token_mint: accounts[11],
|
||||
observation_state: accounts[12],
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
d if d == discriminators::SWAP_BASE_OUTPUT_IX => {
|
||||
println!("data.len(): {:?}", data.len());
|
||||
println!("accounts.len(): {:?}", accounts.len());
|
||||
if data.len() < 16 || accounts.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
let max_amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
Some(RaydiumInstruction::SwapBaseOutput(
|
||||
crate::common::raydium::SwapBaseOutputEvent {
|
||||
timestamp: current_timestamp(),
|
||||
max_amount_in: max_amount_in,
|
||||
amount_out: 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_program: accounts[8],
|
||||
output_token_program: accounts[9],
|
||||
input_token_mint: accounts[10],
|
||||
output_token_mint: accounts[11],
|
||||
observation_state: accounts[12],
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
_ => Some(RaydiumInstruction::Other),
|
||||
}
|
||||
} else if program_id == &crate::constants::raydium::accounts::AMMV4_PROGRAM {
|
||||
let discriminator = &instruction.data[0];
|
||||
let data = &instruction.data[1..];
|
||||
|
||||
let mut accounts: Vec<Pubkey> = instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|&idx| _accounts[idx as usize])
|
||||
.collect();
|
||||
|
||||
match discriminator {
|
||||
d if d == discriminators::V4_SWAP_IX => {
|
||||
if data.len() < 16 || accounts.len() < 17 {
|
||||
return None;
|
||||
}
|
||||
let amount_in = u64::from_le_bytes(data[0..8].try_into().ok()?);
|
||||
let minimum_amount_out = u64::from_le_bytes(data[8..16].try_into().ok()?);
|
||||
|
||||
if accounts.len() == 17 {
|
||||
accounts.insert(4, Pubkey::default());
|
||||
}
|
||||
|
||||
Some(RaydiumInstruction::V4Swap(V4SwapEvent {
|
||||
timestamp: current_timestamp(),
|
||||
amount_in: amount_in,
|
||||
minimum_amount_out: minimum_amount_out,
|
||||
amm: accounts[1],
|
||||
amm_authority: accounts[2],
|
||||
amm_open_orders: accounts[3],
|
||||
amm_target_orders: accounts[4],
|
||||
pool_coin_token_account: accounts[5],
|
||||
pool_pc_token_account: accounts[6],
|
||||
serum_program: accounts[7],
|
||||
serum_market: accounts[8],
|
||||
serum_bids: accounts[9],
|
||||
serum_asks: accounts[10],
|
||||
serum_event_queue: accounts[11],
|
||||
serum_coin_vault_account: accounts[12],
|
||||
serum_pc_vault_account: accounts[13],
|
||||
serum_vault_signer: accounts[14],
|
||||
user_source_token_account: accounts[15],
|
||||
user_destination_token_account: accounts[16],
|
||||
user_source_owner: accounts[17],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
_ => Some(RaydiumInstruction::Other),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
pub mod logs_data;
|
||||
pub mod logs_parser;
|
||||
pub mod logs_filters;
|
||||
pub mod logs_events;
|
||||
|
||||
pub use logs_data::*;
|
||||
pub use logs_parser::*;
|
||||
pub use logs_filters::*;
|
||||
pub use logs_events::*;
|
||||
Reference in New Issue
Block a user