Skip to main content

evm_oracle_state/
pending.rs

1//! Speculative oracle updates observed before canonical inclusion.
2//!
3//! Pending values are intentionally isolated from [`crate::OracleTracker`].
4
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    sync::{
8        Arc, Mutex,
9        atomic::{AtomicU64, Ordering},
10    },
11    time::SystemTime,
12};
13
14use alloy_primitives::{Address, B256, Bytes, I256};
15use tokio::sync::broadcast;
16
17use crate::{FeedId, FeedRegistration, OracleAdapterId};
18
19mod adapter;
20mod chainlink;
21#[cfg(feature = "pending-oracle-ethereum")]
22mod ethereum;
23#[cfg(feature = "pending-oracle-mev-share")]
24mod mev_share;
25mod source;
26
27use adapter::adapter_owns_update;
28pub use adapter::{
29    ChainlinkPendingAdapter, PendingOracleAdapter, PendingOracleAdapterError,
30    PendingOracleAdapterFailure, PendingOracleAdapterObservation, PendingOracleCandidateReport,
31    PendingOracleInterest,
32};
33pub use chainlink::{
34    CHAINLINK_FORWARD_SELECTOR, CHAINLINK_TRANSMIT_SECONDARY_SELECTOR, CHAINLINK_TRANSMIT_SELECTOR,
35    ChainlinkPendingReport, DecodedChainlinkOcr2, PendingOracleDecodeError,
36    decode_chainlink_ocr2_calldata,
37};
38#[cfg(feature = "pending-oracle-ethereum")]
39pub use ethereum::AlchemyPendingTransactionSource;
40#[cfg(feature = "pending-oracle-mev-share")]
41pub use mev_share::MevSharePendingTransactionSource;
42pub use source::{
43    PendingOracleCandidateSource, PendingOracleCoverageGap, PendingOracleSourceDescriptor,
44    PendingOracleSourceError, PendingOracleSourceFuture, PendingOracleSourceHealth,
45    PendingOracleSourceSession, PendingOracleSourceSink, PendingOracleSourceState,
46};
47
48/// Ethereum mainnet chain id.
49pub const ETHEREUM_MAINNET_CHAIN_ID: u64 = 1;
50
51/// Opt-in configuration for the pending-oracle channel.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct PendingOracleConfig {
54    chain_id: u64,
55    feed_filter: PendingFeedFilter,
56    sources: BTreeSet<PendingOracleSource>,
57    channel_capacity: usize,
58}
59
60impl PendingOracleConfig {
61    /// Observe pending-capable feeds registered on an explicit chain.
62    pub const fn for_chain(chain_id: u64) -> Self {
63        Self {
64            chain_id,
65            feed_filter: PendingFeedFilter::AllRegistered,
66            sources: BTreeSet::new(),
67            channel_capacity: 1_024,
68        }
69    }
70
71    /// Observe pending-capable feeds registered on Ethereum mainnet.
72    pub const fn ethereum_mainnet() -> Self {
73        Self::for_chain(ETHEREUM_MAINNET_CHAIN_ID)
74    }
75
76    /// Return the configured chain id.
77    pub const fn chain_id(&self) -> u64 {
78        self.chain_id
79    }
80
81    /// Restrict pending observation to ids already present in the oracle registry.
82    pub fn registered_feeds(mut self, filter: PendingFeedFilter) -> Self {
83        self.feed_filter = filter;
84        self
85    }
86
87    /// Return the filter applied to registered feeds.
88    pub const fn feed_filter(&self) -> &PendingFeedFilter {
89        &self.feed_filter
90    }
91
92    /// Enable candidates received from an Ethereum public pending-transaction stream.
93    pub fn public_mempool(mut self) -> Self {
94        self.sources.insert(PendingOracleSource::PublicMempool);
95        self
96    }
97
98    /// Enable candidates received from the Flashbots MEV-Share event stream.
99    pub fn mev_share(mut self) -> Self {
100        self.sources.insert(PendingOracleSource::MevShare);
101        self
102    }
103
104    /// Enable candidates received from one built-in or caller-defined transport family.
105    pub fn source(mut self, source: PendingOracleSource) -> Self {
106        self.sources.insert(source);
107        self
108    }
109
110    /// Return whether a pending transport is enabled.
111    pub fn source_enabled(&self, source: &PendingOracleSource) -> bool {
112        self.sources.contains(source)
113    }
114
115    /// Iterate over enabled pending transports.
116    pub fn sources(&self) -> impl Iterator<Item = PendingOracleSource> + '_ {
117        self.sources.iter().cloned()
118    }
119
120    /// Set the bounded pending-event capacity. Zero is clamped to one.
121    pub fn channel_capacity(mut self, channel_capacity: usize) -> Self {
122        self.channel_capacity = channel_capacity.max(1);
123        self
124    }
125
126    /// Return the bounded pending-event capacity.
127    pub const fn channel_capacity_value(&self) -> usize {
128        self.channel_capacity
129    }
130}
131
132/// Built-in and caller-defined pre-inclusion transport families.
133#[non_exhaustive]
134#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
135pub enum PendingOracleSource {
136    /// Ethereum node pending-transaction propagation.
137    PublicMempool,
138    /// Flashbots MEV-Share transaction and bundle hints.
139    MevShare,
140    /// Caller-defined transport family with adapter-owned semantics.
141    Custom(String),
142}
143
144impl PendingOracleSource {
145    /// Construct a caller-defined transport family.
146    pub fn custom(id: impl Into<String>) -> Self {
147        Self::Custom(id.into())
148    }
149
150    /// Return the stable transport-family identifier.
151    pub fn as_str(&self) -> &str {
152        match self {
153            Self::PublicMempool => "ethereum-public-mempool",
154            Self::MevShare => "flashbots-mev-share",
155            Self::Custom(id) => id,
156        }
157    }
158}
159
160/// Selection over feeds already registered with [`crate::OracleRuntime`].
161#[derive(Clone, Debug, Default, PartialEq, Eq)]
162pub enum PendingFeedFilter {
163    /// Observe every registered feed supported by an installed pending adapter.
164    #[default]
165    AllRegistered,
166    /// Observe only these stable registered feed ids.
167    Only(BTreeSet<FeedId>),
168}
169
170impl PendingFeedFilter {
171    /// Select a subset by stable feed id without repeating registration metadata.
172    pub fn only<I, S>(ids: I) -> Self
173    where
174        I: IntoIterator<Item = S>,
175        S: Into<String>,
176    {
177        Self::Only(ids.into_iter().map(|id| FeedId::new(id.into())).collect())
178    }
179
180    fn includes(&self, id: &FeedId) -> bool {
181        match self {
182            Self::AllRegistered => true,
183            Self::Only(ids) => ids.contains(id),
184        }
185    }
186}
187
188/// Runtime view of registered feeds eligible for pending observation.
189#[derive(Clone)]
190pub struct PendingOracleRuntime {
191    config: PendingOracleConfig,
192    feed_ids: Arc<Mutex<Vec<FeedId>>>,
193    registrations: Arc<Mutex<Vec<FeedRegistration>>>,
194    adapters: Arc<Mutex<BTreeMap<OracleAdapterId, Arc<dyn PendingOracleAdapter>>>>,
195    sender: broadcast::Sender<PendingOracleStreamEvent>,
196    sequence: Arc<AtomicU64>,
197    updates: Arc<Mutex<BTreeMap<PendingOracleUpdateId, PendingOracleUpdate>>>,
198    source_health: Arc<Mutex<BTreeMap<PendingOracleSourceId, PendingOracleSourceHealth>>>,
199}
200
201impl std::fmt::Debug for PendingOracleRuntime {
202    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        formatter
204            .debug_struct("PendingOracleRuntime")
205            .field("config", &self.config)
206            .field(
207                "feed_ids",
208                &self
209                    .feed_ids
210                    .lock()
211                    .unwrap_or_else(|error| error.into_inner()),
212            )
213            .field(
214                "adapter_ids",
215                &self
216                    .adapters
217                    .lock()
218                    .unwrap_or_else(|error| error.into_inner())
219                    .keys()
220                    .collect::<Vec<_>>(),
221            )
222            .field("update_count", &self.update_count())
223            .finish_non_exhaustive()
224    }
225}
226
227impl PendingOracleRuntime {
228    pub(crate) fn from_registrations<'a>(
229        config: PendingOracleConfig,
230        registrations: impl IntoIterator<Item = &'a FeedRegistration>,
231        adapters: impl IntoIterator<Item = Arc<dyn PendingOracleAdapter>>,
232    ) -> Self {
233        let feed_filter = config.feed_filter().clone();
234        let (sender, _) = broadcast::channel(config.channel_capacity_value());
235        let registrations = registrations
236            .into_iter()
237            .filter(|registration| feed_filter.includes(&registration.id))
238            .cloned()
239            .collect::<Vec<_>>();
240        let mut installed = BTreeMap::<OracleAdapterId, Arc<dyn PendingOracleAdapter>>::new();
241        let chainlink: Arc<dyn PendingOracleAdapter> = Arc::new(ChainlinkPendingAdapter);
242        installed.insert(chainlink.adapter_id(), chainlink);
243        for adapter in adapters {
244            installed.entry(adapter.adapter_id()).or_insert(adapter);
245        }
246        Self {
247            config,
248            feed_ids: Arc::new(Mutex::new(
249                registrations
250                    .iter()
251                    .map(|registration| registration.id.clone())
252                    .collect(),
253            )),
254            registrations: Arc::new(Mutex::new(registrations)),
255            adapters: Arc::new(Mutex::new(installed)),
256            sender,
257            sequence: Arc::new(AtomicU64::new(0)),
258            updates: Arc::new(Mutex::new(BTreeMap::new())),
259            source_health: Arc::new(Mutex::new(BTreeMap::new())),
260        }
261    }
262
263    /// Return adapter-owned transport filters for the current inherited feed scope.
264    pub fn interests(&self) -> Vec<PendingOracleInterest> {
265        let registrations = self
266            .registrations
267            .lock()
268            .unwrap_or_else(|error| error.into_inner())
269            .clone();
270        self.adapters
271            .lock()
272            .unwrap_or_else(|error| error.into_inner())
273            .values()
274            .flat_map(|adapter| adapter.interests(&registrations))
275            .collect()
276    }
277
278    /// Register an additional oracle-family pending adapter.
279    ///
280    /// Returns `false` when an adapter with the same stable id is already installed.
281    pub fn register_adapter<A>(&self, adapter: A) -> bool
282    where
283        A: PendingOracleAdapter,
284    {
285        let id = adapter.adapter_id();
286        let mut adapters = self
287            .adapters
288            .lock()
289            .unwrap_or_else(|error| error.into_inner());
290        if adapters.contains_key(&id) {
291            return false;
292        }
293        adapters.insert(id, Arc::new(adapter));
294        true
295    }
296
297    /// Return the pending-channel configuration.
298    pub const fn config(&self) -> &PendingOracleConfig {
299        &self.config
300    }
301
302    /// Return registered feed ids inherited by the pending channel.
303    pub fn feed_ids(&self) -> Vec<FeedId> {
304        self.feed_ids
305            .lock()
306            .unwrap_or_else(|error| error.into_inner())
307            .clone()
308    }
309
310    /// Subscribe to the speculative pending-event channel.
311    pub fn subscribe(&self) -> PendingOracleSubscription {
312        PendingOracleSubscription {
313            receiver: self.sender.subscribe(),
314        }
315    }
316
317    /// Create a publisher for an installed pending source or adapter.
318    pub fn publisher(&self) -> PendingOraclePublisher {
319        PendingOraclePublisher {
320            sender: self.sender.clone(),
321            sequence: Arc::clone(&self.sequence),
322        }
323    }
324
325    /// Start a pluggable pending source and return its owned task session.
326    pub fn start_source<S>(
327        &self,
328        source: S,
329    ) -> Result<PendingOracleSourceSession, PendingOracleSourceError>
330    where
331        S: PendingOracleCandidateSource,
332    {
333        let descriptor = source.descriptor();
334        if !self.config.source_enabled(&descriptor.source) {
335            return Err(PendingOracleSourceError::SourceDisabled {
336                transport: descriptor.source.clone(),
337            });
338        }
339        if self
340            .source_health
341            .lock()
342            .unwrap_or_else(|error| error.into_inner())
343            .get(&descriptor.id)
344            .is_some_and(|health| health.state != PendingOracleSourceState::Stopped)
345        {
346            return Err(PendingOracleSourceError::DuplicateSource {
347                id: descriptor.id.clone(),
348            });
349        }
350        let handle = tokio::runtime::Handle::try_current()
351            .map_err(|_| PendingOracleSourceError::RuntimeUnavailable)?;
352        let (shutdown, receiver) = tokio::sync::watch::channel(false);
353        let sink = PendingOracleSourceSink::new(self.clone(), descriptor.clone());
354        sink.connecting();
355        let task_sink = sink.clone();
356        let task = handle.spawn(async move {
357            let result = Box::new(source).run(task_sink.clone(), receiver).await;
358            match &result {
359                Ok(()) => task_sink.stopped(),
360                Err(error) => task_sink.failed(error),
361            }
362            result
363        });
364        Ok(PendingOracleSourceSession::new(descriptor, shutdown, task))
365    }
366
367    /// Return the latest health snapshot for one concrete source.
368    pub fn source_health(
369        &self,
370        source_id: &PendingOracleSourceId,
371    ) -> Option<PendingOracleSourceHealth> {
372        self.source_health
373            .lock()
374            .unwrap_or_else(|error| error.into_inner())
375            .get(source_id)
376            .cloned()
377    }
378
379    pub(crate) fn insert_source_health(&self, health: PendingOracleSourceHealth) {
380        self.source_health
381            .lock()
382            .unwrap_or_else(|error| error.into_inner())
383            .insert(health.descriptor.id.clone(), health.clone());
384        self.publisher()
385            .publish(PendingOracleEvent::SourceHealthChanged(health));
386    }
387
388    pub(crate) fn mutate_source_health(
389        &self,
390        descriptor: &PendingOracleSourceDescriptor,
391        mutate: impl FnOnce(&mut PendingOracleSourceHealth),
392    ) {
393        let mut health = self
394            .source_health
395            .lock()
396            .unwrap_or_else(|error| error.into_inner());
397        let health =
398            health
399                .entry(descriptor.id.clone())
400                .or_insert_with(|| PendingOracleSourceHealth {
401                    descriptor: descriptor.clone(),
402                    state: PendingOracleSourceState::Connecting,
403                    last_transport_message_at: None,
404                    last_candidate_at: None,
405                    coverage_gap_count: 0,
406                    last_error: None,
407                });
408        mutate(health);
409    }
410
411    pub(crate) fn update_source_health(
412        &self,
413        descriptor: &PendingOracleSourceDescriptor,
414        mutate: impl FnOnce(&mut PendingOracleSourceHealth),
415    ) {
416        let health = {
417            let mut sources = self
418                .source_health
419                .lock()
420                .unwrap_or_else(|error| error.into_inner());
421            let health =
422                sources
423                    .entry(descriptor.id.clone())
424                    .or_insert_with(|| PendingOracleSourceHealth {
425                        descriptor: descriptor.clone(),
426                        state: PendingOracleSourceState::Connecting,
427                        last_transport_message_at: None,
428                        last_candidate_at: None,
429                        coverage_gap_count: 0,
430                        last_error: None,
431                    });
432            mutate(health);
433            health.clone()
434        };
435        self.publisher()
436            .publish(PendingOracleEvent::SourceHealthChanged(health));
437    }
438
439    /// Record and stream one proposed update, collapsing exact repeats by update id.
440    pub fn observe(&self, update: PendingOracleUpdate) -> PendingOracleObserveOutcome {
441        if update.status != PendingOracleStatus::Pending
442            || update
443                .transmissions
444                .iter()
445                .any(|transmission| transmission.status != PendingOracleTransmissionStatus::Pending)
446        {
447            return PendingOracleObserveOutcome::Conflict;
448        }
449        let feed_ids = self
450            .feed_ids
451            .lock()
452            .unwrap_or_else(|error| error.into_inner());
453        if update.feed_ids.iter().any(|id| !feed_ids.contains(id)) {
454            return PendingOracleObserveOutcome::OutsideScope;
455        }
456        drop(feed_ids);
457        let mut updates = self
458            .updates
459            .lock()
460            .unwrap_or_else(|error| error.into_inner());
461        if let Some(existing) = updates.get_mut(&update.id) {
462            if existing.status != PendingOracleStatus::Pending {
463                return PendingOracleObserveOutcome::Conflict;
464            }
465            if existing.feed_ids != update.feed_ids
466                || existing.evidence != update.evidence
467                || existing.proposed_value != update.proposed_value
468            {
469                return PendingOracleObserveOutcome::Conflict;
470            }
471
472            if update.transmissions.iter().any(|candidate| {
473                existing
474                    .transmissions
475                    .iter()
476                    .find(|tracked| tracked.id == candidate.id)
477                    .is_some_and(|tracked| !tracked.same_variant_content(candidate))
478            }) {
479                return PendingOracleObserveOutcome::Conflict;
480            }
481
482            let added = update
483                .transmissions
484                .into_iter()
485                .filter(|candidate| {
486                    !existing
487                        .transmissions
488                        .iter()
489                        .any(|tracked| tracked.id == candidate.id)
490                })
491                .collect::<Vec<_>>();
492            existing.transmissions.extend(added.iter().cloned());
493            drop(updates);
494
495            for transmission in &added {
496                self.publisher()
497                    .publish(PendingOracleEvent::TransmissionAdded {
498                        update_id: update.id,
499                        transmission: transmission.clone(),
500                    });
501            }
502            return if added.is_empty() {
503                PendingOracleObserveOutcome::Duplicate
504            } else {
505                PendingOracleObserveOutcome::TransmissionAdded { count: added.len() }
506            };
507        }
508        updates.insert(update.id, update.clone());
509        drop(updates);
510        self.publisher()
511            .publish(PendingOracleEvent::Observed(update));
512        PendingOracleObserveOutcome::Observed
513    }
514
515    /// Route one transport candidate through every interested pending adapter.
516    pub fn observe_candidate(
517        &self,
518        candidate: PendingTransportCandidate,
519    ) -> Result<PendingOracleCandidateReport, PendingOracleAdapterError> {
520        if candidate.chain_id != self.config.chain_id() {
521            return Err(PendingOracleAdapterError::WrongChain {
522                expected: self.config.chain_id(),
523                observed: candidate.chain_id,
524            });
525        }
526        if !self.config.source_enabled(&candidate.source) {
527            return Err(PendingOracleAdapterError::SourceDisabled(
528                candidate.source.clone(),
529            ));
530        }
531        let registrations = self
532            .registrations
533            .lock()
534            .unwrap_or_else(|error| error.into_inner())
535            .clone();
536        let adapters = self
537            .adapters
538            .lock()
539            .unwrap_or_else(|error| error.into_inner())
540            .values()
541            .cloned()
542            .collect::<Vec<_>>();
543        let mut report = PendingOracleCandidateReport::default();
544        for adapter in adapters {
545            if !adapter
546                .interests(&registrations)
547                .iter()
548                .any(|interest| interest.matches(&candidate))
549            {
550                continue;
551            }
552            let adapter_id = adapter.adapter_id();
553            let updates = match adapter.decode(&candidate, &registrations) {
554                Ok(updates) => updates,
555                Err(error) => {
556                    report
557                        .failures
558                        .push(PendingOracleAdapterFailure { adapter_id, error });
559                    continue;
560                }
561            };
562            for update in updates {
563                let update_id = update.id;
564                report.observations.push(PendingOracleAdapterObservation {
565                    adapter_id: adapter_id.clone(),
566                    update_id,
567                    outcome: self.observe(update),
568                });
569            }
570        }
571        Ok(report)
572    }
573
574    /// Return one tracked pending update by content identity.
575    pub fn update(&self, id: &PendingOracleUpdateId) -> Option<PendingOracleUpdate> {
576        self.updates
577            .lock()
578            .unwrap_or_else(|error| error.into_inner())
579            .get(id)
580            .cloned()
581    }
582
583    /// Return the number of distinct tracked pending updates.
584    pub fn update_count(&self) -> usize {
585        self.updates
586            .lock()
587            .unwrap_or_else(|error| error.into_inner())
588            .len()
589    }
590
591    /// Resolve a transmission after an adapter matched its committed oracle signal.
592    ///
593    /// A successful receipt alone is not sufficient evidence. Callers should
594    /// invoke this only after matching the oracle-family confirmation emitted by
595    /// the same committed transaction.
596    pub fn resolve_confirmed_transmission(
597        &self,
598        resolution: PendingOracleResolution,
599    ) -> PendingOracleResolveOutcome {
600        let mut updates = self
601            .updates
602            .lock()
603            .unwrap_or_else(|error| error.into_inner());
604        let Some(update) = updates.get_mut(&resolution.update_id) else {
605            return PendingOracleResolveOutcome::UnknownUpdate;
606        };
607        let Some(transmission) = update
608            .transmissions
609            .iter_mut()
610            .find(|transmission| transmission.id == resolution.transmission_id)
611        else {
612            return PendingOracleResolveOutcome::UnknownTransmission;
613        };
614        if transmission
615            .ordering
616            .transaction_hash()
617            .is_some_and(|hash| hash != resolution.transaction_hash)
618        {
619            return PendingOracleResolveOutcome::Conflict;
620        }
621        let target = match resolution.kind {
622            PendingOracleResolutionKind::Landed => PendingOracleTransmissionStatus::Landed,
623            PendingOracleResolutionKind::Reverted => PendingOracleTransmissionStatus::Reverted,
624        };
625        if transmission.status == target {
626            return PendingOracleResolveOutcome::Duplicate;
627        }
628        if transmission.status != PendingOracleTransmissionStatus::Pending {
629            return PendingOracleResolveOutcome::Conflict;
630        }
631        transmission.status = target;
632        update.status =
633            if resolution.kind == PendingOracleResolutionKind::Landed {
634                PendingOracleStatus::Landed
635            } else if update.transmissions.iter().all(|transmission| {
636                transmission.status == PendingOracleTransmissionStatus::Reverted
637            }) {
638                PendingOracleStatus::Reverted
639            } else {
640                PendingOracleStatus::Pending
641            };
642        drop(updates);
643        self.publisher()
644            .publish(PendingOracleEvent::Resolved(resolution));
645        PendingOracleResolveOutcome::Resolved
646    }
647
648    /// Correlate one committed oracle log with pending transaction variants.
649    ///
650    /// Only an adapter-specific confirmation emitted by the same transaction
651    /// hash resolves a candidate as landed. Unrelated successful transactions
652    /// and removed logs are ignored.
653    pub fn observe_confirmed_log(
654        &self,
655        log: &alloy_rpc_types_eth::Log,
656    ) -> Vec<PendingOracleResolution> {
657        if log.removed {
658            return Vec::new();
659        }
660        let (Some(transaction_hash), Some(block_number)) = (log.transaction_hash, log.block_number)
661        else {
662            return Vec::new();
663        };
664        let updates = self
665            .updates
666            .lock()
667            .unwrap_or_else(|error| error.into_inner())
668            .values()
669            .filter(|update| update.status == PendingOracleStatus::Pending)
670            .cloned()
671            .collect::<Vec<_>>();
672        let adapters = self
673            .adapters
674            .lock()
675            .unwrap_or_else(|error| error.into_inner())
676            .values()
677            .cloned()
678            .collect::<Vec<_>>();
679        let mut resolutions = Vec::new();
680        for update in updates {
681            if !adapters.iter().any(|adapter| {
682                adapter_owns_update(&adapter.adapter_id(), &update)
683                    && adapter.confirms(&update, log)
684            }) {
685                continue;
686            }
687            for transmission in update.transmissions.iter().filter(|transmission| {
688                transmission.status == PendingOracleTransmissionStatus::Pending
689                    && transmission.ordering.transaction_hash() == Some(transaction_hash)
690            }) {
691                let resolution = PendingOracleResolution::new(
692                    update.id,
693                    transmission.id,
694                    transaction_hash,
695                    block_number,
696                    PendingOracleResolutionKind::Landed,
697                );
698                if self.resolve_confirmed_transmission(resolution.clone())
699                    == PendingOracleResolveOutcome::Resolved
700                {
701                    resolutions.push(resolution);
702                }
703            }
704        }
705        resolutions
706    }
707
708    /// Decode, route, and record one possible Chainlink OCR2 transmission.
709    pub fn observe_chainlink_candidate(
710        &self,
711        candidate: PendingTransportCandidate,
712    ) -> Result<PendingOracleCandidateOutcome, PendingOracleAdapterError> {
713        match self.observe_candidate(candidate) {
714            Ok(report) => {
715                if let Some(failure) = report
716                    .failures
717                    .into_iter()
718                    .find(|failure| failure.adapter_id.as_str() == ChainlinkPendingAdapter::ID)
719                {
720                    return Err(failure.error);
721                }
722                Ok(report
723                    .observations
724                    .into_iter()
725                    .find(|observation| {
726                        observation.adapter_id.as_str() == ChainlinkPendingAdapter::ID
727                    })
728                    .map_or(PendingOracleCandidateOutcome::Ignored, |observation| {
729                        PendingOracleCandidateOutcome::Tracked(observation.outcome)
730                    }))
731            }
732            Err(PendingOracleAdapterError::WrongChain { expected, observed }) => {
733                Ok(PendingOracleCandidateOutcome::WrongChain { expected, observed })
734            }
735            Err(PendingOracleAdapterError::SourceDisabled(source)) => {
736                Ok(PendingOracleCandidateOutcome::SourceDisabled(source))
737            }
738            Err(error) => Err(error),
739        }
740    }
741
742    pub(crate) fn refresh<'a>(
743        &mut self,
744        registrations: impl IntoIterator<Item = &'a FeedRegistration>,
745    ) {
746        let registrations = registrations
747            .into_iter()
748            .filter(|registration| self.config.feed_filter().includes(&registration.id))
749            .cloned()
750            .collect::<Vec<_>>();
751        *self
752            .feed_ids
753            .lock()
754            .unwrap_or_else(|error| error.into_inner()) = registrations
755            .iter()
756            .map(|registration| registration.id.clone())
757            .collect();
758        *self
759            .registrations
760            .lock()
761            .unwrap_or_else(|error| error.into_inner()) = registrations.clone();
762
763        let in_scope = self
764            .feed_ids
765            .lock()
766            .unwrap_or_else(|error| error.into_inner())
767            .iter()
768            .cloned()
769            .collect::<BTreeSet<_>>();
770        let mut expired = Vec::new();
771        let mut updates = self
772            .updates
773            .lock()
774            .unwrap_or_else(|error| error.into_inner());
775        updates.retain(|id, update| {
776            update.feed_ids.retain(|feed_id| {
777                if !in_scope.contains(feed_id) {
778                    return false;
779                }
780                match &update.evidence {
781                    PendingOracleEvidence::ChainlinkReport(report) => {
782                        registrations.iter().any(|registration| {
783                            &registration.id == feed_id
784                                && registration.current_aggregator == Some(report.aggregator)
785                        })
786                    }
787                    PendingOracleEvidence::Adapter { .. } => true,
788                }
789            });
790            if update.feed_ids.is_empty() {
791                expired.push(*id);
792                false
793            } else {
794                true
795            }
796        });
797        drop(updates);
798        for id in expired {
799            self.publisher().publish(PendingOracleEvent::Expired(id));
800        }
801    }
802}
803
804/// Outcome of submitting one observation to the pending tracker.
805#[derive(Clone, Copy, Debug, PartialEq, Eq)]
806pub enum PendingOracleObserveOutcome {
807    /// A new update identity was recorded and streamed.
808    Observed,
809    /// The report was known but one or more new transaction variants were added.
810    TransmissionAdded {
811        /// Number of newly recorded variants.
812        count: usize,
813    },
814    /// The update identity was already present.
815    Duplicate,
816    /// The same update or transmission identity carried different immutable content.
817    Conflict,
818    /// At least one affected feed is not in this runtime's inherited pending scope.
819    OutsideScope,
820}
821
822/// Outcome of routing one generic transport candidate through a pending adapter.
823#[non_exhaustive]
824#[derive(Clone, Debug, PartialEq, Eq)]
825pub enum PendingOracleCandidateOutcome {
826    /// The candidate did not match a supported pending oracle call.
827    Ignored,
828    /// Its transport was not enabled by [`PendingOracleConfig`].
829    SourceDisabled(PendingOracleSource),
830    /// The observation came from a chain other than the explicitly configured chain.
831    WrongChain {
832        /// Configured chain id.
833        expected: u64,
834        /// Candidate chain id.
835        observed: u64,
836    },
837    /// The report was decoded but its aggregator is not registered in pending scope.
838    AggregatorOutsideScope(Address),
839    /// The candidate produced or updated a tracked pending report.
840    Tracked(PendingOracleObserveOutcome),
841}
842
843/// Report-level pending lifecycle state.
844#[non_exhaustive]
845#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
846pub enum PendingOracleStatus {
847    /// At least one transport variant may still land.
848    #[default]
849    Pending,
850    /// A variant landed and emitted its matching committed oracle signal.
851    Landed,
852    /// Every known variant resolved without the matching oracle update.
853    Reverted,
854}
855
856/// Oracle-confirmed outcome of one committed transmission.
857#[non_exhaustive]
858#[derive(Clone, Copy, Debug, PartialEq, Eq)]
859pub enum PendingOracleResolutionKind {
860    /// The transaction landed and emitted the expected oracle signal.
861    Landed,
862    /// The transaction reverted or landed without the expected oracle signal.
863    Reverted,
864}
865
866/// Correlation between one pending transport variant and its committed outcome.
867#[derive(Clone, Debug, PartialEq, Eq)]
868pub struct PendingOracleResolution {
869    /// Stable report identity.
870    pub update_id: PendingOracleUpdateId,
871    /// Stable transport variant identity.
872    pub transmission_id: PendingOracleTransmissionId,
873    /// Hash of the committed transaction.
874    pub transaction_hash: B256,
875    /// Block containing the resolved attempt.
876    pub block_number: u64,
877    /// Oracle-confirmed outcome.
878    pub kind: PendingOracleResolutionKind,
879}
880
881impl PendingOracleResolution {
882    /// Construct a committed resolution after matching its oracle signal.
883    pub const fn new(
884        update_id: PendingOracleUpdateId,
885        transmission_id: PendingOracleTransmissionId,
886        transaction_hash: B256,
887        block_number: u64,
888        kind: PendingOracleResolutionKind,
889    ) -> Self {
890        Self {
891            update_id,
892            transmission_id,
893            transaction_hash,
894            block_number,
895            kind,
896        }
897    }
898}
899
900/// Outcome of correlating a committed transmission with pending state.
901#[non_exhaustive]
902#[derive(Clone, Copy, Debug, PartialEq, Eq)]
903pub enum PendingOracleResolveOutcome {
904    /// Pending state and stream were updated.
905    Resolved,
906    /// This exact outcome was already recorded.
907    Duplicate,
908    /// No pending report has this update identity.
909    UnknownUpdate,
910    /// The report exists but not this transport variant.
911    UnknownTransmission,
912    /// Immutable transaction identity or a previous resolution disagreed.
913    Conflict,
914}
915
916/// Transport observation supplied to an installed pending oracle adapter.
917#[derive(Clone, Debug, PartialEq, Eq)]
918pub struct PendingTransportCandidate {
919    /// Chain where the transaction was observed.
920    pub chain_id: u64,
921    /// Stable identity of this transport variant.
922    pub id: PendingOracleTransmissionId,
923    /// Built-in or caller-defined transport family.
924    pub source: PendingOracleSource,
925    /// Concrete node, relay, or consumer-defined source identifier.
926    pub source_id: PendingOracleSourceId,
927    /// Outer transaction destination.
928    pub to: Address,
929    /// Outer transaction calldata.
930    pub calldata: Bytes,
931    /// Ordering reference disclosed by the source.
932    pub ordering: PendingOracleOrderingHandle,
933    /// Chain head at which the candidate was observed.
934    pub observed_at_head: Option<u64>,
935}
936
937impl PendingTransportCandidate {
938    /// Construct a transport candidate for pending adapter routing.
939    #[allow(clippy::too_many_arguments)]
940    pub fn new(
941        chain_id: u64,
942        id: PendingOracleTransmissionId,
943        source: PendingOracleSource,
944        source_id: PendingOracleSourceId,
945        to: Address,
946        calldata: Bytes,
947        ordering: PendingOracleOrderingHandle,
948        observed_at_head: Option<u64>,
949    ) -> Self {
950        Self {
951            chain_id,
952            id,
953            source,
954            source_id,
955            to,
956            calldata,
957            ordering,
958            observed_at_head,
959        }
960    }
961}
962
963/// Stable identity for one proposed oracle update across transport variants.
964#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
965pub struct PendingOracleUpdateId(B256);
966
967impl PendingOracleUpdateId {
968    /// Construct an update id from an adapter-defined content hash.
969    pub const fn from_hash(hash: B256) -> Self {
970        Self(hash)
971    }
972
973    /// Return the underlying content hash.
974    pub const fn as_hash(&self) -> B256 {
975        self.0
976    }
977}
978
979/// Stable identity for one transaction variant carrying a proposed update.
980#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
981pub struct PendingOracleTransmissionId(B256);
982
983impl PendingOracleTransmissionId {
984    /// Construct a transmission id from a transaction or transport content hash.
985    pub const fn from_hash(hash: B256) -> Self {
986        Self(hash)
987    }
988
989    /// Return the underlying content hash.
990    pub const fn as_hash(&self) -> B256 {
991        self.0
992    }
993}
994
995/// Stable identifier for a pending transport such as a node or relay stream.
996#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
997pub struct PendingOracleSourceId(String);
998
999impl PendingOracleSourceId {
1000    /// Construct a source identifier.
1001    pub fn new(id: impl Into<String>) -> Self {
1002        Self(id.into())
1003    }
1004
1005    /// Borrow the source identifier.
1006    pub fn as_str(&self) -> &str {
1007        &self.0
1008    }
1009}
1010
1011impl std::fmt::Display for PendingOracleSourceId {
1012    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013        self.0.fmt(formatter)
1014    }
1015}
1016
1017/// Oracle-specific call path carrying a proposed update.
1018#[non_exhaustive]
1019#[derive(Clone, Debug, PartialEq, Eq)]
1020pub enum PendingOracleRoute {
1021    /// A Chainlink OCR transmission sent directly to an aggregator.
1022    ChainlinkDirect {
1023        /// Destination aggregator.
1024        aggregator: Address,
1025        /// Whether the secondary transmit entry point was used.
1026        secondary: bool,
1027    },
1028    /// A Chainlink OCR transmission wrapped by a forwarder.
1029    ChainlinkForwarded {
1030        /// Outer call destination.
1031        forwarder: Address,
1032        /// Inner aggregator destination.
1033        aggregator: Address,
1034        /// Whether the secondary transmit entry point was used.
1035        secondary: bool,
1036    },
1037    /// Adapter-owned route not represented by a built-in variant.
1038    Adapter {
1039        /// Adapter owning the route semantics.
1040        adapter_id: OracleAdapterId,
1041        /// Adapter-local route kind.
1042        kind: String,
1043    },
1044}
1045
1046impl PendingOracleRoute {
1047    /// Construct an adapter-owned route.
1048    pub fn adapter(adapter_id: OracleAdapterId, kind: impl Into<String>) -> Self {
1049        Self::Adapter {
1050            adapter_id,
1051            kind: kind.into(),
1052        }
1053    }
1054}
1055
1056/// Concrete handle a downstream searcher may use to order after a transmission.
1057#[non_exhaustive]
1058#[derive(Clone, Debug, Default, PartialEq, Eq)]
1059pub enum PendingOracleOrderingHandle {
1060    /// A hash exposed by MEV-Share for hash-bound backrunning.
1061    MevShare {
1062        /// Relay-provided transaction hash.
1063        hash: B256,
1064    },
1065    /// Complete signed Ethereum transaction suitable for exact simulation.
1066    RawEthereumTransaction {
1067        /// Hash of the signed transaction.
1068        tx_hash: B256,
1069        /// Encoded signed transaction envelope.
1070        signed_envelope: Bytes,
1071    },
1072    /// A transaction hash with no claim that a specific relay accepts it as a constraint.
1073    TransactionHashOnly {
1074        /// Observed transaction hash.
1075        tx_hash: B256,
1076    },
1077    /// Chain-specific sequencer preconfirmation metadata.
1078    SequencerPreconfirmation {
1079        /// Preconfirmed transaction hash.
1080        tx_hash: B256,
1081        /// Proposed block number.
1082        block_number: u64,
1083        /// Sequencer payload identity.
1084        payload_id: B256,
1085        /// Transaction position within the preconfirmed payload.
1086        sequence_index: u64,
1087    },
1088    /// The source disclosed no actionable ordering reference.
1089    #[default]
1090    None,
1091}
1092
1093impl PendingOracleOrderingHandle {
1094    /// Return whether this handle contains a concrete transaction reference.
1095    pub const fn is_referenceable(&self) -> bool {
1096        !matches!(self, Self::None)
1097    }
1098
1099    /// Return the transaction hash when this handle exposes one.
1100    pub const fn transaction_hash(&self) -> Option<B256> {
1101        match self {
1102            Self::MevShare { hash } => Some(*hash),
1103            Self::RawEthereumTransaction { tx_hash, .. }
1104            | Self::TransactionHashOnly { tx_hash }
1105            | Self::SequencerPreconfirmation { tx_hash, .. } => Some(*tx_hash),
1106            Self::None => None,
1107        }
1108    }
1109}
1110
1111/// Fidelity available for an optimistic simulation.
1112#[non_exhaustive]
1113#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1114pub enum PendingOracleSimulationFidelity {
1115    /// No sufficient transaction or state projection material was disclosed.
1116    InsufficientMaterial,
1117    /// Only the decoded proposed answer can be projected.
1118    AnswerOnlyProjection,
1119    /// Adapter-specific state effects were independently verified.
1120    VerifiedStateProjection,
1121    /// The complete signed transaction can execute against a disposable fork.
1122    ExactSignedTransaction,
1123}
1124
1125/// Simulation material disclosed for one transmission.
1126#[non_exhaustive]
1127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1128pub enum PendingOracleSimulationMaterial {
1129    /// Execute the signed envelope carried by the ordering handle.
1130    ExactSignedTransaction,
1131    /// Apply independently verified adapter-specific state effects.
1132    VerifiedStateProjection,
1133    /// Project only the decoded answer.
1134    AnswerOnlyProjection,
1135    /// Do not use this candidate for an optimistic state simulation.
1136    InsufficientMaterial,
1137}
1138
1139impl PendingOracleSimulationMaterial {
1140    /// Return the advertised simulation fidelity.
1141    pub const fn fidelity(self) -> PendingOracleSimulationFidelity {
1142        match self {
1143            Self::ExactSignedTransaction => PendingOracleSimulationFidelity::ExactSignedTransaction,
1144            Self::VerifiedStateProjection => {
1145                PendingOracleSimulationFidelity::VerifiedStateProjection
1146            }
1147            Self::AnswerOnlyProjection => PendingOracleSimulationFidelity::AnswerOnlyProjection,
1148            Self::InsufficientMaterial => PendingOracleSimulationFidelity::InsufficientMaterial,
1149        }
1150    }
1151}
1152
1153/// Lifecycle state of one transaction variant.
1154#[non_exhaustive]
1155#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1156pub enum PendingOracleTransmissionStatus {
1157    /// The variant has been observed but not canonically resolved.
1158    #[default]
1159    Pending,
1160    /// The transaction landed and emitted the matching oracle confirmation signal.
1161    Landed,
1162    /// The transaction landed but reverted or did not emit the expected signal.
1163    Reverted,
1164    /// The source no longer exposes this variant and it was not observed landing.
1165    Dropped,
1166    /// A newer variant replaced this transport attempt.
1167    Superseded,
1168}
1169
1170/// One transaction or relay variant carrying a proposed oracle update.
1171#[derive(Clone, Debug, PartialEq, Eq)]
1172pub struct PendingOracleTransmission {
1173    /// Stable transport identity.
1174    pub id: PendingOracleTransmissionId,
1175    /// Source that disclosed this variant.
1176    pub source: PendingOracleSourceId,
1177    /// Oracle-specific outer and inner call path.
1178    pub route: PendingOracleRoute,
1179    /// Concrete downstream ordering reference, if one was disclosed.
1180    pub ordering: PendingOracleOrderingHandle,
1181    /// Available optimistic simulation material.
1182    pub simulation: PendingOracleSimulationMaterial,
1183    /// Hash of the complete outer calldata.
1184    pub calldata_hash: B256,
1185    /// Chain head at which this source observation was made.
1186    pub observed_at_head: Option<u64>,
1187    /// Local first-observation timestamp.
1188    pub first_seen_at: SystemTime,
1189    /// Current transport lifecycle state.
1190    pub status: PendingOracleTransmissionStatus,
1191}
1192
1193impl PendingOracleTransmission {
1194    /// Construct an unresolved transmission observed at the current local time.
1195    pub fn new(
1196        id: PendingOracleTransmissionId,
1197        source: PendingOracleSourceId,
1198        route: PendingOracleRoute,
1199        ordering: PendingOracleOrderingHandle,
1200        simulation: PendingOracleSimulationMaterial,
1201        calldata_hash: B256,
1202        observed_at_head: Option<u64>,
1203    ) -> Self {
1204        Self {
1205            id,
1206            source,
1207            route,
1208            ordering,
1209            simulation,
1210            calldata_hash,
1211            observed_at_head,
1212            first_seen_at: SystemTime::now(),
1213            status: PendingOracleTransmissionStatus::Pending,
1214        }
1215    }
1216
1217    /// Return whether the transmission has a usable transaction reference.
1218    pub const fn is_referenceable(&self) -> bool {
1219        self.ordering.is_referenceable()
1220    }
1221
1222    /// Return a coarse ranking suitable for selecting among otherwise valid variants.
1223    pub const fn actionability_rank(&self) -> u8 {
1224        match (&self.ordering, self.simulation.fidelity()) {
1225            (
1226                PendingOracleOrderingHandle::RawEthereumTransaction { .. },
1227                PendingOracleSimulationFidelity::ExactSignedTransaction,
1228            ) => 4,
1229            (PendingOracleOrderingHandle::MevShare { .. }, _) => 3,
1230            (PendingOracleOrderingHandle::RawEthereumTransaction { .. }, _) => 2,
1231            (PendingOracleOrderingHandle::TransactionHashOnly { .. }, _) => 1,
1232            (PendingOracleOrderingHandle::SequencerPreconfirmation { .. }, _) => 1,
1233            (PendingOracleOrderingHandle::None, _) => 0,
1234        }
1235    }
1236
1237    fn same_variant_content(&self, other: &Self) -> bool {
1238        self.id == other.id
1239            && self.source == other.source
1240            && self.route == other.route
1241            && self.ordering == other.ordering
1242            && self.simulation == other.simulation
1243            && self.calldata_hash == other.calldata_hash
1244    }
1245}
1246
1247/// Adapter-specific evidence supporting a pending oracle observation.
1248#[non_exhaustive]
1249#[derive(Clone, Debug, PartialEq, Eq)]
1250pub enum PendingOracleEvidence {
1251    /// A decoded Chainlink OCR2 report.
1252    ChainlinkReport(ChainlinkPendingReport),
1253    /// Evidence decoded by an oracle adapter not otherwise represented here.
1254    Adapter {
1255        /// Adapter that owns the evidence semantics.
1256        adapter_id: OracleAdapterId,
1257        /// Adapter-local evidence kind.
1258        kind: String,
1259        /// Content digest used for correlation and deduplication.
1260        digest: B256,
1261    },
1262}
1263
1264impl PendingOracleEvidence {
1265    /// Construct generic adapter-owned evidence.
1266    pub fn adapter(adapter_id: OracleAdapterId, kind: impl Into<String>, digest: B256) -> Self {
1267        Self::Adapter {
1268            adapter_id,
1269            kind: kind.into(),
1270            digest,
1271        }
1272    }
1273}
1274
1275/// Proposed update for one or more feeds already registered with the runtime.
1276#[derive(Clone, Debug, PartialEq, Eq)]
1277pub struct PendingOracleUpdate {
1278    /// Stable content identity.
1279    pub id: PendingOracleUpdateId,
1280    /// Registered feeds affected by this update.
1281    pub feed_ids: Vec<FeedId>,
1282    /// Adapter evidence used to recognize the update.
1283    pub evidence: PendingOracleEvidence,
1284    /// Decoded proposed raw value when the adapter can supply one.
1285    pub proposed_value: Option<PendingOracleValue>,
1286    /// Distinct transaction or relay variants carrying the same update content.
1287    pub transmissions: Vec<PendingOracleTransmission>,
1288    /// Current report-level speculative lifecycle.
1289    pub status: PendingOracleStatus,
1290}
1291
1292impl PendingOracleUpdate {
1293    /// Construct a pending update from its identity, affected feeds, and evidence.
1294    pub fn new(
1295        id: PendingOracleUpdateId,
1296        feed_ids: impl IntoIterator<Item = FeedId>,
1297        evidence: PendingOracleEvidence,
1298    ) -> Self {
1299        Self {
1300            id,
1301            feed_ids: feed_ids.into_iter().collect(),
1302            evidence,
1303            proposed_value: None,
1304            transmissions: Vec::new(),
1305            status: PendingOracleStatus::Pending,
1306        }
1307    }
1308
1309    /// Attach the decoded proposed raw oracle value.
1310    pub fn with_proposed_value(mut self, proposed_value: PendingOracleValue) -> Self {
1311        self.proposed_value = Some(proposed_value);
1312        self
1313    }
1314
1315    /// Attach one observed transaction or relay variant.
1316    pub fn with_transmission(mut self, transmission: PendingOracleTransmission) -> Self {
1317        self.transmissions.push(transmission);
1318        self
1319    }
1320}
1321
1322/// Oracle-agnostic proposed raw answer decoded before canonical inclusion.
1323#[derive(Clone, Debug, PartialEq, Eq)]
1324pub struct PendingOracleValue {
1325    /// Signed unscaled answer in the registered feed's native decimals.
1326    pub raw_answer: I256,
1327    /// Oracle-family timestamp associated with this proposed answer.
1328    pub observed_at: u64,
1329}
1330
1331/// Typed event emitted by the speculative pending pipeline.
1332#[non_exhaustive]
1333#[derive(Clone, Debug, PartialEq, Eq)]
1334pub enum PendingOracleEvent {
1335    /// A newly observed proposed oracle update.
1336    Observed(PendingOracleUpdate),
1337    /// Another transaction or relay variant was observed for a known update.
1338    TransmissionAdded {
1339        /// Known update identity.
1340        update_id: PendingOracleUpdateId,
1341        /// Newly observed transport variant.
1342        transmission: PendingOracleTransmission,
1343    },
1344    /// A candidate no longer maps to an eligible feed or outlived its observation window.
1345    Expired(PendingOracleUpdateId),
1346    /// A committed attempt was correlated with its oracle-family confirmation.
1347    Resolved(PendingOracleResolution),
1348    /// A concrete source changed connection or coverage state.
1349    SourceHealthChanged(PendingOracleSourceHealth),
1350    /// A source reported an interval that cannot be backfilled reliably.
1351    CoverageGap(PendingOracleCoverageGap),
1352}
1353
1354/// Sequence-numbered event delivered to pending subscribers.
1355#[derive(Clone, Debug, PartialEq, Eq)]
1356pub struct PendingOracleStreamEvent {
1357    /// Monotonic sequence within one pending runtime.
1358    pub sequence: u64,
1359    /// Typed pending event.
1360    pub event: PendingOracleEvent,
1361}
1362
1363/// Cloneable publishing handle used by pending sources and adapters.
1364#[derive(Clone, Debug)]
1365pub struct PendingOraclePublisher {
1366    sender: broadcast::Sender<PendingOracleStreamEvent>,
1367    sequence: Arc<AtomicU64>,
1368}
1369
1370impl PendingOraclePublisher {
1371    /// Publish an event and return the number of active receivers that accepted it.
1372    pub fn publish(&self, event: PendingOracleEvent) -> usize {
1373        let sequence = self.sequence.fetch_add(1, Ordering::Relaxed) + 1;
1374        self.sender
1375            .send(PendingOracleStreamEvent { sequence, event })
1376            .unwrap_or(0)
1377    }
1378}
1379
1380/// Receiving side of a bounded pending-oracle channel.
1381#[derive(Debug)]
1382pub struct PendingOracleSubscription {
1383    receiver: broadcast::Receiver<PendingOracleStreamEvent>,
1384}
1385
1386impl PendingOracleSubscription {
1387    /// Wait for the next pending event.
1388    pub async fn recv(&mut self) -> Result<PendingOracleStreamEvent, PendingOracleRecvError> {
1389        self.receiver.recv().await.map_err(Into::into)
1390    }
1391}
1392
1393/// Pending receiver failure with explicit lag reporting.
1394#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
1395pub enum PendingOracleRecvError {
1396    /// The publisher closed because the owning runtime was dropped.
1397    #[error("pending oracle channel closed")]
1398    Closed,
1399    /// This receiver fell behind the bounded channel.
1400    #[error("pending oracle receiver lagged by {missed} events")]
1401    Lagged {
1402        /// Number of events skipped by this receiver.
1403        missed: u64,
1404    },
1405}
1406
1407impl From<broadcast::error::RecvError> for PendingOracleRecvError {
1408    fn from(error: broadcast::error::RecvError) -> Self {
1409        match error {
1410            broadcast::error::RecvError::Closed => Self::Closed,
1411            broadcast::error::RecvError::Lagged(missed) => Self::Lagged { missed },
1412        }
1413    }
1414}