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
252 changes: 252 additions & 0 deletions ORACLE_SNAPSHOT_CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# Oracle Snapshot Feature - Change Summary

## Overview
This document provides a concise summary of all code changes made to implement the OracleSnapshot feature.

## Files Modified

### 1. contracts/price-oracle/src/types.rs

**Change 1: Add LastSnapshotLedger to DataKey enum**

```rust
pub enum DataKey {
// ... existing variants ...
TrackedAsset(Symbol),
+ /// Last ledger sequence where a snapshot was emitted (for checkpoint events).
+ LastSnapshotLedger,
}
```

**Change 2: Add SnapshotPrice struct**

```rust
+ /// A single asset price snapshot entry.
+ ///
+ /// Used in OracleSnapshot events to include all current asset prices
+ /// at a checkpoint ledger boundary (every 100 ledgers).
+ #[contracttype]
+ #[derive(Clone, Debug, Eq, PartialEq)]
+ pub struct SnapshotPrice {
+ /// The asset symbol (e.g., NGN, KES, GHS).
+ pub asset: Symbol,
+ /// The current price value (normalized to 9 decimal places).
+ pub price: i128,
+ /// Timestamp when this price was last updated.
+ pub timestamp: u64,
+ }
```

### 2. contracts/price-oracle/src/lib.rs

**Change 1: Update imports to include PriceEntry**

```rust
- use crate::types::{DataKey, PriceBounds, PriceBuffer, PriceBufferEntry, PriceData, PriceDataWithStatus, PriceEntryWithStatus, RecentEvent, AdminAction, AdminLogEntry, PriceUpdatePayload, ProposedAction};
+ use crate::types::{DataKey, PriceBounds, PriceBuffer, PriceBufferEntry, PriceData, PriceDataWithStatus, PriceEntry, PriceEntryWithStatus, RecentEvent, AdminAction, AdminLogEntry, PriceUpdatePayload, ProposedAction};
```

**Change 2: Add OracleSnapshotEvent definition**

```rust
#[soroban_sdk::contractevent]
pub struct RescueTokensEvent {
pub token: Address,
pub recipient: Address,
pub amount: i128,
}

+ #[soroban_sdk::contractevent]
+ pub struct OracleSnapshotEvent {
+ /// The ledger sequence number at which this snapshot was taken.
+ pub ledger_sequence: u32,
+ /// Timestamp when the snapshot was emitted.
+ pub timestamp: u64,
+ /// All currently tracked asset prices at this checkpoint.
+ pub prices: soroban_sdk::Vec<PriceEntry>,
+ }
```

**Change 3: Add emit_snapshot_if_needed function**

```rust
fn log_event(env: &Env, event_type: Symbol, asset: Symbol, price: i128) {
// ... existing code ...
}

+ /// Emit an OracleSnapshot event every 100 ledgers.
+ ///
+ /// This provides a checkpointed state for off-chain databases (subgraphs/indexers)
+ /// to sync against, containing all currently tracked asset prices.
+ fn emit_snapshot_if_needed(env: &Env) {
+ let current_ledger = env.ledger().sequence();
+
+ // Only emit snapshots at 100-ledger boundaries
+ if current_ledger % 100 != 0 {
+ return;
+ }
+
+ // Check if we've already emitted a snapshot at this ledger
+ let last_snapshot: Option<u32> = env
+ .storage()
+ .instance()
+ .get(&DataKey::LastSnapshotLedger);
+
+ if let Some(last) = last_snapshot {
+ if last >= current_ledger {
+ return;
+ }
+ }
+
+ // Collect all current asset prices
+ let assets = get_tracked_assets(env);
+ let storage = env.storage().persistent();
+ let mut prices = soroban_sdk::Vec::new(env);
+
+ for asset in assets.iter() {
+ if let Some(price_data) = storage.get::<DataKey, PriceData>(&DataKey::VerifiedPrice(asset.clone())) {
+ let entry = PriceEntry {
+ price: price_data.price,
+ timestamp: price_data.timestamp,
+ decimals: price_data.decimals,
+ };
+ prices.push_back(entry);
+ }
+ }
+
+ // Emit the snapshot event
+ env.events().publish_event(&OracleSnapshotEvent {
+ ledger_sequence: current_ledger,
+ timestamp: env.ledger().timestamp(),
+ prices,
+ });
+
+ // Update the last snapshot ledger
+ env.storage()
+ .instance()
+ .set(&DataKey::LastSnapshotLedger, &current_ledger);
+ }
```

