refactor(oracle): split price feed core modules

This commit is contained in:
Ricardo Guilherme Schmidt 2026-05-14 02:22:59 -03:00
parent 7bcde920fd
commit 713d26ec36
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
4 changed files with 428 additions and 412 deletions

View File

@ -0,0 +1,46 @@
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::account::{AccountId, Data};
use serde::{Deserialize, Serialize};
use spel_framework_macros::account_type;
/// Canonical oracle price account consumed by LEZ programs.
///
/// Oracle producers own how this account is written; consumers only read and validate it.
#[account_type]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct OraclePriceAccount {
/// Canonical identifier for the priced asset.
pub base_asset: AccountId,
/// Canonical identifier for the quote asset that denominates `price`.
pub quote_asset: AccountId,
/// Amount of `quote_asset` one unit of `base_asset` is worth.
///
/// `u128` keeps the consumer-side interface non-negative; zero is rejected on read.
pub price: u128,
/// Price observation timestamp. Consumers choose the time unit by matching this with
/// `max_age`.
pub timestamp: u64,
/// Identifier of the source that populated this account, such as a TWAP or external adaptor.
pub source_identifier: String,
/// Source-provided confidence interval, or zero when the source does not provide one.
pub confidence_interval: u128,
}
impl TryFrom<&Data> for OraclePriceAccount {
type Error = borsh::io::Error;
fn try_from(data: &Data) -> Result<Self, Self::Error> {
Self::try_from_slice(data.as_ref())
}
}
impl From<&OraclePriceAccount> for Data {
fn from(price_account: &OraclePriceAccount) -> Self {
let serialized_len =
borsh::object_length(price_account).expect("Oracle price account length must be known");
let mut data = Vec::with_capacity(serialized_len);
BorshSerialize::serialize(price_account, &mut data)
.expect("Serialization to Vec should not fail");
Self::try_from(data).expect("Oracle price account encoded data should fit into Data")
}
}

139
oracle/core/src/feed.rs Normal file
View File

