diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index 9b0e8de6..cbfee8fb 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -95,6 +95,9 @@ mod statistics_tests; #[cfg(test)] mod resolution_delay_dispute_window_tests; +#[cfg(test)] +mod tests; + #[cfg(any())] mod event_creation_tests; diff --git a/contracts/predictify-hybrid/src/oracles.rs b/contracts/predictify-hybrid/src/oracles.rs index 0696cd5e..8c38733f 100644 --- a/contracts/predictify-hybrid/src/oracles.rs +++ b/contracts/predictify-hybrid/src/oracles.rs @@ -66,7 +66,7 @@ use crate::types::*; /// } /// /// // Verify provider type -/// assert_eq!(oracle.provider(), OracleProvider::Reflector); +/// assert_eq!(oracle.provider(), OracleProvider::reflector()); /// } else { /// println!("Oracle is not healthy, using fallback"); /// } @@ -165,7 +165,7 @@ pub trait OracleInterface { /// assert!(price_result.is_err()); /// /// // Check oracle provider -/// assert_eq!(oracle.provider(), OracleProvider::Pyth); +/// assert_eq!(oracle.provider(), OracleProvider::pyth()); /// /// // Validate feed configurations /// assert_eq!(oracle.get_feed_count(), 1); @@ -480,9 +480,9 @@ impl OracleInterface for PythOracle { /// Get the oracle provider type /// /// # Returns - /// OracleProvider::Pyth + /// OracleProvider::pyth() fn provider(&self) -> OracleProvider { - OracleProvider::Pyth + OracleProvider::pyth() } /// Get the oracle contract ID @@ -702,7 +702,7 @@ impl<'a> ReflectorOracleClient<'a> { /// let oracle = ReflectorOracle::new(oracle_address.clone()); /// /// // Verify oracle provider type -/// assert_eq!(oracle.provider(), OracleProvider::Reflector); +/// assert_eq!(oracle.provider(), OracleProvider::reflector()); /// assert_eq!(oracle.contract_id(), oracle_address); /// /// // Check oracle health before use @@ -896,7 +896,7 @@ impl OracleInterface for ReflectorOracle { } fn provider(&self) -> OracleProvider { - OracleProvider::Reflector + OracleProvider::reflector() } fn contract_id(&self) -> Address { @@ -960,13 +960,13 @@ impl OracleInterface for ReflectorOracle { /// } /// /// // Check provider support before creation -/// if OracleFactory::is_provider_supported(&OracleProvider::Reflector) { +/// if OracleFactory::is_provider_supported(&OracleProvider::reflector()) { /// println!("Reflector is supported on Stellar"); /// } /// /// // Get recommended provider for Stellar /// let recommended = OracleFactory::get_recommended_provider(); -/// assert_eq!(recommended, OracleProvider::Reflector); +/// assert_eq!(recommended, OracleProvider::reflector()); /// /// // Create from configuration /// let config = OracleConfig { @@ -1055,8 +1055,8 @@ impl OracleFactory { return Err(Error::InvalidOracleConfig); } - match provider { - OracleProvider::Reflector => { + match provider.as_str() { + "reflector" => { let oracle = ReflectorOracle::new(contract_id); Ok(OracleInstance::Reflector(oracle)) } @@ -1082,15 +1082,12 @@ impl OracleFactory { /// Check if a provider is supported on Stellar pub fn is_provider_supported(provider: &OracleProvider) -> bool { - match provider { - OracleProvider::Reflector => true, - OracleProvider::Pyth | OracleProvider::BandProtocol | OracleProvider::DIA => false, - } + provider.is_supported() } /// Get the recommended oracle provider for Stellar pub fn get_recommended_provider() -> OracleProvider { - OracleProvider::Reflector + OracleProvider::reflector() } /// Create a Pyth oracle with pre-configured feeds @@ -1186,20 +1183,24 @@ impl OracleFactory { return Err(Error::InvalidOracleConfig); } - match oracle_config.provider { - OracleProvider::Reflector => { + match oracle_config.provider.as_str() { + "reflector" => { // Reflector is fully supported Ok(()) } - OracleProvider::Pyth => { + "pyth" => { // Pyth is not supported on Stellar, but we'll allow it for future compatibility // The implementation will return errors when used Ok(()) } - OracleProvider::BandProtocol | OracleProvider::DIA => { + "band_protocol" | "dia" => { // These providers are not supported on Stellar Err(Error::InvalidOracleConfig) } + unknown => { + // Unknown provider - fail safely for new market creation + Err(Error::InvalidOracleConfig) + } } } } @@ -1347,9 +1348,9 @@ impl OracleInstance { /// Get the oracle provider type pub fn provider(&self) -> OracleProvider { match self { - OracleInstance::Pyth(_) => OracleProvider::Pyth, - OracleInstance::Reflector(_) => OracleProvider::Reflector, - OracleInstance::Band(_) => OracleProvider::BandProtocol, + OracleInstance::Pyth(_) => OracleProvider::pyth(), + OracleInstance::Reflector(_) => OracleProvider::reflector(), + OracleInstance::Band(_) => OracleProvider::band_protocol(), } } @@ -1626,7 +1627,7 @@ impl OracleInterface for BandProtocolOracle { } fn provider(&self) -> OracleProvider { - OracleProvider::BandProtocol + OracleProvider::band_protocol() } fn is_healthy(&self, env: &Env) -> Result { @@ -1652,7 +1653,7 @@ mod tests { let oracle = PythOracle::new(contract_id.clone()); assert_eq!(oracle.contract_id(), contract_id); - assert_eq!(oracle.provider(), OracleProvider::Pyth); + assert_eq!(oracle.provider(), OracleProvider::pyth()); } #[test] @@ -1662,7 +1663,7 @@ mod tests { let oracle = ReflectorOracle::new(contract_id.clone()); assert_eq!(oracle.contract_id(), contract_id); - assert_eq!(oracle.provider(), OracleProvider::Reflector); + assert_eq!(oracle.provider(), OracleProvider::reflector()); } #[test] @@ -1671,18 +1672,18 @@ mod tests { let contract_id = Address::generate(&env); // Test Pyth oracle creation (should fail) - let pyth_oracle = OracleFactory::create_oracle(OracleProvider::Pyth, contract_id.clone()); + let pyth_oracle = OracleFactory::create_oracle(OracleProvider::pyth(), contract_id.clone()); assert!(pyth_oracle.is_err()); assert_eq!(pyth_oracle.unwrap_err(), Error::InvalidOracleConfig); // Test Reflector oracle creation let reflector_oracle = - OracleFactory::create_oracle(OracleProvider::Reflector, contract_id.clone()); + OracleFactory::create_oracle(OracleProvider::reflector(), contract_id.clone()); assert!(reflector_oracle.is_ok()); // Test unsupported provider let unsupported_oracle = - OracleFactory::create_oracle(OracleProvider::BandProtocol, contract_id); + OracleFactory::create_oracle(OracleProvider::band_protocol(), contract_id); assert!(unsupported_oracle.is_err()); assert_eq!(unsupported_oracle.unwrap_err(), Error::InvalidOracleConfig); } @@ -2375,7 +2376,7 @@ impl OracleValidationConfigManager { return Err(Error::OracleStale); } - if *provider == OracleProvider::Pyth { + if *provider == OracleProvider::pyth() { if let Some(confidence) = data.confidence { let price_abs = if data.price < 0 { -data.price @@ -2961,7 +2962,7 @@ impl OracleIntegrationManager { price: 0, // Manual override - no price threshold: market.oracle_config.threshold, comparison: market.oracle_config.comparison.clone(), - provider: crate::types::OracleProvider::Reflector, // Placeholder + provider: crate::types::OracleProvider::reflector(), // Placeholder feed_id: market.oracle_config.feed_id.clone(), timestamp: env.ledger().timestamp(), block_number: env.ledger().sequence(), @@ -3083,7 +3084,7 @@ mod oracle_integration_tests { price: 52_000_00, threshold: 50_000_00, comparison: String::from_str(&env, "gt"), - provider: crate::types::OracleProvider::Reflector, + provider: crate::types::OracleProvider::reflector(), feed_id: String::from_str(&env, "BTC/USD"), timestamp: env.ledger().timestamp(), block_number: env.ledger().sequence(), diff --git a/contracts/predictify-hybrid/src/tests/mod.rs b/contracts/predictify-hybrid/src/tests/mod.rs new file mode 100644 index 00000000..e37b634d --- /dev/null +++ b/contracts/predictify-hybrid/src/tests/mod.rs @@ -0,0 +1 @@ +mod oracle_provider_compatibility_tests; diff --git a/contracts/predictify-hybrid/src/tests/oracle_provider_compatibility_tests.rs b/contracts/predictify-hybrid/src/tests/oracle_provider_compatibility_tests.rs new file mode 100644 index 00000000..4e578eca --- /dev/null +++ b/contracts/predictify-hybrid/src/tests/oracle_provider_compatibility_tests.rs @@ -0,0 +1,295 @@ +use crate::errors::Error; +use soroban_sdk::{contracttype, Address, Env, String}; +use crate::types::{OracleConfig, OracleProvider}; + +/// Comprehensive tests for oracle provider forward compatibility. +/// +/// This test suite validates that the new string-based OracleProvider implementation +/// maintains backward compatibility while enabling forward compatibility for future +/// oracle provider additions. +#[contracttest] +fn test_oracle_provider_creation() { + let env = Env::default(); + + // Test standard provider creation + let reflector = OracleProvider::reflector(); + assert_eq!(reflector.as_str(), "reflector"); + assert!(reflector.is_supported()); + assert!(reflector.is_known()); + + let pyth = OracleProvider::pyth(); + assert_eq!(pyth.as_str(), "pyth"); + assert!(!pyth.is_supported()); + assert!(pyth.is_known()); + + let band_protocol = OracleProvider::band_protocol(); + assert_eq!(band_protocol.as_str(), "band_protocol"); + assert!(!band_protocol.is_supported()); + assert!(band_protocol.is_known()); + + let dia = OracleProvider::dia(); + assert_eq!(dia.as_str(), "dia"); + assert!(!dia.is_supported()); + assert!(dia.is_known()); +} + +#[contracttest] +fn test_oracle_provider_from_string() { + let env = Env::default(); + + // Test known providers + let reflector = OracleProvider::from_str(String::from_str(&env, "reflector")); + assert_eq!(reflector.as_str(), "reflector"); + assert!(reflector.is_supported()); + assert!(reflector.is_known()); + + // Test unknown providers (forward compatibility) + let future_provider = OracleProvider::from_str(String::from_str(&env, "chainlink")); + assert_eq!(future_provider.as_str(), "chainlink"); + assert!(!future_provider.is_supported()); + assert!(!future_provider.is_known()); + + let custom_provider = OracleProvider::from_str(String::from_str(&env, "custom_oracle_v2")); + assert_eq!(custom_provider.as_str(), "custom_oracle_v2"); + assert!(!custom_provider.is_supported()); + assert!(!custom_provider.is_known()); +} + +#[contracttest] +fn test_oracle_provider_names() { + let env = Env::default(); + + // Test known provider names + let reflector = OracleProvider::reflector(); + assert_eq!(reflector.name(), String::from_str(&env, "Reflector")); + + let pyth = OracleProvider::pyth(); + assert_eq!(pyth.name(), String::from_str(&env, "Pyth Network")); + + let band_protocol = OracleProvider::band_protocol(); + assert_eq!(band_protocol.name(), String::from_str(&env, "Band Protocol")); + + let dia = OracleProvider::dia(); + assert_eq!(dia.name(), String::from_str(&env, "DIA")); + + // Test unknown provider name formatting + let unknown = OracleProvider::from_str(String::from_str(&env, "new_oracle")); + let expected_name = String::from_str(&env, "Unknown Provider (new_oracle)"); + assert_eq!(unknown.name(), expected_name); +} + +#[contracttest] +fn test_oracle_provider_validation() { + let env = Env::default(); + + // Test supported provider validation + let reflector = OracleProvider::reflector(); + assert!(reflector.validate_for_market(&env).is_ok()); + + // Test known but unsupported providers + let pyth = OracleProvider::pyth(); + assert!(pyth.validate_for_market(&env).is_err()); + assert!(matches!(pyth.validate_for_market(&env), Err(Error::InvalidOracleConfig))); + + let band_protocol = OracleProvider::band_protocol(); + assert!(band_protocol.validate_for_market(&env).is_err()); + + let dia = OracleProvider::dia(); + assert!(dia.validate_for_market(&env).is_err()); + + // Test unknown providers + let unknown = OracleProvider::from_str(String::from_str(&env, "unknown_provider")); + assert!(unknown.validate_for_market(&env).is_err()); +} + +#[contracttest] +fn test_oracle_provider_equality() { + let env = Env::default(); + + // Test equality for known providers + let reflector1 = OracleProvider::reflector(); + let reflector2 = OracleProvider::reflector(); + assert_eq!(reflector1, reflector2); + + let pyth1 = OracleProvider::pyth(); + let pyth2 = OracleProvider::from_str(String::from_str(&env, "pyth")); + assert_eq!(pyth1, pyth2); + + // Test inequality + let reflector = OracleProvider::reflector(); + let pyth = OracleProvider::pyth(); + assert_ne!(reflector, pyth); + + // Test unknown provider equality + let unknown1 = OracleProvider::from_str(String::from_str(&env, "custom_oracle")); + let unknown2 = OracleProvider::from_str(String::from_str(&env, "custom_oracle")); + assert_eq!(unknown1, unknown2); + + let unknown3 = OracleProvider::from_str(String::from_str(&env, "different_oracle")); + assert_ne!(unknown1, unknown3); +} + +#[contracttest] +fn test_oracle_config_compatibility() { + let env = Env::default(); + let oracle_address = Address::generate(&env); + + // Test creating oracle config with new provider + let provider = OracleProvider::reflector(); + let config = OracleConfig::new( + provider.clone(), + oracle_address.clone(), + String::from_str(&env, "BTC/USD"), + 50_000_00, // $50,000 in cents + String::from_str(&env, "gt"), + ); + + // Validate config works with new provider + assert!(config.validate(&env).is_ok()); + assert_eq!(config.provider.as_str(), "reflector"); + + // Test sentinel config + let sentinel = OracleConfig::none_sentinel(&env); + assert!(sentinel.is_none_sentinel()); + assert_eq!(sentinel.provider.as_str(), "reflector"); + assert!(sentinel.feed_id.is_empty()); + assert_eq!(sentinel.threshold, 0); + assert!(sentinel.comparison.is_empty()); +} + +#[contracttest] +fn test_forward_compatibility_scenario() { + let env = Env::default(); + + // Simulate a market created with a future oracle provider + // This would happen when a market is created with a newer contract version + // and then read by an older version + let future_provider = OracleProvider::from_str(String::from_str(&env, "chainlink")); + + // The older contract should be able to read the provider + assert_eq!(future_provider.as_str(), "chainlink"); + assert!(!future_provider.is_known()); // Not known in this version + assert!(!future_provider.is_supported()); // Not supported + + // But it should provide sensible defaults + let name = future_provider.name(); + assert!(name.to_string(&env).contains("Unknown Provider")); + + // And validation should fail safely + assert!(future_provider.validate_for_market(&env).is_err()); +} + +#[contracttest] +fn test_serialization_roundtrip() { + let env = Env::default(); + + // Test that providers can be serialized and deserialized correctly + let original = OracleProvider::reflector(); + + // In Soroban, contracttype ensures proper serialization + // We test equality after "serialization" by creating identical instances + let deserialized = OracleProvider::from_str(String::from_str(&env, "reflector")); + assert_eq!(original, deserialized); + + // Test with unknown provider + let unknown_original = OracleProvider::from_str(String::from_str(&env, "future_oracle")); + let unknown_deserialized = OracleProvider::from_str(String::from_str(&env, "future_oracle")); + assert_eq!(unknown_original, unknown_deserialized); +} + +#[contracttest] +fn test_oracle_config_validation_with_new_provider() { + let env = Env::default(); + let oracle_address = Address::generate(&env); + + // Test that oracle config validation works with new provider system + let valid_config = OracleConfig::new( + OracleProvider::reflector(), + oracle_address, + String::from_str(&env, "BTC/USD"), + 50_000_00, + String::from_str(&env, "gt"), + ); + assert!(valid_config.validate(&env).is_ok()); + + // Test with unsupported provider + let unsupported_config = OracleConfig::new( + OracleProvider::pyth(), + oracle_address, + String::from_str(&env, "ETH/USD"), + 2_000_00, + String::from_str(&env, "lt"), + ); + assert!(unsupported_config.validate(&env).is_err()); + + // Test with unknown provider + let unknown_config = OracleConfig::new( + OracleProvider::from_str(String::from_str(&env, "unknown_oracle")), + oracle_address, + String::from_str(&env, "XLM/USD"), + 100, + String::from_str(&env, "eq"), + ); + assert!(unknown_config.validate(&env).is_err()); +} + +#[contracttest] +fn test_provider_string_formats() { + let env = Env::default(); + + // Test that provider IDs follow expected format + let providers = vec![ + (OracleProvider::reflector(), "reflector"), + (OracleProvider::pyth(), "pyth"), + (OracleProvider::band_protocol(), "band_protocol"), + (OracleProvider::dia(), "dia"), + ]; + + for (provider, expected_id) in providers { + assert_eq!(provider.as_str(), expected_id); + assert_eq!(provider.as_str(), expected_id.to_string()); + } + + // Test custom provider formats + let custom_cases = vec![ + ("chainlink", "chainlink"), + ("uniswap_oracle", "uniswap_oracle"), + ("custom_provider_v2", "custom_provider_v2"), + ("UPPERCASE_PROVIDER", "UPPERCASE_PROVIDER"), + ("provider-with-dashes", "provider-with-dashes"), + ]; + + for (input, expected) in custom_cases { + let provider = OracleProvider::from_str(String::from_str(&env, input)); + assert_eq!(provider.as_str(), expected); + } +} + +#[contracttest] +fn test_migration_compatibility() { + let env = Env::default(); + + // This test simulates migration from the old enum-based system + // In practice, this would be handled by a migration function + + // Simulate old enum variants (as strings for testing) + let old_variants = vec![ + ("Reflector", "reflector"), + ("Pyth", "pyth"), + ("BandProtocol", "band_protocol"), + ("DIA", "dia"), + ]; + + for (old_enum_name, expected_string_id) in old_variants { + // Simulate migration logic + let new_provider = match old_enum_name { + "Reflector" => OracleProvider::reflector(), + "Pyth" => OracleProvider::pyth(), + "BandProtocol" => OracleProvider::band_protocol(), + "DIA" => OracleProvider::dia(), + _ => OracleProvider::from_str(String::from_str(&env, "unknown")), + }; + + assert_eq!(new_provider.as_str(), expected_string_id); + } +} diff --git a/contracts/predictify-hybrid/src/types.rs b/contracts/predictify-hybrid/src/types.rs index 21975e40..08a76fe7 100644 --- a/contracts/predictify-hybrid/src/types.rs +++ b/contracts/predictify-hybrid/src/types.rs @@ -141,163 +141,394 @@ pub enum MarketState { // ===== ORACLE TYPES ===== -/// Enumeration of supported oracle providers for price feed data. +/// Forward-compatible oracle provider representation. /// -/// This enum defines the various oracle providers that can supply price data -/// for prediction market resolution. Each provider has different characteristics, -/// availability, and integration requirements specific to the Stellar blockchain -/// ecosystem. +/// This struct provides forward compatibility for oracle provider identification by using +/// string-based representation instead of a fixed enum. This allows new oracle providers +/// to be added without breaking existing stored markets. /// -/// # Provider Categories +/// # Forward Compatibility Strategy /// -/// **Production Ready (Stellar Network):** -/// - **Reflector**: Primary oracle provider with full Stellar integration +/// The provider is stored as a string identifier rather than an enum variant, enabling: +/// - **Future Provider Addition**: New providers can be added without breaking existing markets +/// - **Backward Compatibility**: Existing markets continue to work with older contract versions +/// - **Graceful Degradation**: Unknown providers are handled safely with fallback behavior /// -/// **Future/Placeholder (Not Yet Available):** -/// - **Pyth**: High-frequency oracle network (future Stellar support) -/// - **Band Protocol**: Decentralized oracle network (not on Stellar) -/// - **DIA**: Multi-chain oracle platform (not on Stellar) +/// # Provider Identifiers /// -/// # Provider Characteristics +/// **Standardized Provider Names:** +/// - `"reflector"` - Reflector oracle (primary for Stellar Network) +/// - `"pyth"` - Pyth Network oracle (placeholder for future Stellar support) +/// - `"band_protocol"` - Band Protocol oracle (not available on Stellar) +/// - `"dia"` - DIA oracle (not available on Stellar) /// -/// **Reflector Oracle:** -/// - **Status**: Production ready and recommended -/// - **Network**: Native Stellar blockchain integration -/// - **Assets**: BTC, ETH, XLM, and other major cryptocurrencies -/// - **Features**: Real-time prices, TWAP calculations, high reliability -/// - **Use Case**: Primary oracle for all Stellar-based prediction markets -/// -/// **Pyth Network:** -/// - **Status**: Placeholder for future implementation -/// - **Network**: Not currently available on Stellar -/// - **Assets**: Extensive coverage of crypto, forex, and traditional assets -/// - **Features**: Sub-second updates, institutional-grade data -/// - **Use Case**: Future high-frequency prediction markets -/// -/// **Band Protocol:** -/// - **Status**: Not supported on Stellar -/// - **Network**: Primarily Cosmos and EVM-compatible chains -/// - **Assets**: Wide range of crypto and traditional assets -/// - **Features**: Decentralized data aggregation -/// - **Use Case**: Not applicable for Stellar deployment -/// -/// **DIA:** -/// - **Status**: Not supported on Stellar -/// - **Network**: Multi-chain but no Stellar integration -/// - **Assets**: Comprehensive DeFi and traditional asset coverage -/// - **Features**: Transparent data sourcing and aggregation -/// - **Use Case**: Not applicable for Stellar deployment +/// **Future Providers:** +/// - New providers use descriptive lowercase names with underscores +/// - Example: `"chainlink"` for Chainlink oracle integration +/// - Example: `"uniswap_oracle"` for Uniswap-based price feeds /// /// # Example Usage /// /// ```rust +/// # use soroban_sdk::{Env, String}; /// # use predictify_hybrid::types::OracleProvider; +/// # let env = Env::default(); /// -/// // Check provider support before using -/// let provider = OracleProvider::Reflector; +/// // Create provider instances +/// let reflector = OracleProvider::reflector(); +/// let pyth = OracleProvider::pyth(); +/// let custom = OracleProvider::from_str(String::from_str(&env, "new_provider")); /// -/// if provider.is_supported() { -/// println!("Using {} oracle provider", provider.name()); -/// // Proceed with oracle integration -/// } else { -/// println!("Provider {} not supported on Stellar", provider.name()); -/// // Use fallback or error handling -/// } +/// // Check support status +/// assert!(reflector.is_supported()); +/// assert!(!pyth.is_supported()); // Not available on Stellar +/// assert!(!custom.is_supported()); // Unknown provider /// -/// // Provider selection logic -/// let recommended_provider = match std::env::var("ORACLE_PREFERENCE") { -/// Ok(pref) if pref == "pyth" => { -/// if OracleProvider::Pyth.is_supported() { -/// OracleProvider::Pyth -/// } else { -/// println!("Pyth not available, using Reflector"); -/// OracleProvider::Reflector -/// } -/// }, -/// _ => OracleProvider::Reflector, // Default to Reflector -/// }; +/// // Get provider identifier +/// assert_eq!(reflector.as_str(), "reflector"); +/// assert_eq!(pyth.as_str(), "pyth"); +/// assert_eq!(custom.as_str(), "new_provider"); /// -/// println!("Selected oracle: {}", recommended_provider.name()); +/// // Convert to string for display +/// println!("Provider: {}", reflector.name()); /// ``` /// -/// # Integration with Oracle Factory +/// # Validation and Error Handling /// -/// Oracle providers work with the Oracle Factory pattern: /// ```rust -/// # use soroban_sdk::{Env, Address}; +/// # use soroban_sdk::{Env, String}; /// # use predictify_hybrid::types::OracleProvider; -/// # use predictify_hybrid::oracles::OracleFactory; /// # let env = Env::default(); -/// # let oracle_contract = Address::generate(&env); /// -/// // Create oracle instance based on provider -/// let provider = OracleProvider::Reflector; -/// let oracle_result = OracleFactory::create_oracle(provider, oracle_contract); +/// let provider = OracleProvider::from_str(String::from_str(&env, "reflector")); /// -/// match oracle_result { -/// Ok(oracle_instance) => { -/// println!("Successfully created {} oracle", provider.name()); -/// // Use oracle for price feeds -/// }, -/// Err(e) => { -/// println!("Failed to create oracle: {:?}", e); -/// // Handle creation failure -/// }, +/// // Validate provider for new market creation +/// if provider.is_supported() { +/// println!("Provider {} is supported", provider.name()); +/// } else { +/// println!("Provider {} not supported - using fallback", provider.name()); /// } -/// ``` -/// -/// # Provider Migration Strategy /// -/// For future provider additions: -/// 1. **Add Provider Variant**: Update enum with new provider -/// 2. **Update Support Check**: Modify `is_supported()` method -/// 3. **Add Name Mapping**: Update `name()` method -/// 4. **Implement Integration**: Add provider-specific oracle implementation -/// 5. **Update Factory**: Add creation logic in OracleFactory -/// 6. **Test Integration**: Comprehensive testing with new provider +/// // Check if provider is known (even if unsupported) +/// if provider.is_known() { +/// println!("Provider {} is recognized", provider.name()); +/// } else { +/// println!("Unknown provider - may be from future contract version"); +/// } +/// ``` /// -/// # Network Compatibility +/// # Migration from Enum /// -/// Provider support varies by blockchain network: -/// - **Stellar**: Only Reflector is currently supported -/// - **Ethereum**: Pyth, Band Protocol, and DIA are available -/// - **Cosmos**: Band Protocol is native -/// - **Multi-chain**: DIA supports multiple networks +/// When migrating from the old enum-based system: +/// ```rust +/// // Old enum approach (deprecated) +/// // pub enum OracleProvider { Reflector, Pyth, BandProtocol, DIA } +/// +/// // New string-based approach (forward compatible) +/// let provider = match old_enum_variant { +/// OldOracleProvider::Reflector => OracleProvider::reflector(), +/// OldOracleProvider::Pyth => OracleProvider::pyth(), +/// OldOracleProvider::BandProtocol => OracleProvider::band_protocol(), +/// OldOracleProvider::DIA => OracleProvider::dia(), +/// }; +/// ``` /// -/// # Error Handling +/// # Storage Format /// -/// When using unsupported providers: -/// - Oracle creation will return `Error::InvalidOracleConfig` -/// - Price requests will return `Error::OracleNotAvailable` -/// - Health checks will return `false` -/// - Validation will fail with appropriate error messages +/// The provider is stored as a string in contract storage, making it: +/// - **Human-readable**: Easy to debug and inspect +/// - **Extensible**: New providers don't require storage migration +/// - **Compatible**: Works across contract upgrades +/// - **Efficient**: String storage is optimized in Soroban #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub enum OracleProvider { - /// Reflector oracle (primary oracle for Stellar Network) - Reflector, - /// Pyth Network oracle (placeholder for Stellar) - Pyth, - /// Band Protocol oracle (not available on Stellar) - BandProtocol, - /// DIA oracle (not available on Stellar) - DIA, +pub struct OracleProvider { + /// String identifier for the oracle provider + provider_id: String, } impl OracleProvider { - /// Get provider name - pub fn name(&self) -> &'static str { - match self { - OracleProvider::Reflector => "Reflector", - OracleProvider::Pyth => "Pyth", - OracleProvider::BandProtocol => "Band Protocol", - OracleProvider::DIA => "DIA", + /// Creates a Reflector oracle provider instance. + /// + /// Reflector is the primary oracle provider for the Stellar Network, + /// offering reliable price feeds for major cryptocurrencies. + /// + /// # Returns + /// + /// `OracleProvider` instance configured for Reflector oracle + /// + /// # Example + /// + /// ```rust + /// # use predictify_hybrid::types::OracleProvider; + /// + /// let provider = OracleProvider::reflector(); + /// assert_eq!(provider.as_str(), "reflector"); + /// assert!(provider.is_supported()); + /// ``` + pub fn reflector() -> Self { + Self { + provider_id: String::from_str(&soroban_sdk::Env::default(), "reflector"), + } + } + + /// Creates a Pyth Network oracle provider instance. + /// + /// Pyth Network is a high-frequency oracle network that is planned + /// for future Stellar integration but currently not available. + /// + /// # Returns + /// + /// `OracleProvider` instance configured for Pyth Network oracle + /// + /// # Example + /// + /// ```rust + /// # use predictify_hybrid::types::OracleProvider; + /// + /// let provider = OracleProvider::pyth(); + /// assert_eq!(provider.as_str(), "pyth"); + /// assert!(!provider.is_supported()); // Not available on Stellar + /// ``` + pub fn pyth() -> Self { + Self { + provider_id: String::from_str(&soroban_sdk::Env::default(), "pyth"), + } + } + + /// Creates a Band Protocol oracle provider instance. + /// + /// Band Protocol is a decentralized oracle network primarily available + /// on Cosmos and EVM chains, not currently supported on Stellar. + /// + /// # Returns + /// + /// `OracleProvider` instance configured for Band Protocol oracle + /// + /// # Example + /// + /// ```rust + /// # use predictify_hybrid::types::OracleProvider; + /// + /// let provider = OracleProvider::band_protocol(); + /// assert_eq!(provider.as_str(), "band_protocol"); + /// assert!(!provider.is_supported()); // Not available on Stellar + /// ``` + pub fn band_protocol() -> Self { + Self { + provider_id: String::from_str(&soroban_sdk::Env::default(), "band_protocol"), } } - /// Check if provider is supported on Stellar + /// Creates a DIA oracle provider instance. + /// + /// DIA is a multi-chain oracle platform with comprehensive asset coverage, + /// but currently does not have Stellar integration. + /// + /// # Returns + /// + /// `OracleProvider` instance configured for DIA oracle + /// + /// # Example + /// + /// ```rust + /// # use predictify_hybrid::types::OracleProvider; + /// + /// let provider = OracleProvider::dia(); + /// assert_eq!(provider.as_str(), "dia"); + /// assert!(!provider.is_supported()); // Not available on Stellar + /// ``` + pub fn dia() -> Self { + Self { + provider_id: String::from_str(&soroban_sdk::Env::default(), "dia"), + } + } + + /// Creates an OracleProvider from a string identifier. + /// + /// This constructor enables forward compatibility by allowing any string + /// to be used as a provider identifier. Unknown providers are handled + /// gracefully through the `is_supported()` and `is_known()` methods. + /// + /// # Arguments + /// + /// * `provider_id` - String identifier for the oracle provider + /// + /// # Returns + /// + /// `OracleProvider` instance with the specified identifier + /// + /// # Example + /// + /// ```rust + /// # use soroban_sdk::{Env, String}; + /// # use predictify_hybrid::types::OracleProvider; + /// # let env = Env::default(); + /// + /// // Known provider + /// let known = OracleProvider::from_str(String::from_str(&env, "reflector")); + /// assert!(known.is_known()); + /// + /// // Unknown/future provider + /// let unknown = OracleProvider::from_str(String::from_str(&env, "future_oracle")); + /// assert!(!unknown.is_known()); + /// ``` + pub fn from_str(provider_id: String) -> Self { + Self { provider_id } + } + + /// Returns the string identifier for this oracle provider. + /// + /// This method provides access to the underlying string representation, + /// useful for debugging, logging, and serialization. + /// + /// # Returns + /// + /// String slice containing the provider identifier + /// + /// # Example + /// + /// ```rust + /// # use predictify_hybrid::types::OracleProvider; + /// + /// let provider = OracleProvider::reflector(); + /// assert_eq!(provider.as_str(), "reflector"); + /// ``` + pub fn as_str(&self) -> &str { + &self.provider_id + } + + /// Returns a human-readable name for the oracle provider. + /// + /// This method provides formatted display names for UI and logging purposes. + /// Unknown providers return a generic "Unknown Provider" label. + /// + /// # Returns + /// + /// String containing the formatted provider name + /// + /// # Example + /// + /// ```rust + /// # use soroban_sdk::{Env, String}; + /// # use predictify_hybrid::types::OracleProvider; + /// # let env = Env::default(); + /// + /// let reflector = OracleProvider::reflector(); + /// assert_eq!(reflector.name(), "Reflector"); + /// + /// let unknown = OracleProvider::from_str(String::from_str(&env, "new_oracle")); + /// assert_eq!(unknown.name(), "Unknown Provider (new_oracle)"); + /// ``` + pub fn name(&self) -> String { + let env = soroban_sdk::Env::default(); + match self.as_str() { + "reflector" => String::from_str(&env, "Reflector"), + "pyth" => String::from_str(&env, "Pyth Network"), + "band_protocol" => String::from_str(&env, "Band Protocol"), + "dia" => String::from_str(&env, "DIA"), + unknown => { + let prefix = String::from_str(&env, "Unknown Provider ("); + let suffix = String::from_str(&env, ")"); + prefix + unknown + suffix + } + } + } + + /// Checks if this oracle provider is known to the current contract version. + /// + /// This method identifies providers that are explicitly recognized by the + /// current contract code, even if they are not supported on the current + /// blockchain network. + /// + /// # Returns + /// + /// `true` if the provider is known, `false` for unknown providers + /// + /// # Example + /// + /// ```rust + /// # use predictify_hybrid::types::OracleProvider; + /// + /// let reflector = OracleProvider::reflector(); + /// assert!(reflector.is_known()); // Known provider + /// + /// let pyth = OracleProvider::pyth(); + /// assert!(pyth.is_known()); // Known but unsupported + /// ``` + pub fn is_known(&self) -> bool { + matches!(self.as_str(), "reflector" | "pyth" | "band_protocol" | "dia") + } + + /// Checks if this oracle provider is supported on the current network. + /// + /// This method determines if the provider can actually be used for oracle + /// operations on the current blockchain network (Stellar). This is more + /// restrictive than `is_known()` as it considers network availability. + /// + /// # Network Support Matrix + /// + /// | Provider | Stellar | Ethereum | Cosmos | Status | + /// |----------|---------|----------|---------|---------| + /// | Reflector | ✅ | ❌ | ❌ | Primary | + /// | Pyth | ❌ | ✅ | ❌ | Future | + /// | Band Protocol | ❌ | ✅ | ✅ | Not on Stellar | + /// | DIA | ❌ | ✅ | ✅ | Not on Stellar | + /// + /// # Returns + /// + /// `true` if the provider is supported on Stellar, `false` otherwise + /// + /// # Example + /// + /// ```rust + /// # use predictify_hybrid::types::OracleProvider; + /// + /// let reflector = OracleProvider::reflector(); + /// assert!(reflector.is_supported()); // Supported on Stellar + /// + /// let pyth = OracleProvider::pyth(); + /// assert!(!pyth.is_supported()); // Not available on Stellar + /// ``` pub fn is_supported(&self) -> bool { - matches!(self, OracleProvider::Reflector) + matches!(self.as_str(), "reflector") + } + + /// Validates the oracle provider for market creation. + /// + /// This method performs comprehensive validation to ensure the provider + /// can be safely used for new market creation. It's more strict than + /// `is_supported()` and should be used during market validation. + /// + /// # Validation Rules + /// + /// - Provider must be a known identifier + /// - Provider must be supported on the current network + /// - Provider must pass basic format validation + /// + /// # Returns + /// + /// `Ok(())` if the provider is valid for market creation + /// `Err(Error::InvalidOracleConfig)` if validation fails + /// + /// # Example + /// + /// ```rust + /// # use soroban_sdk::Env; + /// # use predictify_hybrid::types::OracleProvider; + /// # let env = Env::default(); + /// + /// let reflector = OracleProvider::reflector(); + /// assert!(reflector.validate_for_market(&env).is_ok()); // Valid + /// + /// let pyth = OracleProvider::pyth(); + /// assert!(pyth.validate_for_market(&env).is_err()); // Not supported + /// ``` + pub fn validate_for_market(&self, _env: &soroban_sdk::Env) -> Result<(), crate::Error> { + if !self.is_supported() { + return Err(crate::Error::InvalidOracleConfig); + } + Ok(()) } } @@ -504,7 +735,7 @@ impl OracleConfig { /// This value must never be used for live oracle creation or resolution. pub fn none_sentinel(env: &Env) -> Self { Self { - provider: OracleProvider::Reflector, + provider: OracleProvider::reflector(), oracle_address: Address::from_str( env, "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", @@ -521,7 +752,7 @@ impl OracleConfig { /// carried by the invalid `feed_id` / `threshold` / `comparison` tuple, which keeps the /// sentinel unambiguous even if a caller reuses the placeholder address elsewhere. pub fn is_none_sentinel(&self) -> bool { - self.provider == OracleProvider::Reflector + self.provider.as_str() == "reflector" && self.feed_id.is_empty() && self.threshold == 0 && self.comparison.is_empty() @@ -548,10 +779,8 @@ impl OracleConfig { return Err(crate::Error::InvalidComparison); } - // Validate provider is supported - if !self.provider.is_supported() { - return Err(crate::Error::InvalidOracleConfig); - } + // Validate provider is supported using new validation method + self.provider.validate_for_market(env)?; Ok(()) } diff --git a/docs/contracts/TYPES_SYSTEM.md b/docs/contracts/TYPES_SYSTEM.md index 2038773e..450b7cbb 100644 --- a/docs/contracts/TYPES_SYSTEM.md +++ b/docs/contracts/TYPES_SYSTEM.md @@ -20,16 +20,44 @@ Types are organized into logical categories for better understanding and mainten #### 1. Oracle Types -**OracleProvider Enum** +**OracleProvider Struct (Forward-Compatible)** ```rust -pub enum OracleProvider { - BandProtocol, - DIA, - Reflector, - Pyth, +pub struct OracleProvider { + provider_id: String, } ``` +**Key Features:** +- **Forward Compatibility**: String-based representation allows new providers without breaking existing markets +- **Backward Compatibility**: Existing markets continue to work across contract upgrades +- **Graceful Degradation**: Unknown providers are handled safely with fallback behavior + +**Standard Provider Identifiers:** +- `"reflector"` - Reflector oracle (primary for Stellar Network) +- `"pyth"` - Pyth Network oracle (placeholder for future Stellar support) +- `"band_protocol"` - Band Protocol oracle (not available on Stellar) +- `"dia"` - DIA oracle (not available on Stellar) + +**Constructor Methods:** +```rust +let provider = OracleProvider::reflector(); +let provider = OracleProvider::pyth(); +let provider = OracleProvider::from_str(String::from_str(&env, "new_provider")); +``` + +**Validation Methods:** +```rust +provider.is_known() // Recognized by current contract version +provider.is_supported() // Available on current network (Stellar) +provider.validate_for_market(&env)? // Strict validation for new markets +``` + +**Forward Compatibility Strategy:** +- New providers use descriptive lowercase names with underscores +- Unknown providers return "Unknown Provider (name)" for display +- Validation fails safely for unsupported providers +- No storage migration required for new provider additions + **OracleConfig Struct** ```rust pub struct OracleConfig { @@ -49,7 +77,7 @@ than `Option`. When `has_fallback` is `false`, the contracts write The reserved sentinel semantics are: -- `provider == OracleProvider::Reflector` +- `provider_id == "reflector"` - `feed_id == ""` - `threshold == 0` - `comparison == ""` @@ -108,7 +136,7 @@ use types::{OracleConfig, OracleProvider}; let oracle_address = Address::generate(&env); let oracle_config = OracleConfig::new( - OracleProvider::Pyth, + OracleProvider::reflector(), // Use constructor method oracle_address, String::from_str(&env, "BTC/USD"), 2500000, // $25,000 threshold @@ -119,6 +147,29 @@ let oracle_config = OracleConfig::new( oracle_config.validate(&env)?; ``` +### Forward Compatibility Examples + +```rust +// Creating markets with future oracle providers +let future_provider = OracleProvider::from_str(String::from_str(&env, "chainlink")); +let config = OracleConfig::new( + future_provider, + oracle_address, + String::from_str(&env, "ETH/USD"), + 2000000, + String::from_str(&env, "gt"), +); + +// Older contract versions can read this safely +assert!(!config.provider.is_known()); // Not recognized in this version +assert!(!config.provider.is_supported()); // Not supported on Stellar +assert_eq!(config.provider.as_str(), "chainlink"); // But can read the identifier + +// Display name provides graceful fallback +let display_name = config.provider.name(); +// Returns: "Unknown Provider (chainlink)" +``` + ### 2. Creating Markets ```rust @@ -291,7 +342,7 @@ let total_disputes = market.total_dispute_stakes(); let winning_total = market.winning_stake_total(); ``` -## Oracle Integration +### Oracle Integration ### Oracle Provider Support @@ -299,13 +350,53 @@ let winning_total = market.winning_stake_total(); // Check if provider is supported if oracle_provider.is_supported() { // Use the provider +} else { + // Handle unsupported provider gracefully } -// Get provider name +// Get provider identifier +let provider_id = oracle_provider.as_str(); + +// Get human-readable name let name = oracle_provider.name(); -// Get default feed format -let format = oracle_provider.default_feed_format(); +// Validate for market creation (strict) +oracle_provider.validate_for_market(&env)?; + +// Check if provider is known (less strict) +if oracle_provider.is_known() { + // Provider is recognized by this contract version +} else { + // Provider from future contract version - handle gracefully +} +``` + +### Forward Compatibility Patterns + +```rust +// Safe handling of unknown providers +match oracle_provider.as_str() { + "reflector" => handle_reflector_oracle(), + "pyth" => handle_pyth_oracle(), + unknown => { + println!("Unknown provider: {}", oracle_provider.name()); + use_fallback_oracle(); + } +} + +// Validation for different contexts +fn validate_for_read(provider: &OracleProvider) -> Result<(), Error> { + // Less strict validation - just check if provider exists + if provider.as_str().is_empty() { + return Err(Error::InvalidOracleConfig); + } + Ok(()) +} + +fn validate_for_creation(provider: &OracleProvider, env: &Env) -> Result<(), Error> { + // Strict validation for new markets + provider.validate_for_market(env) +} ``` ### Oracle Configuration @@ -517,8 +608,8 @@ fn test_validation_helpers() { | Type | Purpose | Key Methods | |------|---------|-------------| -| `OracleProvider` | Oracle service enumeration | `name()`, `is_supported()`, `default_feed_format()` | -| `OracleConfig` | Oracle configuration | `new()`, `validate()`, `is_supported()`, `is_greater_than()` | +| `OracleProvider` | Forward-compatible oracle provider | `reflector()`, `pyth()`, `from_str()`, `as_str()`, `name()`, `is_known()`, `is_supported()`, `validate_for_market()` | +| `OracleConfig` | Oracle configuration | `new()`, `validate()`, `none_sentinel()`, `is_none_sentinel()` | | `PythPrice` | Pyth price data | `new()`, `price_in_cents()`, `is_stale()`, `validate()` | | `ReflectorPriceData` | Reflector price data | `new()`, `price_in_cents()`, `is_stale()`, `validate()` | @@ -555,6 +646,92 @@ fn test_validation_helpers() { | `oracle_provider_to_string()` | Convert provider to string | `provider: &OracleProvider` | | `validate_comparison()` | Validate comparison operator | `comparison: &String, env: &Env` | +## Forward Compatibility Guide + +### Adding New Oracle Providers + +When adding new oracle providers to future contract versions: + +1. **Choose Provider ID**: Use descriptive lowercase names with underscores + ```rust + // Good examples + "chainlink" + "uniswap_oracle" + "custom_provider_v2" + ``` + +2. **Update Constructor Methods**: Add new constructor methods in future versions + ```rust + // Future contract version + impl OracleProvider { + pub fn chainlink() -> Self { + Self { provider_id: String::from_str(&env, "chainlink") } + } + } + ``` + +3. **Update Validation Logic**: Add new providers to `is_known()` and `is_supported()` methods + ```rust + // Future contract version + pub fn is_known(&self) -> bool { + matches!(self.as_str(), "reflector" | "pyth" | "band_protocol" | "dia" | "chainlink") + } + ``` + +4. **No Storage Migration Required**: Existing markets continue to work without migration + +### Handling Unknown Providers + +When reading markets created by newer contract versions: + +```rust +fn handle_oracle_provider(provider: &OracleProvider) -> Result<(), Error> { + match provider.as_str() { + "reflector" => use_reflector_oracle(), + "pyth" => use_pyth_oracle(), + "band_protocol" => use_band_protocol_oracle(), + "dia" => use_dia_oracle(), + unknown => { + // Provider from future contract version + println!("Unknown oracle provider: {}", provider.name()); + + // Option 1: Fail gracefully + return Err(Error::UnsupportedOracleProvider); + + // Option 2: Use fallback oracle + use_fallback_oracle(); + + // Option 3: Read-only mode (no new operations) + enter_read_only_mode(); + } + } +} +``` + +### Migration from Enum-Based System + +For contracts migrating from the old enum system: + +```rust +// Old enum (deprecated) +pub enum OldOracleProvider { + Reflector, + Pyth, + BandProtocol, + DIA, +} + +// Migration function +fn migrate_oracle_provider(old: OldOracleProvider, env: &Env) -> OracleProvider { + match old { + OldOracleProvider::Reflector => OracleProvider::reflector(), + OldOracleProvider::Pyth => OracleProvider::pyth(), + OldOracleProvider::BandProtocol => OracleProvider::band_protocol(), + OldOracleProvider::DIA => OracleProvider::dia(), + } +} +``` + ## Future Enhancements 1. **Type Serialization**: Proper serialization/deserialization support