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;
363 for entry in entries {
364 for transaction in entry.transactions.iter() {
365 if transaction.sanitize().is_err() {
366 log::debug!("Ignoring unsanitized ShredStream transaction");
367 tx_index += 1;
368 continue;
369 }
370 events.clear();
371 Self::process_transaction(
372 transaction,
373 slot,
374 recv_us,
375 tx_index,
376 event_type_filter,
377 events,
378 sink,
379 resolver,
380 );
381 tx_index += 1;
382 }
383 }
384 }
385
386 #[inline]
388 fn process_transaction(
389 transaction: &solana_sdk::transaction::VersionedTransaction,
390 slot: u64,
391 recv_us: i64,
392 tx_index: u64,
393 event_type_filter: Option<&EventTypeFilter>,
394 events: &mut Vec<DexEvent>,
395 sink: &EventSink<'_>,
396 resolver: Option<&AddressLookupResolver>,
397 ) {
398 Self::parse_transaction_events(
399 transaction,
400 slot,
401 recv_us,
402 tx_index,
403 event_type_filter,
404 events,
405 resolver,
406 );
407
408 for event in events.drain(..) {
409 sink.deliver(event);
410 }
411 }
412
413 #[inline]
414 fn parse_transaction_events(
415 transaction: &solana_sdk::transaction::VersionedTransaction,
416 slot: u64,
417 recv_us: i64,
418 tx_index: u64,
419 event_type_filter: Option<&EventTypeFilter>,
420 events: &mut Vec<DexEvent>,
421 resolver: Option<&AddressLookupResolver>,
422 ) {
423 let Some(&signature) = transaction.signatures.first() else {
424 return;
425 };
426 if transaction.message.address_table_lookups().is_some_and(|lookups| !lookups.is_empty()) {
427 if let Some(resolver) = resolver {
428 let result = resolver(transaction, slot).and_then(|loaded| {
429 super::pump_ix::parse_transaction_dex_events_with_loaded_addresses(
430 transaction,
431 &loaded.writable,
432 &loaded.readonly,
433 signature,
434 slot,
435 tx_index,
436 recv_us,
437 event_type_filter,
438 events,
439 )?;
440 Ok(())
441 });
442 if let Err(error) = result {
443 log::debug!(
444 "Skipping unresolved ALT transaction {signature} at {slot}: {error}"
445 );
446 }
447 return;
448 }
449 }
450 super::pump_ix::parse_transaction_dex_events_with_filter(
452 transaction,
453 signature,
454 slot,
455 tx_index,
456 recv_us,
457 event_type_filter,
458 events,
459 );
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 use crate::core::events::{EventMetadata, PumpFunCreateTokenEvent};
467 use crate::instr::program_ids::PUMPFUN_PROGRAM_ID;
468 use solana_sdk::hash::Hash;
469 use solana_sdk::message::{
470 compiled_instruction::CompiledInstruction, v0, MessageHeader, VersionedMessage,
471 };
472 use solana_sdk::pubkey::Pubkey;
473 use solana_sdk::signature::Signature;
474 use solana_sdk::transaction::VersionedTransaction;
475 use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
476
477 fn push_string(data: &mut Vec<u8>, value: &str) {
478 data.extend_from_slice(&(value.len() as u32).to_le_bytes());
479 data.extend_from_slice(value.as_bytes());
480 }
481
482 fn pumpfun_create_data() -> Vec<u8> {
483 let mut data = Vec::new();
484 data.extend_from_slice(&[24, 30, 200, 40, 5, 28, 7, 119]);
485 push_string(&mut data, "Callback Test");
486 push_string(&mut data, "CBT");
487 push_string(&mut data, "https://example.invalid/callback.json");
488 data.extend_from_slice(Pubkey::new_unique().as_ref());
489 data
490 }
491
492 fn pumpfun_create_tx() -> VersionedTransaction {
493 let mut account_keys = (0..10).map(|_| Pubkey::new_unique()).collect::<Vec<_>>();
494 account_keys.push(PUMPFUN_PROGRAM_ID);
495
496 VersionedTransaction {
497 signatures: vec![Signature::default()],
498 message: VersionedMessage::V0(v0::Message {
499 header: MessageHeader {
500 num_required_signatures: 1,
501 num_readonly_signed_accounts: 0,
502 num_readonly_unsigned_accounts: 1,
503 },
504 account_keys,
505 recent_blockhash: Hash::default(),
506 instructions: vec![CompiledInstruction::new_from_raw_parts(
507 10,
508 pumpfun_create_data(),
509 (0..10).collect(),
510 )],
511 address_table_lookups: Vec::new(),
512 }),
513 }
514 }
515
516 #[test]
517 fn dropped_counter_increments_without_panicking() {
518 let before = SHREDSTREAM_DROPPED_EVENTS.load(Ordering::Relaxed);
519 let queue = ArrayQueue::new(1);
520
521 queue
522 .push(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
523 metadata: EventMetadata::default(),
524 ..Default::default()
525 }))
526 .expect("first push fits");
527
528 if queue
529 .push(DexEvent::PumpFunCreate(PumpFunCreateTokenEvent {
530 metadata: EventMetadata::default(),
531 ..Default::default()
532 }))
533 .is_err()
534 {
535 record_shredstream_dropped_event();
536 }
537
538 assert!(SHREDSTREAM_DROPPED_EVENTS.load(Ordering::Relaxed) > before);
539 }
540
541 #[test]
542 fn callback_path_delivers_events_without_queue() {
543 let entries = vec![SolanaEntry {
544 num_hashes: 1,
545 hash: Hash::default(),
546 transactions: vec![pumpfun_create_tx()],
547 }];
548 let entry = Entry { slot: 42, entries: wincode::serialize(&entries).unwrap() };
549 let count = AtomicUsize::new(0);
550
551 let mut events = Vec::with_capacity(4);
552 ShredStreamClient::process_entry(
553 entry,
554 None,
555 &EventSink::Callback(&|event| {
556 assert!(matches!(event, DexEvent::PumpFunCreate(_)));
557 assert_eq!(event.metadata().slot, 42);
558 count.fetch_add(1, AtomicOrdering::Relaxed);
559 }),
560 &mut events,
561 );
562
563 assert_eq!(count.load(AtomicOrdering::Relaxed), 1);
564 }
565
566 #[test]
567 fn wincode_decodes_solana4_v1_entries() {
568 let entries = vec![SolanaEntry {
569 num_hashes: 1,
570 hash: Hash::new_unique(),
571 transactions: vec![VersionedTransaction {
572 signatures: vec![Signature::from([9; 64])],
573 message: VersionedMessage::V1(solana_sdk::message::v1::Message {
574 header: MessageHeader { num_required_signatures: 1, ..Default::default() },
575 config: solana_sdk::message::v1::TransactionConfig::empty()
576 .with_compute_unit_limit(250_000)
577 .with_priority_fee(1_500),
578 lifetime_specifier: Hash::new_unique(),
579 account_keys: vec![Pubkey::new_unique()],
580 instructions: Vec::new(),
581 }),
582 }],
583 }];
584 let bytes = wincode::serialize(&entries).expect("serialize V1 Entry");
585 let decoded: Vec<SolanaEntry> = wincode::deserialize(&bytes).expect("decode V1 Entry");
586
587 assert_eq!(decoded, entries);
588 }
589
590 #[test]
591 fn process_entry_rejects_unsanitized_transactions() {
592 let mut transaction = pumpfun_create_tx();
593 let VersionedMessage::V0(message) = &mut transaction.message else {
594 panic!("expected V0 fixture");
595 };
596 message.instructions[0].program_id_index = u8::MAX;
597 let entries = vec![SolanaEntry {
598 num_hashes: 1,
599 hash: Hash::default(),
600 transactions: vec![transaction],
601 }];
602 let entry = Entry { slot: 42, entries: wincode::serialize(&entries).unwrap() };
603 let count = AtomicUsize::new(0);
604 let mut events = Vec::with_capacity(4);
605
606 ShredStreamClient::process_entry(
607 entry,
608 None,
609 &EventSink::Callback(&|_| {
610 count.fetch_add(1, AtomicOrdering::Relaxed);
611 }),
612 &mut events,
613 );
614
615 assert_eq!(count.load(AtomicOrdering::Relaxed), 0);
616 }
617 #[test]
618 fn cached_alt_routes_reach_queue_and_callback_without_network() {
619 use base64::{engine::general_purpose::STANDARD, Engine};
620 let fixtures: serde_json::Value =
621 serde_json::from_str(include_str!("../../tests/fixtures/shred_routes/reported.json"))
622 .unwrap();
623 let f = fixtures
624 .as_array()
625 .unwrap()
626 .iter()
627 .find(|f| !f["loaded_writable"].as_array().unwrap().is_empty())
628 .unwrap();
629 let tx: VersionedTransaction =
630 wincode::deserialize_exact(&STANDARD.decode(f["wire"].as_str().unwrap()).unwrap())
631 .unwrap();
632 let loaded = LoadedAddresses {
633 writable: f["loaded_writable"]
634 .as_array()
635 .unwrap()
636 .iter()
637 .map(|s| s.as_str().unwrap().parse().unwrap())
638 .collect(),
639 readonly: f["loaded_readonly"]
640 .as_array()
641 .unwrap()
642 .iter()
643 .map(|s| s.as_str().unwrap().parse().unwrap())
644 .collect(),
645 };
646 let slot = f["slot"].as_u64().unwrap();
647 let signature = tx.signatures[0];
648 let calls = Arc::new(AtomicUsize::new(0));
649 let resolver_calls = Arc::clone(&calls);
650 let resolver = move |transaction: &VersionedTransaction,
651 at: u64|
652 -> crate::common::AnyResult<LoadedAddresses> {
653 assert_eq!(transaction.signatures[0], signature);
654 assert_eq!(at, slot);
655 resolver_calls.fetch_add(1, AtomicOrdering::Relaxed);
656 Ok(loaded.clone())
657 };
658 let entry = || Entry {
659 slot,
660 entries: wincode::serialize(&vec![SolanaEntry {
661 num_hashes: 1,
662 hash: Hash::default(),
663 transactions: vec![tx.clone()],
664 }])
665 .unwrap(),
666 };
667 let filter = EventTypeFilter::include_only(vec![crate::grpc::EventType::PumpFunTrade]);
668 let queue = Arc::new(ArrayQueue::new(4));
669 let mut events = Vec::new();
670 ShredStreamClient::process_entry_with_resolver(
671 entry(),
672 Some(&filter),
673 &EventSink::Queue(&queue),
674 &mut events,
675 Some(&resolver),
676 );
677 assert!(matches!(queue.pop(), Some(DexEvent::PumpRouteIntent(_))));
678 let callback_count = AtomicUsize::new(0);
679 ShredStreamClient::process_entry_with_resolver(
680 entry(),
681 Some(&filter),
682 &EventSink::Callback(&|event| {
683 assert!(matches!(event, DexEvent::PumpRouteIntent(_)));
684 assert_eq!(event.metadata().slot, slot);
685 callback_count.fetch_add(1, AtomicOrdering::Relaxed);
686 }),
687 &mut events,
688 Some(&resolver),
689 );
690 assert_eq!(callback_count.load(AtomicOrdering::Relaxed), 1);
691 assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
692 ShredStreamClient::process_entry_with_resolver(
694 entry(),
695 None,
696 &EventSink::Queue(&queue),
697 &mut events,
698 None,
699 );
700 assert!(queue.pop().is_none());
701 let miss =
702 |_: &VersionedTransaction, _: u64| -> crate::common::AnyResult<LoadedAddresses> {
703 Err(std::io::Error::new(std::io::ErrorKind::NotFound, "ALT cache miss").into())
704 };
705 ShredStreamClient::process_entry_with_resolver(
706 entry(),
707 None,
708 &EventSink::Queue(&queue),
709 &mut events,
710 Some(&miss),
711 );
712 assert!(queue.pop().is_none());
713 let wrong_count =
714 |_: &VersionedTransaction, _: u64| -> crate::common::AnyResult<LoadedAddresses> {
715 Ok(LoadedAddresses::default())
716 };
717 ShredStreamClient::process_entry_with_resolver(
718 entry(),
719 None,
720 &EventSink::Queue(&queue),
721 &mut events,
722 Some(&wrong_count),
723 );
724 assert!(queue.pop().is_none());
725 }
726}