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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
Executable
+62
@@ -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)
|
||||
}
|
||||
}
|
||||
Executable
+155
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
Reference in New Issue
Block a user