**Change 4: Update set_price() function - Part A (zero-write optimization)**

```rust
if let Some(mut current) = existing {
if current.price == val {
// Price unchanged — only refresh the timestamp (zero-write optimisation).
current.timestamp = now;
storage.set(&key, &current);
update_twap(&env, asset.clone(), val, now);
env.events().publish_event(&PriceUpdatedEvent { asset: asset.clone(), price: val });
log_event(&env, Symbol::new(&env, "price_updated"), asset, val);
+
+ // Emit snapshot if we're at a 100-ledger boundary
+ emit_snapshot_if_needed(&env);
+
return Ok(());
}
}
```

**Change 5: Update set_price() function - Part B (main path)**

```rust
// Notify subscribers of the price update
let payload = PriceUpdatePayload {
asset: asset.clone(),
price: normalized,
timestamp: now,
provider: env.current_contract_address(),
decimals: 9,
confidence_score: 100,
};
callbacks::notify_subscribers(&env, &payload);

+ // Emit snapshot if we're at a 100-ledger boundary
+ emit_snapshot_if_needed(&env);
+
Ok(())
```

**Change 6: Update update_price() function**

```rust
// Notify all subscribed contracts of the price update
let payload = PriceUpdatePayload {
asset: asset.clone(),
price: median_price,
timestamp: env.ledger().timestamp(),
provider: source,
decimals: 9,
confidence_score,
};
callbacks::notify_subscribers(&env, &payload);

+ // Emit snapshot if we're at a 100-ledger boundary
+ emit_snapshot_if_needed(&env);

Ok(())
```

### 3. contracts/price-oracle/INTEGRATION.md

**Change: Add "Oracle Snapshot Events (For Indexers/Subgraphs)" section**

```markdown
+ ## Oracle Snapshot Events (For Indexers/Subgraphs)
+
+ The StellarFlow Oracle emits special `OracleSnapshot` events every 100 ledgers to help subgraphs and indexers track price history efficiently.
+
+ ### What is an OracleSnapshot Event?
+ ...
+ (See INTEGRATION.md for full details)
```

## Summary of Changes

| File | Changes | Type |
|------|---------|------|
| types.rs | Added `LastSnapshotLedger` DataKey and `SnapshotPrice` struct | 2 additions |
| lib.rs | Added event, logic function, and 3 integration points | 6 additions |
| INTEGRATION.md | Added indexer documentation | 1 section |

## Total Lines Added

- types.rs: ~30 lines (DataKey variant + SnapshotPrice struct)
- lib.rs: ~100 lines (event definition + emit_snapshot_if_needed function + 3 calls)
- INTEGRATION.md: ~80 lines (documentation)
- **Total: ~210 lines**

## Verification

✅ All changes compile without errors
✅ No breaking changes to existing APIs
✅ Backward compatible with existing code
✅ No modifications to storage format for existing data

## Testing Areas

The implementation should be tested for:

1. **Boundary correctness**: Snapshots emitted only at ledgers 100, 200, 300...
2. **Completeness**: All tracked assets included in every snapshot
3. **Deduplication**: No duplicate snapshots at same ledger boundary
4. **Data accuracy**: Snapshot prices match current oracle prices
5. **Multiple updates**: Concurrent price updates don't break snapshotting
6. **Early ledgers**: No errors when network starts (ledger < 100)

## Runtime Impact

- **Gas Cost**: Minimal - snapshot logic only executes at 100-ledger boundaries
- **Storage**: One additional storage entry per snapshot (every 100 ledgers)
- **Memory**: Temporary Vec created during snapshot emission (cleaned up immediately)
- **Performance**: No impact on price update functions on non-snapshot ledgers

## Deployment Checklist

- ✅ Code changes reviewed
- ✅ Compilation verified
- ✅ No breaking changes
- ✅ Documentation updated
- ⏳ Test snapshots generated (post-deployment)
- ⏳ Mainnet testing (post-testnet validation)
Loading