Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion contracts/oracle-integration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use soroban_sdk::{
contract, contracterror, contractevent, contractimpl, contracttype, Address, Bytes, BytesN,
Env, Vec,
Env, Symbol, Vec,
};

#[contract]
Expand Down Expand Up @@ -38,6 +38,29 @@ pub struct LatestPriceData {
pub updated_ledger: u32,
}

/// Snapshot of the configured oracle source addresses at read time.
/// Returns `None` from `source_config_snapshot` when the contract has not been initialized.
#[derive(Clone)]
#[contracttype]
pub struct OracleSourceSnapshot {
/// Whitelisted oracle addresses that may fulfill data requests.
pub sources: Vec<Address>,
/// Number of configured sources (convenience field for clients).
pub source_count: u32,
}

/// Summary of the update cadence and staleness policy in effect.
/// Deterministic: the same value is returned on every call.
#[derive(Clone)]
#[contracttype]
pub struct UpdatePolicySummary {
/// Number of ledgers after which a price is considered stale.
pub stale_threshold_ledgers: u32,
/// Describes when updates occur. `"on_request"` means data is fetched
/// per-request rather than on a fixed schedule.
pub cadence: Symbol,
}

#[derive(Clone)]
#[contracttype]
pub struct PriceFreshness {
Expand Down Expand Up @@ -299,6 +322,43 @@ impl OracleIntegration {
result
}

// ───────── SOURCE CONFIG SNAPSHOT ─────────

/// Returns a snapshot of the configured oracle source addresses.
///
/// Returns `None` when the contract has not been initialized; callers should
/// treat a `None` result as "no sources configured" and not attempt data
/// requests until the contract is initialized.
pub fn source_config_snapshot(env: Env) -> Option<OracleSourceSnapshot> {
let sources: Option<Vec<Address>> = env
.storage()
.instance()
.get(&DataKey::OracleSources);

sources.map(|s| {
let source_count = s.len();
OracleSourceSnapshot {
sources: s,
source_count,
}
})
}

// ───────── UPDATE POLICY SUMMARY ─────────

/// Returns a deterministic summary of the staleness and update policy.
///
/// The summary is safe to cache by clients: it does not change after
/// initialization and does not require any feed-specific parameters.
/// The `cadence` field is `"on_request"` — data is pulled per-request
/// rather than pushed on a fixed schedule.
pub fn update_policy_summary(env: Env) -> UpdatePolicySummary {
UpdatePolicySummary {
stale_threshold_ledgers: STALE_THRESHOLD_LEDGERS,
cadence: Symbol::new(&env, "on_request"),
}
}

pub fn last_price_freshness(env: Env, feed_id: BytesN<32>) -> PriceFreshness {
let current_ledger = env.ledger().sequence();
let key = DataKey::Latest(feed_id);
Expand Down
71 changes: 71 additions & 0 deletions contracts/oracle-integration/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,74 @@ fn last_price_freshness_handles_missing_prices() {
assert_eq!(freshness.age_ledgers, 0);
assert!(freshness.is_stale);
}

// --- source_config_snapshot ---

#[test]
fn source_config_snapshot_returns_configured_sources() {
let env = Env::default();
let contract_id = env.register(OracleIntegration, ());
let client = OracleIntegrationClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let oracle1 = Address::generate(&env);
let oracle2 = Address::generate(&env);

env.mock_all_auths();
let sources = vec![&env, oracle1.clone(), oracle2.clone()];
client.init(&admin, &sources);

let snapshot = client.source_config_snapshot().expect("snapshot present after init");
assert_eq!(snapshot.source_count, 2);
assert!(snapshot.sources.contains(&oracle1));
assert!(snapshot.sources.contains(&oracle2));
}

#[test]
fn source_config_snapshot_returns_none_before_init() {
let env = Env::default();
let contract_id = env.register(OracleIntegration, ());
let client = OracleIntegrationClient::new(&env, &contract_id);

assert!(client.source_config_snapshot().is_none());
}

#[test]
fn source_config_snapshot_count_matches_sources_length() {
let env = Env::default();
let (client, _, _, _, _) = setup_initialized(&env);

let snapshot = client.source_config_snapshot().expect("initialized");
assert_eq!(snapshot.source_count, snapshot.sources.len());
}

// --- update_policy_summary ---

#[test]
fn update_policy_summary_returns_correct_stale_threshold() {
let env = Env::default();
let (client, _, _, _, _) = setup_initialized(&env);

let summary = client.update_policy_summary();
// Matches the STALE_THRESHOLD_LEDGERS constant (20).
assert_eq!(summary.stale_threshold_ledgers, 20);
}

#[test]
fn update_policy_summary_cadence_is_on_request() {
let env = Env::default();
let (client, _, _, _, _) = setup_initialized(&env);

let summary = client.update_policy_summary();
assert_eq!(summary.cadence, soroban_sdk::Symbol::new(&env, "on_request"));
}

#[test]
fn update_policy_summary_is_deterministic_across_calls() {
let env = Env::default();
let (client, _, _, _, _) = setup_initialized(&env);

let s1 = client.update_policy_summary();
let s2 = client.update_policy_summary();
assert_eq!(s1.stale_threshold_ledgers, s2.stale_threshold_ledgers);
assert_eq!(s1.cadence, s2.cadence);
}
Loading