Skip to main content

sol_shred_sdk/shredstream/
client.rs

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/// High-level multi-source shred subscription client.
67#[derive(Clone)]
68pub struct ShredStreamClient {
69    config: ShredStreamConfig,
70    subscription_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
71}
72
73impl ShredStreamClient {
74    /// Create a client bound to a raw UDP address, for example `0.0.0.0:8001`.
75    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    /// Subscribe to all supported DEX events and return a lock-free queue.
96    ///
97    /// All supported decode modes feed the same parser:
98    /// source -> `Entry` -> `VersionedTransaction` -> `DexEvent`.
99    pub async fn subscribe(&self) -> AnyResult<Arc<ArrayQueue<DexEvent>>> {
100        self.subscribe_with_filter(None).await
101    }
102
103    /// Subscribe to DEX events with parser-side event filtering.
104    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    /// Lowest-latency built-in DEX path: event callback runs in the source
117    /// receive task.
118    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    /// Lowest-latency built-in DEX path with event filtering.
126    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    /// Compatibility convenience for the legacy PumpFun/Bonk parser.
140    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    /// Lowest-latency built-in PumpFun/Bonk path: event callback runs in the
149    /// source receive task.
150    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    /// Subscribe with a custom parser and return a queue of parser events.
163    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    /// Subscribe with a custom parser and direct event callback.
177    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}