1use std::collections::BTreeMap;
2
3use alloy_primitives::{Address, I256, U256};
4use evm_fork_cache::{StateUpdate, StateView, cache::EvmOverlay, reactive::StateEffectQuality};
5use thiserror::Error;
6
7use crate::{
8 FeedId, FeedRegistration, OracleError, OraclePriceUpdate, OracleRuntime, OracleSnapshot,
9 OracleStorageEffect, OracleStorageError, OracleValueSource, OracleValueStatus,
10 state::classify_round,
11};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct OraclePriceMock {
20 pub raw_answer: I256,
22 pub round_id: Option<U256>,
25 pub started_at: Option<u64>,
27 pub updated_at: Option<u64>,
30}
31
32impl OraclePriceMock {
33 pub fn new(raw_answer: I256) -> Self {
35 Self {
36 raw_answer,
37 round_id: None,
38 started_at: None,
39 updated_at: None,
40 }
41 }
42
43 pub fn round_id(mut self, round_id: U256) -> Self {
45 self.round_id = Some(round_id);
46 self
47 }
48
49 pub fn started_at(mut self, started_at: u64) -> Self {
51 self.started_at = Some(started_at);
52 self
53 }
54
55 pub fn updated_at(mut self, updated_at: u64) -> Self {
57 self.updated_at = Some(updated_at);
58 self
59 }
60
61 fn to_update(
62 &self,
63 registration: &FeedRegistration,
64 snapshot: &OracleSnapshot,
65 overlay_timestamp: Option<u64>,
66 ) -> OraclePriceUpdate {
67 let updated_at = self
68 .updated_at
69 .or(overlay_timestamp)
70 .unwrap_or_else(|| snapshot.round.updated_at.saturating_add(1));
71 let started_at = self.started_at.unwrap_or(updated_at);
72 let round_id = self
73 .round_id
74 .unwrap_or_else(|| next_local_round_id(snapshot.round.round_id));
75 let aggregator = registration
76 .current_aggregator
77 .or(snapshot.aggregator)
78 .unwrap_or(registration.proxy);
79
80 let round = crate::RoundData {
81 round_id,
82 answer: self.raw_answer,
83 started_at,
84 updated_at,
85 answered_in_round: round_id,
86 };
87 let round_status = classify_round(&round, updated_at, ®istration.staleness);
88
89 OraclePriceUpdate {
90 id: registration.id.clone(),
91 proxy: registration.proxy,
92 aggregator,
93 label: registration.label.clone(),
94 base: registration.base.clone(),
95 quote: registration.quote.clone(),
96 raw_answer: self.raw_answer,
97 decimals: registration.metadata.decimals,
98 event_round_id: round_id,
99 started_at,
100 updated_at,
101 block_number: None,
102 block_hash: None,
103 log_index: None,
104 round_status,
105 value_status: OracleValueStatus::Confirmed,
106 source: OracleValueSource::Mock,
107 }
108 }
109}
110
111impl From<I256> for OraclePriceMock {
112 fn from(raw_answer: I256) -> Self {
113 Self::new(raw_answer)
114 }
115}
116
117#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct OracleMockReport {
120 pub feed_id: FeedId,
122 pub proxy: Address,
124 pub aggregator: Option<Address>,
126 pub applied: bool,
128 pub adapter: Option<&'static str>,
130 pub updates: Vec<StateUpdate>,
132 pub effect_quality: StateEffectQuality,
134}
135
136impl OracleMockReport {
137 pub fn applied(&self) -> bool {
139 self.applied
140 }
141}
142
143#[derive(Debug, Error)]
145pub enum OracleMockError {
146 #[error("oracle feed `{0}` is not registered")]
148 FeedNotFound(String),
149 #[error("oracle feed `{0}` has no current snapshot")]
151 MissingSnapshot(String),
152 #[error(transparent)]
154 Storage(#[from] OracleStorageError),
155 #[error("oracle mock cannot apply {kind} update to an overlay")]
157 UnsupportedStateUpdate {
158 kind: &'static str,
160 },
161 #[error("oracle mock needs hot slot {address:?}[{slot}] for masked write")]
163 ColdMaskedSlot {
164 address: Address,
166 slot: U256,
168 },
169}
170
171impl From<OracleMockError> for OracleError {
172 fn from(value: OracleMockError) -> Self {
173 match value {
174 OracleMockError::FeedNotFound(_) => Self::FeedNotFound,
175 other => Self::Config(crate::error::OracleConfigError::InvalidRequest(
176 other.to_string(),
177 )),
178 }
179 }
180}
181
182pub trait OracleOverlayMockExt {
189 fn mock_oracle_price<P, F, M>(
191 &mut self,
192 state: &dyn StateView,
193 runtime: &OracleRuntime<P>,
194 feed_id: F,
195 mock: M,
196 ) -> Result<OracleMockReport, OracleMockError>
197 where
198 F: AsRef<str>,
199 M: Into<OraclePriceMock>;
200}
201
202impl OracleOverlayMockExt for EvmOverlay {
203 fn mock_oracle_price<P, F, M>(
204 &mut self,
205 state: &dyn StateView,
206 runtime: &OracleRuntime<P>,
207 feed_id: F,
208 mock: M,
209 ) -> Result<OracleMockReport, OracleMockError>
210 where
211 F: AsRef<str>,
212 M: Into<OraclePriceMock>,
213 {
214 runtime.mock_price(self, state, feed_id, mock)
215 }
216}
217
218impl<P> OracleRuntime<P> {
219 pub fn mock_price<F, M>(
226 &self,
227 overlay: &mut EvmOverlay,
228 state: &dyn StateView,
229 feed_id: F,
230 mock: M,
231 ) -> Result<OracleMockReport, OracleMockError>
232 where
233 F: AsRef<str>,
234 M: Into<OraclePriceMock>,
235 {
236 let feed_id_ref = feed_id.as_ref();
237 let registration = self
238 .tracker()
239 .registration_by_id_str(feed_id_ref)
240 .ok_or_else(|| OracleMockError::FeedNotFound(feed_id_ref.to_string()))?;
241 let snapshot = self
242 .tracker()
243 .latest(registration.proxy)
244 .ok_or_else(|| OracleMockError::MissingSnapshot(feed_id_ref.to_string()))?;
245 let update = mock
246 .into()
247 .to_update(registration, snapshot, overlay.timestamp());
248 let effect = self
249 .storage_sync()
250 .state_effect_for_answer(registration, &update, state)?;
251
252 let mut report = OracleMockReport {
253 feed_id: registration.id.clone(),
254 proxy: registration.proxy,
255 aggregator: registration.current_aggregator,
256 applied: false,
257 adapter: None,
258 updates: Vec::new(),
259 effect_quality: StateEffectQuality::RequiresRepair,
260 };
261
262 let OracleStorageEffect::StateUpdates { adapter, updates } = effect else {
263 return Ok(report);
264 };
265
266 let mut staged = OverlayWriteState::new(state);
267 for update in &updates {
268 staged.stage(update)?;
269 }
270 staged.apply_to(overlay);
271
272 report.applied = true;
273 report.adapter = Some(adapter);
274 report.updates = updates;
275 report.effect_quality = StateEffectQuality::ExactFromInput;
276 Ok(report)
277 }
278}
279
280struct OverlayWriteState<'a> {
281 base: &'a dyn StateView,
282 writes: BTreeMap<(Address, U256), U256>,
283}
284
285impl<'a> OverlayWriteState<'a> {
286 fn new(base: &'a dyn StateView) -> Self {
287 Self {
288 base,
289 writes: BTreeMap::new(),
290 }
291 }
292
293 fn stage(&mut self, update: &StateUpdate) -> Result<(), OracleMockError> {
294 match update {
295 StateUpdate::Slot {
296 address,
297 slot,
298 value,
299 } => {
300 self.writes.insert((*address, *slot), *value);
301 Ok(())
302 }
303 StateUpdate::SlotMasked {
304 address,
305 slot,
306 mask,
307 value,
308 } => {
309 let current =
310 self.storage(*address, *slot)
311 .ok_or(OracleMockError::ColdMaskedSlot {
312 address: *address,
313 slot: *slot,
314 })?;
315 let next = (current & !*mask) | (*value & *mask);
316 self.writes.insert((*address, *slot), next);
317 Ok(())
318 }
319 StateUpdate::SlotDelta { .. } => {
320 Err(OracleMockError::UnsupportedStateUpdate { kind: "slot delta" })
321 }
322 StateUpdate::BalanceDelta { .. } => Err(OracleMockError::UnsupportedStateUpdate {
323 kind: "balance delta",
324 }),
325 StateUpdate::Account { .. } => Err(OracleMockError::UnsupportedStateUpdate {
326 kind: "account patch",
327 }),
328 StateUpdate::AccountUpsert { .. } => Err(OracleMockError::UnsupportedStateUpdate {
329 kind: "account upsert",
330 }),
331 StateUpdate::Purge { .. } => {
332 Err(OracleMockError::UnsupportedStateUpdate { kind: "purge" })
333 }
334 _ => Err(OracleMockError::UnsupportedStateUpdate { kind: "unknown" }),
335 }
336 }
337
338 fn apply_to(self, overlay: &mut EvmOverlay) {
339 for ((address, slot), value) in self.writes {
340 overlay.override_slot(address, slot, value);
341 }
342 }
343}
344
345impl StateView for OverlayWriteState<'_> {
346 fn storage(&self, address: Address, slot: U256) -> Option<U256> {
347 self.writes
348 .get(&(address, slot))
349 .copied()
350 .or_else(|| self.base.storage(address, slot))
351 }
352}
353
354fn next_local_round_id(round_id: U256) -> U256 {
355 let local = round_id & U256::from(u64::MAX);
356 if local == U256::ZERO {
357 U256::from(1_u64)
358 } else {
359 local.saturating_add(U256::from(1_u64))
360 }
361}