use std::collections::BTreeMap;
use alloy_primitives::{Address, I256, U256};
use evm_fork_cache::{StateUpdate, StateView, cache::EvmOverlay, reactive::StateEffectQuality};
use thiserror::Error;
use crate::{
FeedId, FeedRegistration, OracleError, OraclePriceUpdate, OracleRuntime, OracleSnapshot,
OracleStorageEffect, OracleStorageError, OracleValueSource, OracleValueStatus,
state::classify_round,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OraclePriceMock {
pub raw_answer: I256,
pub round_id: Option<U256>,
pub started_at: Option<u64>,
pub updated_at: Option<u64>,
}
impl OraclePriceMock {
pub fn new(raw_answer: I256) -> Self {
Self {
raw_answer,
round_id: None,
started_at: None,
updated_at: None,
}
}
pub fn round_id(mut self, round_id: U256) -> Self {
self.round_id = Some(round_id);
self
}
pub fn started_at(mut self, started_at: u64) -> Self {
self.started_at = Some(started_at);
self
}
pub fn updated_at(mut self, updated_at: u64) -> Self {
self.updated_at = Some(updated_at);
self
}
fn to_update(
&self,
registration: &FeedRegistration,
snapshot: &OracleSnapshot,
overlay_timestamp: Option<u64>,
) -> OraclePriceUpdate {
let updated_at = self
.updated_at
.or(overlay_timestamp)
.unwrap_or_else(|| snapshot.round.updated_at.saturating_add(1));
let started_at = self.started_at.unwrap_or(updated_at);
let round_id = self
.round_id
.unwrap_or_else(|| next_local_round_id(snapshot.round.round_id));
let aggregator = registration
.current_aggregator
.or(snapshot.aggregator)
.unwrap_or(registration.proxy);
let round = crate::RoundData {
round_id,
answer: self.raw_answer,
started_at,
updated_at,
answered_in_round: round_id,
};
let round_status = classify_round(&round, updated_at, ®istration.staleness);
OraclePriceUpdate {
id: registration.id.clone(),
proxy: registration.proxy,
aggregator,
label: registration.label.clone(),
base: registration.base.clone(),
quote: registration.quote.clone(),
raw_answer: self.raw_answer,
decimals: registration.metadata.decimals,
event_round_id: round_id,
started_at,
updated_at,
block_number: None,
block_hash: None,
log_index: None,
round_status,
value_status: OracleValueStatus::Confirmed,
source: OracleValueSource::Mock,
}
}
}
impl From<I256> for OraclePriceMock {
fn from(raw_answer: I256) -> Self {
Self::new(raw_answer)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OracleMockReport {
pub feed_id: FeedId,
pub proxy: Address,
pub aggregator: Option<Address>,
pub applied: bool,
pub adapter: Option<&'static str>,
pub updates: Vec<StateUpdate>,
pub effect_quality: StateEffectQuality,
}
impl OracleMockReport {
pub fn applied(&self) -> bool {
self.applied
}
}
#[derive(Debug, Error)]
pub enum OracleMockError {
#[error("oracle feed `{0}` is not registered")]
FeedNotFound(String),
#[error("oracle feed `{0}` has no current snapshot")]
MissingSnapshot(String),
#[error(transparent)]
Storage(#[from] OracleStorageError),
#[error("oracle mock cannot apply {kind} update to an overlay")]
UnsupportedStateUpdate {
kind: &'static str,
},
#[error("oracle mock needs hot slot {address:?}[{slot}] for masked write")]
ColdMaskedSlot {
address: Address,
slot: U256,
},
}
impl From<OracleMockError> for OracleError {
fn from(value: OracleMockError) -> Self {
match value {
OracleMockError::FeedNotFound(_) => Self::FeedNotFound,
other => Self::Config(crate::error::OracleConfigError::InvalidRequest(
other.to_string(),
)),
}
}
}
pub trait OracleOverlayMockExt {
fn mock_oracle_price<P, F, M>(
&mut self,
state: &dyn StateView,
runtime: &OracleRuntime<P>,
feed_id: F,
mock: M,
) -> Result<OracleMockReport, OracleMockError>
where
F: AsRef<str>,
M: Into<OraclePriceMock>;
}
impl OracleOverlayMockExt for EvmOverlay {
fn mock_oracle_price<P, F, M>(
&mut self,
state: &dyn StateView,
runtime: &OracleRuntime<P>,
feed_id: F,
mock: M,
) -> Result<OracleMockReport, OracleMockError>
where
F: AsRef<str>,
M: Into<OraclePriceMock>,
{
runtime.mock_price(self, state, feed_id, mock)
}
}
impl<P> OracleRuntime<P> {
pub fn mock_price<F, M>(
&self,
overlay: &mut EvmOverlay,
state: &dyn StateView,
feed_id: F,
mock: M,
) -> Result<OracleMockReport, OracleMockError>
where
F: AsRef<str>,
M: Into<OraclePriceMock>,
{
let feed_id_ref = feed_id.as_ref();
let registration = self
.tracker()
.registration_by_id_str(feed_id_ref)
.ok_or_else(|| OracleMockError::FeedNotFound(feed_id_ref.to_string()))?;
let snapshot = self
.tracker()
.latest(registration.proxy)
.ok_or_else(|| OracleMockError::MissingSnapshot(feed_id_ref.to_string()))?;
let update = mock
.into()
.to_update(registration, snapshot, overlay.timestamp());
let effect = self
.storage_sync()
.state_effect_for_answer(registration, &update, state)?;
let mut report = OracleMockReport {
feed_id: registration.id.clone(),
proxy: registration.proxy,
aggregator: registration.current_aggregator,
applied: false,
adapter: None,
updates: Vec::new(),
effect_quality: StateEffectQuality::RequiresRepair,
};
let OracleStorageEffect::StateUpdates { adapter, updates } = effect else {
return Ok(report);
};
let mut staged = OverlayWriteState::new(state);
for update in &updates {
staged.stage(update)?;
}
staged.apply_to(overlay);
report.applied = true;
report.adapter = Some(adapter);
report.updates = updates;
report.effect_quality = StateEffectQuality::ExactFromInput;
Ok(report)
}
}
struct OverlayWriteState<'a> {
base: &'a dyn StateView,
writes: BTreeMap<(Address, U256), U256>,
}
impl<'a> OverlayWriteState<'a> {
fn new(base: &'a dyn StateView) -> Self {
Self {
base,
writes: BTreeMap::new(),
}
}
fn stage(&mut self, update: &StateUpdate) -> Result<(), OracleMockError> {
match update {
StateUpdate::Slot {
address,
slot,
value,
} => {
self.writes.insert((*address, *slot), *value);
Ok(())
}
StateUpdate::SlotMasked {
address,
slot,
mask,
value,
} => {
let current =
self.storage(*address, *slot)
.ok_or(OracleMockError::ColdMaskedSlot {
address: *address,
slot: *slot,
})?;
let next = (current & !*mask) | (*value & *mask);
self.writes.insert((*address, *slot), next);
Ok(())
}
StateUpdate::SlotDelta { .. } => {
Err(OracleMockError::UnsupportedStateUpdate { kind: "slot delta" })
}
StateUpdate::BalanceDelta { .. } => Err(OracleMockError::UnsupportedStateUpdate {
kind: "balance delta",
}),
StateUpdate::Account { .. } => Err(OracleMockError::UnsupportedStateUpdate {
kind: "account patch",
}),
StateUpdate::AccountUpsert { .. } => Err(OracleMockError::UnsupportedStateUpdate {
kind: "account upsert",
}),
StateUpdate::Purge { .. } => {
Err(OracleMockError::UnsupportedStateUpdate { kind: "purge" })
}
_ => Err(OracleMockError::UnsupportedStateUpdate { kind: "unknown" }),
}
}
fn apply_to(self, overlay: &mut EvmOverlay) {
for ((address, slot), value) in self.writes {
overlay.override_slot(address, slot, value);
}
}
}
impl StateView for OverlayWriteState<'_> {
fn storage(&self, address: Address, slot: U256) -> Option<U256> {
self.writes
.get(&(address, slot))
.copied()
.or_else(|| self.base.storage(address, slot))
}
}
fn next_local_round_id(round_id: U256) -> U256 {
let local = round_id & U256::from(u64::MAX);
if local == U256::ZERO {
U256::from(1_u64)
} else {
local.saturating_add(U256::from(1_u64))
}
}