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#[derive(Debug, Clone)]
34pub struct TradeEvent {
35 pub signature: String,
36 pub mint: String,
37 pub trade_event_type: TradeLogInstructionType,
38}
39
40#[derive(Debug, Clone)]
42pub struct LiquidityEvent {
43 pub signature: String,
44 pub mint: String,
45 pub liquidity_event_type: LiquidityLogInstructionType,
46}
47
48pub 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 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 if let Err(e) = self.try_connection().await {
131 return Err(format!("Try Connection error: {:?}", e));
132 }
133 self.start_handle_event(trade_callback, liquidity_callback)
135 .await;
136 self.start_program_listener(PUMP_DOT_FUN_PROGRAM_ID.to_string())
138 .await?;
139 Ok(())
140 }
141 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 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 tokio::spawn(async move {
166 while let Ok(event) = trade_receiver.recv().await {
167 trade_callback(event);
168 }
169 });
170 tokio::spawn(async move {
172 while let Ok(event) = liquidity_receiver.recv().await {
173 liquidity_callback(event);
174 }
175 });
176 }
177 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 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 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 = ¬ification.logs;
221 let signature = ¬ification.signature;
222 if let Some(trade_event) = Self::handle_trade_event(&response, logs, signature).await {
223 let _ = trade_sender.send(trade_event);
225 }
226 if let Some(liquidity_event) =
227 Self::handle_liquidity_event(&response, logs, signature).await
228 {
229 let _ = liquidity_sender.send(liquidity_event);
231 }
232 Self::handle_other_events(&response, logs, signature).await;
234 }
235 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 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 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 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}