Skip to main content

sol_parser_sdk/shredstream/
client.rs

1//! ShredStream 客户端
2//!
3//! `solana_entry::entry::Entry` 在 Agave SDK 中带 `deprecated`(需显式启用不稳定 feature 才消除);
4//! 本模块仍依赖其 bincode 布局解码 Shred 侧 `entries` 负载。
5#![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
60/// Synchronous ALT lookup from a prewarmed, slot-aware local cache. Called on
61/// the receive task; never perform RPC or blocking I/O here. Address identity
62/// and table activation at the requested slot are the caller's responsibility.
63pub type AddressLookupResolver =
64    dyn Fn(&VersionedTransaction, u64) -> crate::common::AnyResult<LoadedAddresses> + Send + Sync;
65
66/// ShredStream 客户端
67#[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    /// 创建新客户端
78    pub async fn new(endpoint: impl Into<String>) -> crate::common::AnyResult<Self> {
79        Self::new_with_config(endpoint, ShredStreamConfig::default()).await
80    }
81
82    /// 使用自定义配置创建客户端
83    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        // 测试连接
89        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    /// Resolve ALT keys from a caller-owned cache before parsing. A cache miss
101    /// returns an error and skips the transaction, never emits placeholder keys.
102    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    /// 订阅 DEX 事件(自动重连)
114    ///
115    /// 返回一个队列,事件会被推送到该队列中
116    pub async fn subscribe(&self) -> crate::common::AnyResult<Arc<ArrayQueue<DexEvent>>> {
117        self.subscribe_with_filter(None).await
118    }
119
120    /// 订阅 DEX 事件,并在 ShredStream 热路径中按 SDK 事件类型提前过滤。
121    ///
122    /// 过滤发生在解析分发前,用于低延迟场景避免解析不需要的协议/事件。
123    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    /// 订阅 DEX 事件,并在解析热路径中直接回调事件,避免跨任务队列调度。
177    ///
178    /// 这是最低延迟路径;回调会在 ShredStream 读流任务内执行,应避免阻塞 I/O 或重计算。
179    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    /// 停止订阅
235    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    /// 核心事件流处理
261    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    /// 处理单个 Entry 消息
340    #[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        // 反序列化 Entry 数据
352        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        // 处理每个 Entry 中的交易
361        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    /// 处理单个交易
386    #[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        // 热路径:`static_account_keys` 零拷贝、`pump_ix` 内不克隆 CompiledInstruction。
450        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        // Without a cache, skip the unresolved route rather than emit zero pubkeys.
692        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}