From 6bec56c212cde00baba0a64d486b34bcb5423f27 Mon Sep 17 00:00:00 2001 From: Divine-designs Date: Tue, 2 Jun 2026 01:49:47 +0100 Subject: [PATCH 1/2] feat(contract): add get_match_predictions to return all predictions for a match Implements a public `get_match_predictions(match_id) -> Vec` view: reads the MatchPredictions(match_id) index of prediction IDs, loads each Prediction, and returns them in submission order. Matches with no predictions (and unknown match ids) return an empty Vec. Useful for analytics and displaying a match's prediction distribution. - prediction::get_match_predictions plus the contract entry point in lib.rs - integration tests: full retrieval, empty case, correct count, unknown match id Closes #808 https://github.com/Arena1X/InsightArena/issues/808 Co-Authored-By: Claude Opus 4.8 (1M context) --- contracts/creator-event-manager/src/lib.rs | 10 ++ .../creator-event-manager/src/prediction.rs | 22 +++ .../tests/get_match_predictions_tests.rs | 152 ++++++++++++++++++ contracts/creator-event-manager/tests/mod.rs | 1 + 4 files changed, 185 insertions(+) create mode 100644 contracts/creator-event-manager/tests/get_match_predictions_tests.rs diff --git a/contracts/creator-event-manager/src/lib.rs b/contracts/creator-event-manager/src/lib.rs index 1c577fe0..dfa95108 100644 --- a/contracts/creator-event-manager/src/lib.rs +++ b/contracts/creator-event-manager/src/lib.rs @@ -451,6 +451,16 @@ impl CreatorEventManagerContract { prediction::get_prediction_distribution(&env, match_id) } + /// Retrieve every prediction submitted for a specific match (#808). + /// + /// Returns a `Vec` in submission order. Returns an empty `Vec` + /// when the match has no predictions (or the match id does not exist). + /// Useful for analytics and displaying a match's full prediction + /// distribution. + pub fn get_match_predictions(env: Env, match_id: u64) -> Vec { + prediction::get_match_predictions(&env, match_id) + } + // ========================================================================= // Oracle / Winner Verification (#798–#801) // ========================================================================= diff --git a/contracts/creator-event-manager/src/prediction.rs b/contracts/creator-event-manager/src/prediction.rs index 508c81ee..0ff701a5 100644 --- a/contracts/creator-event-manager/src/prediction.rs +++ b/contracts/creator-event-manager/src/prediction.rs @@ -238,6 +238,28 @@ pub fn get_user_predictions(env: &Env, user: Address, event_id: u64) -> Vec` in submission +/// order (the order in which predictions were placed). +/// +/// Returns an empty `Vec` when the match has no predictions — including for a +/// `match_id` that does not exist, since no prediction index is stored for it. +/// Useful for analytics and displaying a match's full prediction distribution. +pub fn get_match_predictions(env: &Env, match_id: u64) -> Vec { + let prediction_ids = storage::get_match_predictions(env, match_id); + + let mut predictions: Vec = Vec::new(env); + for prediction_id in prediction_ids.iter() { + if let Ok(prediction) = storage::get_prediction(env, prediction_id) { + predictions.push_back(prediction); + } + } + + predictions +} + /// Calculate how many users predicted each outcome for a match. /// /// Returns a tuple `(team_a_count, team_b_count, draw_count)` where each diff --git a/contracts/creator-event-manager/tests/get_match_predictions_tests.rs b/contracts/creator-event-manager/tests/get_match_predictions_tests.rs new file mode 100644 index 00000000..01c155c0 --- /dev/null +++ b/contracts/creator-event-manager/tests/get_match_predictions_tests.rs @@ -0,0 +1,152 @@ +//! #808 — Integration tests for the public `get_match_predictions` view +//! (exercised through the contract client), covering: +//! - Returns all predictions for a match +//! - Empty list for a match with no predictions +//! - Correct count +//! - Unknown match id yields an empty list + +use creator_event_manager::storage; +use creator_event_manager::CreatorEventManagerContractClient; +use soroban_sdk::testutils::Address 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_contract(None, 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_match( + env: &Env, + contract_id: &Address, + client: &CreatorEventManagerContractClient<'static>, + creator: &Address, + xlm_token: &Address, + match_time_offset: u64, +) -> (u64, Symbol, u64) { + fund(env, xlm_token, creator, FEE); + let (event_id, invite_code) = client.create_event(creator, &title(env), &desc(env), &10u32); + + let match_id = env.as_contract(contract_id, || { + 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, "Team A"), + String::from_str(env, "Team B"), + env.ledger().timestamp() + match_time_offset, + ); + storage::set_match(env, match_id, &match_record); + storage::add_event_match(env, event_id, match_id); + + let mut event = storage::get_event(env, event_id).expect("event exists"); + event.add_match(); + storage::set_event(env, event_id, &event); + match_id + }); + + (event_id, invite_code, match_id) +} + +#[test] +fn test_returns_all_predictions_for_match() { + let (env, client, contract_id, _admin, _ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); + + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + let u3 = Address::generate(&env); + client.join_event(&u1, &invite); + client.join_event(&u2, &invite); + client.join_event(&u3, &invite); + client.submit_prediction(&u1, &match_id, &Symbol::new(&env, "TEAM_A")); + client.submit_prediction(&u2, &match_id, &Symbol::new(&env, "TEAM_B")); + client.submit_prediction(&u3, &match_id, &Symbol::new(&env, "DRAW")); + + let predictions = client.get_match_predictions(&match_id); + assert_eq!(predictions.len(), 3); + + let mut matched = 0u32; + for p in predictions.iter() { + assert_eq!(p.match_id, match_id); + if p.predictor == u1 || p.predictor == u2 || p.predictor == u3 { + matched += 1; + } + } + assert_eq!(matched, 3); +} + +#[test] +fn test_empty_list_for_match_with_no_predictions() { + let (env, client, contract_id, _admin, _ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, _invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); + + let predictions = client.get_match_predictions(&match_id); + assert_eq!(predictions.len(), 0); +} + +#[test] +fn test_correct_count() { + let (env, client, contract_id, _admin, _ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); + + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + client.join_event(&u1, &invite); + client.join_event(&u2, &invite); + client.submit_prediction(&u1, &match_id, &Symbol::new(&env, "TEAM_A")); + client.submit_prediction(&u2, &match_id, &Symbol::new(&env, "TEAM_A")); + + assert_eq!(client.get_match_predictions(&match_id).len(), 2); +} + +#[test] +fn test_unknown_match_returns_empty() { + let (_env, client, _contract_id, _admin, _ai_agent, _xlm_token) = setup(); + let predictions = client.get_match_predictions(&99_999u64); + assert_eq!(predictions.len(), 0); +} diff --git a/contracts/creator-event-manager/tests/mod.rs b/contracts/creator-event-manager/tests/mod.rs index c61ce0d3..68d55167 100644 --- a/contracts/creator-event-manager/tests/mod.rs +++ b/contracts/creator-event-manager/tests/mod.rs @@ -1,5 +1,6 @@ mod admin_tests; mod event_tests; +mod get_match_predictions_tests; mod match_tests; mod oracle_tests; mod prediction_tests; From 2de08bd79eb99f51c34e2dceab25be9d3db14255 Mon Sep 17 00:00:00 2001 From: Divine-designs Date: Tue, 2 Jun 2026 01:50:21 +0100 Subject: [PATCH 2/2] feat(contract): add submit_match_result oracle function Implements the core AI-oracle function that resolves a match and grades its predictions. submit_match_result(caller, match_id, winning_team): - requires the caller to be the stored AI agent and the contract to be unpaused - verifies the match exists, has not already been resolved, and has started (current time >= match_time) - validates winning_team is one of TEAM_A / TEAM_B / DRAW - records the result on the match (result_submitted, winning_team, submitted_by, submitted_at) and grades every prediction's is_correct field - emits a MatchResultSubmitted event - adds OracleError variants: Unauthorized, MatchNotFound, ResultAlreadySubmitted, MatchNotStarted, InvalidOutcome Integration tests cover AI-agent submission, non-agent rejection, submission before match time, duplicate submission, invalid outcome, unknown match, predictions graded correct/incorrect, all three outcomes, and a full prediction-to-scoring flow. Closes #810 https://github.com/Arena1X/InsightArena/issues/810 Co-Authored-By: Claude Opus 4.8 (1M context) --- contracts/creator-event-manager/src/lib.rs | 29 ++- contracts/creator-event-manager/src/oracle.rs | 128 ++++++++- contracts/creator-event-manager/tests/mod.rs | 1 + .../submit_match_result_contract_tests.rs | 244 ++++++++++++++++++ 4 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 contracts/creator-event-manager/tests/submit_match_result_contract_tests.rs diff --git a/contracts/creator-event-manager/src/lib.rs b/contracts/creator-event-manager/src/lib.rs index dfa95108..645fefeb 100644 --- a/contracts/creator-event-manager/src/lib.rs +++ b/contracts/creator-event-manager/src/lib.rs @@ -462,9 +462,36 @@ impl CreatorEventManagerContract { } // ========================================================================= - // Oracle / Winner Verification (#798–#801) + // Oracle / Winner Verification (#798–#801, #810) // ========================================================================= + /// Submit a match result as the authorized AI oracle agent (#810). + /// + /// Resolves the match, records the winning outcome, and grades every + /// prediction for the match (sets each `is_correct`). `winning_team` must be + /// one of the `TEAM_A`, `TEAM_B`, or `DRAW` symbols, and the match must have + /// started (current time >= match_time). + /// + /// # Panics + /// * `"contract_paused"` — the contract is paused. + /// * `"unauthorized"` — caller is not the configured AI agent. + /// * `"match_not_found"` — no match exists with the given ID. + /// * `"result_already_submitted"` — a result was already submitted. + /// * `"match_not_started"` — current time is before the match start time. + /// * `"invalid_outcome"` — `winning_team` is not a valid outcome symbol. + pub fn submit_match_result(env: Env, caller: Address, match_id: u64, winning_team: Symbol) { + match oracle::submit_match_result(&env, caller, match_id, winning_team) { + Ok(()) => {} + Err(oracle::OracleError::Paused) => panic!("contract_paused"), + Err(oracle::OracleError::Unauthorized) => panic!("unauthorized"), + Err(oracle::OracleError::MatchNotFound) => panic!("match_not_found"), + Err(oracle::OracleError::ResultAlreadySubmitted) => panic!("result_already_submitted"), + Err(oracle::OracleError::MatchNotStarted) => panic!("match_not_started"), + Err(oracle::OracleError::InvalidOutcome) => panic!("invalid_outcome"), + Err(_) => panic!("unexpected_error"), + } + } + /// Verify and record all perfect scorers for an event. /// /// After all matches in an event are resolved, calculate which users diff --git a/contracts/creator-event-manager/src/oracle.rs b/contracts/creator-event-manager/src/oracle.rs index 710e3835..edd24645 100644 --- a/contracts/creator-event-manager/src/oracle.rs +++ b/contracts/creator-event-manager/src/oracle.rs @@ -2,7 +2,7 @@ use soroban_sdk::{Address, Env, Symbol, Vec}; use crate::admin; use crate::storage::{self, StorageError}; -use crate::storage_types::{DataKey, Event, Match, Winner}; +use crate::storage_types::{DataKey, Event, Match, MatchResult, Winner}; // --------------------------------------------------------------------------- // Error type @@ -23,6 +23,16 @@ pub enum OracleError { CreationFeeNotSet = 5, /// Arithmetic overflow occurred during calculation. Overflow = 6, + /// Caller is not the authorized AI agent. (#810) + Unauthorized = 7, + /// No match found for the given match_id. (#810) + MatchNotFound = 8, + /// A result has already been submitted for this match. (#810) + ResultAlreadySubmitted = 9, + /// The match has not started yet (current time < match_time). (#810) + MatchNotStarted = 10, + /// The provided outcome is not one of TEAM_A, TEAM_B, or DRAW. (#810) + InvalidOutcome = 11, } impl From for OracleError { @@ -45,6 +55,122 @@ fn emit_winners_verified(env: &Env, event_id: u64, winner_count: u32) { ); } +fn emit_match_result_submitted( + env: &Env, + match_id: u64, + winning_team: &Symbol, + submitted_by: &Address, +) { + env.events().publish( + ( + Symbol::new(env, "match"), + Symbol::new(env, "result_submitted"), + ), + (match_id, winning_team.clone(), submitted_by.clone()), + ); +} + +// --------------------------------------------------------------------------- +// submit_match_result (#810) +// --------------------------------------------------------------------------- + +/// Map an outcome `Symbol` ("TEAM_A" / "TEAM_B" / "DRAW") to a [`MatchResult`]. +/// +/// Returns `None` for any symbol that is not one of the three valid outcomes. +fn symbol_to_result(env: &Env, outcome: &Symbol) -> Option { + if *outcome == Symbol::new(env, crate::storage_types::OUTCOME_TEAM_A) { + Some(MatchResult::TeamA) + } else if *outcome == Symbol::new(env, crate::storage_types::OUTCOME_TEAM_B) { + Some(MatchResult::TeamB) + } else if *outcome == Symbol::new(env, crate::storage_types::OUTCOME_DRAW) { + Some(MatchResult::Draw) + } else { + None + } +} + +/// Submit a match result as the authorized AI oracle agent (#810). +/// +/// This is the core oracle function that resolves a match and grades every +/// prediction made for it. +/// +/// # Flow +/// 1. Require caller authorization. +/// 2. Reject if the contract is paused. +/// 3. Reject if the caller is not the stored AI agent address. +/// 4. Retrieve the match and verify it exists. +/// 5. Verify a result has not already been submitted. +/// 6. Verify the match has started (`now >= match_time`). +/// 7. Validate `winning_team` is one of TEAM_A / TEAM_B / DRAW. +/// 8. Update the match (result_submitted, winning_team, submitted_by/at). +/// 9. Grade every prediction for the match (`is_correct`). +/// 10. Emit a `MatchResultSubmitted` event. +/// +/// # Errors +/// * [`OracleError::Paused`] — the contract is paused. +/// * [`OracleError::Unauthorized`] — caller is not the AI agent. +/// * [`OracleError::MatchNotFound`] — no match with the given id. +/// * [`OracleError::ResultAlreadySubmitted`] — result already recorded. +/// * [`OracleError::MatchNotStarted`] — match has not started yet. +/// * [`OracleError::InvalidOutcome`] — `winning_team` is not a valid outcome. +pub fn submit_match_result( + env: &Env, + caller: Address, + match_id: u64, + winning_team: Symbol, +) -> Result<(), OracleError> { + caller.require_auth(); + + // 1. Contract must not be paused. + if admin::is_paused(env) { + return Err(OracleError::Paused); + } + + // 2. Caller must be the authorized AI agent. + let ai_agent = admin::get_ai_agent(env).ok_or(OracleError::Unauthorized)?; + if caller != ai_agent { + return Err(OracleError::Unauthorized); + } + + // 3. Match must exist. + let mut match_record: Match = + storage::get_match(env, match_id).map_err(|_| OracleError::MatchNotFound)?; + + // 4. Result must not already be submitted. + if match_record.result_submitted { + return Err(OracleError::ResultAlreadySubmitted); + } + + // 5. Match must have started. + let now = env.ledger().timestamp(); + if now < match_record.match_time { + return Err(OracleError::MatchNotStarted); + } + + // 6. Outcome must be valid. + let result = symbol_to_result(env, &winning_team).ok_or(OracleError::InvalidOutcome)?; + + // 7. Record the result on the match. + match_record + .submit_result(result, caller.clone(), now) + .map_err(|_| OracleError::ResultAlreadySubmitted)?; + storage::set_match(env, match_id, &match_record); + + // 8. Grade every prediction submitted for this match. + let prediction_ids = storage::get_match_predictions(env, match_id); + for prediction_id in prediction_ids.iter() { + if let Ok(mut prediction) = storage::get_prediction(env, prediction_id) { + prediction.grade(&winning_team); + storage::set_prediction(env, prediction_id, &prediction); + } + } + + // 9. Emit the result event. + emit_match_result_submitted(env, match_id, &winning_team, &caller); + + Ok(()) +} + // --------------------------------------------------------------------------- // verify_event_winners (#798) // --------------------------------------------------------------------------- diff --git a/contracts/creator-event-manager/tests/mod.rs b/contracts/creator-event-manager/tests/mod.rs index 68d55167..9679bd12 100644 --- a/contracts/creator-event-manager/tests/mod.rs +++ b/contracts/creator-event-manager/tests/mod.rs @@ -5,5 +5,6 @@ mod match_tests; mod oracle_tests; mod prediction_tests; mod storage_types_tests; +mod submit_match_result_contract_tests; mod verification_tests; mod views_tests; diff --git a/contracts/creator-event-manager/tests/submit_match_result_contract_tests.rs b/contracts/creator-event-manager/tests/submit_match_result_contract_tests.rs new file mode 100644 index 00000000..403bf168 --- /dev/null +++ b/contracts/creator-event-manager/tests/submit_match_result_contract_tests.rs @@ -0,0 +1,244 @@ +//! #810 — Integration tests for the public `submit_match_result` oracle entry +//! point (exercised through the contract client), covering: +//! - AI agent can submit a result +//! - Non-agent cannot submit +//! - Result before match time is rejected +//! - Duplicate submission is rejected +//! - Invalid outcome is rejected +//! - Predictions are graded correct/incorrect +//! - All outcomes (TEAM_A, TEAM_B, DRAW) work +//! - Full prediction flow (submit -> grade -> score) + +use creator_event_manager::storage; +use creator_event_manager::storage_types::Match; +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_contract(None, 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") +} + +/// Create an event with a single match starting `match_time_offset` seconds +/// from now. Returns `(event_id, invite_code, match_id)`. +fn create_event_with_match( + env: &Env, + contract_id: &Address, + client: &CreatorEventManagerContractClient<'static>, + creator: &Address, + xlm_token: &Address, + match_time_offset: u64, +) -> (u64, Symbol, u64) { + fund(env, xlm_token, creator, FEE); + let (event_id, invite_code) = client.create_event(creator, &title(env), &desc(env), &10u32); + + let match_id = env.as_contract(contract_id, || { + 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, "Team A"), + String::from_str(env, "Team B"), + env.ledger().timestamp() + match_time_offset, + ); + storage::set_match(env, match_id, &match_record); + storage::add_event_match(env, event_id, match_id); + + let mut event = storage::get_event(env, event_id).expect("event exists"); + event.add_match(); + storage::set_event(env, event_id, &event); + match_id + }); + + (event_id, invite_code, match_id) +} + +fn read_match(env: &Env, contract_id: &Address, match_id: u64) -> Match { + env.as_contract(contract_id, || storage::get_match(env, match_id).unwrap()) +} + +#[test] +fn test_ai_agent_can_submit_result() { + let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, _invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 1_000); + + env.ledger().with_mut(|l| l.timestamp += 2_000); + client.submit_match_result(&ai_agent, &match_id, &Symbol::new(&env, "TEAM_A")); + + let m = read_match(&env, &contract_id, match_id); + assert!(m.result_submitted); + assert_eq!(m.winning_team, Some(0)); + assert_eq!(m.submitted_by, Some(ai_agent)); +} + +#[test] +#[should_panic(expected = "unauthorized")] +fn test_non_agent_cannot_submit() { + let (env, client, contract_id, _admin, _ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, _invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 1_000); + + env.ledger().with_mut(|l| l.timestamp += 2_000); + let imposter = Address::generate(&env); + client.submit_match_result(&imposter, &match_id, &Symbol::new(&env, "TEAM_A")); +} + +#[test] +#[should_panic(expected = "match_not_started")] +fn test_result_before_match_time_rejected() { + let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, _invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); + + // Do NOT advance time — the match has not started yet. + client.submit_match_result(&ai_agent, &match_id, &Symbol::new(&env, "TEAM_A")); +} + +#[test] +#[should_panic(expected = "result_already_submitted")] +fn test_duplicate_submission_rejected() { + let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, _invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 1_000); + + env.ledger().with_mut(|l| l.timestamp += 2_000); + client.submit_match_result(&ai_agent, &match_id, &Symbol::new(&env, "TEAM_A")); + // Second submission must be rejected. + client.submit_match_result(&ai_agent, &match_id, &Symbol::new(&env, "TEAM_B")); +} + +#[test] +#[should_panic(expected = "invalid_outcome")] +fn test_invalid_outcome_rejected() { + let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, _invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 1_000); + + env.ledger().with_mut(|l| l.timestamp += 2_000); + client.submit_match_result(&ai_agent, &match_id, &Symbol::new(&env, "NOT_A_TEAM")); +} + +#[test] +#[should_panic(expected = "match_not_found")] +fn test_unknown_match_rejected() { + let (env, client, _contract_id, _admin, ai_agent, _xlm_token) = setup(); + env.ledger().with_mut(|l| l.timestamp += 2_000); + client.submit_match_result(&ai_agent, &404u64, &Symbol::new(&env, "TEAM_A")); +} + +#[test] +fn test_predictions_marked_correct_and_incorrect() { + let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (_event_id, invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); + + let winner = Address::generate(&env); + let loser = Address::generate(&env); + client.join_event(&winner, &invite); + client.join_event(&loser, &invite); + let winner_pred = client.submit_prediction(&winner, &match_id, &Symbol::new(&env, "TEAM_A")); + let loser_pred = client.submit_prediction(&loser, &match_id, &Symbol::new(&env, "TEAM_B")); + + env.ledger().with_mut(|l| l.timestamp += 20_000); + client.submit_match_result(&ai_agent, &match_id, &Symbol::new(&env, "TEAM_A")); + + assert_eq!(client.get_prediction(&winner_pred).is_correct, Some(true)); + assert_eq!(client.get_prediction(&loser_pred).is_correct, Some(false)); +} + +#[test] +fn test_all_outcomes_work() { + let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + + let (_e1, _i1, m_a) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 1_000); + let (_e2, _i2, m_b) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 1_000); + let (_e3, _i3, m_d) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 1_000); + + env.ledger().with_mut(|l| l.timestamp += 2_000); + client.submit_match_result(&ai_agent, &m_a, &Symbol::new(&env, "TEAM_A")); + client.submit_match_result(&ai_agent, &m_b, &Symbol::new(&env, "TEAM_B")); + client.submit_match_result(&ai_agent, &m_d, &Symbol::new(&env, "DRAW")); + + assert_eq!(read_match(&env, &contract_id, m_a).winning_team, Some(0)); + assert_eq!(read_match(&env, &contract_id, m_b).winning_team, Some(1)); + assert_eq!(read_match(&env, &contract_id, m_d).winning_team, Some(2)); +} + +#[test] +fn test_full_prediction_flow_with_scoring() { + let (env, client, contract_id, _admin, ai_agent, xlm_token) = setup(); + let creator = Address::generate(&env); + let (event_id, invite, match_id) = + create_event_with_match(&env, &contract_id, &client, &creator, &xlm_token, 10_000); + + let alice = Address::generate(&env); + let bob = Address::generate(&env); + client.join_event(&alice, &invite); + client.join_event(&bob, &invite); + client.submit_prediction(&alice, &match_id, &Symbol::new(&env, "DRAW")); + client.submit_prediction(&bob, &match_id, &Symbol::new(&env, "TEAM_A")); + + env.ledger().with_mut(|l| l.timestamp += 20_000); + client.submit_match_result(&ai_agent, &match_id, &Symbol::new(&env, "DRAW")); + + // Alice predicted the winning outcome; Bob did not. + assert_eq!(client.get_user_score(&alice, &event_id), (1, 1)); + assert_eq!(client.get_user_score(&bob, &event_id), (0, 1)); + + // And the match is fully resolved. + let m = read_match(&env, &contract_id, match_id); + assert!(m.result_submitted); + assert_eq!(m.winning_team, Some(2)); +}