pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
// Your price update logic here
// payload contains: asset, price, timestamp, provider, decimals, confidence_score
}let oracle = PriceOracleClient::new(&env, &oracle_address);
oracle.subscribe_to_price_updates(&my_contract_address)?Callbacks now fire automatically when prices update. No polling needed!
subscribe_to_price_updates(callback_contract: Address) -> Result<(), String>Returns: Ok(()) on success, error if already subscribed
unsubscribe_from_price_updates(callback_contract: Address) -> Result<(), String>Returns: Ok(()) on success, error if not found
get_price_update_subscribers() -> Vec<Address>Returns: List of all subscribed contracts
pub fn on_price_update(env: Env, payload: PriceUpdatePayload)pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
let positions = get_positions_for_asset(&env, &payload.asset);
for position in positions.iter() {
if is_undercollateralized(&env, &position, payload.price) {
liquidate(&env, &position);
}
}
}pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
let old_price = get_last_price(&env, &payload.asset);
let pct_change = ((payload.price - old_price) * 10000) / old_price;
if pct_change.abs() > 200 { // >2% change
rebalance_pool(&env, &payload.asset, payload.price);
}
}pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
env.storage().instance().set(
&DataKey::LatestPrice(payload.asset),
&payload.price,
);
}pub struct PriceUpdatePayload {
pub asset: Symbol, // "NGN", "KES", "GHS", etc.
pub price: i128, // Price value (9 decimals)
pub timestamp: u64, // When it was updated
pub provider: Address, // Who submitted it
pub decimals: u32, // Always 9
pub confidence_score: u32, // 0-100
}const ORACLE_ADDRESS: &str = "C...";
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
let caller = env.invoker();
assert_eq!(caller, Address::from_contract_id(&env, ORACLE_ADDRESS));
// Process update...
}pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
let now = env.ledger().timestamp();
assert!(payload.timestamp <= now && payload.timestamp > now - 300);
// Process update...
}pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
let current_version = get_price_version(&env, &payload.asset);
// Only update if newer
if payload.timestamp > current_version.timestamp {
update_price(&env, payload);
}
}pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
store_price(&env, &payload);
env.events().publish(
(Symbol::new(&env, "price_update_received"),),
(payload.asset, payload.price, payload.timestamp),
);
}| Issue | Cause | Solution |
|---|---|---|
| Callback not called | Not subscribed | Call subscribe_to_price_updates first |
| "Already subscribed" error | Duplicate subscription | Check if already subscribed first |
| "Not found" error | Not subscribed | Call subscribe_to_price_updates |
| Callback panics | Invalid data | Add validation in on_price_update |
| Gas limit exceeded | Too complex callback | Simplify callback logic |
- Keep callbacks simple and fast
- Use early exits (return on first check)
- Store frequently-accessed data
- Validate inputs
- Emit events for monitoring
- Don't do complex computations in callback
- Don't call other contracts in callback
- Don't store too much data per call
- Don't ignore validation errors
- Don't subscribe too many contracts (max ~10)
#[test]
fn test_my_callback() {
let env = Env::default();
let contract = env.register_contract(None, MyProtocol);
// Simulate callback
let payload = PriceUpdatePayload {
asset: symbol_short!("NGN"),
price: 1_500_000,
timestamp: 1_000_000,
provider: Address::generate(&env),
decimals: 9,
confidence_score: 95,
};
// Call callback directly
MyProtocolClient::new(&env, &contract).on_price_update(&payload);
// Verify state changed correctly
// assert_eq!(get_updated_state(&env), expected_state);
}- Read CALLBACK_INTERFACE.md
- Implement
on_price_updatein your contract - Add validation in callback (caller, timestamp, price)
- Test callback with various inputs
- Call
subscribe_to_price_updatesto register - Monitor callback gas usage
- Set up event listeners for debugging
- Deploy and verify callbacks fire
Before (Polling):
Your Contract → repeatedly calls oracle.get_price()
After (Callbacks):
Oracle → automatically calls your contract.on_price_update()
If callback fails:
✅ Price still updates
✅ Other subscribers still get callbacks
✅ Transaction succeeds
Price Update
↓
validate & store price
↓
publish PriceUpdatedEvent
↓
notify_subscribers()
↓
for each subscriber:
call on_price_update(payload)
(ignore errors)
↓
Done
Common assets in StellarFlow:
| Symbol | Asset | Decimals |
|---|---|---|
| NGN | Nigerian Naira | 2 |
| KES | Kenyan Shilling | 2 |
| GHS | Ghanaian Cedi | 2 |
| XLM | Stellar Lumens | 7 |
| USDT | Tether USD | 6 |
All prices normalized to 9 decimals internally.
❌ Mistake 1: Not validating caller
// BAD
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
// Anyone could call this!
}
// GOOD
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
assert_eq!(env.invoker(), ORACLE_ADDRESS);
}❌ Mistake 2: Ignoring timestamp
// BAD
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
update_price(&env, payload.price); // Old price?
}
// GOOD
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
if payload.timestamp > last_update_time {
update_price(&env, payload.price);
}
}❌ Mistake 3: Doing too much in callback
// BAD
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
for position in get_all_positions() { // Expensive!
for history in get_all_history() { // Expensive!
calculate_complex_math(); // Expensive!
}
}
}
// GOOD
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
check_liquidations_for_asset(&env, &payload.asset); // Focused
}| Resource | Link |
|---|---|
| Full Guide | CALLBACK_INTERFACE.md |
| Implementation | Summary |
| Example | Lending Integration |
| Tests | test.rs |
| Main Readme | README.md |
// 1. Import what you need
use price_oracle::types::PriceUpdatePayload;
// 2. Implement callback
#[contractimpl]
impl MyProtocol {
pub fn on_price_update(env: Env, payload: PriceUpdatePayload) {
// Validate caller
assert_eq!(env.invoker(), ORACLE_ADDRESS);
// Process price update
let asset = payload.asset;
let price = payload.price;
let timestamp = payload.timestamp;
// Do your thing...
}
}
// 3. Subscribe
let oracle = PriceOracleClient::new(&env, &oracle_address);
oracle.subscribe_to_price_updates(&my_contract)?
// 4. Done! Callbacks fire automatically- How do I subscribe? → Call
subscribe_to_price_updates - How are callbacks triggered? → Automatically on price update
- What if my callback fails? → Price still updates, other callbacks run
- Can I unsubscribe? → Yes, call
unsubscribe_from_price_updates - How many subscribers? → Recommend max 10 for production
- What's the gas cost? → ~5,000 per callback + your logic
- Can I filter assets? → No, subscribe to all (future: per-asset filtering)
- Do I still need polling? → No! Callbacks are real-time
Last Updated: April 25, 2026
Version: 1.0.0
Status: ✅ Ready to Use