1use std::net::SocketAddr;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4use std::time::Duration;
5
6use crossbeam_queue::ArrayQueue;
7use futures::StreamExt;
8use solana_entry::entry::Entry;
9use solana_sdk::message::VersionedMessage;
10use solana_sdk::transaction::VersionedTransaction;
11use tokio::sync::Mutex;
12use tokio::task::JoinHandle;
13
14use super::config::{JitoShredStreamConfig, ShredDecodeMode, ShredStreamConfig};
15use crate::common::logs_events::PumpfunEvent;
16use crate::common::AnyResult;
17use crate::core::{now_micros, DexEvent};
18use crate::grpc::shredstream::shredstream_proxy_client::ShredstreamProxyClient;
19use crate::grpc::shredstream::SubscribeEntriesRequest;
20use crate::grpc::EventTypeFilter;
21use crate::parser::{PumpfunEventParser, PumpfunParserConfig, TransactionEventParser};
22use crate::shred::{RawShredClient, RawShredConfig, ShredEntryBatch};
23
24static DROPPED_EVENTS: AtomicU64 = AtomicU64::new(0);
25
26enum EventSink<E> {
27 Queue(Arc<ArrayQueue<E>>),
28 Callback(Arc<dyn Fn(E) + Send + Sync>),
29}
30
31impl<E> Clone for EventSink<E> {
32 fn clone(&self) -> Self {
33 match self {
34 Self::Queue(queue) => Self::Queue(Arc::clone(queue)),
35 Self::Callback(callback) => Self::Callback(Arc::clone(callback)),
36 }
37 }
38}
39
40impl<E> EventSink<E> {
41 #[inline]
42 fn deliver(&self, event: E) {
43 match self {
44 EventSink::Queue(queue) => {
45 if queue.push(event).is_err() {
46 record_dropped_event();
47 }
48 }
49 EventSink::Callback(callback) => callback(event),
50 }
51 }
52}
53
54pub fn dropped_events() -> u64 {
55 DROPPED_EVENTS.load(Ordering::Relaxed)
56}
57
58#[inline]
59fn record_dropped_event() {
60 let dropped = DROPPED_EVENTS.fetch_add(1, Ordering::Relaxed) + 1;
61 if dropped <= 10 || dropped.is_power_of_two() {
62 log::warn!("raw shred event queue is full; dropped_event_count={dropped}");
63 }
64}
65
66#[derive(Clone)]
68pub struct ShredStreamClient {
69 config: ShredStreamConfig,
70 subscription_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
71}
72
73impl ShredStreamClient {
74 pub async fn new(udp_bind: impl Into<String>) -> AnyResult<Self> {
76 let udp_bind: SocketAddr = udp_bind.into().parse()?;
77 Self::new_with_config(ShredStreamConfig::default().with_udp_bind(udp_bind)).await
78 }
79
80 pub async fn new_with_decode_mode(decode_mode: ShredDecodeMode) -> AnyResult<Self> {
81 Self::new_with_config(ShredStreamConfig::default().with_decode_mode(decode_mode)).await
82 }
83
84 pub async fn new_jito_grpc(endpoint: impl Into<String>) -> AnyResult<Self> {
85 Self::new_with_config(ShredStreamConfig::jito_grpc(endpoint)).await
86 }
87
88 pub async fn new_with_config(config: ShredStreamConfig) -> AnyResult<Self> {
89 Ok(Self {
90 config,
91 subscription_handle: Arc::new(Mutex::new(None)),
92 })
93 }
94
95 pub async fn subscribe(&self) -> AnyResult<Arc<ArrayQueue<DexEvent>>> {
100 self.subscribe_with_filter(None).await
101 }
102
103 pub async fn subscribe_with_filter(
105 &self,
106 event_type_filter: Option<EventTypeFilter>,
107 ) -> AnyResult<Arc<ArrayQueue<DexEvent>>> {
108 self.stop().await;
109
110 let queue = Arc::new(ArrayQueue::new(self.config.event_queue_capacity.max(1)));
111 self.spawn_dex_parser(event_type_filter, EventSink::Queue(Arc::clone(&queue)))
112 .await?;
113 Ok(queue)
114 }
115
116 pub async fn subscribe_callback<F>(&self, callback: F) -> AnyResult<()>
119 where
120 F: Fn(DexEvent) + Send + Sync + 'static,
121 {
122 self.subscribe_with_filter_callback(None, callback).await
123 }
124
125 pub async fn subscribe_with_filter_callback<F>(
127 &self,
128 event_type_filter: Option<EventTypeFilter>,
129 callback: F,
130 ) -> AnyResult<()>
131 where
132 F: Fn(DexEvent) + Send + Sync + 'static,
133 {
134 self.stop().await;
135 self.spawn_dex_parser(event_type_filter, EventSink::Callback(Arc::new(callback)))
136 .await
137 }
138
139 pub async fn subscribe_pumpfun(
141 &self,
142 parser_config: PumpfunParserConfig,
143 ) -> AnyResult<Arc<ArrayQueue<PumpfunEvent>>> {
144 self.subscribe_with_parser(PumpfunEventParser::new(parser_config))
145 .await
146 }
147
148 pub async fn subscribe_pumpfun_callback<F>(
151 &self,
152 parser_config: PumpfunParserConfig,
153 callback: F,
154 ) -> AnyResult<()>
155 where
156 F: Fn(PumpfunEvent) + Send + Sync + 'static,
157 {
158 self.subscribe_with_parser_callback(PumpfunEventParser::new(parser_config), callback)
159 .await
160 }
161
162 pub async fn subscribe_with_parser<P>(&self, parser: P) -> AnyResult<Arc<ArrayQueue<P::Event>>>
164 where
165 P: TransactionEventParser + Send + 'static,
166 P::Event: Send + 'static,
167 {
168 self.stop().await;
169
170 let queue = Arc::new(ArrayQueue::new(self.config.event_queue_capacity.max(1)));
171 self.spawn_parser(parser, EventSink::Queue(Arc::clone(&queue)))
172 .await?;
173 Ok(queue)
174 }
175
176 pub async fn subscribe_with_parser_callback<P, F>(
178 &self,
179 parser: P,
180 callback: F,
181 ) -> AnyResult<()>
182 where
183 P: TransactionEventParser + Send + 'static,
184 P::Event: Send + 'static,
185 F: Fn(P::Event) + Send + Sync + 'static,
186 {
187 self.stop().await;
188 self.spawn_parser(parser, EventSink::Callback(Arc::new(callback)))
189 .await
190 }
191
192 pub async fn stop(&self) {
193 if let Some(handle) = self.subscription_handle.lock().await.take() {
194 handle.abort();
195 }
196 }
197
198 async fn spawn_parser<P>(&self, parser: P, sink: EventSink<P::Event>) -> AnyResult<()>
199 where
200 P: TransactionEventParser + Send + 'static,
201 P::Event: Send + 'static,
202 {
203 let config = self.config.clone();
204 let handle = tokio::spawn(async move {
205 run_with_restarts(config, parser, sink).await;
206 });
207
208 *self.subscription_handle.lock().await = Some(handle);
209 Ok(())
210 }
211
212 async fn spawn_dex_parser(
213 &self,
214 event_type_filter: Option<EventTypeFilter>,
215 sink: EventSink<DexEvent>,
216 ) -> AnyResult<()> {
217 let config = self.config.clone();
218 let handle = tokio::spawn(async move {
219 run_dex_with_restarts(config, event_type_filter, sink).await;
220 });
221
222 *self.subscription_handle.lock().await = Some(handle);
223 Ok(())
224 }
225}
226
227async fn run_dex_with_restarts(
228 config: ShredStreamConfig,
229 event_type_filter: Option<EventTypeFilter>,
230 sink: EventSink<DexEvent>,
231) {
232 let mut attempts = 0u32;
233
234 loop {
235 if config.max_reconnect_attempts > 0 && attempts >= config.max_reconnect_attempts {
236 log::error!(
237 "{} dex client stopped after {attempts} failed restart attempts",
238 config.decode_mode.name()
239 );
240 return;
241 }
242 attempts += 1;
243
244 match run_dex_once(&config, event_type_filter.as_ref(), sink.clone()).await {
245 Ok(()) => {
246 attempts = 0;
247 }
248 Err(error) => {
249 log::error!(
250 "{} dex receive loop failed: {error}; retrying in {}ms",
251 config.decode_mode.name(),
252 config.reconnect_delay_ms
253 );
254 tokio::time::sleep(Duration::from_millis(config.reconnect_delay_ms)).await;
255 }
256 }
257 }
258}
259
260async fn run_dex_once(
261 config: &ShredStreamConfig,
262 event_type_filter: Option<&EventTypeFilter>,
263 sink: EventSink<DexEvent>,
264) -> AnyResult<()> {
265 match &config.decode_mode {
266 ShredDecodeMode::RawUdp(raw) => {
267 run_raw_dex_once(raw.clone(), event_type_filter, sink).await
268 }
269 ShredDecodeMode::JitoGrpc(jito) => run_jito_dex_once(jito, event_type_filter, sink).await,
270 }
271}
272
273async fn run_raw_dex_once(
274 raw: RawShredConfig,
275 event_type_filter: Option<&EventTypeFilter>,
276 sink: EventSink<DexEvent>,
277) -> AnyResult<()> {
278 let mut client = RawShredClient::bind(raw)
279 .await
280 .map_err(|error| anyhow::anyhow!(error.to_string()))?;
281 let mut events = Vec::with_capacity(4);
282 client
283 .run_entries(|batch| process_dex_entry_batch(batch, event_type_filter, &sink, &mut events))
284 .await
285 .map_err(|error| anyhow::anyhow!(error.to_string()))?;
286 Ok(())
287}
288
289async fn run_jito_dex_once(
290 jito: &JitoShredStreamConfig,
291 event_type_filter: Option<&EventTypeFilter>,
292 sink: EventSink<DexEvent>,
293) -> AnyResult<()> {
294 let mut client = ShredstreamProxyClient::connect(jito.endpoint.clone()).await?;
295 let request = tonic::Request::new(SubscribeEntriesRequest {});
296 let mut stream = client.subscribe_entries(request).await?.into_inner();
297 let mut events = Vec::with_capacity(4);
298
299 while let Some(message) = stream.next().await {
300 let message = message?;
301 let Ok(entries) = bincode::deserialize::<Vec<Entry>>(&message.entries) else {
302 log::debug!(
303 "jito grpc entry decode failed slot={} bytes_len={}",
304 message.slot,
305 message.entries.len()
306 );
307 continue;
308 };
309
310 process_dex_entry_batch(
311 ShredEntryBatch {
312 slot: message.slot,
313 entries,
314 },
315 event_type_filter,
316 &sink,
317 &mut events,
318 );
319 }
320
321 Ok(())
322}
323
324#[inline]
325fn process_dex_entry_batch(
326 batch: ShredEntryBatch,
327 event_type_filter: Option<&EventTypeFilter>,
328 sink: &EventSink<DexEvent>,
329 events: &mut Vec<DexEvent>,
330) {
331 let recv_us = now_micros();
332 let mut tx_index = 0u64;
333
334 for entry in batch.entries {
335 for transaction in &entry.transactions {
336 events.clear();
337 process_dex_transaction(
338 transaction,
339 batch.slot,
340 tx_index,
341 recv_us,
342 event_type_filter,
343 events,
344 sink,
345 );
346 tx_index += 1;
347 }
348 }
349}
350
351#[inline]
352fn process_dex_transaction(
353 transaction: &VersionedTransaction,
354 slot: u64,
355 tx_index: u64,
356 recv_us: i64,
357 event_type_filter: Option<&EventTypeFilter>,
358 events: &mut Vec<DexEvent>,
359 sink: &EventSink<DexEvent>,
360) {
361 parse_dex_transaction_events(
362 transaction,
363 slot,
364 tx_index,
365 recv_us,
366 event_type_filter,
367 events,
368 );
369
370 for event in events.drain(..) {
371 sink.deliver(event);
372 }
373}
374
375#[inline]
376fn parse_dex_transaction_events(
377 transaction: &VersionedTransaction,
378 slot: u64,
379 tx_index: u64,
380 recv_us: i64,
381 event_type_filter: Option<&EventTypeFilter>,
382 events: &mut Vec<DexEvent>,
383) {
384 if transaction.signatures.is_empty() {
385 return;
386 }
387
388 let signature = transaction.signatures[0];
389 if let VersionedMessage::V0(message) = &transaction.message {
390 if !message.address_table_lookups.is_empty() {
391 log::trace!(
392 target: "sol_shred_sdk::shredstream",
393 "V0 tx uses address lookup tables; raw shred parser uses static accounts and default placeholders for ALT-loaded accounts"
394 );
395 }
396 }
397
398 super::dex::parse_transaction_dex_events_with_filter(
399 transaction,
400 signature,
401 slot,
402 tx_index,
403 recv_us,
404 event_type_filter,
405 events,
406 );
407 crate::core::pumpfun_fee_enrich::enrich_pumpfun_same_tx_post_merge(events);
408
409 for event in events.iter_mut() {
410 if let Some(metadata) = event.metadata_mut() {
411 metadata.grpc_recv_us = recv_us;
412 }
413 }
414}
415
416async fn run_with_restarts<P>(config: ShredStreamConfig, mut parser: P, sink: EventSink<P::Event>)
417where
418 P: TransactionEventParser + Send + 'static,
419 P::Event: Send + 'static,
420{
421 let mut attempts = 0u32;
422
423 loop {
424 if config.max_reconnect_attempts > 0 && attempts >= config.max_reconnect_attempts {
425 log::error!(
426 "{} client stopped after {attempts} failed restart attempts",
427 config.decode_mode.name()
428 );
429 return;
430 }
431 attempts += 1;
432
433 match run_once(&config, &mut parser, sink.clone()).await {
434 Ok(()) => {
435 attempts = 0;
436 }
437 Err(error) => {
438 log::error!(
439 "{} receive loop failed: {error}; retrying in {}ms",
440 config.decode_mode.name(),
441 config.reconnect_delay_ms
442 );
443 tokio::time::sleep(Duration::from_millis(config.reconnect_delay_ms)).await;
444 }
445 }
446 }
447}
448
449async fn run_once<P>(
450 config: &ShredStreamConfig,
451 parser: &mut P,
452 sink: EventSink<P::Event>,
453) -> AnyResult<()>
454where
455 P: TransactionEventParser + Send + 'static,
456 P::Event: Send + 'static,
457{
458 match &config.decode_mode {
459 ShredDecodeMode::RawUdp(raw) => run_raw_once(raw.clone(), parser, sink).await,
460 ShredDecodeMode::JitoGrpc(jito) => run_jito_once(jito, parser, sink).await,
461 }
462}
463
464async fn run_raw_once<P>(
465 raw: RawShredConfig,
466 parser: &mut P,
467 sink: EventSink<P::Event>,
468) -> AnyResult<()>
469where
470 P: TransactionEventParser + Send + 'static,
471 P::Event: Send + 'static,
472{
473 let mut client = RawShredClient::bind(raw)
474 .await
475 .map_err(|error| anyhow::anyhow!(error.to_string()))?;
476 client
477 .run_entries(|batch| process_entry_batch(batch, parser, &sink))
478 .await
479 .map_err(|error| anyhow::anyhow!(error.to_string()))?;
480 Ok(())
481}
482
483async fn run_jito_once<P>(
484 jito: &JitoShredStreamConfig,
485 parser: &mut P,
486 sink: EventSink<P::Event>,
487) -> AnyResult<()>
488where
489 P: TransactionEventParser + Send + 'static,
490 P::Event: Send + 'static,
491{
492 let mut client = ShredstreamProxyClient::connect(jito.endpoint.clone()).await?;
493 let request = tonic::Request::new(SubscribeEntriesRequest {});
494 let mut stream = client.subscribe_entries(request).await?.into_inner();
495
496 while let Some(message) = stream.next().await {
497 let message = message?;
498 let Ok(entries) = bincode::deserialize::<Vec<Entry>>(&message.entries) else {
499 log::debug!(
500 "jito grpc entry decode failed slot={} bytes_len={}",
501 message.slot,
502 message.entries.len()
503 );
504 continue;
505 };
506
507 process_entry_batch(
508 ShredEntryBatch {
509 slot: message.slot,
510 entries,
511 },
512 parser,
513 &sink,
514 );
515 }
516
517 Ok(())
518}
519
520#[inline]
521fn process_entry_batch<P>(batch: ShredEntryBatch, parser: &mut P, sink: &EventSink<P::Event>)
522where
523 P: TransactionEventParser,
524{
525 let recv_us = now_micros();
526 let mut tx_index = 0u64;
527
528 for entry in batch.entries {
529 for transaction in &entry.transactions {
530 if transaction.signatures.is_empty() {
531 tx_index += 1;
532 continue;
533 }
534
535 if let Err(error) = parser.parse_transaction_events(
536 transaction,
537 batch.slot,
538 tx_index,
539 recv_us,
540 |event| sink.deliver(event),
541 ) {
542 log::debug!(
543 "raw shred transaction parse failed slot={} tx_index={tx_index}: {error}",
544 batch.slot
545 );
546 }
547 tx_index += 1;
548 }
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use crate::instr::program_ids::RAYDIUM_CPMM_PROGRAM_ID;
556 use solana_entry::entry::Entry;
557 use solana_sdk::hash::Hash;
558 use solana_sdk::message::{
559 compiled_instruction::CompiledInstruction, v0, MessageHeader, VersionedMessage,
560 };
561 use solana_sdk::pubkey::Pubkey;
562 use solana_sdk::signature::Signature;
563
564 fn raydium_cpmm_swap_data() -> Vec<u8> {
565 let mut data = Vec::new();
566 data.extend_from_slice(&crate::instr::raydium_cpmm::discriminators::SWAP_BASE_IN);
567 data.extend_from_slice(&100_u64.to_le_bytes());
568 data.extend_from_slice(&90_u64.to_le_bytes());
569 data
570 }
571
572 fn raydium_cpmm_swap_tx() -> VersionedTransaction {
573 VersionedTransaction {
574 signatures: vec![Signature::default()],
575 message: VersionedMessage::V0(v0::Message {
576 header: MessageHeader {
577 num_required_signatures: 1,
578 num_readonly_signed_accounts: 0,
579 num_readonly_unsigned_accounts: 0,
580 },
581 account_keys: vec![RAYDIUM_CPMM_PROGRAM_ID, Pubkey::new_unique()],
582 recent_blockhash: Hash::default(),
583 instructions: vec![CompiledInstruction::new_from_raw_parts(
584 0,
585 raydium_cpmm_swap_data(),
586 vec![1],
587 )],
588 address_table_lookups: Vec::new(),
589 }),
590 }
591 }
592
593 #[test]
594 fn default_dex_path_delivers_non_pump_events() {
595 let queue = Arc::new(ArrayQueue::new(4));
596 let sink = EventSink::Queue(Arc::clone(&queue));
597 let mut events = Vec::with_capacity(4);
598 let batch = ShredEntryBatch {
599 slot: 77,
600 entries: vec![Entry {
601 num_hashes: 1,
602 hash: Hash::default(),
603 transactions: vec![raydium_cpmm_swap_tx()],
604 }],
605 };
606
607 process_dex_entry_batch(batch, None, &sink, &mut events);
608
609 let event = queue.pop().expect("event");
610 assert!(matches!(event, DexEvent::RaydiumCpmmSwap(_)));
611 assert_eq!(event.metadata().slot, 77);
612 assert!(event.metadata().grpc_recv_us > 0);
613 assert!(queue.pop().is_none());
614 }
615
616 #[test]
617 fn dex_filter_is_applied_before_delivery() {
618 let queue = Arc::new(ArrayQueue::new(4));
619 let sink = EventSink::Queue(Arc::clone(&queue));
620 let mut events = Vec::with_capacity(4);
621 let batch = ShredEntryBatch {
622 slot: 77,
623 entries: vec![Entry {
624 num_hashes: 1,
625 hash: Hash::default(),
626 transactions: vec![raydium_cpmm_swap_tx()],
627 }],
628 };
629 let filter = EventTypeFilter::include_only(vec![crate::grpc::EventType::PumpFunBuy]);
630
631 process_dex_entry_batch(batch, Some(&filter), &sink, &mut events);
632
633 assert!(queue.pop().is_none());
634 }
635}