diff --git a/contracts/predictify-hybrid/src/bets.rs b/contracts/predictify-hybrid/src/bets.rs index c0750af7..6294a711 100644 --- a/contracts/predictify-hybrid/src/bets.rs +++ b/contracts/predictify-hybrid/src/bets.rs @@ -700,7 +700,15 @@ impl BetManager { placed_bets.push_back(bet); } - // Phase 4: Consume the idempotency key so replays are rejected. + // Phase 4: Emit batch event for the entire operation + EventEmitter::emit_bet_batch_placed( + env, + &user, + &bets, + total_amount, + ); + + // Phase 5: Consume the idempotency key so replays are rejected. // Stored as temporary (cheaper rent) with PLACE_BETS_IDEM_TTL_LEDGERS TTL. let ttl = crate::storage::PLACE_BETS_IDEM_TTL_LEDGERS; env.storage().persistent().set(&idem_key, &true); @@ -776,6 +784,15 @@ impl BetManager { // Store updated stats BetStorage::store_market_bet_stats(env, market_id, &stats)?; + // Emit bet stats updated event + EventEmitter::emit_bet_stats_updated( + env, + market_id, + stats.total_bets, + stats.total_amount_locked, + stats.unique_bettors, + ); + Ok(()) } diff --git a/contracts/predictify-hybrid/src/event_topic_catalog.rs b/contracts/predictify-hybrid/src/event_topic_catalog.rs index bf8f5ebf..6333e07d 100644 --- a/contracts/predictify-hybrid/src/event_topic_catalog.rs +++ b/contracts/predictify-hybrid/src/event_topic_catalog.rs @@ -26,6 +26,10 @@ pub fn get_event_topic_catalog(env: &Env) -> Vec { ("vote", "vote_cast", "Fired when a user casts a vote on a market"), ("place_bet", "bet_placed", "Fired when a bet is placed on a market"), ("cancel_bet", "bet_cancelled", "Fired when a bet is cancelled by the bettor"), + ("place_bets", "bet_batch_placed", "Fired when multiple bets are placed in a single batch"), + ("resolve_market_bets", "bet_status_updated", "Fired when a bet's status changes (won/lost/refunded/cancelled)"), + ("set_global_bet_limits", "bet_limits_updated", "Fired when global or per-event bet limits are updated"), + ("update_market_bet_stats", "bet_stats_updated", "Fired when per-market betting statistics are updated"), ("resolve_market", "market_resolved", "Fired on successful market resolution"), ("resolve_market_manual", "market_resolved_manual", "Fired on admin manual resolution"), ("force_resolve_market", "market_force_resolved", "Fired on forced resolution"), diff --git a/contracts/predictify-hybrid/src/events.rs b/contracts/predictify-hybrid/src/events.rs index d519406b..ffed8149 100644 --- a/contracts/predictify-hybrid/src/events.rs +++ b/contracts/predictify-hybrid/src/events.rs @@ -3663,3 +3663,209 @@ mod focused_dispute_tests { assert!(found, "DisputeOpenedEvent not found with correct topic structure"); } } + +#[cfg(test)] +mod focused_betting_events_tests { + use super::*; + use soroban_sdk::{testutils::{Address as _, Events}, Address, Env, IntoVal, Symbol, Vec}; + + #[test] + fn test_bet_batch_placed_event_struct() { + let env = Env::default(); + let bettor = Address::generate(&env); + let market1 = Symbol::new(&env, "mkt1"); + let market2 = Symbol::new(&env, "mkt2"); + let outcome1 = String::from_str(&env, "yes"); + let outcome2 = String::from_str(&env, "no"); + + let bets = vec![ + &env, + (market1, outcome1, 10_000_000i128), + (market2, outcome2, 5_000_000i128), + ]; + + let event = BetBatchPlacedEvent { + bettor: bettor.clone(), + bets: bets.clone(), + total_amount: 15_000_000, + bet_count: 2, + timestamp: 1000, + }; + + assert_eq!(event.bettor, bettor); + assert_eq!(event.bet_count, 2); + assert_eq!(event.total_amount, 15_000_000); + assert_eq!(event.bets.len(), 2); + } + + #[test] + fn test_bet_stats_updated_event_struct() { + let env = Env::default(); + let market_id = Symbol::new(&env, "btc_100k"); + + let event = BetStatsUpdatedEvent { + market_id: market_id.clone(), + total_bets: 42u32, + total_amount_locked: 1_000_000_000, + unique_bettors: 25u32, + timestamp: 2000, + }; + + assert_eq!(event.market_id, market_id); + assert_eq!(event.total_bets, 42u32); + assert_eq!(event.total_amount_locked, 1_000_000_000); + assert_eq!(event.unique_bettors, 25u32); + } + + #[test] + fn test_emit_bet_batch_placed_publishes_event() { + let env = Env::default(); + let contract_id = env.register(crate::PredictifyHybrid, ()); + + let bettor = Address::generate(&env); + let market1 = Symbol::new(&env, "mkt1"); + let market2 = Symbol::new(&env, "mkt2"); + let outcome1 = String::from_str(&env, "yes"); + let outcome2 = String::from_str(&env, "no"); + + let bets = vec![ + &env, + (market1, outcome1, 10_000_000i128), + (market2, outcome2, 5_000_000i128), + ]; + + env.as_contract(&contract_id, || { + EventEmitter::emit_bet_batch_placed(&env, &bettor, &bets, 15_000_000); + }); + + let events = env.events().all(); + let mut found = false; + for event in events.iter() { + if event.2.len() > 0 { + let topic0: Symbol = event.2.get(0).unwrap().try_into_val(&env).unwrap(); + if topic0 == symbol_short!("bet_batch") { + found = true; + } + } + } + assert!(found, "BetBatchPlacedEvent not found in published events"); + } + + #[test] + fn test_emit_bet_stats_updated_publishes_event() { + let env = Env::default(); + let contract_id = env.register(crate::PredictifyHybrid, ()); + + let market_id = Symbol::new(&env, "btc_100k"); + + env.as_contract(&contract_id, || { + EventEmitter::emit_bet_stats_updated(&env, &market_id, 42u32, 1_000_000_000, 25u32); + }); + + let events = env.events().all(); + let mut found = false; + for event in events.iter() { + if event.2.len() > 0 { + let topic0: Symbol = event.2.get(0).unwrap().try_into_val(&env).unwrap(); + if topic0 == symbol_short!("bet_stat") { + found = true; + } + } + } + assert!(found, "BetStatsUpdatedEvent not found in published events"); + } + + #[test] + fn test_emit_bet_batch_placed_stores_event() { + let env = Env::default(); + let contract_id = env.register(crate::PredictifyHybrid, ()); + + let bettor = Address::generate(&env); + let bets = vec![ + &env, + ( + Symbol::new(&env, "mkt1"), + String::from_str(&env, "yes"), + 10_000_000i128, + ), + ]; + + env.as_contract(&contract_id, || { + EventEmitter::emit_bet_batch_placed(&env, &bettor, &bets, 10_000_000); + let stored: Option = env + .storage() + .persistent() + .get(&symbol_short!("bet_batch")); + assert!(stored.is_some(), "BetBatchPlacedEvent should be stored"); + let stored_event = stored.unwrap(); + assert_eq!(stored_event.bettor, bettor); + assert_eq!(stored_event.bet_count, 1); + assert_eq!(stored_event.total_amount, 10_000_000); + }); + } + + #[test] + fn test_emit_bet_stats_updated_stores_event() { + let env = Env::default(); + let contract_id = env.register(crate::PredictifyHybrid, ()); + + let market_id = Symbol::new(&env, "btc_100k"); + + env.as_contract(&contract_id, || { + EventEmitter::emit_bet_stats_updated(&env, &market_id, 42u32, 1_000_000_000, 25u32); + let stored: Option = env + .storage() + .persistent() + .get(&symbol_short!("bet_stat")); + assert!(stored.is_some(), "BetStatsUpdatedEvent should be stored"); + let stored_event = stored.unwrap(); + assert_eq!(stored_event.total_bets, 42u32); + assert_eq!(stored_event.total_amount_locked, 1_000_000_000); + assert_eq!(stored_event.unique_bettors, 25u32); + }); + } + + #[test] + fn test_emit_bet_batch_placed_with_empty_bets() { + let env = Env::default(); + let contract_id = env.register(crate::PredictifyHybrid, ()); + + let bettor = Address::generate(&env); + let empty_bets: Vec<(Symbol, String, i128)> = Vec::new(&env); + + env.as_contract(&contract_id, || { + EventEmitter::emit_bet_batch_placed(&env, &bettor, &empty_bets, 0); + let events = env.events().all(); + let mut found = false; + for event in events.iter() { + if event.2.len() > 0 { + let topic0: Symbol = event.2.get(0).unwrap().try_into_val(&env).unwrap(); + if topic0 == symbol_short!("bet_batch") { + found = true; + } + } + } + assert!(found, "Batch event with empty bets should still be published"); + }); + } + + #[test] + fn test_emit_bet_stats_updated_zero_values() { + let env = Env::default(); + let contract_id = env.register(crate::PredictifyHybrid, ()); + + let market_id = Symbol::new(&env, "fresh_market"); + + env.as_contract(&contract_id, || { + EventEmitter::emit_bet_stats_updated(&env, &market_id, 0u32, 0, 0u32); + let stored: Option = env + .storage() + .persistent() + .get(&symbol_short!("bet_stat")); + assert!(stored.is_some(), "Bet stats event should be stored"); + let stored_event = stored.unwrap(); + assert_eq!(stored_event.total_bets, 0u32); + assert_eq!(stored_event.unique_bettors, 0u32); + }); + } +} diff --git a/scripts/check_wasm_size.sh b/scripts/check_wasm_size.sh index ab7fac09..c187bd4d 100755 --- a/scripts/check_wasm_size.sh +++ b/scripts/check_wasm_size.sh @@ -2,6 +2,9 @@ set -e + + + # Default budget: 768 KiB = 768 * 1024 = 786432 bytes BUDGET=${WASM_SIZE_BUDGET:-786432}