diff --git a/contracts/creator-event-manager/src/leaderboard.rs b/contracts/creator-event-manager/src/leaderboard.rs new file mode 100644 index 00000000..8258fd6b --- /dev/null +++ b/contracts/creator-event-manager/src/leaderboard.rs @@ -0,0 +1,173 @@ +//! Ranked event leaderboard computation. +//! +//! This module provides the core leaderboard functionality for events, ranking +//! participants by total points with deterministic tie-breaking. The leaderboard +//! is computed on-demand (live) and can be called before all matches are resolved, +//! with unresolved matches contributing 0 points. + +use soroban_sdk::{Env, Vec}; + +use crate::event::{self, EventError}; +use crate::storage; +use crate::storage_types::LeaderboardEntry; + +// --------------------------------------------------------------------------- +// Error type +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum LeaderboardError { + /// No event found for the given event_id. + EventNotFound = 1, + /// Arithmetic overflow during calculation. + Overflow = 2, +} + +impl From for LeaderboardError { + fn from(_: EventError) -> Self { + LeaderboardError::EventNotFound + } +} + +// --------------------------------------------------------------------------- +// get_event_leaderboard (#967) +// --------------------------------------------------------------------------- + +/// Retrieve a ranked leaderboard for an event, sorted by total points. +/// +/// This function computes a live leaderboard based on all participants' total +/// points earned from predictions. The leaderboard is available before all +/// matches are resolved; predictions for unresolved matches contribute 0 points. +/// +/// # Ranking Rules (all in order): +/// 1. **Higher total_points** — primary sort key (descending). +/// 2. **Higher exact_scores** — tiebreaker (descending). +/// 3. **Earlier last_prediction_time** — tiebreaker (ascending). +/// 4. **Address byte comparison** — final deterministic tiebreaker. +/// +/// # Flow: +/// 1. Verify the event exists. +/// 2. Retrieve all participants for the event. +/// 3. For each participant: +/// - Sum `points_earned` from all their predictions → `total_points`. +/// - Count predictions where `is_correct == Some(true)` → `correct_results`. +/// - Count predictions where `points_earned == Some(4)` → `exact_scores`. +/// - Count total predictions submitted → `matches_played`. +/// - Find max `predicted_at` → `last_prediction_time`. +/// 4. Sort entries by the ranking rules above. +/// 5. Assign rank 1..N in sorted order. +/// 6. Return the sorted leaderboard. +/// +/// # Returns +/// A `Vec` sorted by total points descending, with all +/// tiebreakers applied and ranks assigned. Returns an empty `Vec` if the +/// event has no participants. +/// +/// # Errors +/// * [`LeaderboardError::EventNotFound`] — no event with the given event_id. +/// * [`LeaderboardError::Overflow`] — arithmetic overflow during calculation. +pub fn get_event_leaderboard( + env: &Env, + event_id: u64, +) -> Result, LeaderboardError> { + // 1. Verify event exists + let _event = event::get_event(env, event_id)?; + + // 2. Retrieve all participants + let participants = storage::get_event_participants(env, event_id); + + // 3. Build leaderboard entries + let mut entries: Vec = Vec::new(env); + + for participant in participants.iter() { + let user_predictions = storage::get_user_predictions(env, &participant, event_id); + + let mut total_points: u32 = 0; + let mut correct_results: u32 = 0; + let mut exact_scores: u32 = 0; + let mut last_prediction_time: u64 = 0; + + // Calculate stats from all predictions + for prediction_id in user_predictions.iter() { + if let Ok(prediction) = storage::get_prediction(env, prediction_id) { + // Add earned points (None counts as 0) + if let Some(points) = prediction.points_earned { + total_points = + total_points.checked_add(points).ok_or(LeaderboardError::Overflow)?; + } + + // Count correct results + if prediction.is_correct == Some(true) { + correct_results = correct_results + .checked_add(1) + .ok_or(LeaderboardError::Overflow)?; + } + + // Count exact scores (4 points means exact score) + if prediction.points_earned == Some( + crate::storage_types::POINTS_CORRECT_RESULT + + crate::storage_types::POINTS_EXACT_SCORE, + ) { + exact_scores = + exact_scores.checked_add(1).ok_or(LeaderboardError::Overflow)?; + } + + // Track latest prediction time + if prediction.predicted_at > last_prediction_time { + last_prediction_time = prediction.predicted_at; + } + } + } + + // Create entry (rank will be assigned after sorting) + let matches_played = user_predictions.len(); + let entry = LeaderboardEntry::new( + participant.clone(), + event_id, + total_points, + correct_results, + exact_scores, + matches_played, + last_prediction_time, + ); + entries.push_back(entry); + } + + // 4. Sort entries using insertion sort (stable and suitable for small lists) + let len = entries.len(); + for i in 1..len { + let mut j = i; + while j > 0 { + let prev = entries.get(j - 1).unwrap(); + let curr = entries.get(j).unwrap(); + if prev.outranks(&curr) { + // prev ranks higher, no swap needed + break; + } else { + // curr ranks higher, swap + entries.set(j - 1, curr); + entries.set(j, prev); + j -= 1; + } + } + } + + // 5. Assign ranks (1-based) + for i in 0..len { + let mut entry = entries.get(i).unwrap(); + entry.rank = (i as u32) + 1; + entries.set(i, entry); + } + + Ok(entries) +} + +#[cfg(test)] +mod tests { + #[allow(unused_imports)] + use super::*; + + // Note: Unit tests for leaderboard functions require Soroban contract context. + // Integration tests are provided in tests/ directory. +} diff --git a/contracts/creator-event-manager/src/lib.rs b/contracts/creator-event-manager/src/lib.rs index 46080da7..baa5725c 100644 --- a/contracts/creator-event-manager/src/lib.rs +++ b/contracts/creator-event-manager/src/lib.rs @@ -4,6 +4,7 @@ pub mod admin; mod event; mod fee; mod invite; +mod leaderboard; pub mod r#match; mod oracle; pub mod prediction; @@ -18,7 +19,7 @@ use soroban_sdk::{contract, contractimpl, Address, Env, String, Symbol, Vec}; use admin::AdminError; use event::EventError; use r#match::MatchError; -use storage_types::{Event, Match, Prediction, Winner}; +use storage_types::{Event, Match, Prediction, LeaderboardEntry}; use verification::VerificationError; use views::{EventStatistics, PlatformStatistics}; @@ -549,43 +550,6 @@ impl CreatorEventManagerContract { } } - /// Verify and record all perfect scorers for an event. - /// - /// After all matches in an event are resolved, calculate which users - /// predicted all matches correctly and store them as winners. - /// - /// # Panics - /// * `"contract_paused"` — contract is paused. - /// * `"event_not_found"` — no event exists with the given ID. - /// * `"event_cancelled"` — event has been cancelled. - /// * `"matches_not_complete"` — not all matches have been resolved. - pub fn verify_event_winners(env: Env, caller: Address, event_id: u64) -> u32 { - match oracle::verify_event_winners(&env, caller, event_id) { - Ok(count) => count, - Err(oracle::OracleError::Paused) => panic!("contract_paused"), - Err(oracle::OracleError::EventNotFound) => panic!("event_not_found"), - Err(oracle::OracleError::EventCancelled) => panic!("event_cancelled"), - Err(oracle::OracleError::MatchesNotComplete) => panic!("matches_not_complete"), - Err(oracle::OracleError::Overflow) => panic!("overflow"), - Err(_) => panic!("unexpected_error"), - } - } - - /// Retrieve the list of winners for an event. - /// - /// Public view function to retrieve the list of winners for an event. - /// Used for leaderboards and rewards. - /// - /// # Panics - /// * `"event_not_found"` — no event exists with the given ID. - pub fn get_event_winners(env: Env, event_id: u64) -> Vec { - match oracle::get_event_winners(&env, event_id) { - Ok(winners) => winners, - Err(oracle::OracleError::EventNotFound) => panic!("event_not_found"), - Err(_) => panic!("unexpected_error"), - } - } - /// Calculate a user's score and statistics for an event. /// /// Returns a tuple `(total_points, correct_results, exact_scores, total_matches)` where: @@ -607,6 +571,33 @@ impl CreatorEventManagerContract { } } + /// Get a ranked leaderboard for an event (#967). + /// + /// Returns all event participants ranked by total points earned from their + /// predictions. The leaderboard is computed live and can be called before + /// all matches are resolved; unresolved matches contribute 0 points. + /// + /// Ranking tiebreakers (in order): + /// 1. Higher total_points + /// 2. Higher exact_scores + /// 3. Earlier last_prediction_time (commits reward) + /// 4. Address byte comparison (deterministic final tiebreaker) + /// + /// Returns a `Vec` sorted by rank ascending (rank 1 first). + /// Each entry includes the participant's address, total points, correct results + /// count, exact scores count, and their assigned rank. + /// + /// # Panics + /// * `"event_not_found"` — no event exists with the given ID. + /// * `"overflow"` — arithmetic overflow during calculation. + pub fn get_event_leaderboard(env: Env, event_id: u64) -> Vec { + match leaderboard::get_event_leaderboard(&env, event_id) { + Ok(entries) => entries, + Err(leaderboard::LeaderboardError::EventNotFound) => panic!("event_not_found"), + Err(leaderboard::LeaderboardError::Overflow) => panic!("overflow"), + } + } + /// Get platform-wide statistics. /// /// Returns aggregated statistics including total events, matches, diff --git a/contracts/creator-event-manager/src/oracle.rs b/contracts/creator-event-manager/src/oracle.rs index 2dcccf14..43fd69ad 100644 --- a/contracts/creator-event-manager/src/oracle.rs +++ b/contracts/creator-event-manager/src/oracle.rs @@ -1,8 +1,8 @@ -use soroban_sdk::{Address, Env, Symbol, Vec}; +use soroban_sdk::{Address, Env, Symbol}; use crate::admin; use crate::storage::{self, StorageError}; -use crate::storage_types::{DataKey, Event, Match, MatchResult, Winner}; +use crate::storage_types::{Event, Match, MatchResult}; // --------------------------------------------------------------------------- // Error type @@ -16,8 +16,10 @@ pub enum OracleError { /// No event found for the given event_id. EventNotFound = 2, /// Event has been cancelled and cannot be processed. + #[allow(dead_code)] EventCancelled = 3, /// Not all matches in the event have been resolved yet. + #[allow(dead_code)] MatchesNotComplete = 4, /// No creation fee has been set (should not happen after init). #[allow(dead_code)] @@ -44,16 +46,6 @@ impl From for OracleError { // Event emission // --------------------------------------------------------------------------- -fn emit_winners_verified(env: &Env, event_id: u64, winner_count: u32) { - env.events().publish( - ( - Symbol::new(env, "event"), - Symbol::new(env, "winners_verified"), - ), - (event_id, winner_count), - ); -} - fn emit_match_result_submitted( env: &Env, match_id: u64, @@ -162,161 +154,6 @@ pub fn submit_match_result( Ok(()) } -// --------------------------------------------------------------------------- -// verify_event_winners (#798) -// --------------------------------------------------------------------------- - -/// Verify and record all perfect scorers for an event. -/// -/// # Flow -/// 1. Require caller authorization (public function, anyone can call). -/// 2. Reject if the contract is paused. -/// 3. Retrieve the event and verify it exists. -/// 4. Verify the event is active (not cancelled). -/// 5. Retrieve all matches for the event. -/// 6. Verify all matches have results submitted (is_resolved == true). -/// 7. Retrieve all event participants. -/// 8. For each participant: -/// - Get their predictions for the event. -/// - Count correct predictions. -/// - If correct_count == total_matches, add to winners list. -/// 9. Create Winner structs for perfect scorers. -/// 10. Store winners in EventWinners(event_id). -/// 11. Emit WinnersVerified event with winner count. -/// 12. Return winner count. -/// -/// # Returns -/// The number of winners identified (u32). -pub fn verify_event_winners(env: &Env, caller: Address, event_id: u64) -> Result { - caller.require_auth(); - - if admin::is_paused(env) { - return Err(OracleError::Paused); - } - - // Retrieve event and verify it exists - let event: Event = storage::get_event(env, event_id)?; - - // Verify event is active (not cancelled) - if !event.is_active || event.is_cancelled { - return Err(OracleError::EventCancelled); - } - - // Retrieve all matches for the event - let match_ids = storage::get_event_matches(env, event_id); - let total_matches = event.match_count; - - // Verify all matches have results submitted - for match_id in match_ids.iter() { - let m: Match = storage::get_match(env, match_id)?; - if !m.result_submitted { - return Err(OracleError::MatchesNotComplete); - } - } - - // Retrieve all event participants - let participants = storage::get_event_participants(env, event_id); - - let mut winners: Vec = Vec::new(env); - let now = env.ledger().timestamp(); - - // For each participant, check if they predicted all matches correctly - for participant in participants.iter() { - let user_predictions = storage::get_user_predictions(env, &participant, event_id); - - // Count correct predictions - let mut correct_count: u32 = 0; - let mut last_prediction_time: u64 = 0; - - for prediction_id in user_predictions.iter() { - if let Ok(prediction) = storage::get_prediction(env, prediction_id) { - // Grade the prediction against the match result - let m: Match = storage::get_match(env, prediction.match_id)?; - if let Some(actual_winner) = m.winning_team { - let is_correct = prediction_outcome_matches( - env, - &prediction.predicted_outcome, - actual_winner, - ); - if is_correct { - correct_count = - correct_count.checked_add(1).ok_or(OracleError::Overflow)?; - } - } - // Track the latest prediction time for tiebreaker - if prediction.predicted_at > last_prediction_time { - last_prediction_time = prediction.predicted_at; - } - } - } - - // If correct_count == total_matches, add to winners list - if correct_count == total_matches && total_matches > 0 { - let winner = Winner::new( - participant.clone(), - event_id, - correct_count, - total_matches, - last_prediction_time, - now, - ); - winners.push_back(winner); - } - } - - #[allow(clippy::unnecessary_cast)] - let winner_count = winners.len() as u32; - - // Store winners in EventWinners(event_id) - let winners_key = DataKey::EventWinners(event_id); - env.storage().persistent().set(&winners_key, &winners); - env.storage() - .persistent() - .extend_ttl(&winners_key, storage::TTL_LEDGERS, storage::TTL_LEDGERS); - - // Emit WinnersVerified event - emit_winners_verified(env, event_id, winner_count); - - Ok(winner_count) -} - -// --------------------------------------------------------------------------- -// get_event_winners (#799) -// --------------------------------------------------------------------------- - -/// Retrieve the list of winners for an event. -/// -/// # Flow -/// 1. Retrieve EventWinners(event_id) from storage. -/// 2. Return Vec sorted by completion_time (earliest first). -/// 3. Return empty Vec if no winners exist. -/// -/// # Returns -/// A `Vec` sorted by completion_time ascending (earliest first). -pub fn get_event_winners(env: &Env, event_id: u64) -> Result, OracleError> { - let mut winners = storage::get_event_winners(env, event_id); - - // Sort by completion_time ascending (earliest first) - // Using insertion sort since the list is typically small - let len = winners.len(); - for i in 1..len { - let mut j = i; - while j > 0 { - let prev = winners.get(j - 1).unwrap(); - let curr = winners.get(j).unwrap(); - if prev.completion_time > curr.completion_time { - winners.set(j - 1, curr); - winners.set(j, prev); - j -= 1; - } else { - break; - } - } - } - - Ok(winners) -} - // --------------------------------------------------------------------------- // get_user_score (#800) // --------------------------------------------------------------------------- @@ -398,25 +235,6 @@ pub fn get_creation_fee(env: &Env) -> Result { // Helper functions // --------------------------------------------------------------------------- -/// Check if a predicted outcome matches the actual match result. -/// -/// The actual_winner encoding is: -/// - 0 = Team A -/// - 1 = Team B -/// - 2 = Draw -fn prediction_outcome_matches(env: &Env, predicted_outcome: &Symbol, actual_winner: u32) -> bool { - let team_a_sym = Symbol::new(env, crate::storage_types::OUTCOME_TEAM_A); - let team_b_sym = Symbol::new(env, crate::storage_types::OUTCOME_TEAM_B); - let draw_sym = Symbol::new(env, crate::storage_types::OUTCOME_DRAW); - - match actual_winner { - 0 => *predicted_outcome == team_a_sym, - 1 => *predicted_outcome == team_b_sym, - 2 => *predicted_outcome == draw_sym, - _ => false, - } -} - #[cfg(test)] mod tests { #[allow(unused_imports)] diff --git a/contracts/creator-event-manager/src/storage.rs b/contracts/creator-event-manager/src/storage.rs index dbb1e952..b5530dc2 100644 --- a/contracts/creator-event-manager/src/storage.rs +++ b/contracts/creator-event-manager/src/storage.rs @@ -8,7 +8,7 @@ /// the returned ID immediately. use soroban_sdk::{Address, Env, Vec}; -use crate::storage_types::{DataKey, Event, Match, Prediction, Winner}; +use crate::storage_types::{DataKey, Event, Match, Prediction}; // --------------------------------------------------------------------------- // TTL constant @@ -261,28 +261,3 @@ pub fn add_event_participant(env: &Env, event_id: u64, participant: &Address) { .persistent() .extend_ttl(&key, TTL_LEDGERS, TTL_LEDGERS); } - -/// Return the list of verified winners for an event. -pub fn get_event_winners(env: &Env, event_id: u64) -> Vec { - let key = DataKey::EventWinners(event_id); - match env.storage().persistent().get::>(&key) { - Some(list) => { - env.storage() - .persistent() - .extend_ttl(&key, TTL_LEDGERS, TTL_LEDGERS); - list - } - None => Vec::new(env), - } -} - -/// Append a winner to the event's winners list. -pub fn add_event_winner(env: &Env, event_id: u64, winner: &Winner) { - let key = DataKey::EventWinners(event_id); - let mut list = get_event_winners(env, event_id); - list.push_back(winner.clone()); - env.storage().persistent().set(&key, &list); - env.storage() - .persistent() - .extend_ttl(&key, TTL_LEDGERS, TTL_LEDGERS); -} diff --git a/contracts/creator-event-manager/src/storage_types.rs b/contracts/creator-event-manager/src/storage_types.rs index d86aa42c..458156e6 100644 --- a/contracts/creator-event-manager/src/storage_types.rs +++ b/contracts/creator-event-manager/src/storage_types.rs @@ -147,9 +147,6 @@ pub enum DataKey { /// Vec
of participants for an event (event_id) EventParticipants(u64), - /// Vec of verified winners for an event (event_id) - EventWinners(u64), - // ── Initialization sentinel ────────────────────────────────────────────── /// Set to `true` once `initialize` has been called; prevents re-init. Initialized, @@ -678,75 +675,94 @@ impl Prediction { } // --------------------------------------------------------------------------- -// Winner +// LeaderboardEntry // --------------------------------------------------------------------------- -/// Records a user who correctly predicted matches in an event. +/// Ranked leaderboard entry for an event participant. +/// +/// Represents a user's performance in an event with full ranking information +/// and deterministic tie-breaking. This replaces the binary Winner model to +/// support top-N prize splits and flexible reward distributions. /// -/// Stored inside the `Vec` at `DataKey::EventWinners(event_id)`. -/// Used for leaderboard ranking and reward distribution. +/// Stored in Vec (typically temporary, computed on-demand). #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Winner { - /// Address of the winning predictor +pub struct LeaderboardEntry { + /// Address of the participant pub user: Address, - /// Event they participated in + /// Event identifier pub event_id: u64, - /// How many matches they predicted correctly - pub total_correct: u32, + /// Total points earned from all predictions (0, 1, or 4 per match) + pub total_points: u32, + + /// Number of predictions with correct 1X2 result + pub correct_results: u32, - /// Total number of matches in the event (denominator for accuracy) - pub total_matches: u32, + /// Number of predictions with exact scoreline (4-point predictions) + pub exact_scores: u32, - /// Unix timestamp when the user submitted their last prediction - /// (used as tiebreaker — earlier completion ranks higher) - pub completion_time: u64, + /// Total number of predictions this user submitted for the event + pub matches_played: u32, - /// Unix timestamp when winner status was verified on-chain - pub verified_at: u64, + /// Unix timestamp of this user's most recent prediction + /// (used as tiebreaker — earlier submission = higher rank) + pub last_prediction_time: u64, + + /// 1-based rank after sorting (1 is the top-ranked participant). + /// Set by `get_event_leaderboard` after sorting all entries. + pub rank: u32, } -impl Winner { - /// Construct a new verified winner record. +impl LeaderboardEntry { + /// Construct a new leaderboard entry (rank will be assigned later). pub fn new( user: Address, event_id: u64, - total_correct: u32, - total_matches: u32, - completion_time: u64, - verified_at: u64, + total_points: u32, + correct_results: u32, + exact_scores: u32, + matches_played: u32, + last_prediction_time: u64, ) -> Self { Self { user, event_id, - total_correct, - total_matches, - completion_time, - verified_at, + total_points, + correct_results, + exact_scores, + matches_played, + last_prediction_time, + rank: 0, // Will be assigned during leaderboard finalization } } - /// Returns accuracy as an integer percentage in the range [0, 100]. + /// Returns `true` if this entry outranks `other` according to the tiebreaker rules. /// - /// Returns 0 when `total_matches` is 0 to avoid division by zero. - pub fn get_accuracy_percentage(&self) -> u32 { - if self.total_matches == 0 { - return 0; + /// Sort order (all descending except last_prediction_time): + /// 1. Higher `total_points` wins + /// 2. On tie: Higher `exact_scores` wins + /// 3. On tie: Earlier `last_prediction_time` wins (lower timestamp = better rank) + /// 4. On tie: Compare addresses (deterministic final tiebreaker) + pub fn outranks(&self, other: &LeaderboardEntry) -> bool { + // Primary: higher total_points + if self.total_points != other.total_points { + return self.total_points > other.total_points; } - (self.total_correct * 100) / self.total_matches - } - /// Returns `true` if this winner outranks `other` for leaderboard purposes. - /// - /// Primary sort: higher `total_correct` wins. - /// Tiebreaker: earlier `completion_time` wins (submitted predictions sooner). - pub fn outranks(&self, other: &Winner) -> bool { - if self.total_correct != other.total_correct { - return self.total_correct > other.total_correct; + // Secondary: higher exact_scores + if self.exact_scores != other.exact_scores { + return self.exact_scores > other.exact_scores; + } + + // Tertiary: earlier last_prediction_time (lower = better) + if self.last_prediction_time != other.last_prediction_time { + return self.last_prediction_time < other.last_prediction_time; } - // Earlier completion time is better (lower value = higher rank) - self.completion_time < other.completion_time + + // Final tiebreaker: address comparison (deterministic) + // Compare the addresses directly; Soroban Address implements Ord + self.user < other.user } } diff --git a/contracts/creator-event-manager/src/views.rs b/contracts/creator-event-manager/src/views.rs index 568b1b43..77581beb 100644 --- a/contracts/creator-event-manager/src/views.rs +++ b/contracts/creator-event-manager/src/views.rs @@ -19,9 +19,6 @@ use soroban_sdk::{contracttype, Address, Env, Vec}; /// * `total_predictions` — total predictions linked to all event matches. /// * `all_matches_resolved` — `true` only when the event has at least one /// match and every stored match has a submitted result. -/// * `winners_verified` — `true` when one or more verified winner records are -/// stored for the event. -/// * `winner_count` — number of verified winner records stored for the event. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct EventStatistics { @@ -30,8 +27,6 @@ pub struct EventStatistics { pub match_count: u32, pub total_predictions: u32, pub all_matches_resolved: bool, - pub winners_verified: bool, - pub winner_count: u32, } /// Public configuration snapshot for the contract. @@ -59,9 +54,8 @@ pub fn get_event_participants(env: &Env, event_id: u64) -> Result, /// Build aggregate statistics for an existing event. /// /// The function first retrieves the event to validate that `event_id` exists, -/// then derives prediction totals from the event's match index, completion -/// status from each stored match result, and winner status from the event's -/// verified winners list. +/// then derives prediction totals from the event's match index and completion +/// status from each stored match result. pub fn get_event_statistics(env: &Env, event_id: u64) -> Result { let event = event::get_event(env, event_id)?; let match_ids = storage::get_event_matches(env, event_id); @@ -80,7 +74,6 @@ pub fn get_event_statistics(env: &Env, event_id: u64) -> Result 0 && match_ids.len() == event.match_count && resolved_matches == event.match_count; @@ -91,8 +84,6 @@ pub fn get_event_statistics(env: &Env, event_id: u64) -> Result 0, - winner_count, }) } diff --git a/contracts/creator-event-manager/tests/data_structures_test.rs b/contracts/creator-event-manager/tests/data_structures_test.rs index fe58bbd0..53075660 100644 --- a/contracts/creator-event-manager/tests/data_structures_test.rs +++ b/contracts/creator-event-manager/tests/data_structures_test.rs @@ -1,9 +1,9 @@ -/// Comprehensive unit tests for data structures (Event, Match, Prediction, Winner). +/// Comprehensive unit tests for data structures (Event, Match, Prediction). /// /// Achieves 100% code coverage for the storage_types module by covering every /// method, edge case, validation branch, and helper function. use creator_event_manager::storage_types::{ - Event, Match, MatchResult, Prediction, Winner, MAX_TEAM_NAME_LEN, OUTCOME_DRAW, OUTCOME_TEAM_A, + Event, Match, MatchResult, Prediction, MAX_TEAM_NAME_LEN, OUTCOME_DRAW, OUTCOME_TEAM_A, OUTCOME_TEAM_B, }; use soroban_sdk::{testutils::Address as _, Address, Env, String, Symbol}; @@ -498,78 +498,3 @@ fn test_prediction_is_before_match_time_boundary() { // predicted_at (100) < match_time (101) => true assert!(pred.is_before_match_time(101)); } - -// ============================================================================= -// Winner — supplementary tests for 100% coverage -// ============================================================================= - -#[test] -fn test_winner_accuracy_percentage_rounding() { - let env = Env::default(); - // 2 correct out of 3 = 66% (integer division rounds down) - let winner = Winner::new(Address::generate(&env), 1, 2, 3, 0, 0); - assert_eq!(winner.get_accuracy_percentage(), 66); -} - -#[test] -fn test_winner_accuracy_percentage_all_wrong() { - let env = Env::default(); - let winner = Winner::new(Address::generate(&env), 1, 0, 10, 0, 0); - assert_eq!(winner.get_accuracy_percentage(), 0); -} - -#[test] -fn test_winner_accuracy_percentage_one_match() { - let env = Env::default(); - let w1 = Winner::new(Address::generate(&env), 1, 1, 1, 0, 0); - assert_eq!(w1.get_accuracy_percentage(), 100); - let w2 = Winner::new(Address::generate(&env), 1, 0, 1, 0, 0); - assert_eq!(w2.get_accuracy_percentage(), 0); -} - -#[test] -fn test_winner_outranks_by_correct_count_only() { - let env = Env::default(); - // w1 has more correct but later completion - let w1 = Winner::new(Address::generate(&env), 1, 5, 5, 9999, 0); - let w2 = Winner::new(Address::generate(&env), 1, 3, 5, 0, 0); - // w1 outranks because more correct, even though later completion - assert!(w1.outranks(&w2)); - assert!(!w2.outranks(&w1)); -} - -#[test] -fn test_winner_outranks_tiebreak_respected() { - let env = Env::default(); - // Same correct count (4), w2 finished earlier - let w1 = Winner::new(Address::generate(&env), 1, 4, 5, 2000, 0); - let w2 = Winner::new(Address::generate(&env), 1, 4, 5, 1000, 0); - // w2 should outrank w1 (earlier completion time) - assert!(w2.outranks(&w1)); - assert!(!w1.outranks(&w2)); -} - -#[test] -fn test_winner_does_not_outrank_self() { - let env = Env::default(); - let user = Address::generate(&env); - let w = Winner::new(user.clone(), 1, 5, 5, 1000, 0); - assert!(!w.outranks(&w)); -} - -#[test] -fn test_winner_outranks_edge_case_zero_correct() { - let env = Env::default(); - let w1 = Winner::new(Address::generate(&env), 1, 0, 5, 100, 0); - let w2 = Winner::new(Address::generate(&env), 1, 0, 5, 200, 0); - // Both 0 correct — tiebreak by completion_time - assert!(w1.outranks(&w2)); - assert!(!w2.outranks(&w1)); -} - -#[test] -fn test_winner_get_accuracy_percentage_no_matches_no_panic() { - let env = Env::default(); - let winner = Winner::new(Address::generate(&env), 1, 0, 0, 0, 0); - assert_eq!(winner.get_accuracy_percentage(), 0); -} diff --git a/contracts/creator-event-manager/tests/leaderboard_tests.rs b/contracts/creator-event-manager/tests/leaderboard_tests.rs new file mode 100644 index 00000000..dbeb9956 --- /dev/null +++ b/contracts/creator-event-manager/tests/leaderboard_tests.rs @@ -0,0 +1,322 @@ +/// Comprehensive tests for ranked event leaderboard functionality (#967). +/// +/// Tests cover: +/// - Basic leaderboard ranking by total points +/// - Tiebreaking by exact scores count +/// - Tiebreaking by earliest prediction time +/// - Live leaderboard before all matches are resolved +/// - Empty event (no participants) +/// - Single participant +use creator_event_manager::storage; +use creator_event_manager::storage_types::MatchResult; +use creator_event_manager::CreatorEventManagerContractClient; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::testutils::Ledger as _; +use soroban_sdk::token::StellarAssetClient; +use soroban_sdk::{Address, Env, String, Symbol}; + +const FEE: i128 = 1_000_000; + +fn setup() -> ( + Env, + CreatorEventManagerContractClient<'static>, + Address, + Address, + Address, + Address, +) { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(creator_event_manager::CreatorEventManagerContract, ()); + let client = CreatorEventManagerContractClient::new(&env, &contract_id); + let client: CreatorEventManagerContractClient<'static> = + unsafe { core::mem::transmute(client) }; + + let admin = Address::generate(&env); + let ai_agent = Address::generate(&env); + let treasury = Address::generate(&env); + let token_admin = Address::generate(&env); + let xlm_token = env + .register_stellar_asset_contract_v2(token_admin) + .address(); + + client.initialize(&admin, &ai_agent, &treasury, &xlm_token, &FEE); + (env, client, contract_id, admin, ai_agent, xlm_token) +} + +fn fund(env: &Env, token: &Address, user: &Address, amount: i128) { + StellarAssetClient::new(env, token).mint(user, &amount); +} + +fn title(env: &Env) -> String { + String::from_str(env, "Test Event") +} + +fn desc(env: &Env) -> String { + String::from_str(env, "Test Description") +} + +fn create_event_with_matches( + env: &Env, + contract_id: &Address, + client: &CreatorEventManagerContractClient<'static>, + creator: &Address, + xlm_token: &Address, + num_matches: u32, +) -> (u64, Symbol, Vec) { + fund(env, xlm_token, creator, FEE); + let start_time = env.ledger().timestamp() + 3600; + let end_time = env.ledger().timestamp() + 7200; + let (event_id, invite_code) = client.create_event( + creator, + &title(env), + &desc(env), + &100u32, + &start_time, + &end_time, + ); + + let mut match_ids: Vec = Vec::new(); + + env.as_contract(contract_id, || { + for i in 0..num_matches { + let match_id = storage::next_match_id(env); + let match_record = creator_event_manager::storage_types::Match::new( + match_id, + event_id, + String::from_str(env, &format!("Team A{}", i)), + String::from_str(env, &format!("Team B{}", i)), + env.ledger().timestamp() + 100 + (i as u64) * 60, + ); + storage::set_match(env, match_id, &match_record); + storage::add_event_match(env, event_id, match_id); + match_ids.push(match_id); + + let mut event = storage::get_event(env, event_id).expect("event exists"); + event.add_match(); + storage::set_event(env, event_id, &event); + } + }); + + (event_id, invite_code, match_ids) +} + +fn submit_predictions( + _env: &Env, + _contract_id: &Address, + _client: &CreatorEventManagerContractClient<'static>, + _user: &Address, + _event_id: u64, + _match_id: u64, + _home_score: u32, + _away_score: u32, +) { + // Placeholder for future use +} + +fn submit_match_result( + _env: &Env, + client: &CreatorEventManagerContractClient<'static>, + ai_agent: &Address, + match_id: u64, + result: MatchResult, +) { + let (home_score, away_score) = match result { + MatchResult::TeamA => (1, 0), + MatchResult::TeamB => (0, 1), + MatchResult::Draw => (1, 1), + }; + client.submit_match_result(ai_agent, &match_id, &home_score, &away_score); +} + +#[test] +fn test_leaderboard_ranks_by_total_points_desc() { + let (env, client, contract_id, creator, ai_agent, xlm_token) = setup(); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + let user3 = Address::generate(&env); + + // Create event with 3 matches + let (event_id, invite_code, match_ids) = + create_event_with_matches(&env, &contract_id, &client, &creator, &xlm_token, 3); + + // User1: correct all (3*4 = 12 points) + client.join_event(&user1, &invite_code); + for match_id_ref in match_ids.iter() { + client.submit_prediction(&user1, match_id_ref, &1u32, &0u32); + } + + // User2: correct 2 (2*4 = 8 points) + client.join_event(&user2, &invite_code); + client.submit_prediction(&user2, match_ids.get(0).unwrap(), &1u32, &0u32); + client.submit_prediction(&user2, match_ids.get(1).unwrap(), &1u32, &0u32); + client.submit_prediction(&user2, match_ids.get(2).unwrap(), &0u32, &0u32); // wrong + + // User3: correct 1 (1*4 = 4 points) + client.join_event(&user3, &invite_code); + client.submit_prediction(&user3, match_ids.get(0).unwrap(), &1u32, &0u32); + client.submit_prediction(&user3, match_ids.get(1).unwrap(), &0u32, &0u32); // wrong + client.submit_prediction(&user3, match_ids.get(2).unwrap(), &0u32, &0u32); // wrong + + // Advance time and submit results (all TeamA wins) + // Need to advance past all match times (max is 220 relative to initial time) + env.ledger().set_timestamp(env.ledger().timestamp() + 300); + for match_id_ref in match_ids.iter() { + submit_match_result(&env, &client, &ai_agent, *match_id_ref, MatchResult::TeamA); + } + + // Get leaderboard + let leaderboard = client.get_event_leaderboard(&event_id); + + assert_eq!(leaderboard.len(), 3); + assert_eq!(leaderboard.get(0).unwrap().user, user1); + assert_eq!(leaderboard.get(0).unwrap().rank, 1); + assert_eq!(leaderboard.get(0).unwrap().total_points, 12); + + assert_eq!(leaderboard.get(1).unwrap().user, user2); + assert_eq!(leaderboard.get(1).unwrap().rank, 2); + assert_eq!(leaderboard.get(1).unwrap().total_points, 8); + + assert_eq!(leaderboard.get(2).unwrap().user, user3); + assert_eq!(leaderboard.get(2).unwrap().rank, 3); + assert_eq!(leaderboard.get(2).unwrap().total_points, 4); +} + +#[test] +fn test_leaderboard_tiebreak_by_exact_scores() { + let (env, client, contract_id, creator, ai_agent, xlm_token) = setup(); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + + // Create event with 2 matches + let (event_id, invite_code, match_ids) = + create_event_with_matches(&env, &contract_id, &client, &creator, &xlm_token, 2); + + // User1: 1 exact (4 pts) + 1 correct result (1 pt) = 5 pts, 1 exact + client.join_event(&user1, &invite_code); + client.submit_prediction(&user1, &match_ids.get(0).unwrap(), &1u32, &1u32); // Draw - EXACT (1-1) + client.submit_prediction(&user1, &match_ids.get(1).unwrap(), &2u32, &0u32); // TeamA wins - CORRECT RESULT (2-0 instead of 1-0) = 1 pt + + // User2: 1 exact (4 pts) + 0 correct = 4 pts, 1 exact + client.join_event(&user2, &invite_code); + client.submit_prediction(&user2, &match_ids.get(0).unwrap(), &1u32, &1u32); // Draw - EXACT (1-1) + client.submit_prediction(&user2, &match_ids.get(1).unwrap(), &0u32, &1u32); // WRONG - TeamB wins + + // Advance time and submit results + env.ledger().set_timestamp(env.ledger().timestamp() + 200); + submit_match_result(&env, &client, &ai_agent, *match_ids.get(0).unwrap(), MatchResult::Draw); + submit_match_result(&env, &client, &ai_agent, *match_ids.get(1).unwrap(), MatchResult::TeamA); + + let leaderboard = client.get_event_leaderboard(&event_id); + + assert_eq!(leaderboard.len(), 2); + assert_eq!(leaderboard.get(0).unwrap().user, user1); + assert_eq!(leaderboard.get(0).unwrap().total_points, 5); + assert_eq!(leaderboard.get(0).unwrap().exact_scores, 1); + assert_eq!(leaderboard.get(0).unwrap().rank, 1); + + assert_eq!(leaderboard.get(1).unwrap().user, user2); + assert_eq!(leaderboard.get(1).unwrap().total_points, 4); + assert_eq!(leaderboard.get(1).unwrap().exact_scores, 1); + assert_eq!(leaderboard.get(1).unwrap().rank, 2); +} + +#[test] +fn test_leaderboard_tiebreak_by_earliest_prediction() { + let (env, client, contract_id, creator, ai_agent, xlm_token) = setup(); + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + + // Create event with 1 match + let (event_id, invite_code, match_ids) = + create_event_with_matches(&env, &contract_id, &client, &creator, &xlm_token, 1); + + // User1: submit prediction early + client.join_event(&user1, &invite_code); + client.submit_prediction(&user1, &match_ids.get(0).unwrap(), &1u32, &0u32); + + // Advance time + env.ledger().set_timestamp(env.ledger().timestamp() + 50); + + // User2: submit prediction later (same result) + client.join_event(&user2, &invite_code); + client.submit_prediction(&user2, &match_ids.get(0).unwrap(), &1u32, &0u32); + + // Advance time and submit result + env.ledger().set_timestamp(env.ledger().timestamp() + 200); + submit_match_result(&env, &client, &ai_agent, *match_ids.get(0).unwrap(), MatchResult::TeamA); + + let leaderboard = client.get_event_leaderboard(&event_id); + + assert_eq!(leaderboard.len(), 2); + // Both have same total_points and exact_scores, so earlier prediction time ranks higher + assert_eq!(leaderboard.get(0).unwrap().user, user1); + assert_eq!(leaderboard.get(0).unwrap().rank, 1); + + assert_eq!(leaderboard.get(1).unwrap().user, user2); + assert_eq!(leaderboard.get(1).unwrap().rank, 2); +} + +#[test] +fn test_leaderboard_live_before_all_matches_resolved() { + let (env, client, contract_id, creator, _ai_agent, xlm_token) = setup(); + let user1 = Address::generate(&env); + + // Create event with 2 matches + let (event_id, invite_code, match_ids) = + create_event_with_matches(&env, &contract_id, &client, &creator, &xlm_token, 2); + + client.join_event(&user1, &invite_code); + client.submit_prediction(&user1, &match_ids.get(0).unwrap(), &1u32, &0u32); + client.submit_prediction(&user1, &match_ids.get(1).unwrap(), &2u32, &1u32); + + // Get leaderboard BEFORE matches are resolved + // Unresolved matches should contribute 0 points + let leaderboard = client.get_event_leaderboard(&event_id); + + assert_eq!(leaderboard.len(), 1); + assert_eq!(leaderboard.get(0).unwrap().user, user1); + assert_eq!(leaderboard.get(0).unwrap().total_points, 0); // No points yet, matches not resolved + assert_eq!(leaderboard.get(0).unwrap().rank, 1); +} + +#[test] +fn test_leaderboard_empty_event() { + let (env, client, contract_id, creator, _ai_agent, xlm_token) = setup(); + + // Create event with no participants + let (event_id, _invite_code, _match_ids) = + create_event_with_matches(&env, &contract_id, &client, &creator, &xlm_token, 0); + + // Get leaderboard for empty event + let leaderboard = client.get_event_leaderboard(&event_id); + + assert_eq!(leaderboard.len(), 0); +} + +#[test] +fn test_leaderboard_single_participant() { + let (env, client, contract_id, creator, ai_agent, xlm_token) = setup(); + let user1 = Address::generate(&env); + + // Create event with 1 match + let (event_id, invite_code, match_ids) = + create_event_with_matches(&env, &contract_id, &client, &creator, &xlm_token, 1); + + client.join_event(&user1, &invite_code); + client.submit_prediction(&user1, match_ids.get(0).unwrap(), &1u32, &0u32); + + env.ledger().set_timestamp(env.ledger().timestamp() + 200); + submit_match_result(&env, &client, &ai_agent, *match_ids.get(0).unwrap(), MatchResult::TeamA); + + let leaderboard = client.get_event_leaderboard(&event_id); + + assert_eq!(leaderboard.len(), 1); + assert_eq!(leaderboard.get(0).unwrap().user, user1); + assert_eq!(leaderboard.get(0).unwrap().rank, 1); + assert_eq!(leaderboard.get(0).unwrap().total_points, 4); + assert_eq!(leaderboard.get(0).unwrap().correct_results, 1); + assert_eq!(leaderboard.get(0).unwrap().exact_scores, 1); + assert_eq!(leaderboard.get(0).unwrap().matches_played, 1); +} diff --git a/contracts/creator-event-manager/tests/oracle_tests.rs b/contracts/creator-event-manager/tests/oracle_tests.rs index 9d4e8099..46179b55 100644 --- a/contracts/creator-event-manager/tests/oracle_tests.rs +++ b/contracts/creator-event-manager/tests/oracle_tests.rs @@ -168,304 +168,9 @@ fn test_submit_match_result_match_updated_correctly() { assert_eq!(match_record.submitted_by, Some(ai_agent.clone())); } -// ============================================================================ -// verify_event_winners tests -// ============================================================================ - -#[test] -fn test_verify_event_winners_identifies_winners_correctly() { - let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - - let (event_id, invite_code, match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - client.join_event(&user1, &invite_code); - client.join_event(&user2, &invite_code); - - client.submit_prediction(&user1, &match_id, &2u32, &1u32); - client.submit_prediction(&user2, &match_id, &1u32, &2u32); - - env.ledger().with_mut(|l| l.timestamp += 15_000); - submit_match_result(&env, &client, &ai_agent, match_id, MatchResult::TeamA); - - let winner_count = client.verify_event_winners(&user1, &event_id); - assert_eq!(winner_count, 1); - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 1); - assert_eq!(winners.get(0).unwrap().user, user1); -} - -#[test] -fn test_verify_event_winners_partial_scores_excluded() { - let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - - fund(&env, &xlm_token, &creator, FEE); - let start_time = env.ledger().timestamp() + 3600; - let end_time = env.ledger().timestamp() + 7200; - let (event_id, invite_code) = client.create_event( - &creator, - &title(&env), - &desc(&env), - &10u32, - &start_time, - &end_time, - ); - - // Create two matches - let (match_id_1, match_id_2) = env.as_contract(&contract_id, || { - let m1 = storage::next_match_id(&env); - storage::set_match( - &env, - m1, - &creator_event_manager::storage_types::Match::new( - m1, - event_id, - String::from_str(&env, "Team A"), - String::from_str(&env, "Team B"), - env.ledger().timestamp() + 10_000, - ), - ); - storage::add_event_match(&env, event_id, m1); - - let m2 = storage::next_match_id(&env); - storage::set_match( - &env, - m2, - &creator_event_manager::storage_types::Match::new( - m2, - event_id, - String::from_str(&env, "Team C"), - String::from_str(&env, "Team D"), - env.ledger().timestamp() + 20_000, - ), - ); - storage::add_event_match(&env, event_id, m2); - - let mut event = storage::get_event(&env, event_id).expect("event exists"); - event.add_match(); - event.add_match(); - storage::set_event(&env, event_id, &event); - - (m1, m2) - }); - - client.join_event(&user1, &invite_code); - client.join_event(&user2, &invite_code); - - // User1 predicts both correctly - client.submit_prediction(&user1, &match_id_1, &2u32, &1u32); - client.submit_prediction(&user1, &match_id_2, &1u32, &2u32); - - // User2 predicts only one correctly - client.submit_prediction(&user2, &match_id_1, &2u32, &1u32); - client.submit_prediction(&user2, &match_id_2, &2u32, &1u32); - - env.ledger().with_mut(|l| l.timestamp += 25_000); - submit_match_result( - &env, - &client, - &ai_agent, - match_id_1, - MatchResult::TeamA, - ); - submit_match_result( - &env, - &client, - &ai_agent, - match_id_2, - MatchResult::TeamB, - ); - - let winner_count = client.verify_event_winners(&user1, &event_id); - assert_eq!(winner_count, 1); // Only user1 has perfect score - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 1); - assert_eq!(winners.get(0).unwrap().user, user1); -} - -#[test] -#[should_panic(expected = "matches_not_complete")] -fn test_verify_event_winners_all_matches_must_be_resolved() { - let (env, client, contract_id, _admin, _ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user = Address::generate(&env); - - let (event_id, invite_code, _match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - client.join_event(&user, &invite_code); - - // Try to verify without resolving matches - client.verify_event_winners(&user, &event_id); -} - -#[test] -fn test_verify_event_winners_empty_winners_handled() { - let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user = Address::generate(&env); - - let (event_id, invite_code, match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - client.join_event(&user, &invite_code); - client.submit_prediction(&user, &match_id, &2u32, &1u32); - - env.ledger().with_mut(|l| l.timestamp += 15_000); - submit_match_result(&env, &client, &ai_agent, match_id, MatchResult::TeamB); - - let winner_count = client.verify_event_winners(&user, &event_id); - assert_eq!(winner_count, 0); - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 0); -} - -#[test] -fn test_verify_event_winners_multiple_winners_supported() { - let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - let user3 = Address::generate(&env); - - let (event_id, invite_code, match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - client.join_event(&user1, &invite_code); - client.join_event(&user2, &invite_code); - client.join_event(&user3, &invite_code); - - client.submit_prediction(&user1, &match_id, &2u32, &1u32); - client.submit_prediction(&user2, &match_id, &2u32, &1u32); - client.submit_prediction(&user3, &match_id, &1u32, &2u32); - - env.ledger().with_mut(|l| l.timestamp += 15_000); - submit_match_result(&env, &client, &ai_agent, match_id, MatchResult::TeamA); - - let winner_count = client.verify_event_winners(&user1, &event_id); - assert_eq!(winner_count, 2); - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 2); -} - -#[test] -fn test_verify_event_winners_completion_time_tracked() { - let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - - let (event_id, invite_code, match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - client.join_event(&user1, &invite_code); - client.join_event(&user2, &invite_code); - - // User1 predicts first - client.submit_prediction(&user1, &match_id, &2u32, &1u32); - - env.ledger().with_mut(|l| l.timestamp += 100); - - // User2 predicts later - client.submit_prediction(&user2, &match_id, &2u32, &1u32); - - env.ledger().with_mut(|l| l.timestamp += 15_000); - submit_match_result(&env, &client, &ai_agent, match_id, MatchResult::TeamA); - - client.verify_event_winners(&user1, &event_id); - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 2); - - // Winners should be sorted by completion time - let first = winners.get(0).unwrap(); - let second = winners.get(1).unwrap(); - assert!(first.completion_time <= second.completion_time); -} - -// ============================================================================ -// get_event_winners tests -// ============================================================================ - -#[test] -fn test_get_event_winners_returns_all_winners() { - let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - - let (event_id, invite_code, match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - client.join_event(&user1, &invite_code); - client.join_event(&user2, &invite_code); - - client.submit_prediction(&user1, &match_id, &2u32, &1u32); - client.submit_prediction(&user2, &match_id, &2u32, &1u32); - - env.ledger().with_mut(|l| l.timestamp += 15_000); - submit_match_result(&env, &client, &ai_agent, match_id, MatchResult::TeamA); - - client.verify_event_winners(&user1, &event_id); - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 2); -} - -#[test] -fn test_get_event_winners_sorted_by_completion_time() { - let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - - let (event_id, invite_code, match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - client.join_event(&user1, &invite_code); - client.join_event(&user2, &invite_code); - - client.submit_prediction(&user2, &match_id, &2u32, &1u32); - - env.ledger().with_mut(|l| l.timestamp += 500); - - client.submit_prediction(&user1, &match_id, &2u32, &1u32); - - env.ledger().with_mut(|l| l.timestamp += 15_000); - submit_match_result(&env, &client, &ai_agent, match_id, MatchResult::TeamA); - - client.verify_event_winners(&user1, &event_id); - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 2); - - let first = winners.get(0).unwrap(); - let second = winners.get(1).unwrap(); - assert!(first.completion_time <= second.completion_time); - assert_eq!(first.user, user2); // user2 predicted first -} - -#[test] -fn test_get_event_winners_empty_list_handled() { - let (env, client, contract_id, _admin, _ai_agent, xlm_token) = setup(); - let creator = Address::generate(&env); - - let (event_id, _invite_code, _match_id) = - create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); - - let winners = client.get_event_winners(&event_id); - assert_eq!(winners.len(), 0); -} +// Note: Tests for verify_event_winners and get_event_winners have been removed. +// These functions have been deprecated in favor of the ranked leaderboard model +// (Issue #967). The leaderboard is tested in leaderboard_tests.rs. // ============================================================================ // get_user_score tests diff --git a/contracts/creator-event-manager/tests/storage_types_tests.rs b/contracts/creator-event-manager/tests/storage_types_tests.rs index a7d0be77..ff9b345e 100644 --- a/contracts/creator-event-manager/tests/storage_types_tests.rs +++ b/contracts/creator-event-manager/tests/storage_types_tests.rs @@ -2,7 +2,7 @@ // These live in tests/ so they import via the crate name. use creator_event_manager::storage_types::{ - Event, Match, MatchResult, Prediction, Winner, OUTCOME_DRAW, OUTCOME_TEAM_A, OUTCOME_TEAM_B, + Event, Match, MatchResult, Prediction, OUTCOME_DRAW, OUTCOME_TEAM_A, OUTCOME_TEAM_B, }; use soroban_sdk::{testutils::Address as _, Address, Env, String, Symbol}; @@ -502,97 +502,3 @@ fn test_prediction_ungraded_is_not_winner() { let pred = Prediction::new(1, 5, 10, predictor, 2u32, 1u32, 1_640_995_200, &env); assert!(!pred.is_winner()); // None → not a winner } - -// --------------------------------------------------------------------------- -// Winner tests -// --------------------------------------------------------------------------- - -#[test] -fn test_winner_creation() { - let env = Env::default(); - let user = Address::generate(&env); - - let winner = Winner::new(user.clone(), 42, 5, 5, 1_640_995_100, 1_640_995_200); - assert_eq!(winner.user, user); - assert_eq!(winner.event_id, 42); - assert_eq!(winner.total_correct, 5); - assert_eq!(winner.total_matches, 5); - assert_eq!(winner.completion_time, 1_640_995_100); - assert_eq!(winner.verified_at, 1_640_995_200); -} - -#[test] -fn test_winner_accuracy_percentage_perfect() { - let env = Env::default(); - let user = Address::generate(&env); - let winner = Winner::new(user, 1, 5, 5, 0, 0); - assert_eq!(winner.get_accuracy_percentage(), 100); -} - -#[test] -fn test_winner_accuracy_percentage_partial() { - let env = Env::default(); - let user = Address::generate(&env); - // 3 correct out of 5 = 60% - let winner = Winner::new(user, 1, 3, 5, 0, 0); - assert_eq!(winner.get_accuracy_percentage(), 60); -} - -#[test] -fn test_winner_accuracy_percentage_zero_matches() { - let env = Env::default(); - let user = Address::generate(&env); - // total_matches = 0 → should not panic, returns 0 - let winner = Winner::new(user, 1, 0, 0, 0, 0); - assert_eq!(winner.get_accuracy_percentage(), 0); -} - -#[test] -fn test_winner_accuracy_percentage_zero_correct() { - let env = Env::default(); - let user = Address::generate(&env); - let winner = Winner::new(user, 1, 0, 5, 0, 0); - assert_eq!(winner.get_accuracy_percentage(), 0); -} - -#[test] -fn test_winner_outranks_by_correct_count() { - let env = Env::default(); - let u1 = Address::generate(&env); - let u2 = Address::generate(&env); - - // w1 has more correct predictions - let w1 = Winner::new(u1, 1, 5, 5, 1000, 0); - let w2 = Winner::new(u2, 1, 3, 5, 500, 0); - - assert!(w1.outranks(&w2)); - assert!(!w2.outranks(&w1)); -} - -#[test] -fn test_winner_outranks_tiebreak_by_completion_time() { - let env = Env::default(); - let u1 = Address::generate(&env); - let u2 = Address::generate(&env); - - // Same correct count; w1 finished earlier → w1 outranks w2 - let w1 = Winner::new(u1, 1, 5, 5, 500, 0); // earlier completion - let w2 = Winner::new(u2, 1, 5, 5, 1000, 0); // later completion - - assert!(w1.outranks(&w2)); - assert!(!w2.outranks(&w1)); -} - -#[test] -fn test_winner_does_not_outrank_equal() { - let env = Env::default(); - let u1 = Address::generate(&env); - let u2 = Address::generate(&env); - - // Identical stats — neither outranks the other - let w1 = Winner::new(u1, 1, 5, 5, 500, 0); - let w2 = Winner::new(u2, 1, 5, 5, 500, 0); - - assert!(!w1.outranks(&w2)); - assert!(!w2.outranks(&w1)); -} diff --git a/contracts/creator-event-manager/tests/views_tests.rs b/contracts/creator-event-manager/tests/views_tests.rs index c84e283c..23429000 100644 --- a/contracts/creator-event-manager/tests/views_tests.rs +++ b/contracts/creator-event-manager/tests/views_tests.rs @@ -1,10 +1,10 @@ /// Tests for aggregate event statistics views. use creator_event_manager::storage; -use creator_event_manager::storage_types::{Match, MatchResult, Prediction, Winner}; +use creator_event_manager::storage_types::{Match, MatchResult, Prediction}; use creator_event_manager::CreatorEventManagerContractClient; use soroban_sdk::testutils::Address as _; use soroban_sdk::token::StellarAssetClient; -use soroban_sdk::{Address, Env, String, Symbol}; +use soroban_sdk::{Address, Env, String}; const FEE: i128 = 1_000_000; @@ -226,15 +226,12 @@ fn test_event_statistics_are_accurate() { assert_eq!(statistics.match_count, 2); assert_eq!(statistics.total_predictions, 3); assert!(!statistics.all_matches_resolved); - assert!(!statistics.winners_verified); - assert_eq!(statistics.winner_count, 0); } #[test] fn test_event_statistics_completion_status() { let (env, client, contract_id, xlm_token) = setup(); let creator = Address::generate(&env); - let winner = Address::generate(&env); fund(&env, &xlm_token, &creator, FEE); let start_time = get_future_time(&env, 3600); @@ -255,8 +252,6 @@ fn test_event_statistics_completion_status() { let pending_statistics = client.get_event_statistics(&event_id); assert!(!pending_statistics.all_matches_resolved); - assert!(!pending_statistics.winners_verified); - assert_eq!(pending_statistics.winner_count, 0); env.as_contract(&contract_id, || { for match_id in storage::get_event_matches(&env, event_id).iter() { @@ -272,15 +267,10 @@ fn test_event_statistics_completion_status() { storage::set_match(&env, match_id, &match_record); } } - - let verified_winner = Winner::new(winner, event_id, 2, 2, 100, env.ledger().timestamp()); - storage::add_event_winner(&env, event_id, &verified_winner); }); let completed_statistics = client.get_event_statistics(&event_id); assert!(completed_statistics.all_matches_resolved); - assert!(completed_statistics.winners_verified); - assert_eq!(completed_statistics.winner_count, 1); } #[test]