From 9307e2958c07c6de03019524faae3fd1ea0d2b56 Mon Sep 17 00:00:00 2001 From: 1nonlypiece Date: Sat, 5 Jul 2025 10:57:48 +0530 Subject: [PATCH 1/4] docs: Add comprehensive types system documentation for Predictify Hybrid contract, detailing architecture, usage patterns, validation, and best practices --- contracts/predictify-hybrid/TYPES_SYSTEM.md | 541 ++++++++++++++++++++ 1 file changed, 541 insertions(+) create mode 100644 contracts/predictify-hybrid/TYPES_SYSTEM.md diff --git a/contracts/predictify-hybrid/TYPES_SYSTEM.md b/contracts/predictify-hybrid/TYPES_SYSTEM.md new file mode 100644 index 00000000..2b54f3cd --- /dev/null +++ b/contracts/predictify-hybrid/TYPES_SYSTEM.md @@ -0,0 +1,541 @@ +# Predictify Hybrid Types System + +## Overview + +The Predictify Hybrid contract now features a comprehensive, organized type system that centralizes all data structures and provides better organization, validation, and maintainability. This document outlines the architecture, usage patterns, and best practices for working with the types system. + +## Architecture + +### Type Categories + +Types are organized into logical categories for better understanding and maintenance: + +1. **Oracle Types** - Oracle providers, configurations, and data structures +2. **Market Types** - Market data structures and state management +3. **Price Types** - Price data and validation structures +4. **Validation Types** - Input validation and business logic types +5. **Utility Types** - Helper types and conversion utilities + +### Core Components + +#### 1. Oracle Types + +**OracleProvider Enum** +```rust +pub enum OracleProvider { + BandProtocol, + DIA, + Reflector, + Pyth, +} +``` + +**OracleConfig Struct** +```rust +pub struct OracleConfig { + pub provider: OracleProvider, + pub feed_id: String, + pub threshold: i128, + pub comparison: String, +} +``` + +#### 2. Market Types + +**Market Struct** +```rust +pub struct Market { + pub admin: Address, + pub question: String, + pub outcomes: Vec, + pub end_time: u64, + pub oracle_config: OracleConfig, + // ... other fields +} +``` + +#### 3. Price Types + +**PythPrice Struct** +```rust +pub struct PythPrice { + pub price: i128, + pub conf: u64, + pub expo: i32, + pub publish_time: u64, +} +``` + +**ReflectorPriceData Struct** +```rust +pub struct ReflectorPriceData { + pub price: i128, + pub timestamp: u64, +} +``` + +## Usage Patterns + +### 1. Creating Oracle Configurations + +```rust +use types::{OracleProvider, OracleConfig}; + +let oracle_config = OracleConfig::new( + OracleProvider::Pyth, + String::from_str(&env, "BTC/USD"), + 2500000, // $25,000 threshold + String::from_str(&env, "gt"), // greater than +); + +// Validate the configuration +oracle_config.validate(&env)?; +``` + +### 2. Creating Markets + +```rust +use types::{Market, OracleConfig, OracleProvider}; + +let market = Market::new( + &env, + admin, + question, + outcomes, + end_time, + oracle_config, +); + +// Validate market parameters +market.validate(&env)?; +``` + +### 3. Market State Management + +```rust +use types::MarketState; + +let state = MarketState::from_market(&market, current_time); + +if state.is_active() { + // Market is accepting votes +} else if state.has_ended() { + // Market has ended +} else if state.is_resolved() { + // Market is resolved +} +``` + +### 4. Oracle Result Handling + +```rust +use types::OracleResult; + +let result = OracleResult::price(2500000); + +if result.is_available() { + if let Some(price) = result.get_price() { + // Use the price + } +} +``` + +## Type Validation + +### Built-in Validation + +All types include built-in validation methods: + +```rust +// Oracle configuration validation +oracle_config.validate(&env)?; + +// Market validation +market.validate(&env)?; + +// Price validation +pyth_price.validate()?; +``` + +### Validation Helpers + +The types module provides validation helper functions: + +```rust +use types::validation; + +// Validate oracle provider +validation::validate_oracle_provider(&OracleProvider::Pyth)?; + +// Validate price +validation::validate_price(2500000)?; + +// Validate stake +validation::validate_stake(stake, min_stake)?; + +// Validate duration +validation::validate_duration(30)?; +``` + +## Type Conversion + +### Conversion Helpers + +```rust +use types::conversion; + +// Convert string to oracle provider +let provider = conversion::string_to_oracle_provider("pyth") + .ok_or(Error::InvalidOracleConfig)?; + +// Convert oracle provider to string +let provider_name = conversion::oracle_provider_to_string(&provider); + +// Validate comparison operator +conversion::validate_comparison(&comparison, &env)?; +``` + +## Market Operations + +### Market State Queries + +```rust +// Check if market is active +if market.is_active(current_time) { + // Accept votes +} + +// Check if market has ended +if market.has_ended(current_time) { + // Resolve market +} + +// Check if market is resolved +if market.is_resolved() { + // Allow claims +} +``` + +### User Operations + +```rust +// Get user's vote +let user_vote = market.get_user_vote(&user); + +// Get user's stake +let user_stake = market.get_user_stake(&user); + +// Check if user has claimed +let has_claimed = market.has_user_claimed(&user); + +// Get user's dispute stake +let dispute_stake = market.get_user_dispute_stake(&user); +``` + +### Market Modifications + +```rust +// Add vote and stake +market.add_vote(user, outcome, stake); + +// Add dispute stake +market.add_dispute_stake(user, stake); + +// Mark user as claimed +market.mark_claimed(user); + +// Set oracle result +market.set_oracle_result(result); + +// Set winning outcome +market.set_winning_outcome(outcome); + +// Mark fees as collected +market.mark_fees_collected(); +``` + +### Market Calculations + +```rust +// Get total dispute stakes +let total_disputes = market.total_dispute_stakes(); + +// Get winning stake total +let winning_total = market.winning_stake_total(); +``` + +## Oracle Integration + +### Oracle Provider Support + +```rust +// Check if provider is supported +if oracle_provider.is_supported() { + // Use the provider +} + +// Get provider name +let name = oracle_provider.name(); + +// Get default feed format +let format = oracle_provider.default_feed_format(); +``` + +### Oracle Configuration + +```rust +// Check comparison operators +if oracle_config.is_greater_than(&env) { + // Handle greater than comparison +} else if oracle_config.is_less_than(&env) { + // Handle less than comparison +} else if oracle_config.is_equal_to(&env) { + // Handle equal to comparison +} +``` + +## Price Data Handling + +### Pyth Price Data + +```rust +let pyth_price = PythPrice::new(2500000, 1000, -2, timestamp); + +// Get price in cents +let price_cents = pyth_price.price_in_cents(); + +// Check if price is stale +if pyth_price.is_stale(current_time, max_age) { + // Handle stale price +} + +// Validate price data +pyth_price.validate()?; +``` + +### Reflector Price Data + +```rust +let reflector_price = ReflectorPriceData::new(2500000, timestamp); + +// Get price in cents +let price_cents = reflector_price.price_in_cents(); + +// Check if price is stale +if reflector_price.is_stale(current_time, max_age) { + // Handle stale price +} + +// Validate price data +reflector_price.validate()?; +``` + +## Validation Types + +### Market Creation Parameters + +```rust +let params = MarketCreationParams::new( + admin, + question, + outcomes, + duration_days, + oracle_config, +); + +// Validate all parameters +params.validate(&env)?; + +// Calculate end time +let end_time = params.calculate_end_time(&env); +``` + +### Vote Parameters + +```rust +let vote_params = VoteParams::new(user, outcome, stake); + +// Validate vote parameters +vote_params.validate(&env, &market)?; +``` + +## Best Practices + +### 1. Always Validate Types + +```rust +// ❌ Don't skip validation +let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); + +// ✅ Always validate +let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); +market.validate(&env)?; +``` + +### 2. Use Type-Safe Operations + +```rust +// ❌ Manual state checking +if current_time < market.end_time && market.winning_outcome.is_none() { + // Market is active +} + +// ✅ Use type-safe methods +if market.is_active(current_time) { + // Market is active +} +``` + +### 3. Leverage Built-in Methods + +```rust +// ❌ Manual calculations +let mut total = 0; +for (user, outcome) in market.votes.iter() { + if &outcome == winning_outcome { + total += market.stakes.get(user.clone()).unwrap_or(0); + } +} + +// ✅ Use built-in methods +let total = market.winning_stake_total(); +``` + +### 4. Use Validation Helpers + +```rust +// ❌ Manual validation +if stake < min_stake { + return Err(Error::InsufficientStake); +} + +// ✅ Use validation helpers +validation::validate_stake(stake, min_stake)?; +``` + +### 5. Handle Oracle Results Safely + +```rust +// ❌ Direct access +let price = oracle_result.price; + +// ✅ Safe access +if let Some(price) = oracle_result.get_price() { + // Use the price +} +``` + +## Testing + +### Type Testing + +The types module includes comprehensive tests: + +```rust +#[test] +fn test_oracle_provider() { + let provider = OracleProvider::Pyth; + assert_eq!(provider.name(), "Pyth Network"); + assert!(provider.is_supported()); +} + +#[test] +fn test_market_creation() { + let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); + assert!(market.is_active(current_time)); + assert!(!market.is_resolved()); +} + +#[test] +fn test_validation_helpers() { + assert!(validation::validate_oracle_provider(&OracleProvider::Pyth).is_ok()); + assert!(validation::validate_price(2500000).is_ok()); +} +``` + +## Migration Guide + +### From Direct Type Usage + +1. **Replace direct struct creation**: + ```rust + // Old + let market = Market { /* fields */ }; + + // New + let market = Market::new(&env, admin, question, outcomes, end_time, oracle_config); + ``` + +2. **Use validation methods**: + ```rust + // Old + if threshold <= 0 { return Err(Error::InvalidThreshold); } + + // New + oracle_config.validate(&env)?; + ``` + +3. **Use type-safe operations**: + ```rust + // Old + if current_time < market.end_time { /* active */ } + + // New + if market.is_active(current_time) { /* active */ } + ``` + +## Type Reference + +### Oracle Types + +| Type | Purpose | Key Methods | +|------|---------|-------------| +| `OracleProvider` | Oracle service enumeration | `name()`, `is_supported()`, `default_feed_format()` | +| `OracleConfig` | Oracle configuration | `new()`, `validate()`, `is_supported()`, `is_greater_than()` | +| `PythPrice` | Pyth price data | `new()`, `price_in_cents()`, `is_stale()`, `validate()` | +| `ReflectorPriceData` | Reflector price data | `new()`, `price_in_cents()`, `is_stale()`, `validate()` | + +### Market Types + +| Type | Purpose | Key Methods | +|------|---------|-------------| +| `Market` | Market data structure | `new()`, `validate()`, `is_active()`, `add_vote()` | +| `MarketState` | Market state enumeration | `from_market()`, `is_active()`, `has_ended()` | +| `MarketCreationParams` | Market creation parameters | `new()`, `validate()`, `calculate_end_time()` | +| `VoteParams` | Vote parameters | `new()`, `validate()` | + +### Utility Types + +| Type | Purpose | Key Methods | +|------|---------|-------------| +| `OracleResult` | Oracle result wrapper | `price()`, `unavailable()`, `is_available()`, `get_price()` | +| `ReflectorAsset` | Reflector asset types | `stellar()`, `other()`, `is_stellar()`, `is_other()` | + +### Validation Functions + +| Function | Purpose | Parameters | +|----------|---------|------------| +| `validate_oracle_provider()` | Validate oracle provider | `provider: &OracleProvider` | +| `validate_price()` | Validate price value | `price: i128` | +| `validate_stake()` | Validate stake amount | `stake: i128, min_stake: i128` | +| `validate_duration()` | Validate duration | `duration_days: u32` | + +### Conversion Functions + +| Function | Purpose | Parameters | +|----------|---------|------------| +| `string_to_oracle_provider()` | Convert string to provider | `s: &str` | +| `oracle_provider_to_string()` | Convert provider to string | `provider: &OracleProvider` | +| `validate_comparison()` | Validate comparison operator | `comparison: &String, env: &Env` | + +## Future Enhancements + +1. **Type Serialization**: Proper serialization/deserialization support +2. **Type Metrics**: Collection and reporting of type usage statistics +3. **Type Validation**: Enhanced validation with custom rules +4. **Type Events**: Event emission for type state changes +5. **Type Localization**: Support for multiple languages in type messages + +## Conclusion + +The new types system provides a robust foundation for managing data structures in the Predictify Hybrid contract. By following the patterns and best practices outlined in this document, developers can create more maintainable, type-safe, and well-organized code. \ No newline at end of file From f565076bd746a362c3cb3b1f0a625b661c04c0ce Mon Sep 17 00:00:00 2001 From: 1nonlypiece Date: Sat, 5 Jul 2025 10:57:58 +0530 Subject: [PATCH 2/4] refactor: Modularize contract types in Predictify Hybrid by moving them to a dedicated types module --- contracts/predictify-hybrid/src/lib.rs | 88 +++----------------------- 1 file changed, 9 insertions(+), 79 deletions(-) diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 293131c4..cb2fffdd 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -8,50 +8,9 @@ use soroban_sdk::{ pub mod errors; use errors::Error; -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum OracleProvider { - BandProtocol, - DIA, - Reflector, - Pyth, -} - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct OracleConfig { - pub provider: OracleProvider, - pub feed_id: String, // Oracle-specific identifier - pub threshold: i128, // 10_000_00 = $10k (in cents) - pub comparison: String, // "gt", "lt", "eq" -} - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Market { - pub admin: Address, - pub question: String, - pub outcomes: Vec, - pub end_time: u64, - pub oracle_config: OracleConfig, - pub oracle_result: Option, - pub votes: Map, - pub stakes: Map, // User stakes - pub claimed: Map, // Track claims - pub total_staked: i128, - pub dispute_stakes: Map, - pub winning_outcome: Option, - pub fee_collected: bool, // Track fee collection -} - -// Placeholder for Pyth oracle interface -#[contracttype] -pub struct PythPrice { - pub price: i128, - pub conf: u64, - pub expo: i32, - pub publish_time: u64, -} +// Types module +pub mod types; +use types::*; trait OracleInterface { fn get_price(&self, env: &Env, feed_id: &String) -> Result; @@ -84,29 +43,7 @@ impl OracleInterface for PythOracle { } } -// Reflector Oracle Contract Types -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ReflectorAsset { - Stellar(Address), - Other(Symbol), -} - -#[contracttype] -pub struct ReflectorPriceData { - pub price: i128, - pub timestamp: u64, -} -#[contracttype] -pub struct ReflectorConfigData { - pub admin: Address, - pub assets: Vec, - pub base_asset: ReflectorAsset, - pub decimals: u32, - pub period: u64, - pub resolution: u32, -} // Reflector Oracle Client struct ReflectorOracleClient<'a> { @@ -236,22 +173,15 @@ impl PredictifyHybrid { let duration_seconds: u64 = (duration_days as u64) * seconds_per_day; let end_time: u64 = env.ledger().timestamp() + duration_seconds; - // Create a new market - let market = Market { - admin: admin.clone(), + // Create a new market using the types module + let market = Market::new( + &env, + admin.clone(), question, outcomes, end_time, - oracle_config, // Use the provided oracle config - oracle_result: None, - votes: Map::new(&env), - total_staked: 0, - dispute_stakes: Map::new(&env), - stakes: Map::new(&env), - claimed: Map::new(&env), - winning_outcome: None, - fee_collected: false, // Initialize fee collection state - }; + oracle_config, + ); // Deduct 1 XLM fee from the admin let fee_amount: i128 = 10_000_000; // 1 XLM = 10,000,000 stroops From 0d2a2e45f6eadcf62fa47069960cd5c16e9de4a7 Mon Sep 17 00:00:00 2001 From: 1nonlypiece Date: Sat, 5 Jul 2025 10:58:07 +0530 Subject: [PATCH 3/4] feat: Introduce comprehensive type definitions for Predictify Hybrid contract, including oracle, market, price, validation, and utility types --- contracts/predictify-hybrid/src/types.rs | 896 +++++++++++++++++++++++ 1 file changed, 896 insertions(+) create mode 100644 contracts/predictify-hybrid/src/types.rs diff --git a/contracts/predictify-hybrid/src/types.rs b/contracts/predictify-hybrid/src/types.rs new file mode 100644 index 00000000..ea0f4ccc --- /dev/null +++ b/contracts/predictify-hybrid/src/types.rs @@ -0,0 +1,896 @@ +use soroban_sdk::{ + contracttype, Address, Env, Map, String, Symbol, Vec, vec, +}; + +/// Comprehensive type system for Predictify Hybrid contract +/// +/// This module provides organized type definitions categorized by functionality: +/// - Oracle Types: Oracle providers, configurations, and data structures +/// - Market Types: Market data structures and state management +/// - Price Types: Price data and validation structures +/// - Validation Types: Input validation and business logic types +/// - Utility Types: Helper types and conversion utilities + +// ===== ORACLE TYPES ===== + +/// Supported oracle providers for price feeds +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum OracleProvider { + /// Band Protocol oracle + BandProtocol, + /// DIA oracle + DIA, + /// Reflector oracle (Stellar-based) + Reflector, + /// Pyth Network oracle + Pyth, +} + +impl OracleProvider { + /// Get a human-readable name for the oracle provider + pub fn name(&self) -> &'static str { + match self { + OracleProvider::BandProtocol => "Band Protocol", + OracleProvider::DIA => "DIA", + OracleProvider::Reflector => "Reflector", + OracleProvider::Pyth => "Pyth Network", + } + } + + /// Check if the oracle provider is supported + pub fn is_supported(&self) -> bool { + matches!(self, OracleProvider::Pyth | OracleProvider::Reflector) + } + + /// Get the default feed ID format for this provider + pub fn default_feed_format(&self) -> &'static str { + match self { + OracleProvider::BandProtocol => "BTC/USD", + OracleProvider::DIA => "BTC/USD", + OracleProvider::Reflector => "BTC", + OracleProvider::Pyth => "BTC/USD", + } + } +} + +/// Configuration for oracle integration +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OracleConfig { + /// The oracle provider to use + pub provider: OracleProvider, + /// Oracle-specific identifier (e.g., "BTC/USD" for Pyth, "BTC" for Reflector) + pub feed_id: String, + /// Price threshold in cents (e.g., 10_000_00 = $10k) + pub threshold: i128, + /// Comparison operator: "gt", "lt", "eq" + pub comparison: String, +} + +impl OracleConfig { + /// Create a new oracle configuration + pub fn new( + provider: OracleProvider, + feed_id: String, + threshold: i128, + comparison: String, + ) -> Self { + Self { + provider, + feed_id, + threshold, + comparison, + } + } + + /// Validate the oracle configuration + pub fn validate(&self, env: &Env) -> Result<(), crate::errors::Error> { + // Validate threshold + if self.threshold <= 0 { + return Err(crate::errors::Error::InvalidThreshold); + } + + // Validate comparison operator + if self.comparison != String::from_str(env, "gt") && + self.comparison != String::from_str(env, "lt") && + self.comparison != String::from_str(env, "eq") { + return Err(crate::errors::Error::InvalidComparison); + } + + // Validate feed_id is not empty + if self.feed_id.is_empty() { + return Err(crate::errors::Error::InvalidOracleFeed); + } + + // Validate provider is supported + if !self.provider.is_supported() { + return Err(crate::errors::Error::InvalidOracleConfig); + } + + Ok(()) + } + + /// Check if the configuration is for a supported provider + pub fn is_supported(&self) -> bool { + self.provider.is_supported() + } + + /// Get the comparison operator as a string + pub fn comparison_operator(&self) -> &String { + &self.comparison + } + + /// Check if the comparison is "greater than" + pub fn is_greater_than(&self, env: &Env) -> bool { + self.comparison == String::from_str(env, "gt") + } + + /// Check if the comparison is "less than" + pub fn is_less_than(&self, env: &Env) -> bool { + self.comparison == String::from_str(env, "lt") + } + + /// Check if the comparison is "equal to" + pub fn is_equal_to(&self, env: &Env) -> bool { + self.comparison == String::from_str(env, "eq") + } +} + +// ===== MARKET TYPES ===== + +/// Market state and data structure +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Market { + /// Market administrator address + pub admin: Address, + /// Market question/prediction + pub question: String, + /// Available outcomes for the market + pub outcomes: Vec, + /// Market end time (Unix timestamp) + pub end_time: u64, + /// Oracle configuration for this market + pub oracle_config: OracleConfig, + /// Oracle result (set after market ends) + pub oracle_result: Option, + /// User votes mapping (address -> outcome) + pub votes: Map, + /// User stakes mapping (address -> stake amount) + pub stakes: Map, + /// Claimed status mapping (address -> claimed) + pub claimed: Map, + /// Total amount staked in the market + pub total_staked: i128, + /// Dispute stakes mapping (address -> dispute stake) + pub dispute_stakes: Map, + /// Winning outcome (set after resolution) + pub winning_outcome: Option, + /// Whether fees have been collected + pub fee_collected: bool, +} + +impl Market { + /// Create a new market + pub fn new( + env: &Env, + admin: Address, + question: String, + outcomes: Vec, + end_time: u64, + oracle_config: OracleConfig, + ) -> Self { + Self { + admin, + question, + outcomes, + end_time, + oracle_config, + oracle_result: None, + votes: Map::new(env), + stakes: Map::new(env), + claimed: Map::new(env), + total_staked: 0, + dispute_stakes: Map::new(env), + winning_outcome: None, + fee_collected: false, + } + } + + /// Check if the market is active (not ended) + pub fn is_active(&self, current_time: u64) -> bool { + current_time < self.end_time + } + + /// Check if the market has ended + pub fn has_ended(&self, current_time: u64) -> bool { + current_time >= self.end_time + } + + /// Check if the market is resolved + pub fn is_resolved(&self) -> bool { + self.winning_outcome.is_some() + } + + /// Check if the market has oracle result + pub fn has_oracle_result(&self) -> bool { + self.oracle_result.is_some() + } + + /// Get user's vote + pub fn get_user_vote(&self, user: &Address) -> Option { + self.votes.get(user.clone()) + } + + /// Get user's stake + pub fn get_user_stake(&self, user: &Address) -> i128 { + self.stakes.get(user.clone()).unwrap_or(0) + } + + /// Check if user has claimed + pub fn has_user_claimed(&self, user: &Address) -> bool { + self.claimed.get(user.clone()).unwrap_or(false) + } + + /// Get user's dispute stake + pub fn get_user_dispute_stake(&self, user: &Address) -> i128 { + self.dispute_stakes.get(user.clone()).unwrap_or(0) + } + + /// Add user vote and stake + pub fn add_vote(&mut self, user: Address, outcome: String, stake: i128) { + self.votes.set(user.clone(), outcome); + self.stakes.set(user.clone(), stake); + self.total_staked += stake; + } + + /// Add dispute stake + pub fn add_dispute_stake(&mut self, user: Address, stake: i128) { + let current_stake = self.dispute_stakes.get(user.clone()).unwrap_or(0); + self.dispute_stakes.set(user, current_stake + stake); + } + + /// Mark user as claimed + pub fn mark_claimed(&mut self, user: Address) { + self.claimed.set(user, true); + } + + /// Set oracle result + pub fn set_oracle_result(&mut self, result: String) { + self.oracle_result = Some(result); + } + + /// Set winning outcome + pub fn set_winning_outcome(&mut self, outcome: String) { + self.winning_outcome = Some(outcome); + } + + /// Mark fees as collected + pub fn mark_fees_collected(&mut self) { + self.fee_collected = true; + } + + /// Get total dispute stakes + pub fn total_dispute_stakes(&self) -> i128 { + let mut total = 0; + for (_, stake) in self.dispute_stakes.iter() { + total += stake; + } + total + } + + /// Get winning stake total + pub fn winning_stake_total(&self) -> i128 { + if let Some(winning_outcome) = &self.winning_outcome { + let mut total = 0; + for (user, outcome) in self.votes.iter() { + if &outcome == winning_outcome { + total += self.stakes.get(user.clone()).unwrap_or(0); + } + } + total + } else { + 0 + } + } + + /// Validate market parameters + pub fn validate(&self, env: &Env) -> Result<(), crate::errors::Error> { + // Validate question + if self.question.is_empty() { + return Err(crate::errors::Error::InvalidQuestion); + } + + // Validate outcomes + if self.outcomes.len() < 2 { + return Err(crate::errors::Error::InvalidOutcomes); + } + + // Validate oracle config + self.oracle_config.validate(env)?; + + // Validate end time + if self.end_time <= env.ledger().timestamp() { + return Err(crate::errors::Error::InvalidDuration); + } + + Ok(()) + } +} + +// ===== PRICE TYPES ===== + +/// Pyth Network price data structure +#[contracttype] +pub struct PythPrice { + /// Price value + pub price: i128, + /// Confidence interval + pub conf: u64, + /// Price exponent + pub expo: i32, + /// Publish timestamp + pub publish_time: u64, +} + +impl PythPrice { + /// Create a new Pyth price + pub fn new(price: i128, conf: u64, expo: i32, publish_time: u64) -> Self { + Self { + price, + conf, + expo, + publish_time, + } + } + + /// Get the price in cents + pub fn price_in_cents(&self) -> i128 { + self.price + } + + /// Check if the price is stale (older than max_age seconds) + pub fn is_stale(&self, current_time: u64, max_age: u64) -> bool { + current_time - self.publish_time > max_age + } + + /// Validate the price data + pub fn validate(&self) -> Result<(), crate::errors::Error> { + if self.price <= 0 { + return Err(crate::errors::Error::OraclePriceOutOfRange); + } + + if self.conf == 0 { + return Err(crate::errors::Error::OracleDataStale); + } + + Ok(()) + } +} + +/// Reflector asset types +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ReflectorAsset { + /// Stellar asset (using contract address) + Stellar(Address), + /// Other asset (using symbol) + Other(Symbol), +} + +impl ReflectorAsset { + /// Create a Stellar asset + pub fn stellar(contract_id: Address) -> Self { + ReflectorAsset::Stellar(contract_id) + } + + /// Create an other asset + pub fn other(symbol: Symbol) -> Self { + ReflectorAsset::Other(symbol) + } + + /// Get the asset identifier as a string + pub fn to_string(&self, env: &Env) -> String { + match self { + ReflectorAsset::Stellar(addr) => String::from_str(env, "stellar_asset"), + ReflectorAsset::Other(symbol) => String::from_str(env, "other_asset"), + } + } + + /// Check if this is a Stellar asset + pub fn is_stellar(&self) -> bool { + matches!(self, ReflectorAsset::Stellar(_)) + } + + /// Check if this is an other asset + pub fn is_other(&self) -> bool { + matches!(self, ReflectorAsset::Other(_)) + } +} + +/// Reflector price data structure +#[contracttype] +pub struct ReflectorPriceData { + /// Price value + pub price: i128, + /// Timestamp + pub timestamp: u64, +} + +impl ReflectorPriceData { + /// Create new Reflector price data + pub fn new(price: i128, timestamp: u64) -> Self { + Self { price, timestamp } + } + + /// Get the price in cents + pub fn price_in_cents(&self) -> i128 { + self.price + } + + /// Check if the price is stale + pub fn is_stale(&self, current_time: u64, max_age: u64) -> bool { + current_time - self.timestamp > max_age + } + + /// Validate the price data + pub fn validate(&self) -> Result<(), crate::errors::Error> { + if self.price <= 0 { + return Err(crate::errors::Error::OraclePriceOutOfRange); + } + + Ok(()) + } +} + +/// Reflector configuration data +#[contracttype] +pub struct ReflectorConfigData { + /// Admin address + pub admin: Address, + /// Supported assets + pub assets: Vec, + /// Base asset + pub base_asset: ReflectorAsset, + /// Decimal places + pub decimals: u32, + /// Update period + pub period: u64, + /// Resolution + pub resolution: u32, +} + +impl ReflectorConfigData { + /// Create new Reflector config data + pub fn new( + admin: Address, + assets: Vec, + base_asset: ReflectorAsset, + decimals: u32, + period: u64, + resolution: u32, + ) -> Self { + Self { + admin, + assets, + base_asset, + decimals, + period, + resolution, + } + } + + /// Check if an asset is supported + pub fn supports_asset(&self, asset: &ReflectorAsset) -> bool { + self.assets.contains(asset) + } + + /// Validate the configuration + pub fn validate(&self) -> Result<(), crate::errors::Error> { + if self.assets.is_empty() { + return Err(crate::errors::Error::InvalidOracleConfig); + } + + if self.decimals == 0 { + return Err(crate::errors::Error::InvalidOracleConfig); + } + + if self.period == 0 { + return Err(crate::errors::Error::InvalidOracleConfig); + } + + if self.resolution == 0 { + return Err(crate::errors::Error::InvalidOracleConfig); + } + + Ok(()) + } +} + +// ===== VALIDATION TYPES ===== + +/// Market creation parameters +#[derive(Clone, Debug)] +pub struct MarketCreationParams { + pub admin: Address, + pub question: String, + pub outcomes: Vec, + pub duration_days: u32, + pub oracle_config: OracleConfig, +} + +impl MarketCreationParams { + /// Create new market creation parameters + pub fn new( + admin: Address, + question: String, + outcomes: Vec, + duration_days: u32, + oracle_config: OracleConfig, + ) -> Self { + Self { + admin, + question, + outcomes, + duration_days, + oracle_config, + } + } + + /// Validate all parameters + pub fn validate(&self, env: &Env) -> Result<(), crate::errors::Error> { + // Validate question + if self.question.is_empty() { + return Err(crate::errors::Error::InvalidQuestion); + } + + // Validate outcomes + if self.outcomes.len() < 2 { + return Err(crate::errors::Error::InvalidOutcomes); + } + + // Validate duration + if self.duration_days == 0 || self.duration_days > 365 { + return Err(crate::errors::Error::InvalidDuration); + } + + // Validate oracle config + self.oracle_config.validate(env)?; + + Ok(()) + } + + /// Calculate end time from duration + pub fn calculate_end_time(&self, env: &Env) -> u64 { + let seconds_per_day: u64 = 24 * 60 * 60; + let duration_seconds: u64 = (self.duration_days as u64) * seconds_per_day; + env.ledger().timestamp() + duration_seconds + } +} + +/// Vote parameters +#[derive(Clone, Debug)] +pub struct VoteParams { + pub user: Address, + pub outcome: String, + pub stake: i128, +} + +impl VoteParams { + /// Create new vote parameters + pub fn new(user: Address, outcome: String, stake: i128) -> Self { + Self { + user, + outcome, + stake, + } + } + + /// Validate vote parameters + pub fn validate(&self, _env: &Env, market: &Market) -> Result<(), crate::errors::Error> { + // Validate outcome + if !market.outcomes.contains(&self.outcome) { + return Err(crate::errors::Error::InvalidOutcome); + } + + // Validate stake + if self.stake <= 0 { + return Err(crate::errors::Error::InsufficientStake); + } + + // Check if user already voted + if market.get_user_vote(&self.user).is_some() { + return Err(crate::errors::Error::AlreadyVoted); + } + + Ok(()) + } +} + +// ===== UTILITY TYPES ===== + +/// Market state enumeration +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MarketState { + /// Market is active and accepting votes + Active, + /// Market has ended but not resolved + Ended, + /// Market has been resolved + Resolved, + /// Market has been closed + Closed, +} + +impl MarketState { + /// Get state from market + pub fn from_market(market: &Market, current_time: u64) -> Self { + if market.is_resolved() { + MarketState::Resolved + } else if market.has_ended(current_time) { + MarketState::Ended + } else { + MarketState::Active + } + } + + /// Check if market is active + pub fn is_active(&self) -> bool { + matches!(self, MarketState::Active) + } + + /// Check if market has ended + pub fn has_ended(&self) -> bool { + matches!(self, MarketState::Ended | MarketState::Resolved | MarketState::Closed) + } + + /// Check if market is resolved + pub fn is_resolved(&self) -> bool { + matches!(self, MarketState::Resolved) + } +} + +/// Oracle result type +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum OracleResult { + /// Oracle returned a price + Price(i128), + /// Oracle is unavailable + Unavailable, + /// Oracle data is stale + Stale, +} + +impl OracleResult { + /// Create from price + pub fn price(price: i128) -> Self { + OracleResult::Price(price) + } + + /// Create unavailable result + pub fn unavailable() -> Self { + OracleResult::Unavailable + } + + /// Create stale result + pub fn stale() -> Self { + OracleResult::Stale + } + + /// Check if result is available + pub fn is_available(&self) -> bool { + matches!(self, OracleResult::Price(_)) + } + + /// Get price if available + pub fn get_price(&self) -> Option { + match self { + OracleResult::Price(price) => Some(*price), + _ => None, + } + } +} + +// ===== HELPER FUNCTIONS ===== + +/// Type validation helpers +pub mod validation { + use super::*; + + /// Validate oracle provider + pub fn validate_oracle_provider(provider: &OracleProvider) -> Result<(), crate::errors::Error> { + if !provider.is_supported() { + return Err(crate::errors::Error::InvalidOracleConfig); + } + Ok(()) + } + + /// Validate price data + pub fn validate_price(price: i128) -> Result<(), crate::errors::Error> { + if price <= 0 { + return Err(crate::errors::Error::OraclePriceOutOfRange); + } + Ok(()) + } + + /// Validate stake amount + pub fn validate_stake(stake: i128, min_stake: i128) -> Result<(), crate::errors::Error> { + if stake < min_stake { + return Err(crate::errors::Error::InsufficientStake); + } + Ok(()) + } + + /// Validate market duration + pub fn validate_duration(duration_days: u32) -> Result<(), crate::errors::Error> { + if duration_days == 0 || duration_days > 365 { + return Err(crate::errors::Error::InvalidDuration); + } + Ok(()) + } +} + +/// Type conversion helpers +pub mod conversion { + use super::*; + + /// Convert string to oracle provider + pub fn string_to_oracle_provider(s: &str) -> Option { + match s.to_lowercase().as_str() { + "band" | "bandprotocol" => Some(OracleProvider::BandProtocol), + "dia" => Some(OracleProvider::DIA), + "reflector" => Some(OracleProvider::Reflector), + "pyth" => Some(OracleProvider::Pyth), + _ => None, + } + } + + /// Convert oracle provider to string + pub fn oracle_provider_to_string(provider: &OracleProvider) -> &'static str { + provider.name() + } + + /// Convert comparison string to validation + pub fn validate_comparison(comparison: &String, env: &Env) -> Result<(), crate::errors::Error> { + if comparison != &String::from_str(env, "gt") && + comparison != &String::from_str(env, "lt") && + comparison != &String::from_str(env, "eq") { + return Err(crate::errors::Error::InvalidComparison); + } + Ok(()) + } +} + + + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::testutils::Address as _; + + #[test] + fn test_oracle_provider() { + let provider = OracleProvider::Pyth; + assert_eq!(provider.name(), "Pyth Network"); + assert!(provider.is_supported()); + assert_eq!(provider.default_feed_format(), "BTC/USD"); + } + + #[test] + fn test_oracle_config() { + let env = soroban_sdk::Env::default(); + let config = OracleConfig::new( + OracleProvider::Pyth, + String::from_str(&env, "BTC/USD"), + 2500000, + String::from_str(&env, "gt"), + ); + + assert!(config.validate(&env).is_ok()); + assert!(config.is_supported()); + assert!(config.is_greater_than(&env)); + } + + #[test] + fn test_market_creation() { + let env = soroban_sdk::Env::default(); + let admin = Address::generate(&env); + let outcomes = vec![ + &env, + String::from_str(&env, "yes"), + String::from_str(&env, "no"), + ]; + let oracle_config = OracleConfig::new( + OracleProvider::Pyth, + String::from_str(&env, "BTC/USD"), + 2500000, + String::from_str(&env, "gt"), + ); + + let market = Market::new( + &env, + admin.clone(), + String::from_str(&env, "Test question"), + outcomes, + env.ledger().timestamp() + 86400, + oracle_config, + ); + + assert!(market.is_active(env.ledger().timestamp())); + assert!(!market.is_resolved()); + assert_eq!(market.total_staked, 0); + } + + #[test] + fn test_reflector_asset() { + let env = soroban_sdk::Env::default(); + let symbol = Symbol::new(&env, "BTC"); + let asset = ReflectorAsset::other(symbol); + + assert!(asset.is_other()); + assert!(!asset.is_stellar()); + } + + #[test] + fn test_market_state() { + let env = soroban_sdk::Env::default(); + let admin = Address::generate(&env); + let outcomes = vec![ + &env, + String::from_str(&env, "yes"), + String::from_str(&env, "no"), + ]; + let oracle_config = OracleConfig::new( + OracleProvider::Pyth, + String::from_str(&env, "BTC/USD"), + 2500000, + String::from_str(&env, "gt"), + ); + + let market = Market::new( + &env, + admin, + String::from_str(&env, "Test question"), + outcomes, + env.ledger().timestamp() + 86400, + oracle_config, + ); + + let state = MarketState::from_market(&market, env.ledger().timestamp()); + assert!(state.is_active()); + assert!(!state.has_ended()); + assert!(!state.is_resolved()); + } + + #[test] + fn test_oracle_result() { + let result = OracleResult::price(2500000); + assert!(result.is_available()); + assert_eq!(result.get_price(), Some(2500000)); + + let unavailable = OracleResult::unavailable(); + assert!(!unavailable.is_available()); + assert_eq!(unavailable.get_price(), None); + } + + #[test] + fn test_validation_helpers() { + assert!(validation::validate_oracle_provider(&OracleProvider::Pyth).is_ok()); + assert!(validation::validate_price(2500000).is_ok()); + assert!(validation::validate_stake(1000000, 500000).is_ok()); + assert!(validation::validate_duration(30).is_ok()); + } + + #[test] + fn test_conversion_helpers() { + assert_eq!( + conversion::string_to_oracle_provider("pyth"), + Some(OracleProvider::Pyth) + ); + assert_eq!( + conversion::oracle_provider_to_string(&OracleProvider::Pyth), + "Pyth Network" + ); + } +} \ No newline at end of file From b5c0b0be72643cbb8dde92da5cd567cc34bbb4f9 Mon Sep 17 00:00:00 2001 From: 1nonlypiece Date: Sat, 5 Jul 2025 10:58:26 +0530 Subject: [PATCH 4/4] chore: Remove outdated ERROR_MANAGEMENT.md file from Predictify Hybrid contract --- .../predictify-hybrid/ERROR_MANAGEMENT.md | 346 ------------------ 1 file changed, 346 deletions(-) delete mode 100644 contracts/predictify-hybrid/ERROR_MANAGEMENT.md diff --git a/contracts/predictify-hybrid/ERROR_MANAGEMENT.md b/contracts/predictify-hybrid/ERROR_MANAGEMENT.md deleted file mode 100644 index bd70083d..00000000 --- a/contracts/predictify-hybrid/ERROR_MANAGEMENT.md +++ /dev/null @@ -1,346 +0,0 @@ -# Predictify Hybrid Error Management System - -## Overview - -The Predictify Hybrid contract now features a comprehensive, centralized error management system designed to provide better organization, debugging capabilities, and maintainability. This document outlines the architecture, usage patterns, and best practices for working with the error system. - -## Architecture - -### Error Categories - -Errors are organized into logical categories for better understanding and handling: - -1. **Security Errors (1-10)** - - Authentication and authorization failures - - Unauthorized access attempts - -2. **Market Errors (11-30)** - - Market state and operation issues - - Market lifecycle management problems - -3. **Oracle Errors (31-50)** - - Oracle integration failures - - Data availability and validation issues - -4. **Validation Errors (51-70)** - - Input validation failures - - Business logic validation errors - -5. **State Errors (71-90)** - - Contract state inconsistencies - - User action conflicts - -6. **System Errors (91-100)** - - Internal contract errors - - Storage and arithmetic failures - -### Core Components - -#### 1. Error Enum (`Error`) -The main error type with comprehensive categorization and documentation. - -```rust -#[contracterror] -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Error { - // Security errors - Unauthorized = 1, - - // Market errors - MarketClosed = 2, - MarketAlreadyResolved = 5, - // ... more errors -} -``` - -#### 2. Error Categories (`ErrorCategory`) -Logical grouping of errors for better organization and handling. - -```rust -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ErrorCategory { - Security, - Market, - Oracle, - Validation, - State, - System, -} -``` - -#### 3. Error Context (`ErrorContext`) -Additional debugging information for errors. - -```rust -#[derive(Clone, Debug)] -pub struct ErrorContext { - pub operation: String, - pub details: String, - pub timestamp: u64, -} -``` - -## Usage Patterns - -### 1. Basic Error Handling - -```rust -use errors::Error; - -// Direct error usage -if condition { - panic_with_error!(env, Error::Unauthorized); -} -``` - -### 2. Error Helper Functions - -The system provides helper functions for common validation scenarios: - -```rust -use errors::helpers; - -// Admin validation -helpers::require_admin(&env, &caller, &admin); - -// Market state validation -helpers::require_market_open(&env, &market); - -// Input validation -helpers::require_valid_outcome(&env, &outcome, &outcomes); -helpers::require_sufficient_stake(&env, stake, min_stake); -``` - -### 3. Error Context and Debugging - -```rust -use errors::{ErrorContext, debug}; - -// Create error context -let context = ErrorContext::new(&env, "vote", "User attempted to vote on closed market"); - -// Log error with context -debug::log_error(&env, Error::MarketClosed, &context); - -// Create detailed error report -let report = debug::create_error_report(&env, Error::MarketClosed, &context); -``` - -### 4. Error Conversion - -```rust -use errors::conversions::IntoPredictifyError; - -// Convert external results to Predictify errors -let result: Result = external_call(); -result.into_predictify_error(&env, Error::InternalError)?; -``` - -## Error Helper Functions - -### Security Helpers - -- `require_admin(env, caller, admin)` - Validates admin permissions -- `require_authorized(env, caller, authorized_users)` - Validates user authorization - -### Market Helpers - -- `require_market_open(env, market)` - Ensures market is active -- `require_market_resolved(env, market)` - Ensures market is resolved -- `require_market_exists(env, market)` - Ensures market exists - -### Validation Helpers - -- `require_valid_outcome(env, outcome, outcomes)` - Validates outcome choice -- `require_sufficient_stake(env, stake, min_stake)` - Validates stake amount -- `require_valid_market_params(env, question, outcomes, duration)` - Validates market parameters -- `require_valid_oracle_config(env, config)` - Validates oracle configuration - -### State Helpers - -- `require_not_claimed(env, claimed)` - Ensures user hasn't claimed -- `require_not_voted(env, voted)` - Ensures user hasn't voted -- `require_not_staked(env, staked)` - Ensures user hasn't staked - -## Error Properties and Methods - -### Category Information - -```rust -let error = Error::Unauthorized; -assert_eq!(error.category(), ErrorCategory::Security); -``` - -### Human-Readable Messages - -```rust -let error = Error::MarketClosed; -assert_eq!(error.message(), "Market is closed and no longer accepting votes or stakes"); -``` - -### Error Codes - -```rust -let error = Error::OracleUnavailable; -assert_eq!(error.code(), "ORACLE_UNAVAILABLE"); -``` - -### Recoverability - -```rust -let error = Error::InvalidOutcome; -assert!(error.is_recoverable()); // Validation errors are recoverable - -let error = Error::Unauthorized; -assert!(!error.is_recoverable()); // Security errors are not recoverable -``` - -### Criticality - -```rust -let error = Error::InternalError; -assert!(error.is_critical()); // System errors are critical - -let error = Error::InvalidOutcome; -assert!(!error.is_critical()); // Validation errors are not critical -``` - -## Best Practices - -### 1. Use Helper Functions - -Instead of manual validation, use the provided helper functions: - -```rust -// ❌ Manual validation -if caller != admin { - panic_with_error!(env, Error::Unauthorized); -} - -// ✅ Using helper function -helpers::require_admin(&env, &caller, &admin); -``` - -### 2. Provide Context for Debugging - -Always provide meaningful context when logging errors: - -```rust -let context = ErrorContext::new( - &env, - "create_market", - &format!("Admin: {}, Question: {}", admin, question) -); -debug::log_error(&env, error, &context); -``` - -### 3. Handle Errors Appropriately - -- **Security Errors**: Always critical, should halt execution -- **Validation Errors**: Usually recoverable, provide clear feedback -- **State Errors**: May be recoverable depending on context -- **System Errors**: Critical, should be logged and handled gracefully - -### 4. Use Error Categories for Logic - -```rust -match error.category() { - ErrorCategory::Security => { - // Handle security errors - log_security_violation(error); - } - ErrorCategory::Validation => { - // Handle validation errors - provide_user_feedback(error); - } - ErrorCategory::System => { - // Handle system errors - log_system_error(error); - } - _ => { - // Handle other errors - handle_generic_error(error); - } -} -``` - -## Testing - -The error system includes comprehensive tests: - -```rust -#[test] -fn test_error_categories() { - assert_eq!(Error::Unauthorized.category(), ErrorCategory::Security); - assert_eq!(Error::MarketClosed.category(), ErrorCategory::Market); -} - -#[test] -fn test_error_messages() { - assert_eq!(Error::Unauthorized.message(), "Unauthorized access - caller lacks required permissions"); -} - -#[test] -fn test_error_recoverability() { - assert!(!Error::Unauthorized.is_recoverable()); - assert!(Error::InvalidOutcome.is_recoverable()); -} -``` - -## Migration Guide - -### From Old Error System - -1. **Replace direct error usage**: - ```rust - // Old - panic_with_error!(env, Error::Unauthorized); - - // New (same, but with better organization) - panic_with_error!(env, Error::Unauthorized); - ``` - -2. **Use helper functions**: - ```rust - // Old - if admin != stored_admin { - panic_with_error!(env, Error::Unauthorized); - } - - // New - helpers::require_admin(&env, &admin, &stored_admin); - ``` - -3. **Add error context for debugging**: - ```rust - // New - let context = ErrorContext::new(&env, "operation", "details"); - debug::log_error(&env, error, &context); - ``` - -## Error Codes Reference - -| Error | Code | Category | Recoverable | Critical | -|-------|------|----------|-------------|----------| -| Unauthorized | UNAUTHORIZED | Security | No | Yes | -| MarketClosed | MARKET_CLOSED | Market | No | No | -| OracleUnavailable | ORACLE_UNAVAILABLE | Oracle | Yes | No | -| InsufficientStake | INSUFFICIENT_STAKE | Validation | Yes | No | -| MarketAlreadyResolved | MARKET_ALREADY_RESOLVED | Market | No | No | -| InvalidOracleConfig | INVALID_ORACLE_CONFIG | Oracle | Yes | No | -| AlreadyClaimed | ALREADY_CLAIMED | State | No | No | -| NothingToClaim | NOTHING_TO_CLAIM | State | No | No | -| MarketNotResolved | MARKET_NOT_RESOLVED | Market | No | No | -| InvalidOutcome | INVALID_OUTCOME | Validation | Yes | No | - -## Future Enhancements - -1. **Error Chaining**: Support for error chains and causal relationships -2. **Error Metrics**: Collection and reporting of error statistics -3. **Error Recovery**: Automatic recovery mechanisms for certain error types -4. **Error Notifications**: Event emission for critical errors -5. **Error Localization**: Support for multiple languages in error messages - -## Conclusion - -The new error management system provides a robust foundation for handling errors in the Predictify Hybrid contract. By following the patterns and best practices outlined in this document, developers can create more maintainable, debuggable, and user-friendly error handling code. \ No newline at end of file