Skip to content
Merged
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
19 changes: 18 additions & 1 deletion contracts/predictify-hybrid/src/bets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(())
}

Expand Down
4 changes: 4 additions & 0 deletions contracts/predictify-hybrid/src/event_topic_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub fn get_event_topic_catalog(env: &Env) -> Vec<EventTopicEntry> {
("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"),
Expand Down
206 changes: 206 additions & 0 deletions contracts/predictify-hybrid/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BetBatchPlacedEvent> = 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<BetStatsUpdatedEvent> = 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<BetStatsUpdatedEvent> = 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);
});
}
}
3 changes: 3 additions & 0 deletions scripts/check_wasm_size.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
set -e





# Default budget: 768 KiB = 768 * 1024 = 786432 bytes
BUDGET=${WASM_SIZE_BUDGET:-786432}

Expand Down
Loading