Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions contracts/creator-event-manager/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
#![no_std]

pub mod admin;
pub mod storage;
pub mod storage_types;
pub mod verification;
pub mod prediction;
pub mod r#match;
mod event;
mod invite;
pub mod r#match;
pub mod prediction;
pub mod storage;
pub mod storage_types;
mod token;
pub mod verification;
pub mod views;

use soroban_sdk::{contract, contractimpl, Address, Env, String, Symbol, Vec};

use admin::AdminError;
use event::EventError;
use storage_types::{Event, Prediction};
use verification::VerificationError;
use views::EventStatistics;

// ---------------------------------------------------------------------------
// Contract entry point
Expand Down Expand Up @@ -296,6 +298,22 @@ impl CreatorEventManagerContract {
}
}

/// Return aggregate statistics for an event.
///
/// The returned [`EventStatistics`] summarizes participant count, match
/// count, prediction volume, match result completion, and verified winner
/// count for the requested event.
///
/// # Panics
/// * `"event_not_found"` — no event exists with the given ID.
pub fn get_event_statistics(env: Env, event_id: u64) -> EventStatistics {
match views::get_event_statistics(&env, event_id) {
Ok(statistics) => statistics,
Err(EventError::EventNotFound) => panic!("event_not_found"),
Err(_) => panic!("unexpected_error"),
}
}

/// Return the number of matches currently stored for an event.
///
/// This is a lightweight read that loads only the event record, not the
Expand Down
75 changes: 75 additions & 0 deletions contracts/creator-event-manager/src/views.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Read-only aggregate views for creator events.
//!
//! This module keeps derived, dashboard-friendly statistics out of mutation
//! paths so callers can inspect an event's participation, prediction volume,
//! and completion state in a single contract view.

use soroban_sdk::{contracttype, Env};

use crate::event::{self, EventError};
use crate::storage;

/// Aggregate statistics for one creator event.
///
/// Returned by `get_event_statistics(event_id)` as a compact summary of the
/// event's current on-chain state:
/// * `event_id` — event being summarized.
/// * `participant_count` — number of joined participants stored on the event.
/// * `match_count` — number of matches stored on the event.
/// * `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 {
pub event_id: u64,
pub participant_count: u32,
pub match_count: u32,
pub total_predictions: u32,
pub all_matches_resolved: bool,
pub winners_verified: bool,
pub winner_count: u32,
}

/// 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.
pub fn get_event_statistics(env: &Env, event_id: u64) -> Result<EventStatistics, EventError> {
let event = event::get_event(env, event_id)?;
let match_ids = storage::get_event_matches(env, event_id);

let mut total_predictions: u32 = 0;
let mut resolved_matches: u32 = 0;

for match_id in match_ids.iter() {
total_predictions =
total_predictions.saturating_add(storage::get_match_predictions(env, match_id).len());

if let Ok(match_record) = storage::get_match(env, match_id) {
if match_record.result_submitted {
resolved_matches = resolved_matches.saturating_add(1);
}
}
}

let winner_count = storage::get_event_winners(env, event_id).len();
let all_matches_resolved = event.match_count > 0
&& match_ids.len() == event.match_count
&& resolved_matches == event.match_count;

Ok(EventStatistics {
event_id,
participant_count: event.participant_count,
match_count: event.match_count,
total_predictions,
all_matches_resolved,
winners_verified: winner_count > 0,
winner_count,
})
}
1 change: 1 addition & 0 deletions contracts/creator-event-manager/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ mod match_tests;
mod prediction_tests;
mod storage_types_tests;
mod verification_tests;
mod views_tests;
175 changes: 175 additions & 0 deletions contracts/creator-event-manager/tests/views_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/// 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::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,
) {
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, 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, "World Cup 2026 Predictions")
}

fn desc(env: &Env) -> String {
String::from_str(env, "Predict the matches of the 2026 World Cup.")
}

fn add_match(env: &Env, event_id: u64, submitted: bool) -> u64 {
let match_id = storage::next_match_id(env);
let mut match_record = Match::new(
match_id,
event_id,
String::from_str(env, "Team A"),
String::from_str(env, "Team B"),
env.ledger().timestamp() + 10_000,
);

if submitted {
match_record
.submit_result(
MatchResult::TeamA,
Address::generate(env),
env.ledger().timestamp(),
)
.expect("result can be submitted");
}

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
}

fn add_prediction(env: &Env, event_id: u64, match_id: u64, predictor: &Address) {
let prediction_id = storage::next_prediction_id(env);
let prediction = Prediction::new(
prediction_id,
match_id,
event_id,
predictor.clone(),
Symbol::new(env, "TEAM_A"),
env.ledger().timestamp(),
);
storage::set_prediction(env, prediction_id, &prediction);
storage::add_match_prediction(env, match_id, prediction_id);
storage::add_user_prediction(env, predictor, event_id, prediction_id);
}

#[test]
fn test_event_statistics_are_accurate() {
let (env, client, contract_id, xlm_token) = setup();
let creator = Address::generate(&env);
let user_one = Address::generate(&env);
let user_two = Address::generate(&env);
fund(&env, &xlm_token, &creator, FEE);

let (event_id, invite_code) = client.create_event(&creator, &title(&env), &desc(&env), &5u32);
client.join_event(&user_one, &invite_code);
client.join_event(&user_two, &invite_code);

env.as_contract(&contract_id, || {
let first_match = add_match(&env, event_id, false);
let second_match = add_match(&env, event_id, false);

add_prediction(&env, event_id, first_match, &user_one);
add_prediction(&env, event_id, first_match, &user_two);
add_prediction(&env, event_id, second_match, &user_one);
});

let statistics = client.get_event_statistics(&event_id);
assert_eq!(statistics.event_id, event_id);
assert_eq!(statistics.participant_count, 2);
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 (event_id, _) = client.create_event(&creator, &title(&env), &desc(&env), &5u32);

env.as_contract(&contract_id, || {
add_match(&env, event_id, true);
add_match(&env, event_id, false);
});

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() {
let mut match_record = storage::get_match(&env, match_id).expect("match exists");
if !match_record.result_submitted {
match_record
.submit_result(
MatchResult::TeamA,
Address::generate(&env),
env.ledger().timestamp(),
)
.expect("result can be submitted");
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]
#[should_panic(expected = "event_not_found")]
fn test_event_statistics_missing_event_panics() {
let (_env, client, _contract_id, _xlm_token) = setup();
client.get_event_statistics(&999u64);
}
Loading