Skip to content

Commit 229c449

Browse files
Merge pull request #476 from Tola-byte/feature/monitoring-hooks
Feature/monitoring hooks
2 parents 6d50dc3 + c826c39 commit 229c449

File tree

5 files changed

+255
-2
lines changed

5 files changed

+255
-2
lines changed

contracts/predictify-hybrid/src/circuit_breaker.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,12 @@ impl CircuitBreaker {
259259
reason,
260260
Some(admin.clone()),
261261
);
262+
crate::monitoring::ContractMonitor::emit_pause_transition_hook(
263+
env,
264+
&String::from_str(env, "paused"),
265+
Some(admin.clone()),
266+
reason,
267+
);
262268

263269
Ok(())
264270
}
@@ -444,6 +450,12 @@ impl CircuitBreaker {
444450
&String::from_str(env, "Circuit breaker recovered"),
445451
Some(admin.clone()),
446452
);
453+
crate::monitoring::ContractMonitor::emit_pause_transition_hook(
454+
env,
455+
&String::from_str(env, "unpaused"),
456+
Some(admin.clone()),
457+
&String::from_str(env, "manual_recovery"),
458+
);
447459

448460
Ok(())
449461
}
@@ -473,6 +485,12 @@ impl CircuitBreaker {
473485
&String::from_str(env, "Auto-recovery: circuit breaker closed"),
474486
None,
475487
);
488+
crate::monitoring::ContractMonitor::emit_pause_transition_hook(
489+
env,
490+
&String::from_str(env, "unpaused"),
491+
None,
492+
&String::from_str(env, "auto_recovery"),
493+
);
476494
}
477495
}
478496

contracts/predictify-hybrid/src/disputes.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,13 @@ impl DisputeManager {
854854
stake,
855855
reason_for_event,
856856
);
857+
crate::monitoring::ContractMonitor::emit_dispute_transition_hook(
858+
env,
859+
&market_id,
860+
&soroban_sdk::String::from_str(env, "created"),
861+
&user,
862+
&soroban_sdk::String::from_str(env, "dispute_created"),
863+
);
857864

