From 8f00924e5c1b182bf7945634f7717c2fbc7b8c71 Mon Sep 17 00:00:00 2001 From: Obiajulu-gif Date: Fri, 30 Jan 2026 03:54:39 +0100 Subject: [PATCH] feat: add bet deadline tests and coverage output files --- contracts/predictify-hybrid/src/test.rs | 778 ++++++++++++++++++++- docs/test-output/bet-deadline-coverage.txt | Bin 0 -> 8684 bytes docs/test-output/bet-deadline-tests.txt | Bin 0 -> 12672 bytes 3 files changed, 776 insertions(+), 2 deletions(-) create mode 100644 docs/test-output/bet-deadline-coverage.txt create mode 100644 docs/test-output/bet-deadline-tests.txt diff --git a/contracts/predictify-hybrid/src/test.rs b/contracts/predictify-hybrid/src/test.rs index 3de98dac..decac64a 100644 --- a/contracts/predictify-hybrid/src/test.rs +++ b/contracts/predictify-hybrid/src/test.rs @@ -17,7 +17,7 @@ #![cfg(test)] -use crate::events::PlatformFeeSetEvent; +use crate::events::{BetPlacedEvent, PlatformFeeSetEvent}; use super::*; use crate::markets::MarketUtils; @@ -3383,4 +3383,778 @@ fn test_initialize_valid_fee_bound() { }); assert_eq!(stored_fee, 10); } -} \ No newline at end of file +} + +// ===== BET DEADLINE TESTS ===== + +fn set_test_ledger_time(env: &Env, timestamp: u64) { + env.ledger().set(LedgerInfo { + timestamp, + protocol_version: 22, + sequence_number: env.ledger().sequence(), + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 1, + min_persistent_entry_ttl: 1, + max_entry_ttl: 10000, + }); +} + +fn load_market_from_storage(env: &Env, contract_id: &Address, market_id: &Symbol) -> Market { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .get::(market_id) + .unwrap() + }) +} + +fn store_market_to_storage(env: &Env, contract_id: &Address, market_id: &Symbol, market: &Market) { + env.as_contract(contract_id, || { + env.storage().persistent().set(market_id, market); + }); +} + +fn token_balance(env: &Env, token_id: &Address, owner: &Address) -> i128 { + let token_client = StellarAssetClient::new(env, token_id); + token_client.balance(owner) +} + +fn load_bet_placed_event(env: &Env, contract_id: &Address) -> BetPlacedEvent { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .get::(&Symbol::new(env, "bet_plc")) + .unwrap() + }) +} + +#[test] +fn test_bet_deadline_time_matrix() { + struct Scenario { + name: &'static str, + offset_seconds: i64, + expect_ok: bool, + } + + let scenarios = [ + Scenario { + name: "before_deadline", + offset_seconds: -1, + expect_ok: true, + }, + Scenario { + name: "at_deadline", + offset_seconds: 0, + expect_ok: false, + }, + Scenario { + name: "after_deadline", + offset_seconds: 1, + expect_ok: false, + }, + ]; + + for scenario in scenarios.iter() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let target_time = if scenario.offset_seconds < 0 { + market.end_time - (-scenario.offset_seconds as u64) + } else { + market.end_time + scenario.offset_seconds as u64 + }; + set_test_ledger_time(&test.env, target_time); + + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + let stats_before = client.get_market_bet_stats(&market_id); + let market_before = load_market_from_storage(&test.env, &test.contract_id, &market_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + if scenario.expect_ok { + assert!(result.is_ok(), "scenario {}", scenario.name); + let bet = result.unwrap(); + assert_eq!(bet.user, user); + assert_eq!(bet.market_id, market_id); + assert_eq!(bet.outcome, outcome); + assert_eq!(bet.amount, bet_amount); + assert_eq!(bet.status, BetStatus::Active); + + assert!(client.get_bet(&market_id, &user).is_some()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets + 1); + assert_eq!( + stats_after.total_amount_locked, + stats_before.total_amount_locked + bet_amount + ); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_before - bet_amount, user_balance_after); + assert_eq!(contract_balance_before + bet_amount, contract_balance_after); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.total_staked, market_before.total_staked + bet_amount); + assert_eq!(market_after.votes.len(), market_before.votes.len() + 1); + + let bet_event = load_bet_placed_event(&test.env, &test.contract_id); + assert_eq!(bet_event.market_id, market_id); + assert_eq!(bet_event.bettor, user); + assert_eq!(bet_event.outcome, outcome); + assert_eq!(bet_event.amount, bet_amount); + assert_eq!(bet_event.timestamp, test.env.ledger().timestamp()); + } else { + assert_eq!( + result, + Err(Ok(Error::MarketClosed)), + "scenario {}", + scenario.name + ); + assert_eq!(Error::MarketClosed.description(), "Market is closed"); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + assert_eq!(stats_after.unique_bettors, stats_before.unique_bettors); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.total_staked, market_before.total_staked); + assert_eq!(market_after.votes.len(), market_before.votes.len()); + assert_eq!(market_after.state, market_before.state); + } + } +} + +#[test] +fn test_bet_rejected_when_market_state_ended_before_deadline() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + market.state = MarketState::Ended; + let end_time = market.end_time; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, end_time - 1); + + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + let stats_before = client.get_market_bet_stats(&market_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert_eq!(Error::MarketClosed.description(), "Market is closed"); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.state, MarketState::Ended); + assert_eq!(market_after.total_staked, market.total_staked); +} + +#[test] +fn test_bet_rejected_when_market_resolved_before_deadline() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + market.winning_outcome = Some(outcome.clone()); + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, market.end_time - 1); + + let stats_before = client.get_market_bet_stats(&market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketAlreadyResolved))); + assert_eq!( + Error::MarketAlreadyResolved.description(), + "Market is already resolved" + ); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); +} + +#[test] +fn test_bet_rejected_with_epoch_zero_deadline() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + market.end_time = 0; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, 1); + + let stats_before = client.get_market_bet_stats(&market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); +} + +#[test] +fn test_bet_accepts_with_far_future_deadline() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + market.end_time = u64::MAX - 10; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, 1); + + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + assert!(result.is_ok()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, 1); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_before - bet_amount, user_balance_after); + assert_eq!(contract_balance_before + bet_amount, contract_balance_after); +} + +#[test] +fn test_bet_rejected_after_time_progression_post_read() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let observed_end_time = market.end_time; + + set_test_ledger_time(&test.env, observed_end_time - 1); + let _ = client.get_market(&market_id).unwrap(); + + set_test_ledger_time(&test.env, observed_end_time + 1); + + let stats_before = client.get_market_bet_stats(&market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); +} + +#[test] +fn test_bet_allows_after_deadline_extension() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let original_end_time = market.end_time; + + test.env.mock_all_auths(); + let extend_result = client.try_extend_deadline( + &test.admin, + &market_id, + &1u32, + &String::from_str(&test.env, "extend for testing"), + ); + assert!(extend_result.is_ok()); + + let updated_market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(updated_market.end_time, original_end_time + 24 * 60 * 60); + + set_test_ledger_time(&test.env, original_end_time + 1); + + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + assert!(result.is_ok()); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_before - bet_amount, user_balance_after); + assert_eq!(contract_balance_before + bet_amount, contract_balance_after); +} + +#[test] +fn test_bet_rejects_after_manual_earlier_deadline_override() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let original_end_time = market.end_time; + market.end_time = original_end_time - 10; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, original_end_time - 5); + + let stats_before = client.get_market_bet_stats(&market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); +} + +#[test] +fn test_bet_deadline_per_market_enforced_independently() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let early_market_id = test.create_test_market(); + let late_market_id = test.create_test_market(); + + let base_time = test.env.ledger().timestamp(); + + let mut early_market = load_market_from_storage(&test.env, &test.contract_id, &early_market_id); + early_market.end_time = base_time + 10; + store_market_to_storage(&test.env, &test.contract_id, &early_market_id, &early_market); + + let mut late_market = load_market_from_storage(&test.env, &test.contract_id, &late_market_id); + late_market.end_time = base_time + 20; + store_market_to_storage(&test.env, &test.contract_id, &late_market_id, &late_market); + + set_test_ledger_time(&test.env, base_time + 15); + + let user_early = test.create_funded_user(); + let user_late = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let stats_before_early = client.get_market_bet_stats(&early_market_id); + let market_before_early = + load_market_from_storage(&test.env, &test.contract_id, &early_market_id); + let user_balance_before_early = + token_balance(&test.env, &test.token_test.token_id, &user_early); + let contract_balance_before_early = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result_early = client.try_place_bet(&user_early, &early_market_id, &outcome, &bet_amount); + + assert_eq!(result_early, Err(Ok(Error::MarketClosed))); + assert_eq!(Error::MarketClosed.code(), "MARKET_CLOSED"); + assert_eq!(Error::MarketClosed.description(), "Market is closed"); + assert!(client.get_bet(&early_market_id, &user_early).is_none()); + + let stats_after_early = client.get_market_bet_stats(&early_market_id); + assert_eq!(stats_after_early.total_bets, stats_before_early.total_bets); + assert_eq!( + stats_after_early.total_amount_locked, + stats_before_early.total_amount_locked + ); + assert_eq!( + stats_after_early.unique_bettors, + stats_before_early.unique_bettors + ); + + let user_balance_after_early = + token_balance(&test.env, &test.token_test.token_id, &user_early); + let contract_balance_after_early = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after_early, user_balance_before_early); + assert_eq!(contract_balance_after_early, contract_balance_before_early); + + let market_after_early = + load_market_from_storage(&test.env, &test.contract_id, &early_market_id); + assert_eq!(market_after_early.total_staked, market_before_early.total_staked); + assert_eq!(market_after_early.votes.len(), market_before_early.votes.len()); + assert_eq!(market_after_early.state, market_before_early.state); + + let stats_before_late = client.get_market_bet_stats(&late_market_id); + let market_before_late = + load_market_from_storage(&test.env, &test.contract_id, &late_market_id); + let user_balance_before_late = + token_balance(&test.env, &test.token_test.token_id, &user_late); + let contract_balance_before_late = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result_late = client.try_place_bet(&user_late, &late_market_id, &outcome, &bet_amount); + assert!(result_late.is_ok()); + + let bet = result_late.unwrap(); + assert_eq!(bet.user, user_late); + assert_eq!(bet.market_id, late_market_id); + assert_eq!(bet.outcome, outcome); + assert_eq!(bet.amount, bet_amount); + assert_eq!(bet.status, BetStatus::Active); + + assert!(client.get_bet(&late_market_id, &user_late).is_some()); + + let stats_after_late = client.get_market_bet_stats(&late_market_id); + assert_eq!(stats_after_late.total_bets, stats_before_late.total_bets + 1); + assert_eq!( + stats_after_late.total_amount_locked, + stats_before_late.total_amount_locked + bet_amount + ); + + let user_balance_after_late = + token_balance(&test.env, &test.token_test.token_id, &user_late); + let contract_balance_after_late = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_before_late - bet_amount, user_balance_after_late); + assert_eq!( + contract_balance_before_late + bet_amount, + contract_balance_after_late + ); + + let market_after_late = + load_market_from_storage(&test.env, &test.contract_id, &late_market_id); + assert_eq!(market_after_late.total_staked, market_before_late.total_staked + bet_amount); + assert_eq!(market_after_late.votes.len(), market_before_late.votes.len() + 1); + + let bet_event = load_bet_placed_event(&test.env, &test.contract_id); + assert_eq!(bet_event.market_id, late_market_id); + assert_eq!(bet_event.bettor, user_late); + assert_eq!(bet_event.outcome, outcome); + assert_eq!(bet_event.amount, bet_amount); + assert_eq!(bet_event.timestamp, test.env.ledger().timestamp()); +} + +#[test] +fn test_bet_rejects_at_exact_end_time_boundary() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + set_test_ledger_time(&test.env, market.end_time); + + let stats_before = client.get_market_bet_stats(&market_id); + let market_before = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert_eq!(Error::MarketClosed.code(), "MARKET_CLOSED"); + assert_eq!(Error::MarketClosed.description(), "Market is closed"); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + assert_eq!(stats_after.unique_bettors, stats_before.unique_bettors); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.total_staked, market_before.total_staked); + assert_eq!(market_after.votes.len(), market_before.votes.len()); + assert_eq!(market_after.state, market_before.state); +} + +#[test] +fn test_bet_rejects_when_end_time_passed_even_if_state_active() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + set_test_ledger_time(&test.env, 100); + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + market.end_time = 50; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + let stats_before = client.get_market_bet_stats(&market_id); + let market_before = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert_eq!(Error::MarketClosed.code(), "MARKET_CLOSED"); + assert_eq!(Error::MarketClosed.description(), "Market is closed"); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + assert_eq!(stats_after.unique_bettors, stats_before.unique_bettors); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.total_staked, market_before.total_staked); + assert_eq!(market_after.votes.len(), market_before.votes.len()); + assert_eq!(market_after.state, market_before.state); +} + +#[test] +fn test_bet_rejects_when_market_cancelled_or_closed_even_before_end_time() { + let states = [MarketState::Cancelled, MarketState::Closed]; + + for state in states.iter() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let end_time = market.end_time; + market.state = *state; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, end_time - 1); + + let stats_before = client.get_market_bet_stats(&market_id); + let market_before = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert_eq!(Error::MarketClosed.code(), "MARKET_CLOSED"); + assert_eq!(Error::MarketClosed.description(), "Market is closed"); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!( + stats_after.total_amount_locked, + stats_before.total_amount_locked + ); + assert_eq!(stats_after.unique_bettors, stats_before.unique_bettors); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.total_staked, market_before.total_staked); + assert_eq!(market_after.votes.len(), market_before.votes.len()); + assert_eq!(market_after.state, *state); + } +} + +#[test] +fn test_bet_rejects_at_epoch_zero_boundary() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + market.end_time = 0; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, 0); + + let stats_before = client.get_market_bet_stats(&market_id); + let market_before = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert_eq!(Error::MarketClosed.code(), "MARKET_CLOSED"); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.total_staked, market_before.total_staked); + assert_eq!(market_after.votes.len(), market_before.votes.len()); +} + +#[test] +fn test_bet_rejects_at_far_future_deadline_boundary() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + let market_id = test.create_test_market(); + let user = test.create_funded_user(); + let outcome = String::from_str(&test.env, "yes"); + let bet_amount: i128 = 10_000_000; + + let mut market = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let far_future_deadline = u64::MAX - 10; + market.end_time = far_future_deadline; + store_market_to_storage(&test.env, &test.contract_id, &market_id, &market); + + set_test_ledger_time(&test.env, far_future_deadline); + + let stats_before = client.get_market_bet_stats(&market_id); + let market_before = load_market_from_storage(&test.env, &test.contract_id, &market_id); + let user_balance_before = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_before = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + + test.env.mock_all_auths(); + let result = client.try_place_bet(&user, &market_id, &outcome, &bet_amount); + + assert_eq!(result, Err(Ok(Error::MarketClosed))); + assert_eq!(Error::MarketClosed.code(), "MARKET_CLOSED"); + assert_eq!(Error::MarketClosed.description(), "Market is closed"); + assert!(client.get_bet(&market_id, &user).is_none()); + + let stats_after = client.get_market_bet_stats(&market_id); + assert_eq!(stats_after.total_bets, stats_before.total_bets); + assert_eq!(stats_after.total_amount_locked, stats_before.total_amount_locked); + + let user_balance_after = token_balance(&test.env, &test.token_test.token_id, &user); + let contract_balance_after = + token_balance(&test.env, &test.token_test.token_id, &test.contract_id); + assert_eq!(user_balance_after, user_balance_before); + assert_eq!(contract_balance_after, contract_balance_before); + + let market_after = load_market_from_storage(&test.env, &test.contract_id, &market_id); + assert_eq!(market_after.total_staked, market_before.total_staked); + assert_eq!(market_after.votes.len(), market_before.votes.len()); +} diff --git a/docs/test-output/bet-deadline-coverage.txt b/docs/test-output/bet-deadline-coverage.txt new file mode 100644 index 0000000000000000000000000000000000000000..1c0c217c4793ee352f69eb4e177d37d185b92e4a GIT binary patch literal 8684 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