A standardized cross-contract callback interface has been successfully implemented for the StellarFlow Price Oracle. This enables downstream Soroban contracts (Lending protocols, DEXs, etc.) to subscribe to real-time price updates without polling.
Subscription Functions (Public API in PriceOracleClient):
// Register a contract to receive price update callbacks
pub fn subscribe_to_price_updates(callback_contract: Address) -> Result<(), String>
// Unregister a contract from callbacks
pub fn unsubscribe_from_price_updates(callback_contract: Address) -> Result<(), String>
// Get list of all subscribed contracts
pub fn get_price_update_subscribers() -> Vec<Address>Subscriber contracts must implement:
pub fn on_price_update(env: Env, payload: PriceUpdatePayload)Where PriceUpdatePayload contains:
asset: Symbol (NGN, KES, GHS, etc.)price: i128 (normalized to 9 decimals)timestamp: u64 (ledger timestamp)provider: Address (who submitted the price)decimals: u32 (always 9)confidence_score: u32 (0-100)
Callbacks are automatically triggered when prices are updated:
- β
update_price()- authorized provider updates - β
set_price()- admin price setting
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β StellarFlow Price Oracle (lib.rs) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Public Contract Interface β β
β β - update_price() β β
β β - set_price() β β
β β - subscribe_to_price_updates() β β
β β - unsubscribe_from_price_updates() β β
β β - get_price_update_subscribers() β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Callbacks Module (callbacks.rs) β β
β β - subscribe() β β
β β - unsubscribe() β β
β β - get_subscribers() β β
β β - notify_subscribers() β β
β β - try_invoke_callback() β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Types Module (types.rs) β β
β β - PriceUpdatePayload β β
β β - DataKey::PriceUpdateSubscribers β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββ¬βββββββββββββββββββ
β β β
Lending DEX/AMM Other
Protocol Protocol Protocols
on_price_update() callbacks
contracts/price-oracle/src/
βββ lib.rs (Main contract, updated)
β βββ Subscription functions
β βββ Callback integration
βββ types.rs (Updated)
β βββ PriceUpdatePayload struct
β βββ DataKey::PriceUpdateSubscribers
βββ callbacks.rs (NEW)
β βββ Subscription management
β βββ Callback invocation
βββ auth.rs
βββ math.rs
βββ median.rs
βββ asset_symbol.rs
βββ test.rs (Extended)
βββ 11 new callback tests
CALLBACK_INTERFACE.md (NEW)
βββ Comprehensive documentation
-
Implement callback in your contract:
#[contract] pub struct MyLendingPool; #[contractimpl] impl MyLendingPool { pub fn on_price_update(env: Env, payload: PriceUpdatePayload) { // Check for liquidation opportunities // Update collateral requirements // Trigger rebalancing if needed } }
-
Subscribe to oracle:
let oracle = PriceOracleClient::new(&env, &oracle_address); oracle.subscribe_to_price_updates(&my_contract_address)?;
-
Receive automatic updates:
When price changes β Oracle calls on_price_update() β React immediately
-
Implement callback:
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) { // Rebalance liquidity pools // Check slippage limits // Adjust fee tiers // Emit events for off-chain systems }
-
Subscribe:
oracle.subscribe_to_price_updates(&dex_contract)?;
The implementation includes 11 comprehensive tests:
- β
test_subscribe_to_price_updates- Basic subscription - β
test_subscribe_duplicate_fails- Duplicate prevention - β
test_multiple_subscribers- Multiple registrations - β
test_unsubscribe_from_price_updates- Unsubscribe - β
test_unsubscribe_nonexistent_fails- Error handling - β
test_get_empty_subscriber_list- Empty state - β
test_subscribe_unsubscribe_cycle- Lifecycle - β
test_update_price_does_not_crash_with_subscribers- Integration - β
test_set_price_with_subscribers- Admin integration - β
test_subscribe_and_get_subscribers(callbacks.rs) - β
test_unsubscribe(callbacks.rs)
Run tests with:
cd contracts/price-oracle
cargo test- No polling required
- Immediate reaction to price changes
- Real-time synchronization
- Single
on_price_updatefunction signature - All subscribers implement the same contract
- Easy integration for new protocols
- O(n) subscription operations (n = subscriber count)
- O(1) callback dispatch per subscriber
- Recommended max: β€10 subscribers
- Failed callbacks don't block price updates
- Errors logged internally, not propagated
- Non-blocking callback semantics
- Oracle is authoritative source
- Subscribers should validate caller
- Price data immutable after callback
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
let asset = payload.asset;
let new_price = payload.price;
// Find positions to liquidate
let positions = find_undercollateralized(&env, &asset, new_price);
for position in positions.iter() {
trigger_liquidation(&env, &position);
}
}pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
let old_price = get_last_known_price(&env, &payload.asset);
let price_change_pct = calc_pct_change(old_price, payload.price);
// Rebalance if price moved >2%
if price_change_pct.abs() > 200 {
rebalance_pool(&env, &payload.asset, payload.price);
}
}pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
// Store price data for other protocols
env.storage().instance().set(
&DataKey::LatestPrice(payload.asset),
&payload
);
// Emit event for off-chain indexing
env.events().publish((
Symbol::new(&env, "oracle_price_update"),
), (
payload.asset,
payload.price,
payload.timestamp,
));
}| Error | Cause | Resolution |
|---|---|---|
| "Contract is already subscribed" | Duplicate subscription | Call unsubscribe first |
| "Contract not found in subscribers" | Unsubscribing non-subscriber | Verify contract is subscribed |
- Callbacks are best-effort
- Non-blocking: failures don't affect price storage
- Implement defensive validation in
on_price_update
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
// Validate data integrity
assert!(payload.price > 0, "Invalid price");
assert!(payload.timestamp <= env.ledger().timestamp(), "Future timestamp");
// Your logic here
}| Operation | Complexity | Notes |
|---|---|---|
| Subscribe | O(n) | n = subscriber count |
| Unsubscribe | O(n) | Linear search + remove |
| Get Subscribers | O(1) | Storage retrieval |
| Callback Dispatch | O(n*m) | n = subscribers, m = callback complexity |
| Price Update | O(1) | Async callback dispatch |
-
Verify Oracle Source:
const ORACLE_ADDRESS: &str = "C..."; pub fn on_price_update(env: Env, payload: PriceUpdatePayload) { assert_eq!(env.invoker(), Address::from_contract_id(&env, ORACLE_ADDRESS)); }
-
Validate Payload:
// Check timestamp freshness let now = env.ledger().timestamp(); assert!(payload.timestamp <= now && payload.timestamp > now - 300); // Check price sanity assert!(payload.price > 0 && payload.price < MAX_REASONABLE_PRICE);
-
Idempotent Updates:
// Design for replayability // Store both price and update timestamp // Check if update is newer before applying
- Read CALLBACK_INTERFACE.md for detailed documentation
- Review test cases in src/test.rs
- Implement
on_price_updatein your contract - Subscribe to the oracle
- Test with mock contracts
- Add callback interface to your protocol
- Update protocol state on
on_price_update - Monitor callback gas usage
- Set up event listeners for debugging
- Deploy with β€10 initial subscribers
- π Callback Interface Documentation
- π§ͺ Test Cases
- π Integration Guide
- π€ Main README
Version: 1.0.0
Status: β
Ready for Production
Last Updated: April 25, 2026