Skip to main content

evm_oracle_state/
adapter.rs

1use std::{borrow::Cow, collections::BTreeMap, future::Future, pin::Pin, sync::Arc};
2
3use alloy_network::Ethereum;
4use alloy_primitives::{Address, I256};
5use evm_fork_cache::{cache::EvmCache, reactive::ReactiveHandler};
6
7use crate::{FeedRegistration, OracleAdapterFeedSkip, OracleError, OracleStorageSync, RoundData};
8
9/// Boxed async result returned by oracle adapter extension points.
10pub type AdapterFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, OracleError>> + 'a>>;
11
12/// Stable identifier for an oracle adapter family.
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct OracleAdapterId(Cow<'static, str>);
15
16impl OracleAdapterId {
17    /// Create a stable adapter id.
18    pub fn new(id: impl Into<Cow<'static, str>>) -> Self {
19        Self(id.into())
20    }
21
22    /// Borrow the adapter id string.
23    pub fn as_str(&self) -> &str {
24        self.0.as_ref()
25    }
26}
27
28impl std::fmt::Display for OracleAdapterId {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        self.0.fmt(f)
31    }
32}
33
34impl AsRef<str> for OracleAdapterId {
35    fn as_ref(&self) -> &str {
36        self.as_str()
37    }
38}
39
40/// Open transform descriptor for non-Chainlink-native source adapters.
41#[non_exhaustive]
42#[derive(Clone, Debug, Default, PartialEq, Eq)]
43pub enum OracleTransformDescriptor {
44    /// Event/read answers are already in the feed's user-facing denomination.
45    #[default]
46    Identity,
47    /// Clamp answers above a configured cap.
48    PriceCap {
49        /// Maximum answer exposed to users.
50        cap: I256,
51    },
52    /// Adapter-owned transform identified by adapter and transform kind.
53    Custom {
54        /// Adapter responsible for interpreting this transform.
55        adapter_id: OracleAdapterId,
56        /// Adapter-local transform kind.
57        kind: Cow<'static, str>,
58        /// String metadata for adapter-owned transform parameters.
59        metadata: BTreeMap<String, String>,
60    },
61}
62
63/// Open descriptor for an adapter-owned oracle source.
64///
65/// Sources built from this descriptor ([`crate::FeedSource::custom`]) default
66/// to **Chainlink-style proxy reconciliation**: after every accepted event,
67/// and during [`crate::OracleTracker::reconcile`], the tracker calls
68/// `latestRoundData()` on [`Self::read_proxy`] and treats the result as
69/// authoritative. There is no opt-out flag, so `read_proxy` must point at a
70/// contract that answers `latestRoundData()` — for non-Chainlink-shaped
71/// sources, point it at a Chainlink-compatible view wrapper.
72#[non_exhaustive]
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct OracleSourceDescriptor {
75    /// Adapter that owns source-specific semantics.
76    pub adapter_id: OracleAdapterId,
77    /// Adapter-local source kind.
78    pub source_kind: Cow<'static, str>,
79    /// User-facing oracle/proxy address.
80    pub user_facing_proxy: Address,
81    /// Address read for authoritative round data. Must answer Chainlink
82    /// `latestRoundData()`; reconciliation reads it after every accepted
83    /// event (see the struct-level docs).
84    pub read_proxy: Address,
85    /// Address whose events should drive this feed.
86    pub event_source: Address,
87    /// User-facing answer transform.
88    pub transform: OracleTransformDescriptor,
89    /// String metadata for adapter-owned source parameters.
90    pub metadata: BTreeMap<String, String>,
91}
92
93impl OracleSourceDescriptor {
94    /// Create a custom source descriptor with identity answer transform and no metadata.
95    pub fn new(
96        adapter_id: OracleAdapterId,
97        source_kind: impl Into<Cow<'static, str>>,
98        user_facing_proxy: Address,
99        read_proxy: Address,
100        event_source: Address,
101    ) -> Self {
102        Self {
103            adapter_id,
104            source_kind: source_kind.into(),
105            user_facing_proxy,
106            read_proxy,
107            event_source,
108            transform: OracleTransformDescriptor::Identity,
109            metadata: BTreeMap::new(),
110        }
111    }
112
113    /// Set the source transform descriptor.
114    pub fn transform(mut self, transform: OracleTransformDescriptor) -> Self {
115        self.transform = transform;
116        self
117    }
118
119    /// Replace all adapter-owned source metadata.
120    pub fn metadata(mut self, metadata: BTreeMap<String, String>) -> Self {
121        self.metadata = metadata;
122        self
123    }
124
125    /// Add one adapter-owned metadata entry.
126    pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
127        self.metadata.insert(key.into(), value.into());
128        self
129    }
130}
131
132/// One feed discovered and seeded by an oracle adapter plugin.
133#[non_exhaustive]
134#[derive(Clone, Debug)]
135pub struct OracleDiscoveredFeed {
136    /// Registration inserted into the oracle tracker.
137    pub registration: FeedRegistration,
138    /// Initial round used to seed typed state.
139    pub round: RoundData,
140}
141
142impl OracleDiscoveredFeed {
143    /// Create a discovered feed from a registration and initial round.
144    pub fn new(registration: FeedRegistration, round: RoundData) -> Self {
145        Self {
146            registration,
147            round,
148        }
149    }
150}
151
152/// Result of adapter discovery.
153#[non_exhaustive]
154#[derive(Clone, Debug, Default)]
155pub struct OracleDiscoveryReport {
156    /// Feeds that were registered successfully.
157    pub feeds: Vec<OracleDiscoveredFeed>,
158    /// Optional feed skips reported by best-effort adapters.
159    pub skipped: Vec<OracleAdapterFeedSkip>,
160}
161
162impl OracleDiscoveryReport {
163    /// Create an empty discovery report.
164    pub fn new() -> Self {
165        Self::default()
166    }
167
168    /// Add one discovered feed to this report.
169    pub fn with_feed(mut self, feed: OracleDiscoveredFeed) -> Self {
170        self.feeds.push(feed);
171        self
172    }
173
174    /// Add one skipped feed to this report.
175    pub fn with_skip(mut self, skip: OracleAdapterFeedSkip) -> Self {
176        self.skipped.push(skip);
177        self
178    }
179}
180
181/// Cache-native discovery context passed to oracle adapter plugins.
182pub struct OracleDiscoveryContext<'a> {
183    /// Cache used for all on-chain reads during discovery.
184    pub cache: &'a mut EvmCache,
185    /// Wall-clock timestamp used to classify seeded feed state.
186    pub now_timestamp: u64,
187}
188
189/// Public plugin interface for oracle families layered over `evm-fork-cache`.
190pub trait OracleAdapterPlugin: Send + Sync {
191    /// Stable adapter id.
192    fn adapter_id(&self) -> OracleAdapterId;
193
194    /// Discover and seed feed registrations through an `EvmCache`.
195    fn discover<'a>(
196        &'a self,
197        ctx: OracleDiscoveryContext<'a>,
198    ) -> AdapterFuture<'a, OracleDiscoveryReport>;
199
200    /// Build the reactive handler that owns this adapter's event routing.
201    fn reactive_handler(
202        &self,
203        registrations: Vec<FeedRegistration>,
204        storage_sync: OracleStorageSync,
205    ) -> Arc<dyn ReactiveHandler<Ethereum>>;
206}