858865
crate::audit_trail::AuditTrailManager::append_record(
859866
env,
@@ -982,6 +989,13 @@ impl DisputeManager {
982989
// Update market with final outcome
983990
DisputeUtils::finalize_market_with_resolution(&mut market, final_outcome)?;
984991
MarketStateManager::update_market(env, &market_id, &market);
992+
crate::monitoring::ContractMonitor::emit_dispute_transition_hook(
993+
env,
994+
&market_id,
995+
&soroban_sdk::String::from_str(env, "resolved"),
996+
&admin,
997+
&soroban_sdk::String::from_str(env, "dispute_resolved"),
998+
);
985999

9861000
crate::audit_trail::AuditTrailManager::append_record(
9871001
env,

contracts/predictify-hybrid/src/monitoring.rs

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,54 @@ pub struct MonitoringData {
173173
pub system_status: MonitoringStatus,
174174
}
175175

176+
/// Critical contract transitions that indexers should track.
177+
#[derive(Debug, Clone, PartialEq, Eq)]
178+
#[contracttype]
179+
pub enum TransitionDomain {
180+
Resolution,
181+
Dispute,
182+
Pause,
183+
}
184+
185+
/// Generic transition hook payload for indexer consumption.
186+
#[derive(Debug, Clone, PartialEq, Eq)]
187+
#[contracttype]
188+
pub struct TransitionHookEvent {
189+
/// Transition domain.
190+
pub domain: TransitionDomain,
191+
/// Action label within the domain (e.g., "resolved", "created", "paused").
192+
pub action: String,
193+
/// Related market when applicable.
194+
pub market_id: Option<Symbol>,
195+
/// State before transition when applicable.
196+
pub old_state: Option<String>,
197+
/// State after transition when applicable.
198+
pub new_state: Option<String>,
199+
/// Actor that initiated the transition when known.
200+
pub actor: Option<Address>,
201+
/// Human-readable transition details.
202+
pub details: String,
203+
/// Event timestamp.
204+
pub timestamp: u64,
205+
}
206+
176207
// ===== CONTRACT MONITOR STRUCT =====
177208

178209
/// Main contract monitoring system
179210
pub struct ContractMonitor;
180211

181212
impl ContractMonitor {
213+
fn market_state_label(env: &Env, state: &MarketState) -> String {
214+
match state {
215+
MarketState::Active => String::from_str(env, "Active"),
216+
MarketState::Ended => String::from_str(env, "Ended"),
217+
MarketState::Disputed => String::from_str(env, "Disputed"),
218+
MarketState::Resolved => String::from_str(env, "Resolved"),
219+
MarketState::Closed => String::from_str(env, "Closed"),
220+
MarketState::Cancelled => String::from_str(env, "Cancelled"),
221+
}
222+
}
223+
182224
/// Monitor market health for a specific market
183225
pub fn monitor_market_health(
184226
env: &Env,
@@ -384,6 +426,91 @@ impl ContractMonitor {
384426
Ok(())
385427
}
386428

429+
/// Emit an indexer-friendly hook for resolution state transitions.
430+
pub fn emit_resolution_transition_hook(
431+
env: &Env,
432+
market_id: &Symbol,
433+
old_state: &MarketState,
434+
new_state: &MarketState,
435+
details: &String,
436+
) {
437+
let event = TransitionHookEvent {
438+
domain: TransitionDomain::Resolution,
439+
action: String::from_str(env, "state_transition"),
440+
market_id: Some(market_id.clone()),
441+
old_state: Some(Self::market_state_label(env, old_state)),
442+
new_state: Some(Self::market_state_label(env, new_state)),
443+
actor: None,
444+
details: details.clone(),
445+
timestamp: env.ledger().timestamp(),
446+
};
447+
448+
env.events().publish(
449+
(
450+
Symbol::new(env, "idx_transition"),
451+
Symbol::new(env, "resolution"),
452+
market_id.clone(),
453+
),
454+
event,
455+
);
456+
}
457+
458+
/// Emit an indexer-friendly hook for dispute lifecycle transitions.
459+
pub fn emit_dispute_transition_hook(
460+
env: &Env,
461+
market_id: &Symbol,
462+
action: &String,
463+
actor: &Address,
464+
details: &String,
465+
) {
466+
let event = TransitionHookEvent {
467+
domain: TransitionDomain::Dispute,
468+
action: action.clone(),
469+
market_id: Some(market_id.clone()),
470+
old_state: None,
471+
new_state: None,
472+
actor: Some(actor.clone()),
473+
details: details.clone(),
474+
timestamp: env.ledger().timestamp(),
475+
};
476+
477+
env.events().publish(
478+
(
479+
Symbol::new(env, "idx_transition"),
480+
Symbol::new(env, "dispute"),
481+
market_id.clone(),
482+
),
483+
event,
484+
);
485+
}
486+
487+
/// Emit an indexer-friendly hook for contract pause/unpause transitions.
488+
pub fn emit_pause_transition_hook(
489+
env: &Env,
490+
action: &String,
491+
actor: Option<Address>,
492+
details: &String,
493+
) {
494+
let event = TransitionHookEvent {
495+
domain: TransitionDomain::Pause,
496+
action: action.clone(),
497+
market_id: None,
498+
old_state: None,
499+
new_state: None,
500+
actor,
501+
details: details.clone(),
502+
timestamp: env.ledger().timestamp(),
503+
};
504+
505+
env.events().publish(
506+
(
507+
Symbol::new(env, "idx_transition"),
508+
Symbol::new(env, "pause"),
509+
),
510+
event,
511+
);
512+
}
513+
387514
/// Validate monitoring data integrity
388515
pub fn validate_monitoring_data(env: &Env, data: &MonitoringData) -> Result<bool, Error> {
389516
// Validate timestamp
@@ -1018,7 +1145,7 @@ impl MonitoringTestingUtils {
10181145
#[cfg(test)]
10191146
mod tests {
10201147
use super::*;
1021-
use soroban_sdk::testutils::Address;
1148+
use soroban_sdk::testutils::{Address as _, Events};
10221149

10231150
#[test]
10241151
fn test_market_health_monitoring() {
@@ -1175,4 +1302,58 @@ mod tests {
11751302
let data = MonitoringTestingUtils::create_test_monitoring_data(&env);
11761303
assert_eq!(data.system_status, MonitoringStatus::Healthy);
11771304
}
1305+
1306+
#[test]
1307+
fn test_resolution_transition_hook_emits_indexer_event() {
1308+
let env = Env::default();
1309+
let contract_id = env.register(crate::PredictifyHybrid, ());
1310+
let market_id = Symbol::new(&env, "mkt_1");
1311+
env.as_contract(&contract_id, || {
1312+
ContractMonitor::emit_resolution_transition_hook(
1313+
&env,
1314+
&market_id,
1315+
&MarketState::Ended,
1316+
&MarketState::Resolved,
1317+
&String::from_str(&env, "oracle resolution"),
1318+
);
1319+
});
1320+
1321+
assert_eq!(env.events().all().len(), 1);
1322+
}
1323+
1324+
#[test]
1325+
fn test_dispute_transition_hook_emits_indexer_event() {
1326+
let env = Env::default();
1327+
let contract_id = env.register(crate::PredictifyHybrid, ());
1328+
let market_id = Symbol::new(&env, "mkt_2");
1329+
let actor = Address::generate(&env);
1330+
env.as_contract(&contract_id, || {
1331+
ContractMonitor::emit_dispute_transition_hook(
1332+
&env,
1333+
&market_id,
1334+
&String::from_str(&env, "created"),
1335+
&actor,
1336+
&String::from_str(&env, "stake posted"),
1337+
);
1338+
});
1339+
1340+
assert_eq!(env.events().all().len(), 1);
1341+
}
1342+
1343+
#[test]
1344+
fn test_pause_transition_hook_emits_indexer_event() {
1345+
let env = Env::default();
1346+
let contract_id = env.register(crate::PredictifyHybrid, ());
1347+
let actor = Address::generate(&env);
1348+
env.as_contract(&contract_id, || {
1349+
ContractMonitor::emit_pause_transition_hook(
1350+
&env,
1351+
&String::from_str(&env, "paused"),
1352+
Some(actor),
1353+
&String::from_str(&env, "manual emergency pause"),
1354+
);
1355+
});
1356+
1357+
assert_eq!(env.events().all().len(), 1);
1358+
}
11781359
}

contracts/predictify-hybrid/src/resolution.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,13 @@ impl OracleResolutionManager {
991991
&crate::types::MarketState::Cancelled,
992992
&soroban_sdk::String::from_str(env, "Resolution timeout reached, market cancelled"),
993993
);
994+
crate::monitoring::ContractMonitor::emit_resolution_transition_hook(
995+
env,
996+
market_id,
997+
&old_state,
998+
&crate::types::MarketState::Cancelled,
999+
&soroban_sdk::String::from_str(env, "timeout_cancelled"),
1000+
);
9941001

9951002
return Err(Error::InvalidState);
9961003
}
@@ -1414,6 +1421,13 @@ impl MarketResolutionManager {
14141421
&crate::types::MarketState::Resolved,
14151422
&soroban_sdk::String::from_str(env, "Automated resolution completed"),
14161423
);
1424+
crate::monitoring::ContractMonitor::emit_resolution_transition_hook(
1425+
env,
1426+
market_id,
1427+
&old_state,
1428+
&crate::types::MarketState::Resolved,
1429+
&resolution_method_str,
1430+
);
14171431

14181432
Ok(resolution)
14191433
}

docs/gas/GAS_MONITORING.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,34 @@
2424
- Top costly calls and scenarios
2525
- WASM size trend per release
2626

27+
### Indexer Transition Hooks
28+
29+
Critical lifecycle transitions now emit indexer-friendly events via monitoring hooks under a
30+
shared topic prefix:
31+
32+
- Topic root: `idx_transition`
33+
- Domains:
34+
- `resolution`
35+
- `dispute`
36+
- `pause`
37+
38+
Hook payload fields:
39+
40+
- `domain` - transition domain (`Resolution`, `Dispute`, `Pause`)
41+
- `action` - action label (for example `state_transition`, `created`, `resolved`, `paused`, `unpaused`)
42+
- `market_id` - present for resolution/dispute transitions
43+
- `old_state` / `new_state` - present for resolution state transitions
44+
- `actor` - present when an initiating address is known
45+
- `details` - implementation-defined detail string
46+
- `timestamp` - ledger timestamp
47+
48+
This allows indexers to track:
49+
50+
- Resolution transitions such as `Ended -> Resolved` and timeout cancellations
51+
- Dispute lifecycle transitions (`created`, `resolved`)
52+
- Pause lifecycle transitions (`paused`, `unpaused`) from circuit breaker operations
53+
2754
### Operational Playbooks
2855

2956
- If costs climb due to strings: enforce length caps at API layer and/or contract validation.
3057
- If claim/resolve costs spike: batch payouts off-chain via token escrows or staged claims.
31-

0 commit comments

Comments
 (0)