From 8fa2be8f7f76c83e39ebae90857af5ca76552c84 Mon Sep 17 00:00:00 2001 From: Ability Date: Fri, 23 Jan 2026 16:16:45 +0100 Subject: [PATCH] feat: implement event creation mechanism --- contracts/predictify-hybrid/src/events.rs | 130 +++++++++ contracts/predictify-hybrid/src/lib.rs | 189 +++++++++++++ contracts/predictify-hybrid/src/storage.rs | 164 +++++++++++ contracts/predictify-hybrid/src/test.rs | 171 +++++++++++ contracts/predictify-hybrid/src/types.rs | 257 +++++++++++++++++ contracts/predictify-hybrid/src/validation.rs | 266 ++++++++++++++++++ 6 files changed, 1177 insertions(+) diff --git a/contracts/predictify-hybrid/src/events.rs b/contracts/predictify-hybrid/src/events.rs index f313f8e3..4b53b502 100644 --- a/contracts/predictify-hybrid/src/events.rs +++ b/contracts/predictify-hybrid/src/events.rs @@ -102,6 +102,86 @@ pub struct MarketCreatedEvent { pub timestamp: u64, } +/// Event emitted when a new prediction event is successfully created. +/// +/// This event provides comprehensive information about newly created prediction events, +/// including event parameters, outcomes, administrative details, oracle configuration, +/// and timing. Essential for tracking event creation activity and building event indices. +/// +/// # Event Data +/// +/// Contains all critical event creation parameters: +/// - Event identification and description details +/// - Available outcomes for prediction +/// - Administrative and timing information +/// - Oracle provider information +/// - Creation timestamp for chronological ordering +/// +/// # Example Usage +/// +/// ```rust +/// # use soroban_sdk::{Env, Address, Symbol, String, Vec}; +/// # use predictify_hybrid::events::EventCreatedEvent; +/// # let env = Env::default(); +/// # let admin = Address::generate(&env); +/// +/// // Event creation event data +/// let event = EventCreatedEvent { +/// event_id: Symbol::new(&env, "evt_btc_100k"), +/// description: String::from_str(&env, "Will Bitcoin reach $100,000 by end of 2024?"), +/// outcomes: vec![ +/// &env, +/// String::from_str(&env, "Yes"), +/// String::from_str(&env, "No") +/// ], +/// creator: admin.clone(), +/// end_time: 1735689600, // Dec 31, 2024 +/// oracle_provider: String::from_str(&env, "Reflector"), +/// timestamp: env.ledger().timestamp(), +/// }; +/// +/// // Event provides complete event context +/// println!("New event: {}", event.description.to_string()); +/// println!("Event ID: {}", event.event_id.to_string()); +/// println!("Outcomes: {} options", event.outcomes.len()); +/// println!("Ends: {}", event.end_time); +/// println!("Oracle: {}", event.oracle_provider.to_string()); +/// ``` +/// +/// # Integration Points +/// +/// - **Event Indexing**: Build searchable event directories +/// - **Activity Feeds**: Display recent event creation activity +/// - **Analytics**: Track event creation patterns and trends +/// - **Notifications**: Alert users about new events in categories of interest +/// - **Audit Trails**: Maintain complete record of event creation +/// +/// # Event Timing +/// +/// Emitted immediately after successful event creation, providing: +/// - Real-time notification of new events +/// - Chronological ordering via timestamp +/// - Immediate availability for user interfaces +/// - Historical record for analytics and reporting +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EventCreatedEvent { + /// Unique event ID + pub event_id: Symbol, + /// Event description/question + pub description: String, + /// Available outcomes for prediction + pub outcomes: Vec, + /// Event creator (admin) + pub creator: Address, + /// Event end time (Unix timestamp) + pub end_time: u64, + /// Oracle provider type (e.g., "Reflector", "Pyth") + pub oracle_provider: String, + /// Creation timestamp + pub timestamp: u64, +} + /// Event emitted when a user successfully casts a vote on a prediction market. /// /// This event captures all details of voting activity, including voter identity, @@ -1237,6 +1317,56 @@ impl EventEmitter { Self::store_event(env, &symbol_short!("mkt_crt"), &event); } + /// Emit event created event when a new prediction event is created. + /// + /// This function emits an event when an admin successfully creates a new + /// prediction event, recording all relevant details for transparency and indexing. + /// + /// # Parameters + /// + /// - `env` - Soroban environment + /// - `event_id` - Unique event identifier + /// - `description` - Event description/question + /// - `outcomes` - Vector of possible outcomes + /// - `creator` - Admin address that created the event + /// - `end_time` - Unix timestamp when event ends + /// - `oracle_provider` - Name of the oracle provider (e.g., "Reflector") + /// + /// # Example + /// + /// ```rust + /// EventEmitter::emit_event_created( + /// &env, + /// &event_id, + /// &description, + /// &outcomes, + /// &admin, + /// end_time, + /// &String::from_str(&env, "Reflector"), + /// ); + /// ``` + pub fn emit_event_created( + env: &Env, + event_id: &Symbol, + description: &String, + outcomes: &Vec, + creator: &Address, + end_time: u64, + oracle_provider: &String, + ) { + let event = EventCreatedEvent { + event_id: event_id.clone(), + description: description.clone(), + outcomes: outcomes.clone(), + creator: creator.clone(), + end_time, + oracle_provider: oracle_provider.clone(), + timestamp: env.ledger().timestamp(), + }; + + Self::store_event(env, &symbol_short!("evt_crt"), &event); + } + /// Emit vote cast event pub fn emit_vote_cast( env: &Env, diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 2604f316..61d35b65 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -305,6 +305,195 @@ impl PredictifyHybrid { market_id } + /// Creates a new prediction event with specified parameters and oracle configuration. + /// + /// This function allows authorized administrators to create prediction events + /// with custom descriptions, possible outcomes, end time, and oracle integration. + /// Each event gets a unique identifier and is stored in persistent contract storage. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `admin` - The administrator address creating the event (must be authorized) + /// * `description` - The event description/question (must be non-empty, 10-500 chars) + /// * `end_time` - Unix timestamp when the event ends (must be in the future) + /// * `outcomes` - Vector of possible outcomes (minimum 2 required, all non-empty) + /// * `oracle_config` - Configuration for oracle integration (Reflector, Pyth, etc.) + /// + /// # Returns + /// + /// Returns a unique `Symbol` that serves as the event identifier for all future operations. + /// + /// # Panics + /// + /// This function will panic with specific errors if: + /// - `Error::Unauthorized` - Caller is not the contract admin + /// - `Error::InvalidQuestion` - Description is empty or invalid length + /// - `Error::InvalidOutcomes` - Less than 2 outcomes or any outcome is empty + /// - `Error::InvalidDuration` - End time is not in the future + /// - `Error::InvalidOracleConfig` - Oracle configuration is invalid + /// + /// # Example + /// + /// ```rust + /// # use soroban_sdk::{Env, Address, String, Vec}; + /// # use predictify_hybrid::{PredictifyHybrid, OracleConfig, OracleProvider}; + /// # let env = Env::default(); + /// # let admin = Address::generate(&env); + /// + /// let description = String::from_str(&env, "Will Bitcoin reach $100,000 by 2024?"); + /// let outcomes = vec![ + /// String::from_str(&env, "Yes"), + /// String::from_str(&env, "No") + /// ]; + /// let end_time = env.ledger().timestamp() + (30 * 24 * 60 * 60); // 30 days + /// let oracle_config = OracleConfig { + /// provider: OracleProvider::Reflector, + /// feed_id: String::from_str(&env, "BTC/USD"), + /// threshold: 100_000_00, + /// comparison: String::from_str(&env, "gt"), + /// }; + /// + /// let event_id = PredictifyHybrid::create_event( + /// env.clone(), + /// admin, + /// description, + /// end_time, + /// outcomes, + /// oracle_config + /// ); + /// ``` + /// + /// # Event State + /// + /// New events are created in `EventStatus::Active` state, allowing immediate betting. + /// The event will automatically transition to `EventStatus::Pending` when the end time expires. + /// + /// # Security + /// + /// - Only authenticated admins can create events + /// - All input parameters are validated before storage + /// - End time must be strictly in the future + /// - Oracle configuration is validated for supported providers + pub fn create_event( + env: Env, + admin: Address, + description: String, + end_time: u64, + outcomes: Vec, + oracle_config: OracleConfig, + ) -> Symbol { + // Authenticate that the caller is the admin + admin.require_auth(); + + // Verify the caller is an admin + let stored_admin: Address = env + .storage() + .persistent() + .get(&Symbol::new(&env, "Admin")) + .unwrap_or_else(|| { + panic!("Admin not set"); + }); + + if admin != stored_admin { + panic_with_error!(env, Error::Unauthorized); + } + + // Validate description + if description.is_empty() { + panic_with_error!(env, Error::InvalidQuestion); + } + if description.len() < 10 { + panic_with_error!(env, Error::InvalidQuestion); + } + + // Validate outcomes + if outcomes.len() < 2 { + panic_with_error!(env, Error::InvalidOutcomes); + } + + // Validate each outcome is non-empty + for outcome in outcomes.iter() { + if outcome.is_empty() { + panic_with_error!(env, Error::InvalidOutcome); + } + } + + // Validate end time is in the future + if end_time <= env.ledger().timestamp() { + panic_with_error!(env, Error::InvalidDuration); + } + + // Validate oracle configuration + if let Err(e) = oracle_config.validate(&env) { + panic_with_error!(env, e); + } + + // Generate unique event ID + let event_id = storage::EventStorage::generate_event_id(&env, &admin); + + // Create the event + let event = types::Event::new( + &env, + event_id.clone(), + admin.clone(), + description.clone(), + end_time, + outcomes.clone(), + oracle_config.clone(), + ); + + // Store the event + if let Err(e) = storage::EventStorage::store_event(&env, &event) { + panic_with_error!(env, e); + } + + // Get oracle provider name + let oracle_provider = match oracle_config.provider { + types::OracleProvider::Reflector => String::from_str(&env, "Reflector"), + types::OracleProvider::Pyth => String::from_str(&env, "Pyth"), + types::OracleProvider::BandProtocol => String::from_str(&env, "BandProtocol"), + types::OracleProvider::DIA => String::from_str(&env, "DIA"), + }; + + // Emit event created event + EventEmitter::emit_event_created( + &env, + &event_id, + &description, + &outcomes, + &admin, + end_time, + &oracle_provider, + ); + + event_id + } + + /// Retrieves a prediction event by its unique identifier. + /// + /// This function provides read-only access to event data including + /// description, outcomes, status, and oracle configuration. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `event_id` - Unique identifier of the event to retrieve + /// + /// # Returns + /// + /// Returns the `Event` struct if found, panics if event doesn't exist. + /// + /// # Panics + /// + /// This function will panic with `Error::MarketNotFound` if the event doesn't exist. + pub fn get_event(env: Env, event_id: Symbol) -> types::Event { + match storage::EventStorage::get_event(&env, &event_id) { + Ok(event) => event, + Err(e) => panic_with_error!(env, e), + } + } + /// Allows users to vote on a market outcome by staking tokens. /// /// This function enables users to participate in prediction markets by voting diff --git a/contracts/predictify-hybrid/src/storage.rs b/contracts/predictify-hybrid/src/storage.rs index a3d418d3..d98103d0 100644 --- a/contracts/predictify-hybrid/src/storage.rs +++ b/contracts/predictify-hybrid/src/storage.rs @@ -645,6 +645,170 @@ impl StorageUtils { } } +// ===== EVENT STORAGE ===== + +/// Event storage manager for prediction events. +/// +/// This struct provides storage operations for prediction events, including +/// storing new events, retrieving events by ID, checking event existence, +/// and generating unique event IDs. +/// +/// # Example Usage +/// +/// ```rust +/// # use soroban_sdk::{Env, Address, Symbol, String, Vec}; +/// # use predictify_hybrid::storage::EventStorage; +/// # use predictify_hybrid::types::{Event, OracleConfig, OracleProvider}; +/// # let env = Env::default(); +/// # let admin = Address::generate(&env); +/// +/// // Generate a unique event ID +/// let event_id = EventStorage::generate_event_id(&env, &admin); +/// +/// // Store an event +/// let event = Event::new( +/// &env, +/// event_id.clone(), +/// admin.clone(), +/// String::from_str(&env, "Will BTC reach $100k?"), +/// env.ledger().timestamp() + 86400, +/// Vec::from_array(&env, [ +/// String::from_str(&env, "Yes"), +/// String::from_str(&env, "No") +/// ]), +/// OracleConfig::new( +/// OracleProvider::Reflector, +/// String::from_str(&env, "BTC/USD"), +/// 100_000_00, +/// String::from_str(&env, "gt") +/// ), +/// ); +/// +/// EventStorage::store_event(&env, &event).expect("Failed to store event"); +/// +/// // Retrieve the event +/// let retrieved = EventStorage::get_event(&env, &event_id).expect("Event not found"); +/// ``` +pub struct EventStorage; + +impl EventStorage { + /// Store a new prediction event in persistent storage. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// * `event` - The event to store + /// + /// # Returns + /// + /// `Ok(())` if event was stored successfully, `Err(Error)` otherwise. + /// + /// # Storage Key + /// + /// Events are stored using their event_id as the key with an "evt_" prefix. + pub fn store_event(env: &Env, event: &crate::types::Event) -> Result<(), Error> { + let key = Symbol::new(env, &format!("evt_{:?}", event.event_id)); + env.storage().persistent().set(&key, event); + Ok(()) + } + + /// Retrieve a prediction event by its ID. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// * `event_id` - The unique event identifier + /// + /// # Returns + /// + /// `Ok(Event)` if event exists, `Err(Error::MarketNotFound)` otherwise. + pub fn get_event(env: &Env, event_id: &Symbol) -> Result { + let key = Symbol::new(env, &format!("evt_{:?}", event_id)); + env.storage() + .persistent() + .get(&key) + .ok_or(Error::MarketNotFound) + } + + /// Check if an event exists in storage. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// * `event_id` - The unique event identifier + /// + /// # Returns + /// + /// `true` if event exists, `false` otherwise. + pub fn event_exists(env: &Env, event_id: &Symbol) -> bool { + let key = Symbol::new(env, &format!("evt_{:?}", event_id)); + env.storage().persistent().has(&key) + } + + /// Generate a unique event ID. + /// + /// Generates a unique event ID using the creator's address and current timestamp. + /// This ensures collision resistance for event identifiers. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// * `creator` - The admin address creating the event + /// + /// # Returns + /// + /// A unique Symbol to be used as the event ID. + pub fn generate_event_id(env: &Env, creator: &soroban_sdk::Address) -> Symbol { + // Get a counter for uniqueness + let counter_key = Symbol::new(env, "event_counter"); + let counter: u64 = env.storage().persistent().get(&counter_key).unwrap_or(0); + + // Increment and store the counter + env.storage().persistent().set(&counter_key, &(counter + 1)); + + // Generate ID using timestamp and counter + let timestamp = env.ledger().timestamp(); + Symbol::new(env, &format!("evt_{}_{}", timestamp, counter)) + } + + /// Update an existing event in storage. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// * `event` - The updated event + /// + /// # Returns + /// + /// `Ok(())` if event was updated successfully, `Err(Error::MarketNotFound)` if event doesn't exist. + pub fn update_event(env: &Env, event: &crate::types::Event) -> Result<(), Error> { + let key = Symbol::new(env, &format!("evt_{:?}", event.event_id)); + + // Check if event exists + if !env.storage().persistent().has(&key) { + return Err(Error::MarketNotFound); + } + + env.storage().persistent().set(&key, event); + Ok(()) + } + + /// Remove an event from storage. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// * `event_id` - The unique event identifier + /// + /// # Note + /// + /// This permanently removes the event from storage. Use with caution. + pub fn remove_event(env: &Env, event_id: &Symbol) { + let key = Symbol::new(env, &format!("evt_{:?}", event_id)); + env.storage().persistent().remove(&key); + } +} + // ===== STORAGE TESTING ===== #[cfg(test)] diff --git a/contracts/predictify-hybrid/src/test.rs b/contracts/predictify-hybrid/src/test.rs index 058e8feb..f3afd6ad 100644 --- a/contracts/predictify-hybrid/src/test.rs +++ b/contracts/predictify-hybrid/src/test.rs @@ -1487,3 +1487,174 @@ fn test_manual_dispute_resolution_triggers_payout() { // since votes and bets are separate systems. This test verifies the resolution works. assert_eq!(market_after.state, MarketState::Resolved); } + +// ===== PREDICTION EVENT TESTS ===== + +#[test] +fn test_create_event_successful() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let description = String::from_str(&test.env, "Will ETH flip BTC by 2030?"); + let end_time = test.env.ledger().timestamp() + (365 * 24 * 60 * 60); + let outcomes = vec![ + &test.env, + String::from_str(&test.env, "Yes"), + String::from_str(&test.env, "No"), + ]; + let oracle_config = OracleConfig { + provider: OracleProvider::Reflector, + feed_id: String::from_str(&test.env, "ETH/BTC"), + threshold: 100_000_000, + comparison: String::from_str(&test.env, "gt"), + }; + + test.env.mock_all_auths(); + let event_id = client.create_event( + &test.admin, + &description, + &end_time, + &outcomes, + &oracle_config, + ); + + // Verify event storage + let event = client.get_event(&event_id); + assert_eq!(event.description, description); + assert_eq!(event.outcomes.len(), 2); + assert_eq!(event.creator, test.admin); + assert_eq!(event.status, types::EventStatus::Active); +} + +#[test] +#[should_panic(expected = "Error(Contract, #100)")] // Unauthorized +fn test_create_event_unauthorized() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let description = String::from_str(&test.env, "Test Event"); + let end_time = test.env.ledger().timestamp() + 86400; + let outcomes = vec![&test.env, String::from_str(&test.env, "A"), String::from_str(&test.env, "B")]; + let oracle_config = OracleConfig { + provider: OracleProvider::Reflector, + feed_id: String::from_str(&test.env, "TEST"), + threshold: 100, + comparison: String::from_str(&test.env, "eq"), + }; + + // Use non-admin user + test.env.mock_all_auths(); + client.create_event( + &test.user, // Not admin + &description, + &end_time, + &outcomes, + &oracle_config, + ); +} + +#[test] +#[should_panic(expected = "Error(Contract, #300)")] // InvalidQuestion +fn test_create_event_invalid_description() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let end_time = test.env.ledger().timestamp() + 86400; + let outcomes = vec![&test.env, String::from_str(&test.env, "A"), String::from_str(&test.env, "B")]; + let oracle_config = OracleConfig { + provider: OracleProvider::Reflector, + feed_id: String::from_str(&test.env, "TEST"), + threshold: 100, + comparison: String::from_str(&test.env, "eq"), + }; + + test.env.mock_all_auths(); + client.create_event( + &test.admin, + &String::from_str(&test.env, ""), // Empty description + &end_time, + &outcomes, + &oracle_config, + ); +} + +#[test] +#[should_panic(expected = "Error(Contract, #302)")] // InvalidDuration +fn test_create_event_invalid_end_time() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let description = String::from_str(&test.env, "Valid Description"); + // Past timestamp + let end_time = test.env.ledger().timestamp() - 1; + let outcomes = vec![&test.env, String::from_str(&test.env, "A"), String::from_str(&test.env, "B")]; + let oracle_config = OracleConfig { + provider: OracleProvider::Reflector, + feed_id: String::from_str(&test.env, "TEST"), + threshold: 100, + comparison: String::from_str(&test.env, "eq"), + }; + + test.env.mock_all_auths(); + client.create_event( + &test.admin, + &description, + &end_time, + &outcomes, + &oracle_config, + ); +} + +#[test] +#[should_panic(expected = "Error(Contract, #301)")] // InvalidOutcomes +fn test_create_event_invalid_outcomes() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let description = String::from_str(&test.env, "Valid Description"); + let end_time = test.env.ledger().timestamp() + 86400; + // Single outcome (need at least 2) + let outcomes = vec![&test.env, String::from_str(&test.env, "A")]; + let oracle_config = OracleConfig { + provider: OracleProvider::Reflector, + feed_id: String::from_str(&test.env, "TEST"), + threshold: 100, + comparison: String::from_str(&test.env, "eq"), + }; + + test.env.mock_all_auths(); + client.create_event( + &test.admin, + &description, + &end_time, + &outcomes, + &oracle_config, + ); +} + +#[test] +#[should_panic(expected = "Error(Contract, #201)")] // InvalidOracleConfig +fn test_create_event_invalid_oracle() { + let test = PredictifyTest::setup(); + let client = PredictifyHybridClient::new(&test.env, &test.contract_id); + + let description = String::from_str(&test.env, "Valid Description"); + let end_time = test.env.ledger().timestamp() + 86400; + let outcomes = vec![&test.env, String::from_str(&test.env, "A"), String::from_str(&test.env, "B")]; + // Invalid comparison operator + let oracle_config = OracleConfig { + provider: OracleProvider::Reflector, + feed_id: String::from_str(&test.env, "TEST"), + threshold: 100, + comparison: String::from_str(&test.env, "invalid_op"), + }; + + test.env.mock_all_auths(); + client.create_event( + &test.admin, + &description, + &end_time, + &outcomes, + &oracle_config, + ); +} diff --git a/contracts/predictify-hybrid/src/types.rs b/contracts/predictify-hybrid/src/types.rs index d26e0f1b..e60293a9 100644 --- a/contracts/predictify-hybrid/src/types.rs +++ b/contracts/predictify-hybrid/src/types.rs @@ -139,6 +139,263 @@ pub enum MarketState { Cancelled, } +// ===== PREDICTION EVENT TYPES ===== + +/// Status of a prediction event throughout its lifecycle. +/// +/// This enum defines the various states a prediction event can be in, from initial +/// creation through final resolution and closure. Each state represents a distinct +/// phase with specific business rules and available operations. +/// +/// # State Lifecycle +/// +/// The typical event progression follows this pattern: +/// ```text +/// Active → Pending → Resolved +/// ``` +/// +/// **Alternative flows:** +/// - **Cancellation**: `Active → Cancelled` (admin cancellation) +/// +/// # State Descriptions +/// +/// **Active**: Event is live and accepting user predictions +/// - Users can place bets and votes +/// - Event parameters are fixed +/// - Prediction period is ongoing +/// +/// **Pending**: Event prediction period has ended +/// - No new predictions accepted +/// - Oracle resolution can be triggered +/// - Waiting for outcome determination +/// +/// **Resolved**: Event outcome has been determined +/// - Final outcome is established +/// - Payouts can be calculated and distributed +/// - Resolution method recorded +/// +/// **Cancelled**: Event has been cancelled +/// - Administrative cancellation +/// - Stakes returned to participants +/// - No winner determination +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EventStatus { + /// Event is active and accepting predictions + Active, + /// Event has ended, waiting for resolution + Pending, + /// Event has been resolved with final outcome + Resolved, + /// Event was cancelled by admin + Cancelled, +} + +/// Comprehensive prediction event structure for managing prediction markets. +/// +/// This structure contains all data necessary to manage a prediction event throughout +/// its entire lifecycle, from creation through resolution and payout distribution. +/// It represents a single prediction question that users can bet on. +/// +/// # Core Event Components +/// +/// **Event Identity:** +/// - **event_id**: Unique identifier for the event +/// - **creator**: Admin who created the event +/// - **description**: The prediction question being resolved +/// - **outcomes**: Available outcomes users can predict +/// +/// **Timing:** +/// - **end_time**: When the prediction period concludes +/// - **created_at**: When the event was created +/// +/// **Resolution:** +/// - **oracle_source**: Configuration for oracle-based resolution +/// - **status**: Current lifecycle state +/// - **resolved_outcome**: Final outcome (if resolved) +/// +/// # Example Usage +/// +/// ```rust +/// # use soroban_sdk::{Env, Address, String, Vec, Symbol}; +/// # use predictify_hybrid::types::{Event, EventStatus, OracleConfig, OracleProvider}; +/// # let env = Env::default(); +/// # let admin = Address::generate(&env); +/// +/// // Create a new prediction event +/// let event = Event::new( +/// &env, +/// Symbol::new(&env, "evt_btc_100k"), +/// admin.clone(), +/// String::from_str(&env, "Will BTC reach $100,000 by December 31, 2024?"), +/// env.ledger().timestamp() + (30 * 24 * 60 * 60), // 30 days +/// Vec::from_array(&env, [ +/// String::from_str(&env, "Yes"), +/// String::from_str(&env, "No") +/// ]), +/// OracleConfig::new( +/// OracleProvider::Reflector, +/// String::from_str(&env, "BTC/USD"), +/// 100_000_00, // $100,000 +/// String::from_str(&env, "gt") +/// ), +/// ); +/// +/// // Check event status +/// if event.is_active(env.ledger().timestamp()) { +/// println!("Event is active and accepting predictions"); +/// } +/// ``` +/// +/// # Security Considerations +/// +/// - Events can only be created by authorized admins +/// - End time must be validated as future timestamp +/// - Outcomes must be non-empty and contain at least 2 options +/// - Oracle configuration must be valid for the network +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Event { + /// Unique event identifier + pub event_id: Symbol, + /// Event creator (admin address) + pub creator: Address, + /// Event description/prediction question + pub description: String, + /// Unix timestamp when prediction period ends + pub end_time: u64, + /// Possible outcomes for this event + pub outcomes: Vec, + /// Oracle configuration for result verification + pub oracle_source: OracleConfig, + /// Current event status + pub status: EventStatus, + /// Resolved outcome (set after resolution) + pub resolved_outcome: Option, + /// Event creation timestamp + pub created_at: u64, +} + +impl Event { + /// Create a new prediction event. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// * `event_id` - Unique event identifier + /// * `creator` - Admin address creating the event + /// * `description` - Event description/question + /// * `end_time` - Unix timestamp when event ends + /// * `outcomes` - Vector of possible outcomes + /// * `oracle_source` - Oracle configuration + /// + /// # Returns + /// + /// A new Event instance with Active status. + pub fn new( + env: &Env, + event_id: Symbol, + creator: Address, + description: String, + end_time: u64, + outcomes: Vec, + oracle_source: OracleConfig, + ) -> Self { + Self { + event_id, + creator, + description, + end_time, + outcomes, + oracle_source, + status: EventStatus::Active, + resolved_outcome: None, + created_at: env.ledger().timestamp(), + } + } + + /// Check if the event is currently active (accepting predictions). + /// + /// # Parameters + /// + /// * `current_time` - Current blockchain timestamp + /// + /// # Returns + /// + /// `true` if event is active and not past end time, `false` otherwise. + pub fn is_active(&self, current_time: u64) -> bool { + self.status == EventStatus::Active && current_time < self.end_time + } + + /// Check if the event has ended (past end time). + /// + /// # Parameters + /// + /// * `current_time` - Current blockchain timestamp + /// + /// # Returns + /// + /// `true` if current time is past event end time, `false` otherwise. + pub fn has_ended(&self, current_time: u64) -> bool { + current_time >= self.end_time + } + + /// Check if the event has been resolved. + /// + /// # Returns + /// + /// `true` if event has a resolved outcome, `false` otherwise. + pub fn is_resolved(&self) -> bool { + self.resolved_outcome.is_some() && self.status == EventStatus::Resolved + } + + /// Validate event parameters. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment + /// + /// # Returns + /// + /// `Ok(())` if all parameters are valid, `Err(Error)` otherwise. + /// + /// # Validation Rules + /// + /// - Description must not be empty + /// - Must have at least 2 outcomes + /// - All outcomes must be non-empty + /// - End time must be in the future + /// - Oracle configuration must be valid + pub fn validate(&self, env: &Env) -> Result<(), crate::Error> { + // Validate description + if self.description.is_empty() { + return Err(crate::Error::InvalidQuestion); + } + + // Validate outcomes (minimum 2) + if self.outcomes.len() < 2 { + return Err(crate::Error::InvalidOutcomes); + } + + // Validate each outcome is non-empty + for outcome in self.outcomes.iter() { + if outcome.is_empty() { + return Err(crate::Error::InvalidOutcome); + } + } + + // Validate end time is in the future + if self.end_time <= env.ledger().timestamp() { + return Err(crate::Error::InvalidDuration); + } + + // Validate oracle config + self.oracle_source.validate(env)?; + + Ok(()) + } +} + // ===== ORACLE TYPES ===== /// Enumeration of supported oracle providers for price feed data. diff --git a/contracts/predictify-hybrid/src/validation.rs b/contracts/predictify-hybrid/src/validation.rs index d6b594e1..ff921a46 100644 --- a/contracts/predictify-hybrid/src/validation.rs +++ b/contracts/predictify-hybrid/src/validation.rs @@ -1291,6 +1291,272 @@ impl InputValidator { } } +// ===== EVENT VALIDATION ===== + +/// Comprehensive event validation utilities for prediction event operations. +/// +/// This utility class provides specialized validation operations for prediction events, +/// including end time validation, outcomes validation, description validation, +/// and comprehensive event creation parameter validation. +/// +/// # Core Functionality +/// +/// **End Time Validation:** +/// - Validate end time is in the future +/// - Check for reasonable time bounds +/// +/// **Outcomes Validation:** +/// - Validate minimum outcome count (at least 2) +/// - Validate each outcome is non-empty +/// - Check for unique outcomes +/// +/// **Description Validation:** +/// - Validate description is non-empty +/// - Check for minimum and maximum length +/// +/// **Comprehensive Validation:** +/// - Validate all event creation parameters at once +/// - Return detailed validation results +/// +/// # Example Usage +/// +/// ```rust +/// # use soroban_sdk::{Env, String, Vec}; +/// # use predictify_hybrid::validation::{EventValidator, ValidationError}; +/// # use predictify_hybrid::types::{OracleConfig, OracleProvider}; +/// # let env = Env::default(); +/// +/// // Validate end time +/// let future_time = env.ledger().timestamp() + 86400; // 1 day from now +/// match EventValidator::validate_end_time(&env, future_time) { +/// Ok(()) => println!("End time is valid"), +/// Err(e) => println!("End time validation failed: {:?}", e), +/// } +/// +/// // Validate outcomes +/// let outcomes = Vec::from_array(&env, [ +/// String::from_str(&env, "Yes"), +/// String::from_str(&env, "No") +/// ]); +/// match EventValidator::validate_outcomes(&env, &outcomes) { +/// Ok(()) => println!("Outcomes are valid"), +/// Err(e) => println!("Outcomes validation failed: {:?}", e), +/// } +/// ``` +pub struct EventValidator; + +impl EventValidator { + /// Validate that event end time is in the future. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `end_time` - Unix timestamp when event should end + /// + /// # Returns + /// + /// `Ok(())` if end time is in the future, `Err(ValidationError)` otherwise. + /// + /// # Validation Rules + /// + /// - End time must be strictly greater than current timestamp + /// - End time should be reasonable (not too far in the future) + pub fn validate_end_time(env: &Env, end_time: u64) -> Result<(), ValidationError> { + let current_time = env.ledger().timestamp(); + + // End time must be in the future + if end_time <= current_time { + return Err(ValidationError::InvalidTimestamp); + } + + // Optional: Check if end time is not unreasonably far in the future + // (e.g., more than 365 days) + let max_future = current_time + (365 * 24 * 60 * 60); + if end_time > max_future { + return Err(ValidationError::TimestampOutOfBounds); + } + + Ok(()) + } + + /// Validate event outcomes list. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `outcomes` - Vector of possible outcomes for the event + /// + /// # Returns + /// + /// `Ok(())` if outcomes are valid, `Err(ValidationError)` otherwise. + /// + /// # Validation Rules + /// + /// - Must have at least 2 outcomes + /// - Each outcome must be non-empty + /// - Maximum 10 outcomes allowed + pub fn validate_outcomes(env: &Env, outcomes: &Vec) -> Result<(), ValidationError> { + // Must have at least 2 outcomes + if outcomes.len() < 2 { + return Err(ValidationError::ArrayTooSmall); + } + + // Maximum 10 outcomes + if outcomes.len() > 10 { + return Err(ValidationError::ArrayTooLarge); + } + + // Each outcome must be non-empty + for outcome in outcomes.iter() { + if outcome.is_empty() { + return Err(ValidationError::InvalidOutcomeFormat); + } + // Minimum 1 character + if outcome.len() < 1 { + return Err(ValidationError::InvalidOutcomeFormat); + } + } + + Ok(()) + } + + /// Validate event description. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `description` - Event description/question string + /// + /// # Returns + /// + /// `Ok(())` if description is valid, `Err(ValidationError)` otherwise. + /// + /// # Validation Rules + /// + /// - Description must not be empty + /// - Minimum length of 10 characters + /// - Maximum length of 500 characters + pub fn validate_description(env: &Env, description: &String) -> Result<(), ValidationError> { + // Must not be empty + if description.is_empty() { + return Err(ValidationError::InvalidQuestionFormat); + } + + // Minimum 10 characters + if description.len() < 10 { + return Err(ValidationError::StringTooShort); + } + + // Maximum 500 characters + if description.len() > 500 { + return Err(ValidationError::StringTooLong); + } + + Ok(()) + } + + /// Comprehensive event creation parameter validation. + /// + /// This function validates all parameters required for event creation, + /// providing a single entry point for complete validation. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `description` - Event description/question string + /// * `end_time` - Unix timestamp when event should end + /// * `outcomes` - Vector of possible outcomes for the event + /// * `oracle_config` - Oracle configuration for result verification + /// + /// # Returns + /// + /// `Ok(())` if all parameters are valid, `Err(ValidationError)` otherwise. + /// + /// # Validation Performed + /// + /// 1. Description validation (non-empty, proper length) + /// 2. End time validation (must be in the future) + /// 3. Outcomes validation (at least 2, all non-empty) + /// 4. Oracle configuration validation + pub fn validate_event_creation_params( + env: &Env, + description: &String, + end_time: u64, + outcomes: &Vec, + oracle_config: &OracleConfig, + ) -> Result<(), ValidationError> { + // Validate description + Self::validate_description(env, description)?; + + // Validate end time + Self::validate_end_time(env, end_time)?; + + // Validate outcomes + Self::validate_outcomes(env, outcomes)?; + + // Validate oracle configuration + oracle_config.validate(env).map_err(|_| ValidationError::InvalidOracle)?; + + Ok(()) + } + + /// Validate event creation parameters and return detailed result. + /// + /// # Parameters + /// + /// * `env` - The Soroban environment for blockchain operations + /// * `description` - Event description/question string + /// * `end_time` - Unix timestamp when event should end + /// * `outcomes` - Vector of possible outcomes for the event + /// * `oracle_config` - Oracle configuration for result verification + /// + /// # Returns + /// + /// A ValidationResult with detailed error, warning, and recommendation counts. + pub fn validate_event_creation_comprehensive( + env: &Env, + description: &String, + end_time: u64, + outcomes: &Vec, + oracle_config: &OracleConfig, + ) -> ValidationResult { + let mut result = ValidationResult::valid(); + + // Validate description + if Self::validate_description(env, description).is_err() { + result.add_error(); + } else if description.len() < 20 { + result.add_warning(); // Description is quite short + } + + // Validate end time + if Self::validate_end_time(env, end_time).is_err() { + result.add_error(); + } else { + // Check if end time is less than 1 day away + let current_time = env.ledger().timestamp(); + if end_time < current_time + (24 * 60 * 60) { + result.add_warning(); // Very short event duration + } + } + + // Validate outcomes + if Self::validate_outcomes(env, outcomes).is_err() { + result.add_error(); + } else if outcomes.len() == 2 { + // Binary outcome is fine, but more options might provide more granularity + result.add_recommendation(); + } + + // Validate oracle configuration + if oracle_config.validate(env).is_err() { + result.add_error(); + } + + result + } +} + // ===== MARKET VALIDATION ===== /// Comprehensive market validation utilities for prediction market operations. ///