pump_sdk/
listener.rs

1use futures_util::StreamExt;
2use solana_client::{
3    nonblocking::{pubsub_client::PubsubClient, rpc_client::RpcClient},
4    rpc_config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter},
5    rpc_response::Response,
6};
7use solana_commitment_config::CommitmentConfig;
8use std::sync::Arc;
9use tokio::sync::broadcast;
10
11use crate::global::{
12    PUMP_DOT_FUN_PROGRAM_ID, SOLANA_OFFICIAL_MAIN_NET_URL, WSS_SOLANA_OFFICIAL_MAIN_NET_URL,
13};
14
15#[derive(Debug, Clone)]
16pub enum TradeLogInstructionType {
17    Trade,
18    Swap,
19    Buy,
20    Sell,
21    None,
22}
23
24#[derive(Debug, Clone)]
25pub enum LiquidityLogInstructionType {
26    Liquidity,
27    Migrate,
28    Raydium,
29    None,
30}
31
32/// trade event
33#[derive(Debug, Clone)]
34pub struct TradeEvent {
35    pub signature: String,
36    pub mint: String,
37    pub trade_event_type: TradeLogInstructionType,
38}
39
40/// liquidity event
41#[derive(Debug, Clone)]
42pub struct LiquidityEvent {
43    pub signature: String,
44    pub mint: String,
45    pub liquidity_event_type: LiquidityLogInstructionType,
46}
47
48/// Pump.fun Listener
49///
50/// # Example
51/// ``` rust
52/// use pump_sdk::listener::Listener;
53/// #[tokio::main]
54/// pub async fn main() {
55///     let listener = Listener::new(500, 500);
56///     let _ = listener
57///         .start(
58///             |_event| {
59///                 // handle trade event
60///             },
61///             |_event| {
62///                 // handle liquidity event
63///             },
64///         )
65///         .await;
66///     let _ = tokio::signal::ctrl_c().await;
67/// }
68/// ```
69pub struct Listener {
70    rpc_url: String,
71    ws_url: String,
72    client: Arc<RpcClient>,
73    trade_sender: broadcast::Sender<TradeEvent>,
74    liquidity_sender: broadcast::Sender<LiquidityEvent>,
75}
76
77impl Listener {
78    pub fn new(trade_channel: usize, liquidity_channel: usize) -> Self {
79        let client = Arc::new(RpcClient::new(SOLANA_OFFICIAL_MAIN_NET_URL.to_string()));
80        let (trade_channel, _) = broadcast::channel(trade_channel);
81        let (liquidity_channel, _) = broadcast::channel(liquidity_channel);
82        Self {
83            rpc_url: SOLANA_OFFICIAL_MAIN_NET_URL.to_string(),
84            ws_url: WSS_SOLANA_OFFICIAL_MAIN_NET_URL.to_string(),
85            client: client,
86            trade_sender: trade_channel,
87            liquidity_sender: liquidity_channel,
88        }
89    }
90    pub fn from_url(
91        rpc_url: String,
92        ws_url: String,
93        trade_channel: usize,
94        liquidity_channel: usize,
95    ) -> Self {
96        let client = Arc::new(RpcClient::new(rpc_url.to_string()));
97        let (trade_channel, _) = broadcast::channel(trade_channel);
98        let (liquidity_channel, _) = broadcast::channel(liquidity_channel);
99        Self {
100            rpc_url: rpc_url.to_string(),
101            ws_url: ws_url.to_string(),
102            client: client,
103            trade_sender: trade_channel,
104            liquidity_sender: liquidity_channel,
105        }
106    }
107    pub fn from_client(
108        client: Arc<RpcClient>,
109        ws_url: String,
110        trade_channel: usize,
111        liquidity_channel: usize,
112    ) -> Self {
113        let (trade_channel, _) = broadcast::channel(trade_channel);
114        let (liquidity_channel, _) = broadcast::channel(liquidity_channel);
115        Self {
116            rpc_url: SOLANA_OFFICIAL_MAIN_NET_URL.to_string(),
117            ws_url: ws_url.to_string(),
118            client: client,
119            trade_sender: trade_channel,
120            liquidity_sender: liquidity_channel,
121        }
122    }
123    /// start listener
124    pub async fn start<T, L>(&self, trade_callback: T, liquidity_callback: L) -> Result<(), String>
125    where
126        T: Fn(TradeEvent) + 'static + Send,
127        L: Fn(LiquidityEvent) + 'static + Send,
128    {
129        // try connection
130        if let Err(e) = self.try_connection().await {
131            return Err(format!("Try Connection error: {:?}", e));
132        }
133        // start event handle thread
134        self.start_handle_event(trade_callback, liquidity_callback)
135            .await;
136        // start pump.fun listen
137        self.start_program_listener(PUMP_DOT_FUN_PROGRAM_ID.to_string())
138            .await?;
139        Ok(())
140    }
141    /// start listen pump.fun program
142    async fn start_program_listener(&self, program_id: String) -> Result<(), String> {
143        let trade_sender = self.trade_sender.clone();
144        let liquidity_sender = self.liquidity_sender.clone();
145        let ws_url = self.ws_url.clone();
146        tokio::spawn(async move {
147            loop {
148                let _ =
149                    Self::listen_to_program(&ws_url, &program_id, &trade_sender, &liquidity_sender)
150                        .await;
151            }
152        });
153        Ok(())
154    }
155    /// start handle event thread
156    /// get the events that have been successfully monitored from the channel.
157    async fn start_handle_event<T, L>(&self, trade_callback: T, liquidity_callback: L)
158    where
159        T: Fn(TradeEvent) + 'static + Send,
160        L: Fn(LiquidityEvent) + 'static + Send,
161    {
162        let mut trade_receiver = self.trade_sender.subscribe();
163        let mut liquidity_receiver = self.liquidity_sender.subscribe();
164        // trade event
165        tokio::spawn(async move {
166            while let Ok(event) = trade_receiver.recv().await {
167                trade_callback(event);
168            }
169        });
170        // liquidity event
171        tokio::spawn(async move {
172            while let Ok(event) = liquidity_receiver.recv().await {
173                liquidity_callback(event);
174            }
175        });
176    }
177    /// try connection
178    async fn try_connection(&self) -> Result<(), String> {
179        let _slot = self
180            .client
181            .get_slot()
182            .await
183            .map_err(|e| format!("Client Get Slot Error:{:?}", e));
184        let _client = PubsubClient::new(&self.ws_url)
185            .await
186            .map_err(|e| format!("Client Connection Error:{:?}", e));
187        Ok(())
188    }
189    /// listen pump.fun program
190    async fn listen_to_program(
191        ws_url: &str,
192        program_id: &str,
193        trade_sender: &broadcast::Sender<TradeEvent>,
194        liquidity_sender: &broadcast::Sender<LiquidityEvent>,
195    ) -> Result<(), String> {
196        let client = PubsubClient::new(ws_url)
197            .await
198            .map_err(|e| format!("{:?}", e).to_string())?;
199        let filter = RpcTransactionLogsFilter::Mentions(vec![program_id.to_string()]);
200        let config = RpcTransactionLogsConfig {
201            commitment: Some(CommitmentConfig::confirmed()),
202        };
203        let (mut notifications, unsubscribe) = client
204            .logs_subscribe(filter, config)
205            .await
206            .map_err(|e| format!("{:?}", e).to_string())?;
207        while let Some(result) = notifications.next().await {
208            Self::handle_logs(&result, trade_sender, liquidity_sender).await;
209        }
210        let _ = unsubscribe().await;
211        Ok(())
212    }
213    /// handle logs
214    async fn handle_logs(
215        response: &Response<solana_client::rpc_response::RpcLogsResponse>,
216        trade_sender: &broadcast::Sender<TradeEvent>,
217        liquidity_sender: &broadcast::Sender<LiquidityEvent>,
218    ) {
219        let notification = &response.value;
220        let logs = &notification.logs;
221        let signature = &notification.signature;
222        if let Some(trade_event) = Self::handle_trade_event(&response, logs, signature).await {
223            // send to channel
224            let _ = trade_sender.send(trade_event);
225        }
226        if let Some(liquidity_event) =
227            Self::handle_liquidity_event(&response, logs, signature).await
228        {
229            // send to channel
230            let _ = liquidity_sender.send(liquidity_event);
231        }
232        // handle other events
233        Self::handle_other_events(&response, logs, signature).await;
234    }
235    /// handle trade event
236    async fn handle_trade_event(
237        response: &Response<solana_client::rpc_response::RpcLogsResponse>,
238        logs: &[String],
239        signature: &str,
240    ) -> Option<TradeEvent> {
241        let _ = response;
242        for log in logs {
243            let lower_log = log.to_lowercase();
244            let mut flag = false;
245            let mut type_enum: Option<TradeLogInstructionType> =
246                Some(TradeLogInstructionType::None);
247            if lower_log.contains("trade") {
248                flag = true;
249                type_enum = Some(TradeLogInstructionType::Trade);
250            }
251            if lower_log.contains("swap") {
252                flag = true;
253                type_enum = Some(TradeLogInstructionType::Swap);
254            }
255            if lower_log.contains("buy") {
256                flag = true;
257                type_enum = Some(TradeLogInstructionType::Buy);
258            }
259            if lower_log.contains("sell") {
260                flag = true;
261                type_enum = Some(TradeLogInstructionType::Sell);
262            }
263            if flag {
264                let mint = Self::get_mint_from_logs(logs).unwrap_or_else(|| "unknown".to_string());
265                return Some(TradeEvent {
266                    signature: signature.to_string(),
267                    mint,
268                    trade_event_type: type_enum.unwrap_or(TradeLogInstructionType::None),
269                });
270            }
271        }
272        None
273    }
274    /// handle liquidity event
275    async fn handle_liquidity_event(
276        response: &Response<solana_client::rpc_response::RpcLogsResponse>,
277        logs: &[String],
278        signature: &str,
279    ) -> Option<LiquidityEvent> {
280        let _ = response;
281        for log in logs {
282            let lower_log = log.to_lowercase();
283            let mut flag = false;
284            let mut type_enum: Option<LiquidityLogInstructionType> =
285                Some(LiquidityLogInstructionType::None);
286
287            if lower_log.contains("liquidity") {
288                flag = true;
289                type_enum = Some(LiquidityLogInstructionType::Liquidity);
290            }
291            if lower_log.contains("migrate") {
292                flag = true;
293                type_enum = Some(LiquidityLogInstructionType::Migrate);
294            }
295            if lower_log.contains("raydium") {
296                flag = true;
297                type_enum = Some(LiquidityLogInstructionType::Raydium);
298            }
299            if flag {
300                let mint = Self::get_mint_from_logs(logs).unwrap_or_else(|| "unknown".to_string());
301                return Some(LiquidityEvent {
302                    signature: signature.to_string(),
303                    mint,
304                    liquidity_event_type: type_enum.unwrap_or(LiquidityLogInstructionType::None),
305                });
306            }
307        }
308        None
309    }
310    /// handle other event
311    async fn handle_other_events(
312        response: &Response<solana_client::rpc_response::RpcLogsResponse>,
313        logs: &[String],
314        signature: &str,
315    ) {
316        let _ = response;
317        for log in logs {
318            let lower_log = log.to_lowercase();
319        }
320    }
321    /// get mint from logs.
322    fn get_mint_from_logs(logs: &[String]) -> Option<String> {
323        for log in logs {
324            if log.len() >= 32 && log.len() <= 44 {
325                if log.chars().all(|c| c.is_ascii_alphanumeric()) {
326                    return Some(log.clone());
327                }
328            }
329        }
330        for log in logs {
331            let words: Vec<&str> = log.split_whitespace().collect();
332            for word in words {
333                if word.len() >= 32
334                    && word.len() <= 44
335                    && word.chars().all(|c| c.is_ascii_alphanumeric())
336                {
337                    return Some(word.to_string());
338                }
339            }
340        }
341        None
342    }
343}