Skip to main content

evm_oracle_state/
runtime.rs

1use std::{
2    collections::BTreeSet,
3    fmt,
4    sync::Arc,
5    time::{SystemTime, UNIX_EPOCH},
6};
7
8use alloy_network::{Ethereum, Network};
9use alloy_primitives::{Address, Bytes, U256};
10use alloy_sol_types::SolCall;
11use evm_fork_cache::{
12    ColdStartCall, ColdStartConfig, ColdStartPlan, ColdStartPlanner, ColdStartResults,
13    ColdStartStep, StateView,
14    cache::EvmCache,
15    reactive::{
16        EventSubscriber, HandlerId, InterestOwnerSubscriber, ReactiveBatchReport, ReactiveConfig,
17        ReactiveEngine, ReactiveHandler, ReactiveInterest, ReactiveRuntime, SubscriberBackfill,
18    },
19};
20
21use crate::{
22    ChainlinkFeedProvider, FeedConfig, FeedId, FeedRegistration, OracleAdapterFeedSkip,
23    OracleAdapterPlugin, OracleCodeRegistry, OracleCodeWarmupPolicy, OracleCodeWarmupReport,
24    OracleDiscoveryContext, OracleError, OracleFeedReadinessReport, OracleFeedStatus,
25    OracleHookEvent, OraclePrice, OraclePriceCorrected, OraclePriceUpdate, OracleReactiveHandler,
26    OracleReadOverlay, OracleReconciler, OracleRegistry, OracleSignal, OracleStorageAdapter,
27    OracleStorageSync, OracleTracker, RoundData, StalenessPolicy,
28};
29
30#[cfg(feature = "pending-oracle-updates")]
31use evm_fork_cache::reactive::ReactiveInput;
32
33#[cfg(feature = "pending-oracle-updates")]
34use crate::{
35    PendingOracleAdapter, PendingOracleCandidateSource, PendingOracleConfig, PendingOracleRuntime,
36    PendingOracleSourceError, PendingOracleSourceSession,
37};
38
39type OracleEventCallback = Arc<dyn Fn(&OracleHookEvent) + Send + Sync>;
40
41alloy_sol_types::sol! {
42    function latestRoundData() external view returns (
43        uint80 roundId,
44        int256 answer,
45        uint256 startedAt,
46        uint256 updatedAt,
47        uint80 answeredInRound
48    );
49}
50
51/// Builder-friendly Chainlink feed registration.
52#[derive(Clone, Debug)]
53pub struct ChainlinkFeed {
54    config: FeedConfig,
55}
56
57impl ChainlinkFeed {
58    /// Create a feed registration for a Chainlink-compatible proxy.
59    pub fn new(proxy: Address) -> Self {
60        Self {
61            config: FeedConfig {
62                proxy,
63                ..Default::default()
64            },
65        }
66    }
67
68    /// Set a stable feed id.
69    pub fn id(mut self, id: impl Into<String>) -> Self {
70        self.config.id = Some(FeedId::new(id));
71        self
72    }
73
74    /// Set a stable feed id.
75    pub fn feed_id(mut self, id: FeedId) -> Self {
76        self.config.id = Some(id);
77        self
78    }
79
80    /// Set a human-readable label.
81    pub fn label(mut self, label: impl Into<String>) -> Self {
82        self.config.label = Some(label.into());
83        self
84    }
85
86    /// Set the base symbol.
87    pub fn base(mut self, base: impl Into<String>) -> Self {
88        self.config.base = Some(base.into());
89        self
90    }
91
92    /// Set the quote symbol.
93    pub fn quote(mut self, quote: impl Into<String>) -> Self {
94        self.config.quote = Some(quote.into());
95        self
96    }
97
98    /// Set a max-age staleness policy.
99    pub fn max_age_secs(mut self, max_age_secs: u64) -> Self {
100        self.config.staleness = StalenessPolicy::max_age(max_age_secs);
101        self
102    }
103
104    /// Set the full staleness policy.
105    pub fn staleness(mut self, staleness: StalenessPolicy) -> Self {
106        self.config.staleness = staleness;
107        self
108    }
109
110    /// Allow zero or negative answers for signed generic feeds.
111    pub fn allow_zero_or_negative_answer(mut self, allow: bool) -> Self {
112        self.config.staleness = self.config.staleness.allow_zero_or_negative_answer(allow);
113        self
114    }
115
116    /// Convert into the lower-level registration config.
117    pub fn into_config(self) -> FeedConfig {
118        self.config
119    }
120}
121
122impl From<ChainlinkFeed> for FeedConfig {
123    fn from(feed: ChainlinkFeed) -> Self {
124        feed.into_config()
125    }
126}
127
128/// Builder for a high-level oracle runtime facade.
129pub struct OracleRuntimeBuilder<P> {
130    provider: P,
131    feeds: Vec<ChainlinkFeed>,
132    now_timestamp: Option<u64>,
133    storage_sync: OracleStorageSync,
134    callbacks: Vec<OracleEventCallback>,
135    #[cfg(feature = "pending-oracle-updates")]
136    pending_config: Option<PendingOracleConfig>,
137    #[cfg(feature = "pending-oracle-updates")]
138    pending_adapters: Vec<Arc<dyn PendingOracleAdapter>>,
139}
140
141/// Cache-native runtime builder with first-class oracle adapter plugins.
142pub struct OracleCacheRuntimeBuilder {
143    adapters: Vec<Arc<dyn OracleAdapterPlugin>>,
144    now_timestamp: Option<u64>,
145    storage_sync: OracleStorageSync,
146    storage_warmup_enabled: bool,
147    storage_warmup_mode: OracleStorageWarmupMode,
148    code_registry: OracleCodeRegistry,
149    code_warmup_policy: OracleCodeWarmupPolicy,
150    callbacks: Vec<OracleEventCallback>,
151    #[cfg(feature = "pending-oracle-updates")]
152    pending_config: Option<PendingOracleConfig>,
153    #[cfg(feature = "pending-oracle-updates")]
154    pending_adapters: Vec<Arc<dyn PendingOracleAdapter>>,
155}
156
157/// Best-effort cache-native oracle runtime build report.
158#[non_exhaustive]
159#[derive(Debug)]
160pub struct OracleCacheRuntimeBuildReport {
161    /// Runtime seeded with every successfully discovered feed.
162    pub runtime: OracleRuntime<()>,
163    /// Feeds that were skipped because an adapter could not register them.
164    pub skipped: Vec<OracleAdapterFeedSkip>,
165    /// Feed/runtime readiness after discovery, warmup, and skipped-source classification.
166    pub feed_statuses: Vec<OracleFeedReadinessReport>,
167    /// Storage slots warmed for direct oracle event writes.
168    pub storage_warmup: OracleStorageWarmupReport,
169    /// Bytecode seed/verify/etch results.
170    pub code_warmup: OracleCodeWarmupReport,
171}
172
173/// Result of prewarming oracle storage slots through `evm-fork-cache`.
174#[derive(Clone, Debug, Default, PartialEq, Eq)]
175pub struct OracleStorageWarmupReport {
176    /// Warmup mode used for this run.
177    pub mode: OracleStorageWarmupMode,
178    /// Per-feed readiness after storage warmup.
179    pub feed_statuses: Vec<OracleFeedReadinessReport>,
180    /// Slots requested for warmup.
181    pub requested_slots: usize,
182    /// Slots successfully loaded into the cache.
183    pub loaded_slots: usize,
184    /// Slots that failed to load.
185    pub failed_slots: Vec<OracleStorageWarmupFailure>,
186    /// Read calls submitted to cold-start discovery.
187    pub discovery_calls: usize,
188    /// Cold-start summary when the warmup used `EvmCache::run_cold_start`.
189    pub cold_start: Option<OracleColdStartWarmupReport>,
190}
191
192impl OracleStorageWarmupReport {
193    /// Return true when no storage warmup work was requested or performed.
194    pub fn is_empty(&self) -> bool {
195        self.requested_slots == 0
196            && self.loaded_slots == 0
197            && self.failed_slots.is_empty()
198            && self.discovery_calls == 0
199            && self.cold_start.is_none()
200    }
201}
202
203/// Storage warmup implementation to use for oracle direct-write slots.
204#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
205pub enum OracleStorageWarmupMode {
206    /// Bulk-fetch declared hot slots through `EvmCache::prewarm_slots`.
207    #[default]
208    PrewarmSlots,
209    /// Verify declared hot slots through `EvmCache::run_cold_start`.
210    ColdStart,
211}
212
213/// Summary of an oracle cold-start storage warmup run.
214#[derive(Clone, Debug, Default, PartialEq, Eq)]
215pub struct OracleColdStartWarmupReport {
216    /// Cold-start rounds executed.
217    pub rounds: usize,
218    /// Total slots requested in verify phases.
219    pub verified_slots: usize,
220    /// Slots whose fetched value changed and was injected.
221    pub changed_slots: usize,
222    /// Total fetch failures across verify/probe phases.
223    pub failed_slots: usize,
224    /// Total storage slots discovered by optional discovery calls.
225    pub discovered_slots: usize,
226    /// Total accounts discovered by optional discovery calls.
227    pub discovered_accounts: usize,
228    /// Total discovery calls executed.
229    pub discover_calls: usize,
230}
231
232/// One failed oracle storage warmup slot.
233#[derive(Clone, Debug, PartialEq, Eq)]
234pub struct OracleStorageWarmupFailure {
235    /// Contract address.
236    pub address: Address,
237    /// Storage slot.
238    pub slot: U256,
239    /// Fetch error message.
240    pub reason: String,
241}
242
243/// Result of registering oracle handlers with a reactive engine.
244#[derive(Clone, Debug, Default, PartialEq, Eq)]
245pub struct OracleReactiveInstallReport {
246    /// Handler ids submitted to the engine.
247    pub handler_ids: Vec<HandlerId>,
248}
249
250impl OracleReactiveInstallReport {
251    /// Number of handlers submitted.
252    pub fn len(&self) -> usize {
253        self.handler_ids.len()
254    }
255
256    /// Return true when no handlers were submitted.
257    pub fn is_empty(&self) -> bool {
258        self.handler_ids.is_empty()
259    }
260}
261
262/// Result of unregistering oracle handlers from a reactive engine.
263#[derive(Clone, Debug, Default, PartialEq, Eq)]
264pub struct OracleReactiveUninstallReport {
265    /// Handler ids requested for removal.
266    pub handler_ids: Vec<HandlerId>,
267    /// Handler ids that were present and removed.
268    pub removed_handler_ids: Vec<HandlerId>,
269}
270
271/// Result of refreshing oracle handlers after runtime mutation.
272#[derive(Clone, Debug, Default, PartialEq, Eq)]
273pub struct OracleReactiveRefreshReport {
274    /// Handler ids requested for removal from the previous install handle.
275    pub previous_handler_ids: Vec<HandlerId>,
276    /// Previous handler ids that were present and removed.
277    pub removed_handler_ids: Vec<HandlerId>,
278    /// Current handler ids installed after the refresh.
279    pub installed_handler_ids: Vec<HandlerId>,
280}
281
282/// Result of mutating a runtime's oracle registration set.
283#[derive(Clone, Debug, Default)]
284pub struct OracleRuntimeMutationReport {
285    /// Feed ids registered by this mutation.
286    pub registered_feed_ids: Vec<FeedId>,
287    /// Feed ids removed by this mutation.
288    pub removed_feed_ids: Vec<FeedId>,
289    /// Feeds skipped by adapter discovery, when applicable.
290    pub skipped: Vec<OracleAdapterFeedSkip>,
291    /// Feed/runtime readiness after the mutation.
292    pub feed_statuses: Vec<OracleFeedReadinessReport>,
293    /// Current handler ids after this mutation.
294    pub current_handler_ids: Vec<HandlerId>,
295}
296
297#[derive(Clone)]
298struct OracleAdapterRuntimeState {
299    adapter: Arc<dyn OracleAdapterPlugin>,
300    registrations: Vec<FeedRegistration>,
301}
302
303impl fmt::Debug for OracleAdapterRuntimeState {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        f.debug_struct("OracleAdapterRuntimeState")
306            .field("adapter_id", &self.adapter.adapter_id())
307            .field("registrations_len", &self.registrations.len())
308            .finish()
309    }
310}
311
312impl<P> OracleRuntimeBuilder<P> {
313    /// Create a runtime builder around a provider.
314    pub fn new(provider: P) -> Self {
315        Self {
316            provider,
317            feeds: Vec::new(),
318            now_timestamp: None,
319            storage_sync: OracleStorageSync::chainlink_defaults(),
320            callbacks: Vec::new(),
321            #[cfg(feature = "pending-oracle-updates")]
322            pending_config: None,
323            #[cfg(feature = "pending-oracle-updates")]
324            pending_adapters: Vec::new(),
325        }
326    }
327
328    /// Add one feed registration.
329    pub fn feed(mut self, feed: ChainlinkFeed) -> Self {
330        self.feeds.push(feed);
331        self
332    }
333
334    /// Set a fixed timestamp for deterministic registration status classification.
335    pub fn now_timestamp(mut self, now_timestamp: u64) -> Self {
336        self.now_timestamp = Some(now_timestamp);
337        self
338    }
339
340    /// Add one storage adapter.
341    pub fn storage_adapter<A>(mut self, adapter: A) -> Self
342    where
343        A: OracleStorageAdapter + 'static,
344    {
345        self.storage_sync.push_adapter(Arc::new(adapter));
346        self
347    }
348
349    /// Replace the storage-sync registry.
350    pub fn storage_sync(mut self, storage_sync: OracleStorageSync) -> Self {
351        self.storage_sync = storage_sync;
352        self
353    }
354
355    /// Subscribe to every typed oracle event emitted by this runtime.
356    pub fn on_event<F>(mut self, callback: F) -> Self
357    where
358        F: Fn(&OracleHookEvent) + Send + Sync + 'static,
359    {
360        self.callbacks.push(Arc::new(callback));
361        self
362    }
363
364    /// Subscribe to immediate event-derived price updates.
365    pub fn on_price_update<F>(self, callback: F) -> Self
366    where
367        F: Fn(&OraclePriceUpdate) + Send + Sync + 'static,
368    {
369        self.on_event(move |event| {
370            if let OracleHookEvent::PriceUpdate(update) = event {
371                callback(update);
372            }
373        })
374    }
375
376    /// Subscribe to proxy corrections.
377    pub fn on_price_corrected<F>(self, callback: F) -> Self
378    where
379        F: Fn(&OraclePriceCorrected) + Send + Sync + 'static,
380    {
381        self.on_event(move |event| {
382            if let OracleHookEvent::PriceCorrected(corrected) = event {
383                callback(corrected);
384            }
385        })
386    }
387
388    /// Enable speculative pending updates for the feeds registered by this builder.
389    #[cfg(feature = "pending-oracle-updates")]
390    pub fn pending_updates(mut self, config: PendingOracleConfig) -> Self {
391        self.pending_config = Some(config);
392        self
393    }
394
395    /// Install an additional oracle-family decoder for the pending channel.
396    #[cfg(feature = "pending-oracle-updates")]
397    pub fn pending_adapter<A>(mut self, adapter: A) -> Self
398    where
399        A: PendingOracleAdapter,
400    {
401        self.pending_adapters.push(Arc::new(adapter));
402        self
403    }
404}
405
406impl<P: ChainlinkFeedProvider> OracleRuntimeBuilder<P> {
407    /// Register feeds and construct the runtime.
408    ///
409    /// # Errors
410    ///
411    /// Returns [`OracleError::Provider`] when a proxy read fails,
412    /// [`OracleError::DuplicateFeedId`]/[`OracleError::DuplicateProxy`] when
413    /// two feeds collide, and [`OracleError::Config`] when the system clock is
414    /// before the UNIX epoch and no `now_timestamp` was set.
415    pub async fn build(self) -> Result<OracleRuntime<P>, OracleError> {
416        let now_timestamp = match self.now_timestamp {
417            Some(now_timestamp) => now_timestamp,
418            None => SystemTime::now()
419                .duration_since(UNIX_EPOCH)
420                .map_err(crate::error::clock_error)?
421                .as_secs(),
422        };
423        let mut registry = OracleRegistry::new_at_timestamp(now_timestamp);
424        for feed in self.feeds {
425            registry
426                .register_chainlink_feed(&self.provider, feed.into_config())
427                .await?;
428        }
429
430        let tracker = OracleTracker::new(registry);
431        #[cfg(feature = "pending-oracle-updates")]
432        let pending = self.pending_config.map(|config| {
433            PendingOracleRuntime::from_registrations(
434                config,
435                tracker.registrations_iter(),
436                self.pending_adapters,
437            )
438        });
439
440        Ok(OracleRuntime {
441            provider: self.provider,
442            tracker,
443            storage_sync: self.storage_sync,
444            callbacks: self.callbacks,
445            adapter_states: Vec::new(),
446            #[cfg(feature = "pending-oracle-updates")]
447            pending,
448            #[cfg(feature = "pending-oracle-updates")]
449            pending_source_sessions: Vec::new(),
450        })
451    }
452}
453
454impl Default for OracleCacheRuntimeBuilder {
455    fn default() -> Self {
456        Self {
457            adapters: Vec::new(),
458            now_timestamp: None,
459            storage_sync: OracleStorageSync::chainlink_defaults(),
460            storage_warmup_enabled: true,
461            storage_warmup_mode: OracleStorageWarmupMode::default(),
462            code_registry: OracleCodeRegistry::default(),
463            code_warmup_policy: OracleCodeWarmupPolicy::default(),
464            callbacks: Vec::new(),
465            #[cfg(feature = "pending-oracle-updates")]
466            pending_config: None,
467            #[cfg(feature = "pending-oracle-updates")]
468            pending_adapters: Vec::new(),
469        }
470    }
471}
472
473impl OracleCacheRuntimeBuilder {
474    /// Install one oracle adapter plugin.
475    pub fn install_adapter<A>(mut self, adapter: A) -> Self
476    where
477        A: OracleAdapterPlugin + 'static,
478    {
479        self.adapters.push(Arc::new(adapter));
480        self
481    }
482
483    /// Set a fixed timestamp for deterministic registration status classification.
484    pub fn now_timestamp(mut self, now_timestamp: u64) -> Self {
485        self.now_timestamp = Some(now_timestamp);
486        self
487    }
488
489    /// Add one storage adapter.
490    pub fn storage_adapter<A>(mut self, adapter: A) -> Self
491    where
492        A: OracleStorageAdapter + 'static,
493    {
494        self.storage_sync.push_adapter(Arc::new(adapter));
495        self
496    }
497
498    /// Replace the storage-sync registry.
499    pub fn storage_sync(mut self, storage_sync: OracleStorageSync) -> Self {
500        self.storage_sync = storage_sync;
501        self
502    }
503
504    /// Enable or disable oracle storage warmup during cache-native builds.
505    ///
506    /// Enabled by default. When enabled, the builder asks installed storage
507    /// adapters which slots must be hot for direct event writes and bulk-loads
508    /// them through the cache before returning the runtime.
509    pub fn storage_warmup(mut self, enabled: bool) -> Self {
510        self.storage_warmup_enabled = enabled;
511        self
512    }
513
514    /// Set the oracle storage warmup mode.
515    pub fn storage_warmup_mode(mut self, mode: OracleStorageWarmupMode) -> Self {
516        self.storage_warmup_mode = mode;
517        self
518    }
519
520    /// Use `EvmCache::run_cold_start` for declared oracle storage slots.
521    pub fn storage_cold_start(self) -> Self {
522        self.storage_warmup_mode(OracleStorageWarmupMode::ColdStart)
523    }
524
525    /// Disable oracle storage warmup.
526    pub fn disable_storage_warmup(self) -> Self {
527        self.storage_warmup(false)
528    }
529
530    /// Replace the bytecode seed/etch registry.
531    pub fn code_registry(mut self, registry: OracleCodeRegistry) -> Self {
532        self.code_registry = registry;
533        self
534    }
535
536    /// Set the bytecode warmup failure policy.
537    pub fn code_warmup_policy(mut self, policy: OracleCodeWarmupPolicy) -> Self {
538        self.code_warmup_policy = policy;
539        self
540    }
541
542    /// Add a canonical bytecode seed that must verify against on-chain code hash.
543    pub fn code_seed(mut self, address: Address, code: Bytes) -> Self {
544        self.code_registry = self.code_registry.seed(address, code);
545        self
546    }
547
548    /// Add multiple canonical bytecode seeds that must verify against chain code hash.
549    pub fn code_seeds<I>(mut self, seeds: I) -> Self
550    where
551        I: IntoIterator<Item = (Address, Bytes)>,
552    {
553        self.code_registry = self.code_registry.seed_many(seeds);
554        self
555    }
556
557    /// Add explicit simulation-only bytecode at `address`.
558    pub fn code_etch(mut self, address: Address, code: Bytes) -> Self {
559        self.code_registry = self.code_registry.etch(address, code);
560        self
561    }
562
563    /// Add multiple explicit simulation-only bytecode etches.
564    pub fn code_etches<I>(mut self, etches: I) -> Self
565    where
566        I: IntoIterator<Item = (Address, Bytes)>,
567    {
568        self.code_registry = self.code_registry.etch_many(etches);
569        self
570    }
571
572    /// Subscribe to every typed oracle event emitted by this runtime.
573    pub fn on_event<F>(mut self, callback: F) -> Self
574    where
575        F: Fn(&OracleHookEvent) + Send + Sync + 'static,
576    {
577        self.callbacks.push(Arc::new(callback));
578        self
579    }
580
581    /// Subscribe to immediate event-derived price updates.
582    pub fn on_price_update<F>(self, callback: F) -> Self
583    where
584        F: Fn(&OraclePriceUpdate) + Send + Sync + 'static,
585    {
586        self.on_event(move |event| {
587            if let OracleHookEvent::PriceUpdate(update) = event {
588                callback(update);
589            }
590        })
591    }
592
593    /// Enable speculative pending updates for adapter-discovered registrations.
594    #[cfg(feature = "pending-oracle-updates")]
595    pub fn pending_updates(mut self, config: PendingOracleConfig) -> Self {
596        self.pending_config = Some(config);
597        self
598    }
599
600    /// Install an additional oracle-family decoder for the pending channel.
601    #[cfg(feature = "pending-oracle-updates")]
602    pub fn pending_adapter<A>(mut self, adapter: A) -> Self
603    where
604        A: PendingOracleAdapter,
605    {
606        self.pending_adapters.push(Arc::new(adapter));
607        self
608    }
609
610    /// Register adapter-discovered feeds and construct the runtime.
611    ///
612    /// # Errors
613    ///
614    /// Returns [`OracleError::FeedSkipped`] when any adapter reported a
615    /// skipped feed (use [`Self::build_report`] to receive skips as data),
616    /// [`OracleError::Policy`] when code warmup fails the configured
617    /// [`OracleCodeWarmupPolicy`], [`OracleError::Provider`] when adapter
618    /// discovery reads fail, [`OracleError::DuplicateFeedId`]/
619    /// [`OracleError::DuplicateProxy`] when discovered feeds collide, and
620    /// [`OracleError::Config`] when the system clock is before the UNIX epoch
621    /// and no `now_timestamp` was set.
622    pub async fn build(self, cache: &mut EvmCache) -> Result<OracleRuntime<()>, OracleError> {
623        let report = self.build_report(cache).await?;
624        if let Some(skipped) = report.skipped.first() {
625            return Err(cache_runtime_skip_error(skipped));
626        }
627        Ok(report.runtime)
628    }
629
630    /// Register every compatible adapter feed and return skipped feeds instead of failing fast.
631    pub async fn build_report(
632        self,
633        cache: &mut EvmCache,
634    ) -> Result<OracleCacheRuntimeBuildReport, OracleError> {
635        let now_timestamp = match self.now_timestamp {
636            Some(now_timestamp) => now_timestamp,
637            None => SystemTime::now()
638                .duration_since(UNIX_EPOCH)
639                .map_err(crate::error::clock_error)?
640                .as_secs(),
641        };
642
643        let code_warmup = self
644            .code_registry
645            .apply_to_cache_with_policy(cache, self.code_warmup_policy)?;
646        let mut discovered_by_adapter = Vec::new();
647        let mut seeded = Vec::new();
648        let mut warmup_registrations = Vec::new();
649        let mut skipped = Vec::new();
650        for adapter in &self.adapters {
651            let report = adapter
652                .discover(OracleDiscoveryContext {
653                    cache,
654                    now_timestamp,
655                })
656                .await?;
657            skipped.extend(report.skipped);
658            let registrations = report
659                .feeds
660                .iter()
661                .map(|feed| feed.registration.clone())
662                .collect::<Vec<_>>();
663            seeded.extend(report.feeds.into_iter().map(|feed| {
664                warmup_registrations.push(feed.registration.clone());
665                (feed.registration, feed.round)
666            }));
667            discovered_by_adapter.push(OracleAdapterRuntimeState {
668                adapter: Arc::clone(adapter),
669                registrations,
670            });
671        }
672
673        let storage_warmup = if self.storage_warmup_enabled {
674            prewarm_oracle_storage(
675                cache,
676                &self.storage_sync,
677                &warmup_registrations.iter().collect::<Vec<_>>(),
678                self.storage_warmup_mode,
679            )
680        } else {
681            OracleStorageWarmupReport {
682                feed_statuses: feed_readiness_from_registrations(&warmup_registrations),
683                ..OracleStorageWarmupReport::default()
684            }
685        };
686        let mut feed_statuses = storage_warmup.feed_statuses.clone();
687        feed_statuses.extend(skipped.iter().map(skipped_feed_readiness));
688        let tracker = OracleTracker::from_registrations_at_timestamp(seeded, now_timestamp)?;
689        #[cfg(feature = "pending-oracle-updates")]
690        let pending = self.pending_config.map(|config| {
691            PendingOracleRuntime::from_registrations(
692                config,
693                tracker.registrations_iter(),
694                self.pending_adapters,
695            )
696        });
697        Ok(OracleCacheRuntimeBuildReport {
698            runtime: OracleRuntime {
699                provider: (),
700                tracker,
701                storage_sync: self.storage_sync,
702                callbacks: self.callbacks,
703                adapter_states: discovered_by_adapter,
704                #[cfg(feature = "pending-oracle-updates")]
705                pending,
706                #[cfg(feature = "pending-oracle-updates")]
707                pending_source_sessions: Vec::new(),
708            },
709            skipped,
710            feed_statuses,
711            storage_warmup,
712            code_warmup,
713        })
714    }
715}
716
717/// High-level facade for registration, typed events, reads, and reconciliation.
718pub struct OracleRuntime<P> {
719    provider: P,
720    tracker: OracleTracker,
721    storage_sync: OracleStorageSync,
722    callbacks: Vec<OracleEventCallback>,
723    adapter_states: Vec<OracleAdapterRuntimeState>,
724    #[cfg(feature = "pending-oracle-updates")]
725    pending: Option<PendingOracleRuntime>,
726    #[cfg(feature = "pending-oracle-updates")]
727    pending_source_sessions: Vec<PendingOracleSourceSession>,
728}
729
730impl<P: fmt::Debug> fmt::Debug for OracleRuntime<P> {
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        let mut debug = f.debug_struct("OracleRuntime");
733        debug
734            .field("provider", &self.provider)
735            .field("tracker", &self.tracker)
736            .field("storage_sync", &self.storage_sync)
737            .field("callbacks_len", &self.callbacks.len())
738            .field("adapter_states", &self.adapter_states);
739        #[cfg(feature = "pending-oracle-updates")]
740        debug.field("pending", &self.pending);
741        #[cfg(feature = "pending-oracle-updates")]
742        debug.field(
743            "pending_source_sessions_len",
744            &self.pending_source_sessions.len(),
745        );
746        debug.finish()
747    }
748}
749
750impl<P> OracleRuntime<P> {
751    /// Start building a runtime around a provider.
752    pub fn builder(provider: P) -> OracleRuntimeBuilder<P> {
753        OracleRuntimeBuilder::new(provider)
754    }
755
756    /// Borrow the provider used for authoritative proxy reads and reconciliation.
757    pub fn provider(&self) -> &P {
758        &self.provider
759    }
760
761    /// Borrow the typed state store for snapshot, price, and registration reads.
762    pub fn tracker(&self) -> &OracleTracker {
763        &self.tracker
764    }
765
766    /// Mutably borrow the tracker.
767    ///
768    /// **Warning:** mutating the tracker directly (registering or removing
769    /// feeds through [`OracleTracker`] methods) desynchronizes this runtime's
770    /// adapter bookkeeping: `adapter_states` still references removed feeds
771    /// (or misses added ones), so [`Self::reactive_handlers`] and the
772    /// `refresh_handlers*` methods rebuild handler routing from a stale feed
773    /// set. Use [`Self::register_seeded_feed`], [`Self::register_adapter`],
774    /// [`Self::unregister_feed_by_id`], or [`Self::unregister_feed_by_proxy`]
775    /// instead — they keep typed state and handler routing in sync. Reserve
776    /// this accessor for operations that do not change the registration set
777    /// (for example [`OracleTracker::reconcile`] or
778    /// [`OracleTracker::apply_batch_report`]).
779    pub fn tracker_mut(&mut self) -> &mut OracleTracker {
780        &mut self.tracker
781    }
782
783    /// Return feed/runtime readiness for all registered feeds.
784    pub fn feed_readiness(&self) -> Vec<OracleFeedReadinessReport> {
785        self.tracker.feed_readiness()
786    }
787
788    /// Borrow the opt-in speculative pending-update runtime, when enabled.
789    #[cfg(feature = "pending-oracle-updates")]
790    pub fn pending_updates(&self) -> Option<&PendingOracleRuntime> {
791        self.pending.as_ref()
792    }
793
794    /// Start and retain a pending candidate source for the lifetime of this runtime.
795    ///
796    /// The corresponding source family must be enabled in the runtime's
797    /// [`PendingOracleConfig`]. Dropping the runtime requests source shutdown.
798    #[cfg(feature = "pending-oracle-updates")]
799    pub fn start_pending_source<S>(&mut self, source: S) -> Result<(), PendingOracleSourceError>
800    where
801        S: PendingOracleCandidateSource,
802    {
803        let pending = self
804            .pending
805            .as_ref()
806            .ok_or(PendingOracleSourceError::PendingUpdatesDisabled)?;
807        let session = pending.start_source(source)?;
808        self.pending_source_sessions.push(session);
809        Ok(())
810    }
811
812    pub(crate) fn storage_sync(&self) -> &OracleStorageSync {
813        &self.storage_sync
814    }
815
816    /// Replace the direct-storage registry used by subsequently constructed
817    /// reactive handlers and storage warmup calls.
818    ///
819    /// This is primarily useful after cache-native discovery returns an
820    /// [`OracleTracker`] that needs a deployment-specific storage adapter:
821    /// construct the facade with [`OracleRuntime::from_tracker`], then retain
822    /// the custom registry here before calling [`Self::reactive_runtime`],
823    /// [`Self::reactive_handler`], or [`Self::prewarm_storage`].
824    ///
825    /// If this runtime's handlers are already installed in a live reactive
826    /// engine, replace the registry only as part of a handler refresh so the
827    /// installed handlers cannot retain the old storage policy.
828    pub fn with_storage_sync(mut self, storage_sync: OracleStorageSync) -> Self {
829        self.storage_sync = storage_sync;
830        self
831    }
832
833    /// Build a fresh built-in Chainlink-compatible reactive handler from current registrations.
834    ///
835    /// Adapter plugin handlers are not included. Use [`Self::reactive_handlers`],
836    /// [`Self::reactive_runtime`], or [`Self::register_subscriber`] when the runtime
837    /// was built with installed adapter plugins.
838    pub fn reactive_handler(&self) -> OracleReactiveHandler {
839        OracleReactiveHandler::with_storage_sync(
840            self.tracker.registrations().collect(),
841            self.storage_sync.clone(),
842        )
843    }
844
845    /// Build fresh reactive handlers for every source installed in this runtime.
846    ///
847    /// The first handler is the built-in Chainlink-compatible handler. Any adapter
848    /// plugin handlers discovered during cache-native startup follow it.
849    ///
850    /// Handler ids must be unique across installed adapter plugins: when two
851    /// adapters return handlers with the same [`HandlerId`], only the first
852    /// handler is kept and the second adapter's event routing is silently
853    /// dropped (a `tracing` warning is emitted). An adapter handler that
854    /// reuses the built-in Chainlink handler id is fine — the built-in
855    /// handler is rebuilt from *all* current registrations, so it already
856    /// routes that adapter's feeds.
857    pub fn reactive_handlers(&self) -> Vec<Arc<dyn ReactiveHandler<Ethereum>>> {
858        let built_in: Arc<dyn ReactiveHandler<Ethereum>> = Arc::new(self.reactive_handler());
859        let built_in_id = built_in.id();
860        let mut adapter_ids = BTreeSet::new();
861        let mut handlers = vec![built_in];
862        for state in self
863            .adapter_states
864            .iter()
865            .filter(|state| !state.registrations.is_empty())
866        {
867            let handler = state
868                .adapter
869                .reactive_handler(state.registrations.clone(), self.storage_sync.clone());
870            let id = handler.id();
871            if id == built_in_id {
872                // By design: the adapter delegates to the built-in Chainlink
873                // handler, which is already built from all registrations.
874                continue;
875            }
876            if !adapter_ids.insert(id.clone()) {
877                tracing::warn!(
878                    handler_id = %id,
879                    adapter_id = %state.adapter.adapter_id(),
880                    "duplicate reactive HandlerId from adapter plugins; \
881                     dropping this adapter's handler routing"
882                );
883                continue;
884            }
885            handlers.push(handler);
886        }
887        handlers
888    }
889
890    /// Return current reactive handler ids for this runtime.
891    pub fn reactive_handler_ids(&self) -> Vec<HandlerId> {
892        self.reactive_handlers()
893            .into_iter()
894            .map(|handler| handler.id())
895            .collect()
896    }
897
898    /// Build a reactive runtime with this oracle runtime's handlers already installed.
899    pub fn reactive_runtime(&self) -> Result<ReactiveRuntime<Ethereum>, OracleError> {
900        let mut runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
901        for handler in self.reactive_handlers() {
902            runtime.register_handler(handler).map_err(runtime_error)?;
903        }
904        Ok(runtime)
905    }
906
907    /// Register this runtime's handlers with an engine, using continuity-safe backfill.
908    ///
909    /// # Errors
910    ///
911    /// Returns [`OracleError::Reactive`] when the engine rejects a handler
912    /// registration (for example a handler id that is already installed).
913    pub async fn install_handlers<S>(
914        &self,
915        engine: &mut ReactiveEngine<S, Ethereum>,
916    ) -> Result<OracleReactiveInstallReport, OracleError>
917    where
918        S: InterestOwnerSubscriber<Ethereum>,
919    {
920        let mut report = OracleReactiveInstallReport::default();
921        for handler in self.reactive_handlers() {
922            let id = handler.id();
923            engine
924                .register_handler(handler)
925                .await
926                .map_err(runtime_error)?;
927            report.handler_ids.push(id);
928        }
929        Ok(report)
930    }
931
932    /// Refresh an existing engine install after registering or unregistering feeds.
933    ///
934    /// `installed` must be the report returned by a previous install/refresh.
935    /// The old handler ids in that report are removed first, then the runtime's
936    /// current handlers are installed with continuity-safe backfill.
937    pub async fn refresh_handlers<S>(
938        &self,
939        engine: &mut ReactiveEngine<S, Ethereum>,
940        installed: &mut OracleReactiveInstallReport,
941    ) -> Result<OracleReactiveRefreshReport, OracleError>
942    where
943        S: InterestOwnerSubscriber<Ethereum>,
944    {
945        self.refresh_handlers_inner(engine, installed, OracleHandlerRefreshBackfill::Continuity)
946            .await
947    }
948
949    /// Refresh an existing engine install after mutation using explicit backfill.
950    pub async fn refresh_handlers_with_backfill<S>(
951        &self,
952        engine: &mut ReactiveEngine<S, Ethereum>,
953        installed: &mut OracleReactiveInstallReport,
954        backfill: SubscriberBackfill,
955    ) -> Result<OracleReactiveRefreshReport, OracleError>
956    where
957        S: InterestOwnerSubscriber<Ethereum>,
958    {
959        self.refresh_handlers_inner(
960            engine,
961            installed,
962            OracleHandlerRefreshBackfill::Explicit(backfill),
963        )
964        .await
965    }
966
967    /// Refresh an existing engine install after mutation without backfill.
968    pub async fn refresh_handlers_live_only<S>(
969        &self,
970        engine: &mut ReactiveEngine<S, Ethereum>,
971        installed: &mut OracleReactiveInstallReport,
972    ) -> Result<OracleReactiveRefreshReport, OracleError>
973    where
974        S: InterestOwnerSubscriber<Ethereum>,
975    {
976        self.refresh_handlers_inner(engine, installed, OracleHandlerRefreshBackfill::LiveOnly)
977            .await
978    }
979
980    /// Unregister exactly the handlers recorded by a previous install/refresh report.
981    ///
982    /// Await each subscriber removal before changing the report. On failure or
983    /// cancellation, `installed` retains the outstanding ids for retry.
984    pub async fn uninstall_installed_handlers<S>(
985        &self,
986        engine: &mut ReactiveEngine<S, Ethereum>,
987        installed: &mut OracleReactiveInstallReport,
988    ) -> Result<OracleReactiveUninstallReport, OracleError>
989    where
990        S: InterestOwnerSubscriber<Ethereum>,
991    {
992        let handler_ids = installed.handler_ids.clone();
993        let removed_handler_ids =
994            unregister_handler_ids(engine, &mut installed.handler_ids).await?;
995        Ok(OracleReactiveUninstallReport {
996            handler_ids,
997            removed_handler_ids,
998        })
999    }
1000
1001    /// Register this runtime's handlers with explicit owner-scoped backfill.
1002    ///
1003    /// The backfill must identify exactly one hash-certified block retained in
1004    /// the engine's rollback journal. Use global canonical ingestion for deeper
1005    /// history, or [`Self::install_handlers`] for coordinated catch-up.
1006    ///
1007    /// # Errors
1008    ///
1009    /// Returns [`OracleError::Reactive`] when the engine rejects a handler
1010    /// registration (for example a handler id that is already installed).
1011    pub async fn install_handlers_with_backfill<S>(
1012        &self,
1013        engine: &mut ReactiveEngine<S, Ethereum>,
1014        backfill: SubscriberBackfill,
1015    ) -> Result<OracleReactiveInstallReport, OracleError>
1016    where
1017        S: InterestOwnerSubscriber<Ethereum>,
1018    {
1019        let mut report = OracleReactiveInstallReport::default();
1020        for handler in self.reactive_handlers() {
1021            let id = handler.id();
1022            engine
1023                .register_handler_with_backfill(handler, backfill)
1024                .await
1025                .map_err(runtime_error)?;
1026            report.handler_ids.push(id);
1027        }
1028        Ok(report)
1029    }
1030
1031    /// Register this runtime's handlers without any backfill.
1032    ///
1033    /// # Errors
1034    ///
1035    /// Returns [`OracleError::Reactive`] when the engine rejects a handler
1036    /// registration (for example a handler id that is already installed).
1037    pub async fn install_handlers_live_only<S>(
1038        &self,
1039        engine: &mut ReactiveEngine<S, Ethereum>,
1040    ) -> Result<OracleReactiveInstallReport, OracleError>
1041    where
1042        S: InterestOwnerSubscriber<Ethereum>,
1043    {
1044        let mut report = OracleReactiveInstallReport::default();
1045        for handler in self.reactive_handlers() {
1046            let id = handler.id();
1047            engine
1048                .register_handler_live_only(handler)
1049                .await
1050                .map_err(runtime_error)?;
1051            report.handler_ids.push(id);
1052        }
1053        Ok(report)
1054    }
1055
1056    /// Unregister this runtime's handlers from an engine.
1057    ///
1058    /// **Warning:** this recomputes handler ids from the *current*
1059    /// registration set. If the runtime's registrations changed since the
1060    /// handlers were installed (feeds registered or unregistered, adapters
1061    /// added), the recomputed set can miss handlers that are still installed
1062    /// in the engine, leaving them registered. Prefer
1063    /// [`Self::uninstall_installed_handlers`] with the report returned by the
1064    /// original install/refresh call — it removes exactly the handler ids
1065    /// that were installed.
1066    pub async fn uninstall_handlers<S>(
1067        &self,
1068        engine: &mut ReactiveEngine<S, Ethereum>,
1069    ) -> Result<OracleReactiveUninstallReport, OracleError>
1070    where
1071        S: InterestOwnerSubscriber<Ethereum>,
1072    {
1073        let mut report = OracleReactiveUninstallReport::default();
1074        for handler in self.reactive_handlers() {
1075            let id = handler.id();
1076            report.handler_ids.push(id.clone());
1077            if engine
1078                .unregister_handler(&id)
1079                .await
1080                .map_err(runtime_error)?
1081                .is_some()
1082            {
1083                report.removed_handler_ids.push(id);
1084            }
1085        }
1086        Ok(report)
1087    }
1088
1089    /// Register this runtime's oracle interests with an event subscriber.
1090    pub async fn register_subscriber<S>(&self, subscriber: &mut S) -> Result<(), OracleError>
1091    where
1092        S: EventSubscriber<Ethereum>,
1093    {
1094        subscriber
1095            .register_interests(&self.reactive_interests())
1096            .await
1097            .map_err(runtime_error)
1098    }
1099
1100    /// Read one subscriber batch, apply it to the cache, and return the typed
1101    /// batch digest, or `None` when the subscriber has no batch ready.
1102    pub async fn next_subscriber_events<S>(
1103        &mut self,
1104        cache: &mut EvmCache,
1105        runtime: &mut ReactiveRuntime<Ethereum>,
1106        subscriber: &mut S,
1107    ) -> Result<Option<crate::OracleBatchReport>, OracleError>
1108    where
1109        S: EventSubscriber<Ethereum>,
1110    {
1111        let Some(batch) = subscriber.next_batch().await.map_err(runtime_error)? else {
1112            return Ok(None);
1113        };
1114        #[cfg(feature = "pending-oracle-updates")]
1115        let committed_logs = batch
1116            .records()
1117            .iter()
1118            .filter_map(|record| match &record.input {
1119                ReactiveInput::Log(log) => Some(log.clone()),
1120                _ => None,
1121            })
1122            .collect::<Vec<_>>();
1123        let report = runtime.ingest_batch(cache, batch).map_err(runtime_error)?;
1124        let typed = self.apply_batch_report(&report)?;
1125        #[cfg(feature = "pending-oracle-updates")]
1126        if let Some(pending) = &self.pending {
1127            for log in &committed_logs {
1128                pending.observe_confirmed_log(log);
1129            }
1130        }
1131        Ok(Some(typed))
1132    }
1133
1134    /// Build a read overlay over the current tracker state.
1135    pub fn read_overlay(&self) -> OracleReadOverlay<'_> {
1136        OracleReadOverlay::new(&self.tracker)
1137    }
1138
1139    /// Register an already-discovered feed and seed its latest round.
1140    ///
1141    /// This updates typed state immediately. If a reactive engine has already
1142    /// installed this runtime's handlers, call one of the `refresh_handlers*`
1143    /// methods with the previous install report so the subscriber receives the
1144    /// new event interests.
1145    ///
1146    /// # Errors
1147    ///
1148    /// Returns [`OracleError::DuplicateFeedId`] or
1149    /// [`OracleError::DuplicateProxy`] when the registration collides with an
1150    /// existing feed; typed state is unchanged in that case.
1151    pub fn register_seeded_feed(
1152        &mut self,
1153        registration: FeedRegistration,
1154        round: RoundData,
1155    ) -> Result<OracleRuntimeMutationReport, OracleError> {
1156        let id = registration.id.clone();
1157        self.tracker
1158            .insert_seeded_registration(registration, round)?;
1159        #[cfg(feature = "pending-oracle-updates")]
1160        self.refresh_pending_scope();
1161        Ok(self.mutation_report([id], []))
1162    }
1163
1164    /// Register an already-discovered adapter-owned feed and seed its latest round.
1165    ///
1166    /// The adapter must already be installed in this runtime. This is the
1167    /// low-level path for adapter code that discovers one additional feed and
1168    /// wants that feed included in the adapter handler rebuilt on refresh.
1169    ///
1170    /// # Errors
1171    ///
1172    /// Returns [`OracleError::Config`] when `adapter_id` is not installed in
1173    /// this runtime, and [`OracleError::DuplicateFeedId`]/
1174    /// [`OracleError::DuplicateProxy`] when the registration collides with an
1175    /// existing feed.
1176    pub fn register_adapter_seeded_feed(
1177        &mut self,
1178        adapter_id: impl AsRef<str>,
1179        registration: FeedRegistration,
1180        round: RoundData,
1181    ) -> Result<OracleRuntimeMutationReport, OracleError> {
1182        let id = registration.id.clone();
1183        let state_index = self
1184            .adapter_state_index(adapter_id.as_ref())
1185            .ok_or_else(|| adapter_not_installed(adapter_id.as_ref()))?;
1186        self.tracker
1187            .insert_seeded_registration(registration.clone(), round)?;
1188        self.adapter_states[state_index]
1189            .registrations
1190            .push(registration);
1191        #[cfg(feature = "pending-oracle-updates")]
1192        self.refresh_pending_scope();
1193        Ok(self.mutation_report([id], []))
1194    }
1195
1196    /// Unregister a feed by id.
1197    ///
1198    /// This removes typed state, pending reconciliation, and any adapter routing
1199    /// references for the feed. Refresh installed handlers afterward to apply
1200    /// the routing change to a live engine.
1201    ///
1202    /// # Errors
1203    ///
1204    /// Returns [`OracleError::FeedNotFound`] when no feed is registered under
1205    /// `id`.
1206    pub fn unregister_feed_by_id(
1207        &mut self,
1208        id: FeedId,
1209    ) -> Result<OracleRuntimeMutationReport, OracleError> {
1210        let removed = self
1211            .tracker
1212            .remove_by_id(id)
1213            .ok_or(OracleError::FeedNotFound)?;
1214        let id = removed.id.clone();
1215        self.remove_adapter_registration(id.clone());
1216        #[cfg(feature = "pending-oracle-updates")]
1217        self.refresh_pending_scope();
1218        Ok(self.mutation_report([], [id]))
1219    }
1220
1221    /// Unregister a feed by proxy.
1222    ///
1223    /// # Errors
1224    ///
1225    /// Returns [`OracleError::FeedNotFound`] when no feed is registered under
1226    /// `proxy`.
1227    pub fn unregister_feed_by_proxy(
1228        &mut self,
1229        proxy: Address,
1230    ) -> Result<OracleRuntimeMutationReport, OracleError> {
1231        let removed = self
1232            .tracker
1233            .remove_by_proxy(proxy)
1234            .ok_or(OracleError::FeedNotFound)?;
1235        let id = removed.id.clone();
1236        self.remove_adapter_registration(id.clone());
1237        #[cfg(feature = "pending-oracle-updates")]
1238        self.refresh_pending_scope();
1239        Ok(self.mutation_report([], [id]))
1240    }
1241
1242    /// Return the latest typed price by feed id string.
1243    ///
1244    /// # Errors
1245    ///
1246    /// Returns [`OracleError::FeedNotFound`] when no feed is registered under
1247    /// `id` or the feed has no current snapshot.
1248    pub fn price(&self, id: impl AsRef<str>) -> Result<OraclePrice, OracleError> {
1249        self.tracker.price(id)
1250    }
1251
1252    /// Return the latest typed price by proxy address.
1253    ///
1254    /// # Errors
1255    ///
1256    /// Returns [`OracleError::FeedNotFound`] when no feed is registered under
1257    /// `proxy` or the feed has no current snapshot.
1258    pub fn price_by_proxy(&self, proxy: Address) -> Result<OraclePrice, OracleError> {
1259        self.tracker.price_by_proxy(proxy)
1260    }
1261
1262    /// Return the latest round by feed id string.
1263    ///
1264    /// # Errors
1265    ///
1266    /// Returns [`OracleError::FeedNotFound`] when no feed is registered under
1267    /// `id` or the feed has no current snapshot.
1268    pub fn latest_round(&self, id: impl AsRef<str>) -> Result<RoundData, OracleError> {
1269        self.tracker.latest_round(id)
1270    }
1271
1272    /// Apply a committed reactive batch and return the typed batch digest.
1273    ///
1274    /// The returned [`crate::OracleBatchReport`] carries the raw per-event
1275    /// hooks (`events`, in emission order) plus the digested per-feed changes,
1276    /// continuity incidents, and the `requires_full_refresh` routing flag —
1277    /// the same consumer shape as `evm-amm-state`'s `AmmSyncBatchReport`.
1278    /// Registered callbacks fire for every event before this returns.
1279    ///
1280    /// Legacy `oracle.answer_updated` signals (emitted by custom handlers that
1281    /// predate the rich [`OraclePriceUpdate`] payload) update typed tracker
1282    /// state but are **not** decoded into [`OracleHookEvent`]s, so `on_event`/
1283    /// `on_price_update` callbacks do not fire for them. Emit the rich
1284    /// `oracle.price_update` signal from custom handlers to get typed
1285    /// callbacks.
1286    pub fn apply_batch_report<N: Network>(
1287        &mut self,
1288        report: &ReactiveBatchReport<N>,
1289    ) -> Result<crate::OracleBatchReport, OracleError> {
1290        let mut events = Vec::new();
1291        for signal in report
1292            .applied
1293            .iter()
1294            .flat_map(|applied| applied.hook_signals.iter())
1295        {
1296            if let Some(signal) = OracleSignal::from_hook(signal)? {
1297                events.push(signal.to_event());
1298            }
1299        }
1300
1301        self.tracker.apply_batch_report(report)?;
1302        self.emit(&events);
1303        Ok(crate::OracleBatchReport::from_events(events))
1304    }
1305
1306    /// Reconcile all currently pending proxy-kind event updates.
1307    ///
1308    /// Derived-source requests
1309    /// ([`crate::OracleReconciliationKind::DerivedProtocolRead`]) are left
1310    /// queued; satisfy those with [`OracleRuntime::reconcile_derived`].
1311    pub async fn reconcile_pending(&mut self) -> Result<Vec<OracleHookEvent>, OracleError>
1312    where
1313        P: ChainlinkFeedProvider,
1314    {
1315        let mut reconciler = OracleReconciler::default();
1316        for request in self
1317            .tracker
1318            .pending_reconciliations()
1319            .iter()
1320            .filter(|request| request.kind == crate::OracleReconciliationKind::Proxy)
1321            .cloned()
1322        {
1323            reconciler.enqueue(request);
1324        }
1325
1326        let mut events = Vec::new();
1327        while let Some(result) = reconciler
1328            .reconcile_next(&mut self.tracker, &self.provider)
1329            .await?
1330        {
1331            events.extend(result.hooks);
1332        }
1333        #[cfg(feature = "pending-oracle-updates")]
1334        self.refresh_pending_scope();
1335        self.emit(&events);
1336        Ok(events)
1337    }
1338
1339    /// Reconcile all currently pending derived-source updates through their
1340    /// protocols' own view calls (Morpho `price()`, Euler `getQuote`), read
1341    /// through `cache`.
1342    ///
1343    /// Successful reads promote `EventPending` snapshots to `Confirmed` or
1344    /// `Corrected` and fire the registered hook callbacks; failed reads leave
1345    /// their requests queued and are surfaced in
1346    /// [`crate::DerivedReconcileReport::failed`].
1347    pub fn reconcile_derived(
1348        &mut self,
1349        cache: &mut evm_fork_cache::cache::EvmCache,
1350    ) -> crate::DerivedReconcileReport {
1351        let report = self.tracker.reconcile_derived_pending_with(cache);
1352        let events: Vec<OracleHookEvent> = report
1353            .reconciled
1354            .iter()
1355            .flat_map(|result| result.hooks.iter().cloned())
1356            .collect();
1357        self.emit(&events);
1358        report
1359    }
1360
1361    fn emit(&self, events: &[OracleHookEvent]) {
1362        for event in events {
1363            for callback in &self.callbacks {
1364                callback(event);
1365            }
1366        }
1367    }
1368
1369    /// Return the complete log/event interest set for built-in and adapter handlers.
1370    pub fn reactive_interests(&self) -> Vec<ReactiveInterest<Ethereum>> {
1371        let mut interests = Vec::new();
1372        for handler in self.reactive_handlers() {
1373            interests.extend(handler.interests());
1374        }
1375        interests
1376    }
1377
1378    /// Return the oracle storage slots this runtime can prewarm for direct event writes.
1379    pub fn storage_warmup_slots(&self) -> Vec<(Address, U256)> {
1380        self.storage_sync
1381            .warm_slots_for_registrations(self.tracker.registrations_iter())
1382    }
1383
1384    /// Bulk-load direct-write oracle storage slots into the supplied cache.
1385    pub fn prewarm_storage(&self, cache: &mut EvmCache) -> OracleStorageWarmupReport {
1386        let registrations = self.tracker.registrations_iter().collect::<Vec<_>>();
1387        prewarm_oracle_storage(
1388            cache,
1389            &self.storage_sync,
1390            &registrations,
1391            OracleStorageWarmupMode::default(),
1392        )
1393    }
1394
1395    /// Verify direct-write oracle storage slots through `EvmCache::run_cold_start`.
1396    pub fn cold_start_storage(&self, cache: &mut EvmCache) -> OracleStorageWarmupReport {
1397        let registrations = self.tracker.registrations_iter().collect::<Vec<_>>();
1398        prewarm_oracle_storage(
1399            cache,
1400            &self.storage_sync,
1401            &registrations,
1402            OracleStorageWarmupMode::ColdStart,
1403        )
1404    }
1405
1406    async fn refresh_handlers_inner<S>(
1407        &self,
1408        engine: &mut ReactiveEngine<S, Ethereum>,
1409        installed: &mut OracleReactiveInstallReport,
1410        backfill: OracleHandlerRefreshBackfill,
1411    ) -> Result<OracleReactiveRefreshReport, OracleError>
1412    where
1413        S: InterestOwnerSubscriber<Ethereum>,
1414    {
1415        let previous_handler_ids = installed.handler_ids.clone();
1416        let removed_handler_ids =
1417            unregister_handler_ids(engine, &mut installed.handler_ids).await?;
1418        for handler in self.reactive_handlers() {
1419            let id = handler.id();
1420            match backfill {
1421                OracleHandlerRefreshBackfill::Continuity => engine.register_handler(handler).await,
1422                OracleHandlerRefreshBackfill::Explicit(backfill) => {
1423                    engine
1424                        .register_handler_with_backfill(handler, backfill)
1425                        .await
1426                }
1427                OracleHandlerRefreshBackfill::LiveOnly => {
1428                    engine.register_handler_live_only(handler).await
1429                }
1430            }
1431            .map_err(runtime_error)?;
1432            installed.handler_ids.push(id);
1433        }
1434        Ok(OracleReactiveRefreshReport {
1435            previous_handler_ids,
1436            removed_handler_ids,
1437            installed_handler_ids: installed.handler_ids.clone(),
1438        })
1439    }
1440
1441    fn mutation_report(
1442        &self,
1443        registered_feed_ids: impl IntoIterator<Item = FeedId>,
1444        removed_feed_ids: impl IntoIterator<Item = FeedId>,
1445    ) -> OracleRuntimeMutationReport {
1446        OracleRuntimeMutationReport {
1447            registered_feed_ids: registered_feed_ids.into_iter().collect(),
1448            removed_feed_ids: removed_feed_ids.into_iter().collect(),
1449            skipped: Vec::new(),
1450            feed_statuses: feed_readiness_from_registrations(self.tracker.registrations_iter()),
1451            current_handler_ids: self.reactive_handler_ids(),
1452        }
1453    }
1454
1455    fn adapter_state_index(&self, adapter_id: &str) -> Option<usize> {
1456        self.adapter_states
1457            .iter()
1458            .position(|state| state.adapter.adapter_id().as_str() == adapter_id)
1459    }
1460
1461    fn push_adapter_state(
1462        &mut self,
1463        adapter: Arc<dyn OracleAdapterPlugin>,
1464        registrations: Vec<FeedRegistration>,
1465    ) {
1466        if registrations.is_empty() {
1467            return;
1468        }
1469
1470        let adapter_id = adapter.adapter_id();
1471        if let Some(existing) = self
1472            .adapter_states
1473            .iter_mut()
1474            .find(|state| state.adapter.adapter_id().as_str() == adapter_id.as_str())
1475        {
1476            existing.registrations.extend(registrations);
1477            return;
1478        }
1479
1480        self.adapter_states.push(OracleAdapterRuntimeState {
1481            adapter,
1482            registrations,
1483        });
1484    }
1485
1486    fn remove_adapter_registration(&mut self, id: FeedId) {
1487        for state in &mut self.adapter_states {
1488            state
1489                .registrations
1490                .retain(|registration| registration.id != id);
1491        }
1492    }
1493
1494    #[cfg(feature = "pending-oracle-updates")]
1495    fn refresh_pending_scope(&mut self) {
1496        if let Some(pending) = &mut self.pending {
1497            pending.refresh(self.tracker.registrations_iter());
1498        }
1499    }
1500}
1501
1502impl OracleRuntime<()> {
1503    /// Start building a cache-native runtime from oracle adapter plugins.
1504    pub fn cache_builder() -> OracleCacheRuntimeBuilder {
1505        OracleCacheRuntimeBuilder::default()
1506    }
1507
1508    /// Build a runtime facade from a cache-native tracker.
1509    ///
1510    /// The returned runtime starts with **Chainlink-default storage sync and
1511    /// no adapter-plugin routing**, regardless of how the tracker's feeds
1512    /// were discovered: any custom [`OracleStorageSync`] configuration is
1513    /// reset to [`OracleStorageSync::chainlink_defaults`] unless replaced with
1514    /// [`OracleRuntime::with_storage_sync`], and non-Chainlink
1515    /// feeds already in the tracker (Pyth, RedStone, custom adapter families)
1516    /// remain visible in typed state but receive **no reactive events** —
1517    /// their adapter handlers are gone until the adapters are re-installed
1518    /// via [`Self::register_adapter`]. Prefer keeping the runtime returned by
1519    /// [`OracleCacheRuntimeBuilder::build`] when adapter plugins are in play.
1520    pub fn from_tracker(tracker: OracleTracker) -> Self {
1521        Self {
1522            provider: (),
1523            tracker,
1524            storage_sync: OracleStorageSync::chainlink_defaults(),
1525            callbacks: Vec::new(),
1526            adapter_states: Vec::new(),
1527            #[cfg(feature = "pending-oracle-updates")]
1528            pending: None,
1529            #[cfg(feature = "pending-oracle-updates")]
1530            pending_source_sessions: Vec::new(),
1531        }
1532    }
1533
1534    /// Discover and register feeds from an adapter on an existing cache-native runtime.
1535    ///
1536    /// This mutates typed state and retains the adapter for future handler
1537    /// rebuilds. If handlers are already installed in an engine, call one of the
1538    /// `refresh_handlers*` methods with the previous install report afterward.
1539    ///
1540    /// # Errors
1541    ///
1542    /// Returns [`OracleError::Provider`] when adapter discovery reads fail,
1543    /// and [`OracleError::DuplicateFeedId`]/[`OracleError::DuplicateProxy`]
1544    /// when a discovered feed collides with an existing registration. On
1545    /// error the runtime is unchanged: feeds inserted earlier in the same
1546    /// call are rolled back and the adapter is not retained.
1547    pub async fn register_adapter<A>(
1548        &mut self,
1549        adapter: A,
1550        cache: &mut EvmCache,
1551    ) -> Result<OracleRuntimeMutationReport, OracleError>
1552    where
1553        A: OracleAdapterPlugin + 'static,
1554    {
1555        let adapter = Arc::new(adapter);
1556        let discovery = adapter
1557            .discover(OracleDiscoveryContext {
1558                cache,
1559                now_timestamp: self.tracker.now_timestamp(),
1560            })
1561            .await?;
1562
1563        // Insert discovered feeds live and roll back on failure instead of
1564        // cloning the whole tracker for every call. This is an exact inverse:
1565        // discovery ran before any mutation, `insert_seeded_registration`
1566        // touches only the registration map, the proxy-id index, and the
1567        // snapshot map, and `remove_by_id` removes exactly those entries plus
1568        // pending reconciliations — which a just-inserted proxy cannot have,
1569        // because nothing runs between the inserts and the rollback.
1570        let mut registered_feed_ids: Vec<FeedId> = Vec::new();
1571        let mut registrations = Vec::new();
1572        for feed in &discovery.feeds {
1573            if let Err(error) = self
1574                .tracker
1575                .insert_seeded_registration(feed.registration.clone(), feed.round.clone())
1576            {
1577                for id in registered_feed_ids {
1578                    self.tracker.remove_by_id(id);
1579                }
1580                return Err(error);
1581            }
1582            registered_feed_ids.push(feed.registration.id.clone());
1583            registrations.push(feed.registration.clone());
1584        }
1585
1586        self.push_adapter_state(adapter, registrations);
1587        #[cfg(feature = "pending-oracle-updates")]
1588        self.refresh_pending_scope();
1589        let mut feed_statuses =
1590            feed_readiness_from_registrations(self.tracker.registrations_iter());
1591        feed_statuses.extend(discovery.skipped.iter().map(skipped_feed_readiness));
1592        Ok(OracleRuntimeMutationReport {
1593            registered_feed_ids,
1594            removed_feed_ids: Vec::new(),
1595            skipped: discovery.skipped,
1596            feed_statuses,
1597            current_handler_ids: self.reactive_handler_ids(),
1598        })
1599    }
1600}
1601
1602#[derive(Clone, Copy, Debug)]
1603enum OracleHandlerRefreshBackfill {
1604    Continuity,
1605    Explicit(SubscriberBackfill),
1606    LiveOnly,
1607}
1608
1609fn runtime_error(error: impl ToString) -> OracleError {
1610    OracleError::Reactive(error.to_string())
1611}
1612
1613fn adapter_not_installed(adapter_id: &str) -> OracleError {
1614    OracleError::Config(crate::error::OracleConfigError::AdapterNotInstalled {
1615        adapter: crate::OracleAdapterId::new(adapter_id.to_string()),
1616    })
1617}
1618
1619fn feed_readiness_from_registrations<'a>(
1620    registrations: impl IntoIterator<Item = &'a FeedRegistration>,
1621) -> Vec<OracleFeedReadinessReport> {
1622    registrations
1623        .into_iter()
1624        .map(|registration| OracleFeedReadinessReport {
1625            id: Some(registration.id.clone()),
1626            proxy: registration.proxy,
1627            status: registration.status,
1628            reason: None,
1629        })
1630        .collect()
1631}
1632
1633fn skipped_feed_readiness(skipped: &OracleAdapterFeedSkip) -> OracleFeedReadinessReport {
1634    OracleFeedReadinessReport {
1635        id: skipped.feed.id(),
1636        proxy: skipped.proxy,
1637        status: OracleFeedStatus::Unsupported,
1638        reason: Some(skipped.reason.to_string()),
1639    }
1640}
1641
1642fn storage_warmup_feed_readiness(
1643    registrations: &[&FeedRegistration],
1644    failed_slots: &[OracleStorageWarmupFailure],
1645) -> Vec<OracleFeedReadinessReport> {
1646    if failed_slots.is_empty() {
1647        return feed_readiness_from_registrations(registrations.iter().copied());
1648    }
1649
1650    let failed_addresses = failed_slots
1651        .iter()
1652        .map(|failure| failure.address)
1653        .collect::<BTreeSet<_>>();
1654    registrations
1655        .iter()
1656        .map(|registration| {
1657            let failed = feed_storage_addresses(registration)
1658                .into_iter()
1659                .any(|address| failed_addresses.contains(&address));
1660            OracleFeedReadinessReport {
1661                id: Some(registration.id.clone()),
1662                proxy: registration.proxy,
1663                status: if failed {
1664                    OracleFeedStatus::Degraded
1665                } else {
1666                    registration.status
1667                },
1668                reason: failed
1669                    .then(|| "oracle storage warmup failed for one or more feed slots".to_string()),
1670            }
1671        })
1672        .collect()
1673}
1674
1675fn feed_storage_addresses(registration: &FeedRegistration) -> Vec<Address> {
1676    let mut addresses = vec![registration.proxy];
1677    addresses.extend(registration.current_aggregator);
1678    addresses.extend(
1679        registration
1680            .source
1681            .event_aggregators(registration.current_aggregator),
1682    );
1683    addresses.sort_unstable();
1684    addresses.dedup();
1685    addresses
1686}
1687
1688async fn unregister_handler_ids<S>(
1689    engine: &mut ReactiveEngine<S, Ethereum>,
1690    handler_ids: &mut Vec<HandlerId>,
1691) -> Result<Vec<HandlerId>, OracleError>
1692where
1693    S: InterestOwnerSubscriber<Ethereum>,
1694{
1695    let mut removed = Vec::new();
1696    while let Some(id) = handler_ids.first().cloned() {
1697        if engine
1698            .unregister_handler(&id)
1699            .await
1700            .map_err(runtime_error)?
1701            .is_some()
1702        {
1703            removed.push(id);
1704        }
1705        // Retain failed and not-yet-attempted owners for an explicit retry.
1706        handler_ids.remove(0);
1707    }
1708    Ok(removed)
1709}
1710
1711fn cache_runtime_skip_error(skipped: &OracleAdapterFeedSkip) -> OracleError {
1712    OracleError::FeedSkipped(Box::new(crate::OracleFeedSkip::from_adapter_skip(skipped)))
1713}
1714
1715fn prewarm_oracle_storage(
1716    cache: &mut EvmCache,
1717    storage_sync: &OracleStorageSync,
1718    registrations: &[&crate::FeedRegistration],
1719    mode: OracleStorageWarmupMode,
1720) -> OracleStorageWarmupReport {
1721    let slots = storage_sync.warm_slots_for_registrations(registrations.iter().copied());
1722    let discovery_calls = oracle_cold_start_discovery_calls(registrations);
1723    let requested_slots = slots.len();
1724    if slots.is_empty() && discovery_calls.is_empty() {
1725        return OracleStorageWarmupReport {
1726            mode,
1727            feed_statuses: feed_readiness_from_registrations(registrations.iter().copied()),
1728            ..OracleStorageWarmupReport::default()
1729        };
1730    }
1731
1732    match mode {
1733        OracleStorageWarmupMode::PrewarmSlots => {
1734            let report = cache.prewarm_slots(&slots);
1735            let failed_slots = report
1736                .failed
1737                .into_iter()
1738                .map(|(address, slot, error)| OracleStorageWarmupFailure {
1739                    address,
1740                    slot,
1741                    reason: error.to_string(),
1742                })
1743                .collect::<Vec<_>>();
1744            let feed_statuses = storage_warmup_feed_readiness(registrations, &failed_slots);
1745            OracleStorageWarmupReport {
1746                mode,
1747                feed_statuses,
1748                requested_slots,
1749                loaded_slots: report.loaded,
1750                failed_slots,
1751                discovery_calls: 0,
1752                cold_start: None,
1753            }
1754        }
1755        OracleStorageWarmupMode::ColdStart => {
1756            let mut planner =
1757                OracleSlotColdStartPlanner::new(slots.clone(), discovery_calls.clone());
1758            match cache.run_cold_start(&mut planner, ColdStartConfig::default()) {
1759                Ok(report) => {
1760                    let failed = report.failed_slots;
1761                    let cold_start = OracleColdStartWarmupReport::from(report);
1762                    OracleStorageWarmupReport {
1763                        mode,
1764                        feed_statuses: feed_readiness_from_registrations(
1765                            registrations.iter().copied(),
1766                        ),
1767                        requested_slots,
1768                        loaded_slots: requested_slots.saturating_sub(failed),
1769                        failed_slots: Vec::new(),
1770                        discovery_calls: cold_start.discover_calls,
1771                        cold_start: Some(cold_start),
1772                    }
1773                }
1774                Err(error) => {
1775                    let failed_slots = slots
1776                        .into_iter()
1777                        .map(|(address, slot)| OracleStorageWarmupFailure {
1778                            address,
1779                            slot,
1780                            reason: error.to_string(),
1781                        })
1782                        .collect::<Vec<_>>();
1783                    let feed_statuses = storage_warmup_feed_readiness(registrations, &failed_slots);
1784                    OracleStorageWarmupReport {
1785                        mode,
1786                        feed_statuses,
1787                        requested_slots,
1788                        loaded_slots: 0,
1789                        failed_slots,
1790                        discovery_calls: discovery_calls.len(),
1791                        cold_start: None,
1792                    }
1793                }
1794            }
1795        }
1796    }
1797}
1798
1799struct OracleSlotColdStartPlanner {
1800    slots: Vec<(Address, U256)>,
1801    slot_set: BTreeSet<(Address, U256)>,
1802    discover: Vec<ColdStartCall>,
1803    verified_discovered_slots: bool,
1804}
1805
1806impl OracleSlotColdStartPlanner {
1807    fn new(slots: Vec<(Address, U256)>, discover: Vec<ColdStartCall>) -> Self {
1808        let slot_set = slots.iter().copied().collect();
1809        Self {
1810            slots,
1811            slot_set,
1812            discover,
1813            verified_discovered_slots: false,
1814        }
1815    }
1816}
1817
1818impl ColdStartPlanner for OracleSlotColdStartPlanner {
1819    fn initial_plan(&mut self, _state: &dyn StateView) -> ColdStartPlan {
1820        ColdStartPlan {
1821            verify: self.slots.clone(),
1822            discover: self.discover.clone(),
1823            ..Default::default()
1824        }
1825    }
1826
1827    fn on_results(&mut self, results: &ColdStartResults, _state: &dyn StateView) -> ColdStartStep {
1828        if self.verified_discovered_slots {
1829            return ColdStartStep::Done;
1830        }
1831        self.verified_discovered_slots = true;
1832
1833        let mut slots = results
1834            .discovered
1835            .iter()
1836            .flat_map(|discovered| discovered.access.slots.iter().copied())
1837            .filter(|slot| !self.slot_set.contains(slot))
1838            .collect::<Vec<_>>();
1839        slots.sort_unstable();
1840        slots.dedup();
1841        if slots.is_empty() {
1842            return ColdStartStep::Done;
1843        }
1844
1845        ColdStartStep::Continue(ColdStartPlan {
1846            verify: slots,
1847            ..Default::default()
1848        })
1849    }
1850}
1851
1852impl From<evm_fork_cache::ColdStartRunReport> for OracleColdStartWarmupReport {
1853    fn from(report: evm_fork_cache::ColdStartRunReport) -> Self {
1854        Self {
1855            rounds: report.rounds,
1856            verified_slots: report.verified_slots,
1857            changed_slots: report.changed_slots,
1858            failed_slots: report.failed_slots,
1859            discovered_slots: report.discovered_slots,
1860            discovered_accounts: report.discovered_accounts,
1861            discover_calls: report
1862                .per_round
1863                .iter()
1864                .map(|round| round.discover_calls)
1865                .sum(),
1866        }
1867    }
1868}
1869
1870fn oracle_cold_start_discovery_calls(
1871    registrations: &[&crate::FeedRegistration],
1872) -> Vec<ColdStartCall> {
1873    registrations
1874        .iter()
1875        .map(|registration| {
1876            let read_proxy = registration.source.read_proxy(registration.proxy);
1877            ColdStartCall {
1878                from: Address::ZERO,
1879                to: read_proxy,
1880                calldata: Bytes::from(latestRoundDataCall {}.abi_encode()),
1881                restrict_to: Some(vec![read_proxy]),
1882            }
1883        })
1884        .collect()
1885}
1886
1887#[cfg(test)]
1888mod tests {
1889    use super::*;
1890    use evm_fork_cache::{ColdStartCallResult, StorageAccessList};
1891    use revm::context::result::{ExecutionResult, Output, SuccessReason};
1892
1893    #[derive(Default)]
1894    struct EmptyStateView;
1895
1896    impl StateView for EmptyStateView {
1897        fn storage(&self, _address: Address, _slot: U256) -> Option<U256> {
1898            None
1899        }
1900    }
1901
1902    #[test]
1903    fn cold_start_planner_verifies_discovered_slots_in_second_round() {
1904        let declared = (Address::repeat_byte(0x11), U256::from(1_u64));
1905        let discovered = (Address::repeat_byte(0x22), U256::from(2_u64));
1906        let mut planner = OracleSlotColdStartPlanner::new(vec![declared], Vec::new());
1907        let initial = planner.initial_plan(&EmptyStateView);
1908        assert_eq!(initial.verify, vec![declared]);
1909
1910        let mut access = StorageAccessList::default();
1911        access.slots.insert(discovered);
1912        access.slots.insert(declared);
1913        let results = ColdStartResults {
1914            discovered: vec![ColdStartCallResult {
1915                result: ExecutionResult::Success {
1916                    reason: SuccessReason::Return,
1917                    gas_used: 0,
1918                    gas_refunded: 0,
1919                    logs: Vec::new(),
1920                    output: Output::Call(Bytes::new()),
1921                },
1922                access,
1923            }],
1924            ..Default::default()
1925        };
1926
1927        let ColdStartStep::Continue(next) = planner.on_results(&results, &EmptyStateView) else {
1928            panic!("discovered slots should schedule a follow-up verify round");
1929        };
1930        assert_eq!(next.verify, vec![discovered]);
1931        assert!(next.discover.is_empty());
1932
1933        assert!(matches!(
1934            planner.on_results(&ColdStartResults::default(), &EmptyStateView),
1935            ColdStartStep::Done
1936        ));
1937    }
1938}