1#![allow(deprecated)]
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::Duration;
10
11use crossbeam_queue::ArrayQueue;
12use futures::StreamExt;
13use solana_entry::entry::Entry as SolanaEntry;
14use solana_sdk::message::v0::LoadedAddresses;
15use solana_sdk::transaction::VersionedTransaction;
16use tokio::sync::Mutex;
17use tokio::task::JoinHandle;
18use tonic::transport::{Channel, Endpoint};
19
20use crate::core::now_micros;
21use crate::grpc::types::EventTypeFilter;
22use crate::shredstream::config::ShredStreamConfig;
23use crate::shredstream::proto::{Entry, ShredstreamProxyClient, SubscribeEntriesRequest};
24use crate::DexEvent;
25
26static SHREDSTREAM_DROPPED_EVENTS: AtomicU64 = AtomicU64::new(0);
27
28enum EventSink<'a> {
29 Queue(&'a Arc<ArrayQueue<DexEvent>>),
30 Callback(&'a (dyn Fn(DexEvent) + Send + Sync)),
31}
32
33impl EventSink<'_> {
34 #[inline]
35 fn deliver(&self, event: DexEvent) {
36 match self {
37 EventSink::Queue(queue) => {
38 if queue.push(event).is_err() {
39 record_shredstream_dropped_event();
40 }
41 }
42 EventSink::Callback(callback) => callback(event),
43 }
44 }
45}
46
47#[inline]
48fn record_shredstream_dropped_event() -> u64 {
49 let dropped = SHREDSTREAM_DROPPED_EVENTS.fetch_add(1, Ordering::Relaxed) + 1;
50 if dropped <= 10 || dropped.is_power_of_two() {
51 log::warn!(
52 target: "sol_parser_sdk::shredstream",
53 "ShredStream event queue is full; dropped event count={}",
54 dropped
55 );
56 }
57 dropped
58}
59
60pub type AddressLookupResolver =
64 dyn Fn(&VersionedTransaction, u64) -> crate::common::AnyResult<LoadedAddresses> + Send + Sync;
65
66#[derive(Clone)]
68pub struct ShredStreamClient {
69 endpoint: String,
70 address_lookup_resolver: Option<Arc<AddressLookupResolver>>,
71 config: ShredStreamConfig,
72 subscription_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
73 subscription_lifecycle: Arc<Mutex<()>>,
74}
75
76impl ShredStreamClient {
77 pub async fn new(endpoint: impl Into<String>) -> crate::common::AnyResult<Self> {
79 Self::new_with_config(endpoint, ShredStreamConfig::default()).await
80 }
81
82 pub async fn new_with_config(
84 endpoint: impl Into<String>,
85 config: ShredStreamConfig,
86 ) -> crate::common::AnyResult<Self> {
87 let endpoint = endpoint.into();
88 let _ = Self::connect_client(&endpoint, &config).await?;
90
91 Ok(Self {
92 endpoint,
93 address_lookup_resolver: None,
94 config,
95 subscription_handle: Arc::new(Mutex::new(None)),
96 subscription_lifecycle: Arc::new(Mutex::new(())),
97 })
98 }
99
100 pub fn with_address_lookup_resolver<F>(mut self, resolver: F) -> Self
103 where
104 F: Fn(&VersionedTransaction, u64) -> crate::common::AnyResult<LoadedAddresses>
105 + Send
106 + Sync
107 + 'static,
108 {
109 self.address_lookup_resolver = Some(Arc::new(resolver));
110 self
111 }
112
113 pub async fn subscribe(&self) -> crate::common::AnyResult<Arc<ArrayQueue<DexEvent>>> {
117 self.subscribe_with_filter(None).await
118 }
119
120 pub async fn subscribe_with_filter(
124 &self,
125 event_type_filter: Option<EventTypeFilter>,
126 ) -> crate::common::AnyResult<Arc<ArrayQueue<DexEvent>>> {
127 let _lifecycle = self.subscription_lifecycle.lock().await;
128 self.stop_without_lifecycle_lock().await;
129
130 let queue = Arc::new(ArrayQueue::new(100_000));
131 let queue_clone = Arc::clone(&queue);
132
133 let endpoint = self.endpoint.clone();
134 let config = self.config.clone();
135 let resolver = self.address_lookup_resolver.clone();
136
137 let handle = tokio::spawn(async move {
138 let mut delay = config.reconnect_delay_ms;
139 let mut attempts = 0u32;
140
141 loop {
142 if config.max_reconnect_attempts > 0 && attempts >= config.max_reconnect_attempts {
143 log::error!("Max reconnection attempts reached, giving up");
144 break;
145 }
146 attempts += 1;
147
148 match Self::stream_events(
149 &endpoint,
150 &config,
151 event_type_filter.as_ref(),
152 EventSink::Queue(&queue_clone),
153 resolver.as_deref(),
154 )
155 .await
156 {
157 Ok(_) => {
158 log::warn!("ShredStream ended cleanly - reconnecting in {}ms", delay);
159 tokio::time::sleep(tokio::time::Duration::from_millis(delay.max(1))).await;
160 delay = config.reconnect_delay_ms;
161 attempts = 0;
162 }
163 Err(e) => {
164 log::error!("ShredStream error: {} - retry in {}ms", e, delay);
165 tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
166 delay = (delay * 2).min(60_000);
167 }
168 }
169 }
170 });
171
172 *self.subscription_handle.lock().await = Some(handle);
173 Ok(queue)
174 }
175
176 pub async fn subscribe_with_filter_callback<F>(
180 &self,
181 event_type_filter: Option<EventTypeFilter>,
182 callback: F,
183 ) -> crate::common::AnyResult<()>
184 where
185 F: Fn(DexEvent) + Send + Sync + 'static,
186 {
187 let _lifecycle = self.subscription_lifecycle.lock().await;
188 self.stop_without_lifecycle_lock().await;
189
190 let endpoint = self.endpoint.clone();
191 let config = self.config.clone();
192 let resolver = self.address_lookup_resolver.clone();
193 let callback = Arc::new(callback);
194
195 let handle = tokio::spawn(async move {
196 let mut delay = config.reconnect_delay_ms;
197 let mut attempts = 0u32;
198
199 loop {
200 if config.max_reconnect_attempts > 0 && attempts >= config.max_reconnect_attempts {
201 log::error!("Max reconnection attempts reached, giving up");
202 break;
203 }
204 attempts += 1;
205
206 match Self::stream_events_callback(
207 &endpoint,
208 &config,
209 event_type_filter.as_ref(),
210 callback.clone(),
211 resolver.as_deref(),
212 )
213 .await
214 {
215 Ok(_) => {
216 log::warn!("ShredStream ended cleanly - reconnecting in {}ms", delay);
217 tokio::time::sleep(tokio::time::Duration::from_millis(delay.max(1))).await;
218 delay = config.reconnect_delay_ms;
219 attempts = 0;
220 }
221 Err(e) => {
222 log::error!("ShredStream error: {} - retry in {}ms", e, delay);
223 tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
224 delay = (delay * 2).min(60_000);
225 }
226 }
227 }
228 });
229
230 *self.subscription_handle.lock().await = Some(handle);
231 Ok(())
232 }
233
234 pub async fn stop(&self) {
236 let _lifecycle = self.subscription_lifecycle.lock().await;
237 self.stop_without_lifecycle_lock().await;
238 }
239
240 async fn stop_without_lifecycle_lock(&self) {
241 if let Some(handle) = self.subscription_handle.lock().await.take() {
242 handle.abort();
243 let _ = handle.await;
244 }
245 }
246
247 async fn connect_client(
248 endpoint: &str,
249 config: &ShredStreamConfig,
250 ) -> crate::common::AnyResult<ShredstreamProxyClient<Channel>> {
251 let mut builder = Endpoint::from_shared(endpoint.to_string())?;
252 if config.connection_timeout_ms > 0 {
253 builder = builder.connect_timeout(Duration::from_millis(config.connection_timeout_ms));
254 }
255 let channel = builder.connect().await?;
256 Ok(ShredstreamProxyClient::new(channel)
257 .max_decoding_message_size(config.max_decoding_message_size))
258 }
259
260 async fn stream_events(
262 endpoint: &str,
263 config: &ShredStreamConfig,
264 event_type_filter: Option<&EventTypeFilter>,
265 sink: EventSink<'_>,
266 resolver: Option<&AddressLookupResolver>,
267 ) -> Result<(), String> {
268 let mut client = Self::connect_client(endpoint, config).await.map_err(|e| e.to_string())?;
269 let request = tonic::Request::new(SubscribeEntriesRequest {});
270 let response = if config.request_timeout_ms > 0 {
271 tokio::time::timeout(
272 Duration::from_millis(config.request_timeout_ms),
273 client.subscribe_entries(request),
274 )
275 .await
276 .map_err(|_| {
277 format!(
278 "ShredStream subscribe request timed out after {}ms",
279 config.request_timeout_ms
280 )
281 })?
282 .map_err(|e| e.to_string())?
283 } else {
284 client.subscribe_entries(request).await.map_err(|e| e.to_string())?
285 };
286 let mut stream = response.into_inner();
287
288 log::info!("ShredStream connected, receiving entries...");
289
290 let mut events = Vec::with_capacity(4);
291 while let Some(message) = stream.next().await {
292 match message {
293 Ok(entry) => {
294 Self::process_entry_with_resolver(
295 entry,
296 event_type_filter,
297 &sink,
298 &mut events,
299 resolver,
300 );
301 }
302 Err(e) => {
303 log::error!("Stream error: {:?}", e);
304 return Err(e.to_string());
305 }
306 }
307 }
308
309 Ok(())
310 }
311
312 async fn stream_events_callback(
313 endpoint: &str,
314 config: &ShredStreamConfig,
315 event_type_filter: Option<&EventTypeFilter>,
316 callback: Arc<dyn Fn(DexEvent) + Send + Sync>,
317 resolver: Option<&AddressLookupResolver>,
318 ) -> Result<(), String> {
319 Self::stream_events(
320 endpoint,
321 config,
322 event_type_filter,
323 EventSink::Callback(callback.as_ref()),
324 resolver,
325 )
326 .await
327 }
328
329 #[cfg(test)]
330 fn process_entry(
331 entry: Entry,
332 filter: Option<&EventTypeFilter>,
333 sink: &EventSink<'_>,
334 events: &mut Vec<DexEvent>,
335 ) {
336 Self::process_entry_with_resolver(entry, filter, sink, events, None);
337 }
338
339 #[inline]
341 fn process_entry_with_resolver(
342 entry: Entry,
343 event_type_filter: Option<&EventTypeFilter>,
344 sink: &EventSink<'_>,
345 events: &mut Vec<DexEvent>,
346 resolver: Option<&AddressLookupResolver>,
347 ) {
348 let slot = entry.slot;
349 let recv_us = now_micros();
350
351 let entries = match wincode::deserialize::<Vec<SolanaEntry>>(&entry.entries) {
353 Ok(e) => e,
354 Err(e) => {
355 log::debug!("Failed to deserialize entries: {}", e);
356 return;
357 }
358 };
359
360 let mut tx_index = 0u64;
362 for entry in entries {
363 for transaction in entry.transactions.iter() {
364 if transaction.sanitize().is_err() {
365 log::debug!("Ignoring unsanitized ShredStream transaction");
366 tx_index += 1;
367 continue;
368 }
369 events.clear();
370 Self::process_transaction(
371 transaction,
372 slot,
373 recv_us,
374 tx_index,
375 event_type_filter,
376 events,
377 sink,
378 resolver,
379 );
380 tx_index += 1;
381 }
382 }
383 }
384
385 #[inline]
387 fn process_transaction(
388 transaction: &solana_sdk::transaction::VersionedTransaction,
389 slot: u64,
390 recv_us: i64,
391 tx_index: u64,
392 event_type_filter: Option<&EventTypeFilter>,
393 events: &mut Vec<DexEvent>,
394 sink: &EventSink<'_>,
395 resolver: Option<&AddressLookupResolver>,
396 ) {
397 Self::parse_transaction_events(
398 transaction,
399 slot,
400 recv_us,
401 tx_index,
402 event_type_filter,
403 events,
404 resolver,
405 );
406
407 for event in events.drain(..) {
408 sink.deliver(event);
409 }
410 }
411
412 #[inline]
413 fn parse_transaction_events(
414 transaction: &solana_sdk::transaction::VersionedTransaction,
415 slot: u64,
416 recv_us: i64,
417 tx_index: u64,
418 event_type_filter: Option<&EventTypeFilter>,
419 events: &mut Vec<DexEvent>,
420 resolver: Option<&AddressLookupResolver>,
421 ) {
422 let Some(&signature) = transaction.signatures.first() else {
423 return;
424 };
425 if transaction.message.address_table_lookups().is_some_and(|lookups| !lookups.is_empty()) {
426 if let Some(resolver) = resolver {
427 let result = resolver(transaction, slot).and_then(|loaded| {
428 super::pump_ix::parse_transaction_dex_events_with_loaded_addresses(
429 transaction,
430 &loaded.writable,
431 &loaded.readonly,
432 signature,
433 slot,
434 tx_index,
435 recv_us,
436 event_type_filter,
437 events,
438 )?;
439 Ok(())
440 });
441 if let Err(error) = result {
442 log::debug!(
443 "Skipping unresolved ALT transaction {signature} at {slot}: {error}"
444 );
445 }
446 return;
447 }
448 }
449 super::pump_ix::parse_transaction_dex_events_with_filter(
451 transaction,
452 signature,
453 slot,
454 tx_index,
455 recv_us,
456 event_type_filter,
457 events,
458 );
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::core::events::{EventMetadata, PumpFunCreateTokenEvent};
466 use crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
467 use solana_sdk::hash::Hash;
468 use solana_sdk::message::{
469 compiled_instruction::CompiledInstruction, v0, MessageHeader, VersionedMessage,
470 };
471 use solana_sdk::pubkey::Pubkey;
472 use solana_sdk::signature::Signature;
473 use solana_sdk::transaction::VersionedTransaction;
474 use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
475
476 fn push_string(data: &mut Vec<u8>, value: &str) {
477 data.extend_from_slice(&(value.len() as u32).to_le_bytes());
478 data.extend_from_slice(value.as_bytes());
479 }
480
481 fn pumpfun_create_data() -> Vec<u8> {
482 let mut data = Vec::new();
483 data.extend_from_slice(&[24, 30, 200, 40, 5, 28, 7, 119]);
484 push_string(&mut data, "Callback Test");
485 push_string(&mut data, "CBT");
486 push_string(&mut data, "https://example.invalid/callback.json");
487 data.extend_from_slice(Pubkey::new_unique().as_ref());
488 data
489 }
490
491 fn pumpfun_create_tx() -> VersionedTransaction {
492 let mut account_keys = (0..10).map(|_| Pubkey::new_unique()).collect::<Vec<_>>();
493 account_keys.push(PUMPFUN_PROGRAM_ID);
494
495 VersionedTransaction {
496 signatures: vec![Signature::default()],
497 message: VersionedMessage::V0(v0::Message {
498 header: MessageHeader {
499 num_required_signatures: 1,
500 num_readonly_signed_accounts: 0,
501 num_readonly_unsigned_accounts: 1,
502 },
503 account_keys,
504 recent_blockhash: Hash::default(),
505 instructions: vec![CompiledInstruction::new_from_raw_parts(
506 10,
507 pumpfun_create_data(),
508 (0..10).collect(),
509 )],
510 address_table_lookups: Vec::new(),
511 }),
512 }
513 }
514
515 #[test]
516 fn dropped_counter_increments_without_panicking() {
517 let before = SHREDSTREAM_DROPPED_EVENTS.load(Ordering::Relaxed);
518 let queue = ArrayQueue::new(1);
519
520 queue
521 .push(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
522 metadata: EventMetadata::default(),
523 ..Default::default()
524 }))
525 .expect("first push fits");
526
527 if queue
528 .push(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
529 metadata: EventMetadata::default(),
530 ..Default::default()
531 }))
532 .is_err()
533 {
534 record_shredstream_dropped_event();
535 }
536
537 assert!(SHREDSTREAM_DROPPED_EVENTS.load(Ordering::Relaxed) > before);
538 }
539
540 #[test]
541 fn callback_path_delivers_events_without_queue() {
542 let entries = vec![SolanaEntry {
543 num_hashes: 1,
544 hash: Hash::default(),
545 transactions: vec![pumpfun_create_tx()],
546 }];
547 let entry = Entry { slot: 42, entries: wincode::serialize(&entries).unwrap() };
548 let count = AtomicUsize::new(0);
549
550 let mut events = Vec::with_capacity(4);
551 ShredStreamClient::process_entry(
552 entry,
553 None,
554 &EventSink::Callback(&|event| {
555 assert!(matches!(event, DexEvent::PumpFunCreate(_)));
556 assert_eq!(event.metadata().slot, 42);
557 count.fetch_add(1, AtomicOrdering::Relaxed);
558 }),
559 &mut events,
560 );
561
562 assert_eq!(count.load(AtomicOrdering::Relaxed), 1);
563 }
564
565 #[test]
566 fn wincode_decodes_solana4_v1_entries() {
567 let entries = vec![SolanaEntry {
568 num_hashes: 1,
569 hash: Hash::new_unique(),
570 transactions: vec![VersionedTransaction {
571 signatures: vec![Signature::from([9; 64])],
572 message: VersionedMessage::V1(solana_sdk::message::v1::Message {
573 header: MessageHeader { num_required_signatures: 1, ..Default::default() },
574 config: solana_sdk::message::v1::TransactionConfig::empty()
575 .with_compute_unit_limit(250_000)
576 .with_priority_fee(1_500),
577 lifetime_specifier: Hash::new_unique(),
578 account_keys: vec![Pubkey::new_unique()],
579 instructions: Vec::new(),
580 }),
581 }],
582 }];
583 let bytes = wincode::serialize(&entries).expect("serialize V1 Entry");
584 let decoded: Vec<SolanaEntry> = wincode::deserialize(&bytes).expect("decode V1 Entry");
585
586 assert_eq!(decoded, entries);
587 }
588
589 #[test]
590 fn process_entry_rejects_unsanitized_transactions() {
591 let mut transaction = pumpfun_create_tx();
592 let VersionedMessage::V0(message) = &mut transaction.message else {
593 panic!("expected V0 fixture");
594 };
595 message.instructions[0].program_id_index = u8::MAX;
596 let entries = vec![SolanaEntry {
597 num_hashes: 1,
598 hash: Hash::default(),
599 transactions: vec![transaction],
600 }];
601 let entry = Entry { slot: 42, entries: wincode::serialize(&entries).unwrap() };
602 let count = AtomicUsize::new(0);
603 let mut events = Vec::with_capacity(4);
604
605 ShredStreamClient::process_entry(
606 entry,
607 None,
608 &EventSink::Callback(&|_| {
609 count.fetch_add(1, AtomicOrdering::Relaxed);
610 }),
611 &mut events,
612 );
613
614 assert_eq!(count.load(AtomicOrdering::Relaxed), 0);
615 }
616 #[test]
617 fn cached_alt_routes_reach_queue_and_callback_without_network() {
618 use base64::{engine::general_purpose::STANDARD, Engine};
619 let fixtures: serde_json::Value =
620 serde_json::from_str(include_str!("../../tests/fixtures/shred_routes/reported.json"))
621 .unwrap();
622 let f = fixtures
623 .as_array()
624 .unwrap()
625 .iter()
626 .find(|f| !f["loaded_writable"].as_array().unwrap().is_empty())
627 .unwrap();
628 let tx: VersionedTransaction =
629 wincode::deserialize_exact(&STANDARD.decode(f["wire"].as_str().unwrap()).unwrap())
630 .unwrap();
631 let loaded = LoadedAddresses {
632 writable: f["loaded_writable"]
633 .as_array()
634 .unwrap()
635 .iter()
636 .map(|s| s.as_str().unwrap().parse().unwrap())
637 .collect(),
638 readonly: f["loaded_readonly"]
639 .as_array()
640 .unwrap()
641 .iter()
642 .map(|s| s.as_str().unwrap().parse().unwrap())
643 .collect(),
644 };
645 let slot = f["slot"].as_u64().unwrap();
646 let signature = tx.signatures[0];
647 let calls = Arc::new(AtomicUsize::new(0));
648 let resolver_calls = Arc::clone(&calls);
649 let resolver = move |transaction: &VersionedTransaction,
650 at: u64|
651 -> crate::common::AnyResult<LoadedAddresses> {
652 assert_eq!(transaction.signatures[0], signature);
653 assert_eq!(at, slot);
654 resolver_calls.fetch_add(1, AtomicOrdering::Relaxed);
655 Ok(loaded.clone())
656 };
657 let entry = || Entry {
658 slot,
659 entries: wincode::serialize(&vec![SolanaEntry {
660 num_hashes: 1,
661 hash: Hash::default(),
662 transactions: vec![tx.clone()],
663 }])
664 .unwrap(),
665 };
666 let filter = EventTypeFilter::include_only(vec![crate::grpc::EventType::PumpFunTrade]);
667 let queue = Arc::new(ArrayQueue::new(4));
668 let mut events = Vec::new();
669 ShredStreamClient::process_entry_with_resolver(
670 entry(),
671 Some(&filter),
672 &EventSink::Queue(&queue),
673 &mut events,
674 Some(&resolver),
675 );
676 assert!(matches!(queue.pop(), Some(DexEvent::PumpRouteIntent(_))));
677 let callback_count = AtomicUsize::new(0);
678 ShredStreamClient::process_entry_with_resolver(
679 entry(),
680 Some(&filter),
681 &EventSink::Callback(&|event| {
682 assert!(matches!(event, DexEvent::PumpRouteIntent(_)));
683 assert_eq!(event.metadata().slot, slot);
684 callback_count.fetch_add(1, AtomicOrdering::Relaxed);
685 }),
686 &mut events,
687 Some(&resolver),
688 );
689 assert_eq!(callback_count.load(AtomicOrdering::Relaxed), 1);
690 assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
691 ShredStreamClient::process_entry_with_resolver(
693 entry(),
694 None,
695 &EventSink::Queue(&queue),
696 &mut events,
697 None,
698 );
699 assert!(queue.pop().is_none());
700 let miss =
701 |_: &VersionedTransaction, _: u64| -> crate::common::AnyResult<LoadedAddresses> {
702 Err(std::io::Error::new(std::io::ErrorKind::NotFound, "ALT cache miss").into())
703 };
704 ShredStreamClient::process_entry_with_resolver(
705 entry(),
706 None,
707 &EventSink::Queue(&queue),
708 &mut events,
709 Some(&miss),
710 );
711 assert!(queue.pop().is_none());
712 let wrong_count =
713 |_: &VersionedTransaction, _: u64| -> crate::common::AnyResult<LoadedAddresses> {
714 Ok(LoadedAddresses::default())
715 };
716 ShredStreamClient::process_entry_with_resolver(
717 entry(),
718 None,
719 &EventSink::Queue(&queue),
720 &mut events,
721 Some(&wrong_count),
722 );
723 assert!(queue.pop().is_none());
724 }
725}