@ -0,0 +1,139 @@
use nssa_core::account::{Account, AccountId, AccountWithMetadata};
use serde::{Deserialize, Serialize};
use crate::OraclePriceAccount;
/// Price-feed configuration owned by the consuming protocol.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct PriceFeedConfig {
/// External oracle account expected for this feed.
pub feed_account_id: AccountId,
/// Asset the consumer expects to price.
pub expected_base_asset: AccountId,
/// Quote asset expected by the consumer.
pub expected_quote_asset: AccountId,
/// Maximum accepted age for a price observation.
pub max_age: u64,
}
/// Validated price-feed reading returned to consumer logic.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct PriceFeedReading {
pub base_asset: AccountId,
pub quote_asset: AccountId,
pub price: u128,
pub timestamp: u64,
pub source_identifier: String,
pub confidence_interval: u128,
}
/// Recoverable price-feed failures. Stale or unavailable feeds should pause dependent actions.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum PriceFeedError {
/// The supplied account is not the configured feed account.
FeedAccountMismatch {
expected: AccountId,
actual: AccountId,
},
/// The configured feed account is not initialized.
FeedUnavailable,
/// The feed account data does not match [`OraclePriceAccount`].
DecodeFailed,
/// The feed account does not price the configured asset pair.
AssetPairMismatch,
/// The feed reported a zero price.
InvalidPrice,
/// The feed timestamp is newer than the caller's current timestamp.
PriceTimestampInFuture {
current_timestamp: u64,
price_timestamp: u64,
},
/// The feed is older than the configured maximum age.
StalePrice {
current_timestamp: u64,
price_timestamp: u64,
max_age: u64,
},
}
/// Swappable price-feed interface for production oracle accounts and tests.
pub trait PriceFeed {
/// Read and validate the current market price for the configured asset pair.
fn read_market_price(
&self,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError>;
}
/// Price-feed implementation backed by an external oracle account.
pub struct AccountPriceFeed<'a> {
feed_account: &'a AccountWithMetadata,
}
impl<'a> AccountPriceFeed<'a> {
pub fn new(feed_account: &'a AccountWithMetadata) -> Self {
Self { feed_account }
}
}
impl PriceFeed for AccountPriceFeed<'_> {
fn read_market_price(
&self,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError> {
if self.feed_account.account_id != config.feed_account_id {
return Err(PriceFeedError::FeedAccountMismatch {
expected: config.feed_account_id,
actual: self.feed_account.account_id,
});
}
if self.feed_account.account == Account::default() {
return Err(PriceFeedError::FeedUnavailable);
}
let price_account = OraclePriceAccount::try_from(&self.feed_account.account.data)
.map_err(|_| PriceFeedError::DecodeFailed)?;
validate_price_account(&price_account, config, current_timestamp)
}
}
/// Validate a decoded oracle price account using consumer-owned policy.
pub fn validate_price_account(
price_account: &OraclePriceAccount,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError> {
if price_account.base_asset != config.expected_base_asset
|| price_account.quote_asset != config.expected_quote_asset
{
return Err(PriceFeedError::AssetPairMismatch);
}
if price_account.price == 0 {
return Err(PriceFeedError::InvalidPrice);
}
let age = current_timestamp
.checked_sub(price_account.timestamp)
.ok_or(PriceFeedError::PriceTimestampInFuture {
current_timestamp,
price_timestamp: price_account.timestamp,
})?;
if age > config.max_age {
return Err(PriceFeedError::StalePrice {
current_timestamp,
price_timestamp: price_account.timestamp,
max_age: config.max_age,
});
}
Ok(PriceFeedReading {
base_asset: price_account.base_asset,
quote_asset: price_account.quote_asset,
price: price_account.price,
timestamp: price_account.timestamp,
source_identifier: price_account.source_identifier.clone(),
confidence_interval: price_account.confidence_interval,
})
}

View File

@ -1,417 +1,13 @@
//! Shared price-feed account structures and consumer helpers.
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data};
use serde::{Deserialize, Serialize};
use spel_framework_macros::account_type;
/// Canonical oracle price account consumed by LEZ programs.
///
/// Oracle producers own how this account is written; consumers only read and validate it.
#[account_type]
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct OraclePriceAccount {
/// Canonical identifier for the priced asset.
pub base_asset: AccountId,
/// Canonical identifier for the quote asset that denominates `price`.
pub quote_asset: AccountId,
/// Amount of `quote_asset` one unit of `base_asset` is worth.
///
/// `u128` keeps the consumer-side interface non-negative; zero is rejected on read.
pub price: u128,
/// Price observation timestamp. Consumers choose the time unit by matching this with
/// `max_age`.
pub timestamp: u64,
/// Identifier of the source that populated this account, such as a TWAP or external adaptor.
pub source_identifier: String,
/// Source-provided confidence interval, or zero when the source does not provide one.
pub confidence_interval: u128,
}
/// Price-feed configuration owned by the consuming protocol.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub struct PriceFeedConfig {
/// External oracle account expected for this feed.
pub feed_account_id: AccountId,
/// Asset the consumer expects to price.
pub expected_base_asset: AccountId,
/// Quote asset expected by the consumer.
pub expected_quote_asset: AccountId,
/// Maximum accepted age for a price observation.
pub max_age: u64,
}
/// Validated price-feed reading returned to consumer logic.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct PriceFeedReading {
pub base_asset: AccountId,
pub quote_asset: AccountId,
pub price: u128,
pub timestamp: u64,
pub source_identifier: String,
pub confidence_interval: u128,
}
/// Recoverable price-feed failures. Stale or unavailable feeds should pause dependent actions.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum PriceFeedError {
/// The supplied account is not the configured feed account.
FeedAccountMismatch {
expected: AccountId,
actual: AccountId,
},
/// The configured feed account is not initialized.
FeedUnavailable,
/// The feed account data does not match [`OraclePriceAccount`].
DecodeFailed,
/// The feed account does not price the configured asset pair.
AssetPairMismatch,
/// The feed reported a zero price.
InvalidPrice,
/// The feed timestamp is newer than the caller's current timestamp.
PriceTimestampInFuture {
current_timestamp: u64,
price_timestamp: u64,
},
/// The feed is older than the configured maximum age.
StalePrice {
current_timestamp: u64,
price_timestamp: u64,
max_age: u64,
},
}
/// Swappable price-feed interface for production oracle accounts and tests.
pub trait PriceFeed {
/// Read and validate the current market price for the configured asset pair.
fn read_market_price(
&self,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError>;
}
/// Price-feed implementation backed by an external oracle account.
pub struct AccountPriceFeed<'a> {
feed_account: &'a AccountWithMetadata,
}
impl<'a> AccountPriceFeed<'a> {
pub fn new(feed_account: &'a AccountWithMetadata) -> Self {
Self { feed_account }
}
}
impl PriceFeed for AccountPriceFeed<'_> {
fn read_market_price(
&self,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError> {
if self.feed_account.account_id != config.feed_account_id {
return Err(PriceFeedError::FeedAccountMismatch {
expected: config.feed_account_id,
actual: self.feed_account.account_id,
});
}
if self.feed_account.account == Account::default() {
return Err(PriceFeedError::FeedUnavailable);
}
let price_account = OraclePriceAccount::try_from(&self.feed_account.account.data)
.map_err(|_| PriceFeedError::DecodeFailed)?;
validate_price_account(&price_account, config, current_timestamp)
}
}
/// Validate a decoded oracle price account using consumer-owned policy.
pub fn validate_price_account(
price_account: &OraclePriceAccount,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError> {
if price_account.base_asset != config.expected_base_asset
|| price_account.quote_asset != config.expected_quote_asset
{
return Err(PriceFeedError::AssetPairMismatch);
}
if price_account.price == 0 {
return Err(PriceFeedError::InvalidPrice);
}
let age = current_timestamp
.checked_sub(price_account.timestamp)
.ok_or(PriceFeedError::PriceTimestampInFuture {
current_timestamp,
price_timestamp: price_account.timestamp,
})?;
if age > config.max_age {
return Err(PriceFeedError::StalePrice {
current_timestamp,
price_timestamp: price_account.timestamp,
max_age: config.max_age,
});
}
Ok(PriceFeedReading {
base_asset: price_account.base_asset,
quote_asset: price_account.quote_asset,
price: price_account.price,
timestamp: price_account.timestamp,
source_identifier: price_account.source_identifier.clone(),
confidence_interval: price_account.confidence_interval,
})
}
impl TryFrom<&Data> for OraclePriceAccount {
type Error = borsh::io::Error;
fn try_from(data: &Data) -> Result<Self, Self::Error> {
Self::try_from_slice(data.as_ref())
}
}
impl From<&OraclePriceAccount> for Data {
fn from(price_account: &OraclePriceAccount) -> Self {
let serialized_len =
borsh::object_length(price_account).expect("Oracle price account length must be known");
let mut data = Vec::with_capacity(serialized_len);
BorshSerialize::serialize(price_account, &mut data)
.expect("Serialization to Vec should not fail");
Self::try_from(data).expect("Oracle price account encoded data should fit into Data")
}
}
mod account;
mod feed;
#[cfg(test)]
mod tests {
use nssa_core::{
account::{AccountWithMetadata, Nonce},
program::ProgramId,
};
mod tests;
use super::*;
const ORACLE_PROGRAM_ID: ProgramId = [4u32; 8];
fn feed_account_id() -> AccountId {
AccountId::new([0x40u8; 32])
}
fn stablecoin_asset_id() -> AccountId {
AccountId::new([0x50u8; 32])
}
fn usd_asset_id() -> AccountId {
AccountId::new([0x60u8; 32])
}
fn price_account(price: u128, timestamp: u64) -> OraclePriceAccount {
OraclePriceAccount {
base_asset: stablecoin_asset_id(),
quote_asset: usd_asset_id(),
price,
timestamp,
source_identifier: "mock-oracle".to_owned(),
confidence_interval: 5,
}
}
fn config(max_age: u64) -> PriceFeedConfig {
PriceFeedConfig {
feed_account_id: feed_account_id(),
expected_base_asset: stablecoin_asset_id(),
expected_quote_asset: usd_asset_id(),
max_age,
}
}
fn feed_account(price_account: &OraclePriceAccount) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ORACLE_PROGRAM_ID,
balance: 0,
data: Data::from(price_account),
nonce: Nonce(0),
},
is_authorized: false,
account_id: feed_account_id(),
}
}
struct MockPriceFeed {
feed_account_id: AccountId,
price_account: Option<OraclePriceAccount>,
}
impl MockPriceFeed {
fn available(price_account: OraclePriceAccount) -> Self {
Self {
feed_account_id: feed_account_id(),
price_account: Some(price_account),
}
}
}
impl PriceFeed for MockPriceFeed {
fn read_market_price(
&self,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError> {
if self.feed_account_id != config.feed_account_id {
return Err(PriceFeedError::FeedAccountMismatch {
expected: config.feed_account_id,
actual: self.feed_account_id,
});
}
let price_account = self
.price_account
.as_ref()
.ok_or(PriceFeedError::FeedUnavailable)?;
validate_price_account(price_account, config, current_timestamp)
}
}
#[test]
fn account_price_feed_reads_fresh_market_price() {
let oracle_account = price_account(1_050_000, 1_000);
let feed_account = feed_account(&oracle_account);
let reading = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect("fresh price should read");
assert_eq!(
reading,
PriceFeedReading {
base_asset: stablecoin_asset_id(),
quote_asset: usd_asset_id(),
price: 1_050_000,
timestamp: 1_000,
source_identifier: "mock-oracle".to_owned(),
confidence_interval: 5,
}
);
}
#[test]
fn oracle_price_account_data_matches_borsh_object_length() {
let mut oracle_account = price_account(1_050_000, 1_000);
oracle_account.source_identifier = "mock-oracle-with-long-source-name".to_owned();
let data = Data::from(&oracle_account);
assert_eq!(
data.as_ref().len(),
borsh::object_length(&oracle_account).expect("object length should be known")
);
}
#[test]
fn account_price_feed_rejects_wrong_feed_account() {
let oracle_account = price_account(1_050_000, 1_000);
let mut feed_account = feed_account(&oracle_account);
feed_account.account_id = AccountId::new([0x41u8; 32]);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("wrong account must fail");
assert_eq!(
error,
PriceFeedError::FeedAccountMismatch {
expected: feed_account_id(),
actual: AccountId::new([0x41u8; 32]),
}
);
}
#[test]
fn account_price_feed_rejects_stale_price() {
let oracle_account = price_account(1_050_000, 1_000);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_061)
.expect_err("stale price must pause dependent actions");
assert_eq!(
error,
PriceFeedError::StalePrice {
current_timestamp: 1_061,
price_timestamp: 1_000,
max_age: 60,
}
);
}
#[test]
fn account_price_feed_rejects_asset_pair_mismatch() {
let mut oracle_account = price_account(1_050_000, 1_000);
oracle_account.quote_asset = AccountId::new([0x61u8; 32]);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("wrong pair must fail");
assert_eq!(error, PriceFeedError::AssetPairMismatch);
}
#[test]
fn account_price_feed_rejects_zero_price() {
let oracle_account = price_account(0, 1_000);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("zero price must fail");
assert_eq!(error, PriceFeedError::InvalidPrice);
}
#[test]
fn account_price_feed_rejects_future_timestamp() {
let oracle_account = price_account(1_050_000, 1_050);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("future timestamp must fail");
assert_eq!(
error,
PriceFeedError::PriceTimestampInFuture {
current_timestamp: 1_030,
price_timestamp: 1_050,
}
);
}
#[test]
fn account_price_feed_rejects_unavailable_feed_account() {
let feed_account = AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id: feed_account_id(),
};
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("missing feed must fail");
assert_eq!(error, PriceFeedError::FeedUnavailable);
}
#[test]
fn price_feed_trait_accepts_mock_oracle() {
let mock_oracle = MockPriceFeed::available(price_account(1_050_000, 1_000));
let reading = mock_oracle
.read_market_price(&config(60), 1_030)
.expect("mock oracle should use the same consumer checks");
assert_eq!(reading.price, 1_050_000);
assert_eq!(reading.source_identifier, "mock-oracle");
}
}
pub use account::OraclePriceAccount;
pub use feed::{
validate_price_account, AccountPriceFeed, PriceFeed, PriceFeedConfig, PriceFeedError,
PriceFeedReading,
};

