feat: Add Raydium DEX integration support

Add Raydium logs processing, event parsing and GRPC stream handling functionality,
remove redundant PumpSwap subscription module, optimize project structure.
This commit is contained in:
sgxiang
2025-06-16 18:30:54 +08:00
parent 66d6d17996
commit 602b310cf8
13 changed files with 741 additions and 137 deletions
+1
View File
@@ -1,5 +1,6 @@
pub mod pumpfun;
pub mod pumpswap;
pub mod raydium;
pub mod address_lookup;
pub mod nonce_cache;
pub mod tip_cache;
-134
View File
@@ -1,134 +0,0 @@
use solana_client::{
nonblocking::pubsub_client::PubsubClient,
rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter}
};
use solana_sdk::commitment_config::CommitmentConfig;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use futures::StreamExt;
use crate::common::pumpswap::{
logs_events::PumpSwapEvent,
logs_filters::LogFilter
};
use crate::constants::pumpswap::accounts;
/// 订阅句柄,包含任务和取消订阅逻辑
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();
}
}
/// 创建PubSub客户端
pub async fn create_pubsub_client(ws_url: &str) -> PubsubClient {
PubsubClient::new(ws_url).await.unwrap()
}
/// 启动PumpSwap代币订阅
pub async fn tokens_subscription<F>(
ws_url: &str,
commitment: CommitmentConfig,
callback: F,
) -> Result<SubscriptionHandle, Box<dyn std::error::Error>>
where
F: Fn(PumpSwapEvent) + Send + Sync + 'static,
{
// 使用constants中定义的AMM_PROGRAM
let program_address = accounts::AMM_PROGRAM.to_string();
let logs_filter = RpcTransactionLogsFilter::Mentions(vec![program_address]);
let logs_config = RpcTransactionLogsConfig {
commitment: Some(commitment),
};
// 创建PubsubClient
let sub_client = Arc::new(PubsubClient::new(ws_url).await.unwrap());
let sub_client_clone = Arc::clone(&sub_client);
// 创建用于取消订阅的通道
let (unsub_tx, _) = mpsc::channel(1);
// 启动订阅任务
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 events = LogFilter::parse_pumpswap_logs(&msg.value.logs);
for mut event in events {
// 设置签名和slot
match &mut event {
PumpSwapEvent::Buy(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
PumpSwapEvent::Sell(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
PumpSwapEvent::CreatePool(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
PumpSwapEvent::Deposit(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
PumpSwapEvent::Withdraw(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
PumpSwapEvent::Disable(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
PumpSwapEvent::UpdateAdmin(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
PumpSwapEvent::UpdateFeeConfig(e) => {
e.signature = msg.value.signature.clone();
e.slot = msg.context.slot;
},
_ => {}
}
callback(event);
}
}
None => {
println!("PumpSwap subscription stream ended");
}
}
}
});
// 返回订阅句柄和取消订阅逻辑
Ok(SubscriptionHandle {
task,
unsub_fn: Box::new(move || {
let _ = unsub_tx.try_send(());
}),
})
}
/// 停止订阅
pub async fn stop_subscription(handle: SubscriptionHandle) {
handle.shutdown().await;
}
-2
View File
@@ -1,11 +1,9 @@
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::*;
+161
View File
@@ -0,0 +1,161 @@
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];
}
+17
View File
@@ -0,0 +1,17 @@
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),
}
+62
View File
@@ -0,0 +1,62 @@
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)
}
}
+155
View File
@@ -0,0 +1,155 @@
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
}
}
+9
View File
@@ -0,0 +1,9 @@
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::*;