235
oracle/core/src/tests.rs Normal file
View File

@ -0,0 +1,235 @@
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data, Nonce},
program::ProgramId,
};
use crate::{
validate_price_account, AccountPriceFeed, OraclePriceAccount, PriceFeed, PriceFeedConfig,
PriceFeedError, PriceFeedReading,
};
const ORACLE_PROGRAM_ID: ProgramId = [4u32; 8];
fn feed_account_id() -> AccountId {
AccountId::new([0x40u8; 32])
}
fn stablecoin_asset_id() -> AccountId {
AccountId::new([0x50u8; 32])
}
fn usd_asset_id() -> AccountId {
AccountId::new([0x60u8; 32])
}
fn price_account(price: u128, timestamp: u64) -> OraclePriceAccount {
OraclePriceAccount {
base_asset: stablecoin_asset_id(),
quote_asset: usd_asset_id(),
price,
timestamp,
source_identifier: "mock-oracle".to_owned(),
confidence_interval: 5,
}
}
fn config(max_age: u64) -> PriceFeedConfig {
PriceFeedConfig {
feed_account_id: feed_account_id(),
expected_base_asset: stablecoin_asset_id(),
expected_quote_asset: usd_asset_id(),
max_age,
}
}
fn feed_account(price_account: &OraclePriceAccount) -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ORACLE_PROGRAM_ID,
balance: 0,
data: Data::from(price_account),
nonce: Nonce(0),
},
is_authorized: false,
account_id: feed_account_id(),
}
}
struct MockPriceFeed {
feed_account_id: AccountId,
price_account: Option<OraclePriceAccount>,
}
impl MockPriceFeed {
fn available(price_account: OraclePriceAccount) -> Self {
Self {
feed_account_id: feed_account_id(),
price_account: Some(price_account),
}
}
}
impl PriceFeed for MockPriceFeed {
fn read_market_price(
&self,
config: &PriceFeedConfig,
current_timestamp: u64,
) -> Result<PriceFeedReading, PriceFeedError> {
if self.feed_account_id != config.feed_account_id {
return Err(PriceFeedError::FeedAccountMismatch {
expected: config.feed_account_id,
actual: self.feed_account_id,
});
}
let price_account = self
.price_account
.as_ref()
.ok_or(PriceFeedError::FeedUnavailable)?;
validate_price_account(price_account, config, current_timestamp)
}
}
#[test]
fn oracle_price_account_data_matches_borsh_object_length() {
let oracle_account = OraclePriceAccount {
source_identifier: "mock-oracle-with-long-source-name".to_owned(),
..price_account(1_050_000, 1_000)
};
let data = Data::from(&oracle_account);
assert_eq!(
data.as_ref().len(),
borsh::object_length(&oracle_account).expect("object length should be known")
);
}
#[test]
fn account_price_feed_reads_fresh_market_price() {
let oracle_account = price_account(1_050_000, 1_000);
let feed_account = feed_account(&oracle_account);
let reading = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect("fresh price should read");
assert_eq!(
reading,
PriceFeedReading {
base_asset: stablecoin_asset_id(),
quote_asset: usd_asset_id(),
price: 1_050_000,
timestamp: 1_000,
source_identifier: "mock-oracle".to_owned(),
confidence_interval: 5,
}
);
}
#[test]
fn account_price_feed_rejects_wrong_feed_account() {
let oracle_account = price_account(1_050_000, 1_000);
let mut feed_account = feed_account(&oracle_account);
feed_account.account_id = AccountId::new([0x41u8; 32]);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("wrong account must fail");
assert_eq!(
error,
PriceFeedError::FeedAccountMismatch {
expected: feed_account_id(),
actual: AccountId::new([0x41u8; 32]),
}
);
}
#[test]
fn account_price_feed_rejects_stale_price() {
let oracle_account = price_account(1_050_000, 1_000);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_061)
.expect_err("stale price must pause dependent actions");
assert_eq!(
error,
PriceFeedError::StalePrice {
current_timestamp: 1_061,
price_timestamp: 1_000,
max_age: 60,
}
);
}
#[test]
fn account_price_feed_rejects_asset_pair_mismatch() {
let mut oracle_account = price_account(1_050_000, 1_000);
oracle_account.quote_asset = AccountId::new([0x61u8; 32]);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("wrong pair must fail");
assert_eq!(error, PriceFeedError::AssetPairMismatch);
}
#[test]
fn account_price_feed_rejects_zero_price() {
let oracle_account = price_account(0, 1_000);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("zero price must fail");
assert_eq!(error, PriceFeedError::InvalidPrice);
}
#[test]
fn account_price_feed_rejects_future_timestamp() {
let oracle_account = price_account(1_050_000, 1_050);
let feed_account = feed_account(&oracle_account);
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("future timestamp must fail");
assert_eq!(
error,
PriceFeedError::PriceTimestampInFuture {
current_timestamp: 1_030,
price_timestamp: 1_050,
}
);
}
#[test]
fn account_price_feed_rejects_unavailable_feed_account() {
let feed_account = AccountWithMetadata {
account: Account::default(),
is_authorized: false,
account_id: feed_account_id(),
};
let error = AccountPriceFeed::new(&feed_account)
.read_market_price(&config(60), 1_030)
.expect_err("missing feed must fail");
assert_eq!(error, PriceFeedError::FeedUnavailable);
}
#[test]
fn price_feed_trait_accepts_mock_oracle() {
let mock_oracle = MockPriceFeed::available(price_account(1_050_000, 1_000));
let reading = mock_oracle
.read_market_price(&config(60), 1_030)
.expect("mock oracle should use the same consumer checks");
assert_eq!(reading.price, 1_050_000);
assert_eq!(reading.source_identifier, "mock-oracle");
}