diff --git a/contracts/predictify-hybrid/src/admin.rs b/contracts/predictify-hybrid/src/admin.rs index d7f98ffb..8ee93091 100644 --- a/contracts/predictify-hybrid/src/admin.rs +++ b/contracts/predictify-hybrid/src/admin.rs @@ -3,7 +3,7 @@ use alloc::format; use soroban_sdk::{contracttype, Address, Env, Map, String, Symbol, Vec}; // use alloc::string::ToString; // Unused import -use crate::config::{ConfigManager, ConfigUtils, ContractConfig, Environment}; +use crate::config::{ConfigManager, ConfigUtils, ConfigValidator, ContractConfig, Environment}; use crate::err::Error; use crate::events::EventEmitter; use crate::extensions::ExtensionManager; @@ -353,7 +353,7 @@ impl AdminInitializer { Environment::Mainnet => ConfigManager::get_mainnet_config(env), Environment::Custom => ConfigManager::get_development_config(env), }; - ConfigManager::validate_config(env, &config)?; + ConfigValidator::validate_contract_config(&config)?; // Initialize basic admin setup AdminInitializer::initialize(env, admin)?; diff --git a/contracts/predictify-hybrid/src/err.rs b/contracts/predictify-hybrid/src/err.rs index 3bd2ef8d..3e43db17 100644 --- a/contracts/predictify-hybrid/src/err.rs +++ b/contracts/predictify-hybrid/src/err.rs @@ -18,6 +18,20 @@ use soroban_sdk::{contracterror, contracttype, Address, Env, Map, String, Symbol /// - **Validation Errors (300-304)**: Input validation failures. /// - **General Errors (400-418)**: System state and configuration issues. /// - **Circuit Breaker Errors (500-504)**: Safety mechanism activation and management. + +/// Public mapping struct containing all metadata and classification for an error. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublicErrorMapping { + pub contract_code: u32, + pub client_code: u32, + pub code_str: &'static str, + pub description: &'static str, + pub category: ErrorCategory, + pub severity: ErrorSeverity, + pub recoverability: Recoverability, + pub is_known: bool, +} + #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] @@ -1378,104 +1392,138 @@ impl ErrorHandler { /// A tuple of (severity, category, recovery_strategy) for the error. pub(crate) fn get_error_classification(error: &Error) -> (ErrorSeverity, ErrorCategory, RecoveryStrategy) { match error { - // Critical - Error::AdminNotSet => ( - ErrorSeverity::Critical, - ErrorCategory::System, - RecoveryStrategy::ManualIntervention, - ), - Error::AssetDecimalsMismatch => ( - ErrorSeverity::Medium, - ErrorCategory::Validation, - RecoveryStrategy::Abort, - ), - Error::DisputeFeeFailed => ( - ErrorSeverity::Critical, - ErrorCategory::Financial, - RecoveryStrategy::ManualIntervention, - ), - // High - Error::Unauthorized => ( - ErrorSeverity::High, - ErrorCategory::Authentication, - RecoveryStrategy::Abort, - ), - Error::OracleUnavailable => ( - ErrorSeverity::High, - ErrorCategory::Oracle, - RecoveryStrategy::RetryWithDelay, - ), - Error::InvalidState => ( - ErrorSeverity::High, - ErrorCategory::System, - RecoveryStrategy::NoRecovery, - ), - // Medium - Error::MarketNotFound => ( - ErrorSeverity::Medium, - ErrorCategory::Market, - RecoveryStrategy::AlternativeMethod, - ), - Error::MarketClosed | Error::MarketResolved => ( - ErrorSeverity::Medium, - ErrorCategory::Market, - RecoveryStrategy::Abort, - ), - Error::InsufficientStake => ( - ErrorSeverity::Medium, - ErrorCategory::UserOperation, - RecoveryStrategy::Retry, - ), - Error::InvalidInput => ( - ErrorSeverity::Medium, - ErrorCategory::Validation, - RecoveryStrategy::Retry, - ), - Error::InvalidOracleConfig | Error::OracleConfidenceTooWide => ( - ErrorSeverity::Medium, - ErrorCategory::Oracle, - RecoveryStrategy::NoRecovery, - ), - // Low - Error::AlreadyVoted - | Error::AlreadyBet - | Error::AlreadyClaimed - | Error::ForceResolveAlreadyUsed - | Error::NothingToClaim => ( - ErrorSeverity::Low, - ErrorCategory::UserOperation, - RecoveryStrategy::Skip, - ), - Error::ForceResolveReplayed | Error::ForceResolveReasonEmpty => ( - ErrorSeverity::Low, - ErrorCategory::UserOperation, - RecoveryStrategy::Retry, - ), - Error::FeeAlreadyCollected => ( - ErrorSeverity::Low, - ErrorCategory::Financial, - RecoveryStrategy::Skip, - ), - Error::FeeExceedsMax => ( - ErrorSeverity::Low, - ErrorCategory::Financial, - RecoveryStrategy::Retry, - ), - Error::BetExceedsCap => ( - ErrorSeverity::Low, - ErrorCategory::Financial, - RecoveryStrategy::NoRecovery, - ), - Error::OperationWouldExceedBudget => ( - ErrorSeverity::Critical, - ErrorCategory::System, - RecoveryStrategy::NoRecovery, - ), - _ => ( - ErrorSeverity::Medium, - ErrorCategory::Unknown, - RecoveryStrategy::Abort, - ), + Error::IdempotentBatchAlreadyApplied => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::ReasonTableFull => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::Overflow => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::MaxBetCapExceeded => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::InvalidCap => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::Unauthorized => (ErrorSeverity::High, error.category(), RecoveryStrategy::Abort), + Error::MarketNotFound => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::AlternativeMethod), + Error::MarketClosed => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::MarketResolved => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::MarketNotResolved => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::NothingToClaim => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::AlreadyClaimed => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::InsufficientStake => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::InvalidOutcome => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::AlreadyVoted => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::AlreadyBet => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::BetsAlreadyPlaced => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::InsufficientBalance => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::InvalidNonce => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::BetAboveMaximum => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::BetBelowMarketMin => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::BetLimitsInverted => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::BetLimitAboveMaximum => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::BetCapOutOfRange => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::OracleUnavailable => (ErrorSeverity::High, error.category(), RecoveryStrategy::RetryWithDelay), + Error::InvalidOracleConfig => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::NoRecovery), + Error::OracleStale => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::OracleNoConsensus => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::OracleVerified => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::MarketNotReady => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::FallbackOracleUnavailable => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::ResolutionTimeoutReached => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::RetryWithDelay), + Error::OracleConfidenceTooWide => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::NoRecovery), + Error::InvalidOracleFeed => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::OracleCallbackAuthFailed => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::OracleCallbackUnauthorized => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::OracleCallbackInvalidSignature => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::OracleCallbackReplayDetected => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::OracleCallbackTimeout => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::RetryWithDelay), + Error::InvalidQuestion => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::InvalidOutcomes => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::InvalidDuration => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::InvalidThreshold => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::InvalidComparison => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::InvalidState => (ErrorSeverity::High, error.category(), RecoveryStrategy::NoRecovery), + Error::InvalidInput => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::InvalidFeeConfig => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::ConfigNotFound => (ErrorSeverity::High, error.category(), RecoveryStrategy::ManualIntervention), + Error::AlreadyDisputed => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Skip), + Error::DisputeVoteExpired => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::DisputeVoteDenied => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::DisputeAlreadyVoted => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Skip), + Error::DisputeCondNotMet => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::DisputeFeeFailed => (ErrorSeverity::Critical, error.category(), RecoveryStrategy::ManualIntervention), + Error::InvalidInitializationParams => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::DisputeError => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::DisputerCannotVote => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::SweepAlreadyDone => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Skip), + Error::FeeArithmeticOverflow => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::FeeAlreadyCollected => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::NoFeesToCollect => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::InvalidExtensionDays => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::ExtensionDenied => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::GasBudgetExceeded => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::AdminNotSet => (ErrorSeverity::Critical, error.category(), RecoveryStrategy::ManualIntervention), + Error::AssetDecimalsMismatch => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::AdminActionTimelocked => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::OperationWouldExceedBudget => (ErrorSeverity::Critical, error.category(), RecoveryStrategy::NoRecovery), + Error::QuestionTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::OutcomeTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TooManyOutcomes => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::FeedIdTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::ComparisonTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::CategoryTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TagTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TooManyTags => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::ExtensionReasonTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::SourceTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::ErrorMessageTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::SignatureTooLong => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TooManyExtensions => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TooManyOracleResults => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TooManyWinningOutcomes => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::ForceResolveAlreadyUsed => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Skip), + Error::ArchiveFull => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::CategoryTooShort => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TagTooShort => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::DuplicateMarketId => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::CannotArchiveFromState => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::CannotRestoreFromState => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::MarketAlreadyArchived => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Skip), + Error::MarketAlreadyRestored => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Skip), + Error::CBNotInitialized => (ErrorSeverity::High, error.category(), RecoveryStrategy::ManualIntervention), + Error::CBAlreadyOpen => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::CBNotOpen => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::CBOpen => (ErrorSeverity::High, error.category(), RecoveryStrategy::Retry), + Error::CBError => (ErrorSeverity::High, error.category(), RecoveryStrategy::ManualIntervention), + Error::RateLimitExceeded => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::CumulativeExtensionCapHit => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::IllegalMarketStateTransition => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::FeeExceedsMax => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::ForceResolveReplayed => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Retry), + Error::ForceResolveReasonEmpty => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Retry), + Error::NoPendingFeeCommit => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::FeeRevealTooEarly => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::FeePreimageMismatch => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::DisputeStakeCapExceeded => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::InsufficientStorageRentBudget => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::ExtensionCapExceeded => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::UpgradeChainMismatch => (ErrorSeverity::High, error.category(), RecoveryStrategy::ManualIntervention), + Error::OracleQuoteOutlier => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::MaxParticipantsReached => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::BetExceedsCap => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::NoRecovery), + Error::ReplayedOverride => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::OracleAdminCooldownActive => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::RetryWithDelay), + Error::SignerRotationCooldown => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::RetryWithDelay), + Error::UserNotWhitelisted => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::UserBlacklisted => (ErrorSeverity::High, error.category(), RecoveryStrategy::Abort), + Error::CreatorBlacklisted => (ErrorSeverity::High, error.category(), RecoveryStrategy::Abort), + Error::AlreadyInitialized => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Skip), + Error::InvalidTimeLockDelay => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::TimeLockNotExpired => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::NoPendingUpdate => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::PendingUpdateExists => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::ManualIntervention), + Error::InvalidStakeAmount => (ErrorSeverity::Low, error.category(), RecoveryStrategy::Retry), + Error::PerLedgerBetCapExceeded => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::RegistryFull => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::BatchEmpty => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::BatchSizeExceeded => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Retry), + Error::TreasuryUpdateTimelocked => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::NoPendingTreasuryUpdate => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), + Error::PendingTreasuryUpdateExists => (ErrorSeverity::Medium, error.category(), RecoveryStrategy::Abort), } } @@ -1565,6 +1613,11 @@ impl Error { /// A static string describing the error. pub fn description(&self) -> &'static str { match self { + Error::IdempotentBatchAlreadyApplied => "Idempotent Batch Already Applied", + Error::ReasonTableFull => "Reason table has reached its maximum capacity of 256 entries.", + Error::Overflow => "Overflow", + Error::MaxBetCapExceeded => "Max Bet Cap Exceeded", + Error::InvalidCap => "Invalid Cap", Error::Unauthorized => "User is not authorized to perform this action", Error::MarketNotFound => "Market not found", Error::MarketClosed => "Market is closed", @@ -1576,14 +1629,29 @@ impl Error { Error::InvalidOutcome => "Invalid outcome choice", Error::AlreadyVoted => "User has already voted", Error::AlreadyBet => "User has already placed a bet on this market", - Error::BetsAlreadyPlaced => { - "Bets have already been placed on this market (cannot update)" - } + Error::BetsAlreadyPlaced => "{", Error::InsufficientBalance => "Insufficient balance for operation", - Error::InsufficientStorageRentBudget => "Insufficient storage rent for persistent key allocation", + Error::InvalidNonce => "The provided claim nonce does not match the expected nonce for replay protection. Each claim must include the correct nonce to prevent transaction replays.", + Error::BetAboveMaximum => "Bet amount exceeds the effective maximum allowed for the market.", + Error::BetBelowMarketMin => "Bet amount is below the per-market minimum threshold.", + Error::BetLimitsInverted => "Per-market bet limits are inverted (min > max).", + Error::BetLimitAboveMaximum => "Per-market bet limit exceeds the absolute maximum.", + Error::BetCapOutOfRange => "Per-market max single-bet cap is out of range (zero, negative, or above absolute max).", Error::OracleUnavailable => "Oracle is unavailable", Error::InvalidOracleConfig => "Invalid oracle configuration", - Error::GasBudgetExceeded => "Gas budget exceeded", + Error::OracleStale => "Oracle data is stale", + Error::OracleNoConsensus => "Oracle consensus not reached", + Error::OracleVerified => "Oracle result already verified", + Error::MarketNotReady => "Market not ready for verification", + Error::FallbackOracleUnavailable => "Fallback oracle unavailable", + Error::ResolutionTimeoutReached => "Resolution timeout reached", + Error::OracleConfidenceTooWide => "Oracle confidence interval too wide", + Error::InvalidOracleFeed => "Invalid oracle feed ID", + Error::OracleCallbackAuthFailed => "Oracle callback authentication failed", + Error::OracleCallbackUnauthorized => "Oracle callback unauthorized", + Error::OracleCallbackInvalidSignature => "Oracle callback signature invalid", + Error::OracleCallbackReplayDetected => "Oracle callback replay detected", + Error::OracleCallbackTimeout => "Oracle callback timed out", Error::InvalidQuestion => "Invalid question format", Error::InvalidOutcomes => "Invalid outcomes provided", Error::InvalidDuration => "Invalid duration specified", @@ -1599,6 +1667,7 @@ impl Error { Error::DisputeAlreadyVoted => "Already voted in dispute", Error::DisputeCondNotMet => "Dispute resolution conditions not met", Error::DisputeFeeFailed => "Dispute fee distribution failed", + Error::InvalidInitializationParams => "Initialization parameters must be validated atomically. If any parameter is invalid, the entire initialization is rejected and no state is changed.", Error::DisputeError => "Generic dispute subsystem error", Error::DisputerCannotVote => "Dispute opener cannot vote on their own dispute", Error::SweepAlreadyDone => "Unclaimed winnings already swept for this market", @@ -1607,33 +1676,18 @@ impl Error { Error::NoFeesToCollect => "No fees available to collect", Error::InvalidExtensionDays => "Invalid extension days value", Error::ExtensionDenied => "Market extension not allowed", + Error::GasBudgetExceeded => "Gas budget exceeded", Error::AdminNotSet => "Admin address not set", - Error::FeeExceedsMax => "Fee is above the acceptable threshold", - Error::BetExceedsCap => "Bet amount exceeds the per-market maximum bet cap", - Error::OracleStale => "Oracle data is stale", - Error::OracleNoConsensus => "Oracle consensus not reached", - Error::OracleVerified => "Oracle result already verified", - Error::MarketNotReady => "Market not ready for verification", - Error::FallbackOracleUnavailable => "Fallback oracle unavailable", - Error::ResolutionTimeoutReached => "Resolution timeout reached", - Error::OracleConfidenceTooWide => "Oracle confidence interval too wide", - Error::InvalidOracleFeed => "Invalid oracle feed ID", - Error::OracleCallbackAuthFailed => "Oracle callback authentication failed", - Error::OracleCallbackUnauthorized => "Oracle callback unauthorized", - Error::OracleCallbackInvalidSignature => "Oracle callback signature invalid", - Error::OracleCallbackReplayDetected => "Oracle callback replay detected", - Error::OracleCallbackTimeout => "Oracle callback timed out", - - // Metadata length limit errors + Error::AssetDecimalsMismatch => "Asset decimals mismatch between stored and SAC decimals", + Error::AdminActionTimelocked => "A per-market admin action was attempted before the configured timelock period elapsed.", + Error::OperationWouldExceedBudget => "The operation would exceed the remaining CPU instruction budget. This is a pre-emptive guard that aborts before the host runs out of resources. Discriminant 444: AdminNotSet was pinned at 418 in the stability test so OperationWouldExceedBudget is placed after the frozen metadata range.", Error::QuestionTooLong => "Market question exceeds maximum allowed length", Error::OutcomeTooLong => "Outcome label exceeds maximum allowed length", Error::TooManyOutcomes => "Too many outcomes specified for the market", Error::FeedIdTooLong => "Oracle feed ID exceeds maximum allowed length", Error::ComparisonTooLong => "Comparison operator exceeds maximum allowed length", Error::CategoryTooLong => "Category string exceeds maximum allowed length", - Error::CategoryTooShort => "Category string is shorter than the minimum allowed length", Error::TagTooLong => "Tag string exceeds maximum allowed length", - Error::TagTooShort => "Tag string is shorter than the minimum allowed length", Error::TooManyTags => "Too many tags specified for the market", Error::ExtensionReasonTooLong => "Extension reason exceeds maximum allowed length", Error::SourceTooLong => "Source identifier exceeds maximum allowed length", @@ -1642,34 +1696,55 @@ impl Error { Error::TooManyExtensions => "Too many extension history entries", Error::TooManyOracleResults => "Too many oracle results in multi-oracle aggregation", Error::TooManyWinningOutcomes => "Too many winning outcomes specified", + Error::ForceResolveAlreadyUsed => "Force-resolve idempotency key has already been used for this market. The same `(market_id, idempotency_key)` pair was already consumed by a previous `force_resolve_market` call. The operation is safe to treat as a no-op; no resolution was re-applied.", Error::ArchiveFull => "Event archive is full; maximum archive capacity reached", - - // Circuit breaker errors + Error::CategoryTooShort => "Category string is shorter than the minimum allowed length", + Error::TagTooShort => "Tag string is shorter than the minimum allowed length", + Error::DuplicateMarketId => "Market ID already exists in the registry", + Error::CannotArchiveFromState => "Market cannot be archived from current state. Archive only allowed from Resolved or Cancelled.", + Error::CannotRestoreFromState => "Market cannot be restored from current state. Restore only allowed from Archived.", + Error::MarketAlreadyArchived => "Market is already archived. Cannot perform modification operations on archived markets.", + Error::MarketAlreadyRestored => "Market is already restored. Cannot restore a market that is not archived.", Error::CBNotInitialized => "Circuit breaker not initialized", Error::CBAlreadyOpen => "Circuit breaker is already open (paused)", Error::CBNotOpen => "Circuit breaker is not open (cannot recover)", Error::CBOpen => "Circuit breaker is open (operations blocked)", Error::CBError => "Generic circuit breaker subsystem error", Error::RateLimitExceeded => "Rate limit exceeded; too many requests in the time window", + Error::CumulativeExtensionCapHit => "Cumulative extension cap reached; no further extensions allowed", + Error::IllegalMarketStateTransition => "Illegal market state transition attempted", + Error::FeeExceedsMax => "Fee is above the acceptable threshold", + Error::ForceResolveReplayed => "Force-resolve idempotency key has already been used. Use a new unique key.", + Error::ForceResolveReasonEmpty => "Force-resolve reason is empty. Every force-resolve must be justified.", Error::NoPendingFeeCommit => "No pending fee config commit found", Error::FeeRevealTooEarly => "Fee config reveal attempted too early", Error::FeePreimageMismatch => "Preimage does not match the committed hash", Error::DisputeStakeCapExceeded => "Dispute stake cap exceeded for this address", - Error::InsufficientStorageRentBudget => { - "Insufficient storage rent budget for operation" - } + Error::InsufficientStorageRentBudget => "{", Error::ExtensionCapExceeded => "Cumulative extension cap for this market has been reached", Error::UpgradeChainMismatch => "Upgrade chain predecessor hash mismatch", - Error::ReplayedOverride => "Admin override nonce replayed; rejected", - Error::AssetDecimalsMismatch => "Asset decimals mismatch between stored and SAC decimals", - Error::DuplicateMarketId => "Market ID already exists in the registry", - Error::CumulativeExtensionCapHit => "Cumulative extension cap reached; no further extensions allowed", - Error::IllegalMarketStateTransition => "Illegal market state transition attempted", Error::OracleQuoteOutlier => "Oracle quote is an outlier relative to the rolling median", + Error::MaxParticipantsReached => "Maximum number of unique participants has been reached for this market.", + Error::BetExceedsCap => "Bet amount exceeds the per-market maximum bet cap", + Error::ReplayedOverride => "Admin override nonce replayed; rejected", + Error::OracleAdminCooldownActive => "Oracle admin cooldown is currently active.", + Error::SignerRotationCooldown => "Signer rotation cooldown is currently active.", + Error::UserNotWhitelisted => "User is not whitelisted for this operation.", + Error::UserBlacklisted => "User has been blacklisted.", + Error::CreatorBlacklisted => "Creator has been blacklisted.", + Error::AlreadyInitialized => "Contract is already initialized.", + Error::InvalidTimeLockDelay => "Invalid timelock delay.", + Error::TimeLockNotExpired => "Timelock has not yet expired.", + Error::NoPendingUpdate => "No pending update found.", + Error::PendingUpdateExists => "A pending update already exists.", + Error::InvalidStakeAmount => "Invalid stake amount.", + Error::PerLedgerBetCapExceeded => "Per-ledger bet cap exceeded.", + Error::RegistryFull => "Registry is full.", + Error::BatchEmpty => "Batch contains no entries; at least one bet is required.", + Error::BatchSizeExceeded => "Batch exceeds the maximum allowed number of entries.", Error::TreasuryUpdateTimelocked => "Treasury update timelock has not yet expired", Error::NoPendingTreasuryUpdate => "No pending treasury update found", Error::PendingTreasuryUpdateExists => "A pending treasury update already exists", - _ => "An unspecified error occurred.", } } @@ -1684,6 +1759,11 @@ impl Error { /// A static uppercase string code identifying the error. pub fn code(&self) -> &'static str { match self { + Error::IdempotentBatchAlreadyApplied => "IDEMPOTENT_BATCH_ALREADY_APPLIED", + Error::ReasonTableFull => "REASON_TABLE_FULL", + Error::Overflow => "OVERFLOW", + Error::MaxBetCapExceeded => "MAX_BET_CAP_EXCEEDED", + Error::InvalidCap => "INVALID_CAP", Error::Unauthorized => "UNAUTHORIZED", Error::MarketNotFound => "MARKET_NOT_FOUND", Error::MarketClosed => "MARKET_CLOSED", @@ -1697,9 +1777,27 @@ impl Error { Error::AlreadyBet => "ALREADY_BET", Error::BetsAlreadyPlaced => "BETS_ALREADY_PLACED", Error::InsufficientBalance => "INSUFFICIENT_BALANCE", + Error::InvalidNonce => "INVALID_NONCE", + Error::BetAboveMaximum => "BET_ABOVE_MAXIMUM", + Error::BetBelowMarketMin => "BET_BELOW_MARKET_MIN", + Error::BetLimitsInverted => "BET_LIMITS_INVERTED", + Error::BetLimitAboveMaximum => "BET_LIMIT_ABOVE_MAXIMUM", + Error::BetCapOutOfRange => "BET_CAP_OUT_OF_RANGE", Error::OracleUnavailable => "ORACLE_UNAVAILABLE", Error::InvalidOracleConfig => "INVALID_ORACLE_CONFIG", - Error::GasBudgetExceeded => "GAS_BUDGET_EXCEEDED", + Error::OracleStale => "ORACLE_STALE", + Error::OracleNoConsensus => "ORACLE_NO_CONSENSUS", + Error::OracleVerified => "ORACLE_VERIFIED", + Error::MarketNotReady => "MARKET_NOT_READY", + Error::FallbackOracleUnavailable => "FALLBACK_ORACLE_UNAVAILABLE", + Error::ResolutionTimeoutReached => "RESOLUTION_TIMEOUT_REACHED", + Error::OracleConfidenceTooWide => "ORACLE_CONFIDENCE_TOO_WIDE", + Error::InvalidOracleFeed => "INVALID_ORACLE_FEED", + Error::OracleCallbackAuthFailed => "ORACLE_CALLBACK_AUTH_FAILED", + Error::OracleCallbackUnauthorized => "ORACLE_CALLBACK_UNAUTHORIZED", + Error::OracleCallbackInvalidSignature => "ORACLE_CALLBACK_INVALID_SIGNATURE", + Error::OracleCallbackReplayDetected => "ORACLE_CALLBACK_REPLAY_DETECTED", + Error::OracleCallbackTimeout => "ORACLE_CALLBACK_TIMEOUT", Error::InvalidQuestion => "INVALID_QUESTION", Error::InvalidOutcomes => "INVALID_OUTCOMES", Error::InvalidDuration => "INVALID_DURATION", @@ -1715,6 +1813,7 @@ impl Error { Error::DisputeAlreadyVoted => "DISPUTE_ALREADY_VOTED", Error::DisputeCondNotMet => "DISPUTE_RESOLUTION_CONDITIONS_NOT_MET", Error::DisputeFeeFailed => "DISPUTE_FEE_DISTRIBUTION_FAILED", + Error::InvalidInitializationParams => "INVALID_INITIALIZATION_PARAMS", Error::DisputeError => "DISPUTE_ERROR", Error::DisputerCannotVote => "DISPUTER_CANNOT_VOTE", Error::SweepAlreadyDone => "SWEEP_ALREADY_DONE", @@ -1723,33 +1822,18 @@ impl Error { Error::NoFeesToCollect => "NO_FEES_TO_COLLECT", Error::InvalidExtensionDays => "INVALID_EXTENSION_DAYS", Error::ExtensionDenied => "EXTENSION_DENIED", + Error::GasBudgetExceeded => "GAS_BUDGET_EXCEEDED", Error::AdminNotSet => "ADMIN_NOT_SET", - Error::FeeExceedsMax => "FEE_ABOVE_ACCEPTABLE", - Error::BetExceedsCap => "BET_EXCEEDS_CAP", - Error::OracleStale => "ORACLE_STALE", - Error::OracleNoConsensus => "ORACLE_NO_CONSENSUS", - Error::OracleVerified => "ORACLE_VERIFIED", - Error::MarketNotReady => "MARKET_NOT_READY", - Error::FallbackOracleUnavailable => "FALLBACK_ORACLE_UNAVAILABLE", - Error::ResolutionTimeoutReached => "RESOLUTION_TIMEOUT_REACHED", - Error::OracleConfidenceTooWide => "ORACLE_CONFIDENCE_TOO_WIDE", - Error::InvalidOracleFeed => "INVALID_ORACLE_FEED", - Error::OracleCallbackAuthFailed => "ORACLE_CALLBACK_AUTH_FAILED", - Error::OracleCallbackUnauthorized => "ORACLE_CALLBACK_UNAUTHORIZED", - Error::OracleCallbackInvalidSignature => "ORACLE_CALLBACK_INVALID_SIGNATURE", - Error::OracleCallbackReplayDetected => "ORACLE_CALLBACK_REPLAY_DETECTED", - Error::OracleCallbackTimeout => "ORACLE_CALLBACK_TIMEOUT", - - // Metadata length limit errors + Error::AssetDecimalsMismatch => "ASSET_DECIMALS_MISMATCH", + Error::AdminActionTimelocked => "ADMIN_ACTION_TIMELOCKED", + Error::OperationWouldExceedBudget => "OPERATION_WOULD_EXCEED_BUDGET", Error::QuestionTooLong => "QUESTION_TOO_LONG", Error::OutcomeTooLong => "OUTCOME_TOO_LONG", Error::TooManyOutcomes => "TOO_MANY_OUTCOMES", Error::FeedIdTooLong => "FEED_ID_TOO_LONG", Error::ComparisonTooLong => "COMPARISON_TOO_LONG", Error::CategoryTooLong => "CATEGORY_TOO_LONG", - Error::CategoryTooShort => "CATEGORY_TOO_SHORT", Error::TagTooLong => "TAG_TOO_LONG", - Error::TagTooShort => "TAG_TOO_SHORT", Error::TooManyTags => "TOO_MANY_TAGS", Error::ExtensionReasonTooLong => "EXTENSION_REASON_TOO_LONG", Error::SourceTooLong => "SOURCE_TOO_LONG", @@ -1758,15 +1842,26 @@ impl Error { Error::TooManyExtensions => "TOO_MANY_EXTENSIONS", Error::TooManyOracleResults => "TOO_MANY_ORACLE_RESULTS", Error::TooManyWinningOutcomes => "TOO_MANY_WINNING_OUTCOMES", + Error::ForceResolveAlreadyUsed => "FORCE_RESOLVE_ALREADY_USED", Error::ArchiveFull => "ARCHIVE_FULL", - - // Circuit breaker errors + Error::CategoryTooShort => "CATEGORY_TOO_SHORT", + Error::TagTooShort => "TAG_TOO_SHORT", + Error::DuplicateMarketId => "DUPLICATE_MARKET_ID", + Error::CannotArchiveFromState => "CANNOT_ARCHIVE_FROM_STATE", + Error::CannotRestoreFromState => "CANNOT_RESTORE_FROM_STATE", + Error::MarketAlreadyArchived => "MARKET_ALREADY_ARCHIVED", + Error::MarketAlreadyRestored => "MARKET_ALREADY_RESTORED", Error::CBNotInitialized => "CIRCUIT_BREAKER_NOT_INITIALIZED", Error::CBAlreadyOpen => "CIRCUIT_BREAKER_ALREADY_OPEN", Error::CBNotOpen => "CIRCUIT_BREAKER_NOT_OPEN", Error::CBOpen => "CIRCUIT_BREAKER_OPEN", Error::CBError => "CIRCUIT_BREAKER_ERROR", Error::RateLimitExceeded => "RATE_LIMIT_EXCEEDED", + Error::CumulativeExtensionCapHit => "CUMULATIVE_EXTENSION_CAP_HIT", + Error::IllegalMarketStateTransition => "ILLEGAL_MARKET_STATE_TRANSITION", + Error::FeeExceedsMax => "FEE_ABOVE_ACCEPTABLE", + Error::ForceResolveReplayed => "FORCE_RESOLVE_REPLAYED", + Error::ForceResolveReasonEmpty => "FORCE_RESOLVE_REASON_EMPTY", Error::NoPendingFeeCommit => "NO_PENDING_FEE_COMMIT", Error::FeeRevealTooEarly => "FEE_REVEAL_TOO_EARLY", Error::FeePreimageMismatch => "FEE_PREIMAGE_MISMATCH", @@ -1774,16 +1869,28 @@ impl Error { Error::InsufficientStorageRentBudget => "INSUFFICIENT_STORAGE_RENT_BUDGET", Error::ExtensionCapExceeded => "EXTENSION_CAP_EXCEEDED", Error::UpgradeChainMismatch => "UPGRADE_CHAIN_MISMATCH", - Error::ReplayedOverride => "REPLAYED_OVERRIDE", - Error::AssetDecimalsMismatch => "ASSET_DECIMALS_MISMATCH", - Error::DuplicateMarketId => "DUPLICATE_MARKET_ID", - Error::CumulativeExtensionCapHit => "CUMULATIVE_EXTENSION_CAP_HIT", - Error::IllegalMarketStateTransition => "ILLEGAL_MARKET_STATE_TRANSITION", Error::OracleQuoteOutlier => "ORACLE_QUOTE_OUTLIER", + Error::MaxParticipantsReached => "MAX_PARTICIPANTS_REACHED", + Error::BetExceedsCap => "BET_EXCEEDS_CAP", + Error::ReplayedOverride => "REPLAYED_OVERRIDE", + Error::OracleAdminCooldownActive => "ORACLE_ADMIN_COOLDOWN_ACTIVE", + Error::SignerRotationCooldown => "SIGNER_ROTATION_COOLDOWN", + Error::UserNotWhitelisted => "USER_NOT_WHITELISTED", + Error::UserBlacklisted => "USER_BLACKLISTED", + Error::CreatorBlacklisted => "CREATOR_BLACKLISTED", + Error::AlreadyInitialized => "ALREADY_INITIALIZED", + Error::InvalidTimeLockDelay => "INVALID_TIME_LOCK_DELAY", + Error::TimeLockNotExpired => "TIME_LOCK_NOT_EXPIRED", + Error::NoPendingUpdate => "NO_PENDING_UPDATE", + Error::PendingUpdateExists => "PENDING_UPDATE_EXISTS", + Error::InvalidStakeAmount => "INVALID_STAKE_AMOUNT", + Error::PerLedgerBetCapExceeded => "PER_LEDGER_BET_CAP_EXCEEDED", + Error::RegistryFull => "REGISTRY_FULL", + Error::BatchEmpty => "BATCH_EMPTY", + Error::BatchSizeExceeded => "BATCH_SIZE_EXCEEDED", Error::TreasuryUpdateTimelocked => "TREASURY_UPDATE_TIMELOCKED", Error::NoPendingTreasuryUpdate => "NO_PENDING_TREASURY_UPDATE", Error::PendingTreasuryUpdateExists => "PENDING_TREASURY_UPDATE_EXISTS", - _ => "UNSPECIFIED_ERROR", } } @@ -1813,144 +1920,138 @@ impl Error { /// | 1900–1999 | Metadata / Limits | pub fn client_code(&self) -> u32 { match self { - // ── Oracle (1000–1099) ─────────────────────────────────────────── - Error::OracleUnavailable => 1000, - Error::InvalidOracleConfig => 1001, - Error::OracleStale => 1002, - Error::OracleNoConsensus => 1003, - Error::OracleVerified => 1004, - Error::FallbackOracleUnavailable => 1005, - Error::ResolutionTimeoutReached => 1006, - Error::OracleConfidenceTooWide => 1007, - Error::InvalidOracleFeed => 1008, - Error::OracleCallbackAuthFailed => 1009, - Error::OracleCallbackUnauthorized => 1010, + Error::OracleUnavailable => 1000, + Error::InvalidOracleConfig => 1001, + Error::OracleStale => 1002, + Error::OracleNoConsensus => 1003, + Error::OracleVerified => 1004, + Error::FallbackOracleUnavailable => 1005, + Error::ResolutionTimeoutReached => 1006, + Error::OracleConfidenceTooWide => 1007, + Error::InvalidOracleFeed => 1008, + Error::OracleCallbackAuthFailed => 1009, + Error::OracleCallbackUnauthorized => 1010, Error::OracleCallbackInvalidSignature => 1011, - Error::OracleCallbackReplayDetected => 1012, - Error::OracleCallbackTimeout => 1013, - Error::OracleQuoteOutlier => 1014, - - // ── Market (1100–1199) ─────────────────────────────────────────── - Error::MarketNotFound => 1100, - Error::MarketClosed => 1101, - Error::MarketResolved => 1102, - Error::MarketNotResolved => 1103, - Error::MarketNotReady => 1104, - Error::InvalidState => 1105, - Error::IllegalMarketStateTransition => 1106, - Error::DuplicateMarketId => 1107, - Error::MaxParticipantsReached => 1108, - - // ── Validation (1200–1299) ─────────────────────────────────────── - Error::InvalidQuestion => 1200, - Error::InvalidOutcomes => 1201, - Error::InvalidDuration => 1202, - Error::InvalidThreshold => 1203, - Error::InvalidComparison => 1204, - Error::InvalidInput => 1205, - Error::InvalidOutcome => 1206, - Error::AssetDecimalsMismatch => 1207, - Error::InvalidExtensionDays => 1208, - Error::ExtensionDenied => 1209, - Error::CumulativeExtensionCapHit => 1210, - Error::ExtensionCapExceeded => 1211, - - // ── Financial (1300–1399) ──────────────────────────────────────── - Error::InsufficientStake => 1300, - Error::InsufficientBalance => 1301, - Error::NothingToClaim => 1302, - Error::AlreadyClaimed => 1303, - Error::FeeArithmeticOverflow => 1304, - Error::FeeAlreadyCollected => 1305, - Error::NoFeesToCollect => 1306, - Error::InvalidFeeConfig => 1307, - Error::FeeExceedsMax => 1308, - Error::SweepAlreadyDone => 1309, - Error::DisputeFeeFailed => 1310, - Error::NoPendingFeeCommit => 1311, - Error::FeeRevealTooEarly => 1312, - Error::FeePreimageMismatch => 1313, - Error::BetExceedsCap => 1314, - Error::MaxBetCapExceeded => 1315, - - // ── Dispute (1400–1499) ────────────────────────────────────────── - Error::AlreadyDisputed => 1400, - Error::DisputeVoteExpired => 1401, - Error::DisputeVoteDenied => 1402, - Error::DisputeAlreadyVoted => 1403, - Error::DisputeCondNotMet => 1404, - Error::DisputeError => 1405, - Error::DisputerCannotVote => 1406, - Error::DisputeStakeCapExceeded => 1407, - - // ── Authentication (1500–1599) ─────────────────────────────────── - Error::Unauthorized => 1500, - Error::ReplayedOverride => 1501, - Error::UserNotWhitelisted => 1502, - Error::UserBlacklisted => 1503, - Error::CreatorBlacklisted => 1504, - - // ── Circuit Breaker (1600–1699) ────────────────────────────────── - Error::CBNotInitialized => 1600, - Error::CBAlreadyOpen => 1601, - Error::CBNotOpen => 1602, - Error::CBOpen => 1603, - Error::CBError => 1604, - Error::RateLimitExceeded => 1605, - Error::PerLedgerBetCapExceeded => 1606, - - // ── System (1700–1799) ─────────────────────────────────────────── - Error::ConfigNotFound => 1700, - Error::AdminNotSet => 1701, - Error::GasBudgetExceeded => 1702, - Error::OperationWouldExceedBudget => 1703, - Error::InsufficientStorageRentBudget => 1704, - Error::UpgradeChainMismatch => 1705, - Error::AlreadyInitialized => 1706, - Error::InvalidTimeLockDelay => 1707, - Error::TimeLockNotExpired => 1708, - Error::NoPendingUpdate => 1709, - Error::PendingUpdateExists => 1710, - Error::AdminActionTimelocked => 1711, - Error::OracleAdminCooldownActive => 1712, - Error::SignerRotationCooldown => 1713, - Error::Overflow => 1714, - Error::InvalidCap => 1715, - - // ── User Operation (1800–1899) ─────────────────────────────────── - Error::AlreadyVoted => 1800, - Error::AlreadyBet => 1801, - Error::BetsAlreadyPlaced => 1802, - Error::ForceResolveAlreadyUsed => 1803, - Error::ForceResolveReplayed => 1804, - Error::ForceResolveReasonEmpty => 1805, - Error::IdempotentBatchAlreadyApplied => 1806, - Error::InvalidStakeAmount => 1807, - - // ── Metadata / Limits (1900–1999) ──────────────────────────────── - Error::QuestionTooLong => 1900, - Error::OutcomeTooLong => 1901, - Error::TooManyOutcomes => 1902, - Error::FeedIdTooLong => 1903, - Error::ComparisonTooLong => 1904, - Error::CategoryTooLong => 1905, - Error::CategoryTooShort => 1906, - Error::TagTooLong => 1907, - Error::TagTooShort => 1908, - Error::TooManyTags => 1909, - Error::ExtensionReasonTooLong => 1910, - Error::SourceTooLong => 1911, - Error::ErrorMessageTooLong => 1912, - Error::SignatureTooLong => 1913, - Error::TooManyExtensions => 1914, - Error::TooManyOracleResults => 1915, - Error::TooManyWinningOutcomes => 1916, - Error::ArchiveFull => 1917, - Error::ReasonTableFull => 1918, - Error::RegistryFull => 1919, - // Catch-all for variants not yet assigned an off-chain code. Callers - // that hit this value should treat it as an unmapped error. - _ => 0, + Error::OracleCallbackReplayDetected => 1012, + Error::OracleCallbackTimeout => 1013, + Error::OracleQuoteOutlier => 1014, + Error::MarketNotFound => 1100, + Error::MarketClosed => 1101, + Error::MarketResolved => 1102, + Error::MarketNotResolved => 1103, + Error::MarketNotReady => 1104, + Error::InvalidState => 1105, + Error::IllegalMarketStateTransition => 1106, + Error::DuplicateMarketId => 1107, + Error::MaxParticipantsReached => 1108, + Error::CannotArchiveFromState => 1109, + Error::CannotRestoreFromState => 1110, + Error::MarketAlreadyArchived => 1111, + Error::MarketAlreadyRestored => 1112, + Error::InvalidQuestion => 1200, + Error::InvalidOutcomes => 1201, + Error::InvalidDuration => 1202, + Error::InvalidThreshold => 1203, + Error::InvalidComparison => 1204, + Error::InvalidInput => 1205, + Error::InvalidOutcome => 1206, + Error::AssetDecimalsMismatch => 1207, + Error::InvalidExtensionDays => 1208, + Error::ExtensionDenied => 1209, + Error::CumulativeExtensionCapHit => 1210, + Error::ExtensionCapExceeded => 1211, + Error::InvalidInitializationParams => 1212, + Error::InvalidNonce => 1213, + Error::BetLimitsInverted => 1214, + Error::BetLimitAboveMaximum => 1215, + Error::BetCapOutOfRange => 1216, + Error::BetAboveMaximum => 1217, + Error::BetBelowMarketMin => 1218, + Error::InsufficientStake => 1300, + Error::InsufficientBalance => 1301, + Error::NothingToClaim => 1302, + Error::AlreadyClaimed => 1303, + Error::FeeArithmeticOverflow => 1304, + Error::FeeAlreadyCollected => 1305, + Error::NoFeesToCollect => 1306, + Error::InvalidFeeConfig => 1307, + Error::FeeExceedsMax => 1308, + Error::SweepAlreadyDone => 1309, + Error::DisputeFeeFailed => 1310, + Error::NoPendingFeeCommit => 1311, + Error::FeeRevealTooEarly => 1312, + Error::FeePreimageMismatch => 1313, + Error::BetExceedsCap => 1314, + Error::MaxBetCapExceeded => 1315, + Error::TreasuryUpdateTimelocked => 1316, + Error::NoPendingTreasuryUpdate => 1317, + Error::PendingTreasuryUpdateExists => 1318, + Error::AlreadyDisputed => 1400, + Error::DisputeVoteExpired => 1401, + Error::DisputeVoteDenied => 1402, + Error::DisputeAlreadyVoted => 1403, + Error::DisputeCondNotMet => 1404, + Error::DisputeError => 1405, + Error::DisputerCannotVote => 1406, + Error::DisputeStakeCapExceeded => 1407, + Error::Unauthorized => 1500, + Error::ReplayedOverride => 1501, + Error::UserNotWhitelisted => 1502, + Error::UserBlacklisted => 1503, + Error::CreatorBlacklisted => 1504, + Error::CBNotInitialized => 1600, + Error::CBAlreadyOpen => 1601, + Error::CBNotOpen => 1602, + Error::CBOpen => 1603, + Error::CBError => 1604, + Error::RateLimitExceeded => 1605, + Error::PerLedgerBetCapExceeded => 1606, + Error::ConfigNotFound => 1700, + Error::AdminNotSet => 1701, + Error::GasBudgetExceeded => 1702, + Error::OperationWouldExceedBudget => 1703, + Error::InsufficientStorageRentBudget => 1704, + Error::UpgradeChainMismatch => 1705, + Error::AlreadyInitialized => 1706, + Error::InvalidTimeLockDelay => 1707, + Error::TimeLockNotExpired => 1708, + Error::NoPendingUpdate => 1709, + Error::PendingUpdateExists => 1710, + Error::AdminActionTimelocked => 1711, + Error::OracleAdminCooldownActive => 1712, + Error::SignerRotationCooldown => 1713, + Error::Overflow => 1714, + Error::InvalidCap => 1715, + Error::BatchEmpty => 1716, + Error::BatchSizeExceeded => 1717, + Error::AlreadyVoted => 1800, + Error::AlreadyBet => 1801, + Error::BetsAlreadyPlaced => 1802, + Error::ForceResolveAlreadyUsed => 1803, + Error::ForceResolveReplayed => 1804, + Error::ForceResolveReasonEmpty => 1805, + Error::IdempotentBatchAlreadyApplied => 1806, + Error::InvalidStakeAmount => 1807, + Error::QuestionTooLong => 1900, + Error::OutcomeTooLong => 1901, + Error::TooManyOutcomes => 1902, + Error::FeedIdTooLong => 1903, + Error::ComparisonTooLong => 1904, + Error::CategoryTooLong => 1905, + Error::CategoryTooShort => 1906, + Error::TagTooLong => 1907, + Error::TagTooShort => 1908, + Error::TooManyTags => 1909, + Error::ExtensionReasonTooLong => 1910, + Error::SourceTooLong => 1911, + Error::ErrorMessageTooLong => 1912, + Error::SignatureTooLong => 1913, + Error::TooManyExtensions => 1914, + Error::TooManyOracleResults => 1915, + Error::TooManyWinningOutcomes => 1916, + Error::ArchiveFull => 1917, + Error::ReasonTableFull => 1918, + Error::RegistryFull => 1919, } } @@ -1971,48 +2072,566 @@ impl Error { /// | `Terminal` | Permanent; caller MUST NOT retry. | pub fn recoverability(&self) -> Recoverability { match self { - // ── Retryable ──────────────────────────────────────────────────── - // These are transient conditions that may resolve on their own. - Error::OracleUnavailable + Error::ReasonTableFull + | Error::InsufficientStake + | Error::InvalidOutcome + | Error::OracleUnavailable | Error::OracleStale | Error::OracleNoConsensus + | Error::MarketNotReady + | Error::FallbackOracleUnavailable + | Error::ResolutionTimeoutReached | Error::OracleConfidenceTooWide | Error::OracleCallbackTimeout - | Error::FallbackOracleUnavailable - | Error::RateLimitExceeded - | Error::PerLedgerBetCapExceeded + | Error::InvalidQuestion + | Error::InvalidOutcomes + | Error::InvalidDuration + | Error::InvalidThreshold + | Error::InvalidComparison + | Error::InvalidInput + | Error::QuestionTooLong + | Error::OutcomeTooLong + | Error::TooManyOutcomes + | Error::FeedIdTooLong + | Error::ComparisonTooLong + | Error::CategoryTooLong + | Error::TagTooLong + | Error::TooManyTags + | Error::ExtensionReasonTooLong + | Error::SourceTooLong + | Error::ErrorMessageTooLong + | Error::SignatureTooLong + | Error::TooManyExtensions + | Error::TooManyOracleResults + | Error::TooManyWinningOutcomes + | Error::ArchiveFull + | Error::CategoryTooShort + | Error::TagTooShort | Error::CBOpen - | Error::InsufficientStorageRentBudget - | Error::ResolutionTimeoutReached - | Error::TimeLockNotExpired + | Error::RateLimitExceeded + | Error::FeeExceedsMax + | Error::ForceResolveReplayed + | Error::ForceResolveReasonEmpty | Error::FeeRevealTooEarly + | Error::InsufficientStorageRentBudget | Error::OracleAdminCooldownActive - | Error::SignerRotationCooldown => Recoverability::Retryable, + | Error::SignerRotationCooldown + | Error::TimeLockNotExpired + | Error::InvalidStakeAmount + | Error::PerLedgerBetCapExceeded + | Error::RegistryFull + | Error::BatchEmpty + | Error::BatchSizeExceeded => Recoverability::Retryable, - // ── Requires Admin ─────────────────────────────────────────────── - // These cannot resolve without privileged intervention. - Error::AdminNotSet + Error::InvalidOracleConfig + | Error::InvalidOracleFeed + | Error::InvalidFeeConfig + | Error::ConfigNotFound + | Error::DisputeFeeFailed + | Error::AdminNotSet + | Error::AssetDecimalsMismatch + | Error::AdminActionTimelocked | Error::CBNotInitialized | Error::CBAlreadyOpen | Error::CBNotOpen | Error::CBError - | Error::ConfigNotFound - | Error::InvalidOracleConfig - | Error::InvalidFeeConfig + | Error::NoPendingFeeCommit | Error::UpgradeChainMismatch - | Error::PendingUpdateExists | Error::NoPendingUpdate - | Error::AssetDecimalsMismatch - | Error::InvalidOracleFeed - | Error::AdminActionTimelocked - | Error::NoPendingFeeCommit => Recoverability::RequiresAdmin, + | Error::PendingUpdateExists => Recoverability::RequiresAdmin, - // ── Terminal ───────────────────────────────────────────────────── - // All other errors are permanent for the current call; the same - // inputs will always produce the same rejection. _ => Recoverability::Terminal, } } + + /// Resolves an `Error` variant from a raw on-chain contract error code (`u32`). + /// Resolves an `Error` variant from a raw on-chain contract error code (`u32`). + /// Resolves an `Error` variant from a raw on-chain contract error code (`u32`). + pub fn from_contract_code(code: u32) -> Option { + match code { + 100 => Some(Error::Unauthorized), + 101 => Some(Error::MarketNotFound), + 102 => Some(Error::MarketClosed), + 103 => Some(Error::MarketResolved), + 104 => Some(Error::MarketNotResolved), + 105 => Some(Error::NothingToClaim), + 106 => Some(Error::AlreadyClaimed), + 107 => Some(Error::InsufficientStake), + 108 => Some(Error::InvalidOutcome), + 109 => Some(Error::AlreadyVoted), + 110 => Some(Error::AlreadyBet), + 111 => Some(Error::BetsAlreadyPlaced), + 112 => Some(Error::InsufficientBalance), + 113 => Some(Error::InvalidNonce), + 114 => Some(Error::BetAboveMaximum), + 115 => Some(Error::BetBelowMarketMin), + 116 => Some(Error::BetLimitsInverted), + 117 => Some(Error::BetLimitAboveMaximum), + 118 => Some(Error::BetCapOutOfRange), + 200 => Some(Error::OracleUnavailable), + 201 => Some(Error::InvalidOracleConfig), + 202 => Some(Error::OracleStale), + 203 => Some(Error::OracleNoConsensus), + 204 => Some(Error::OracleVerified), + 205 => Some(Error::MarketNotReady), + 206 => Some(Error::FallbackOracleUnavailable), + 207 => Some(Error::ResolutionTimeoutReached), + 208 => Some(Error::OracleConfidenceTooWide), + 209 => Some(Error::InvalidOracleFeed), + 210 => Some(Error::OracleCallbackAuthFailed), + 211 => Some(Error::OracleCallbackUnauthorized), + 212 => Some(Error::OracleCallbackInvalidSignature), + 213 => Some(Error::OracleCallbackReplayDetected), + 214 => Some(Error::OracleCallbackTimeout), + 300 => Some(Error::InvalidQuestion), + 301 => Some(Error::InvalidOutcomes), + 302 => Some(Error::InvalidDuration), + 303 => Some(Error::InvalidThreshold), + 304 => Some(Error::InvalidComparison), + 400 => Some(Error::InvalidState), + 401 => Some(Error::InvalidInput), + 402 => Some(Error::InvalidFeeConfig), + 403 => Some(Error::ConfigNotFound), + 404 => Some(Error::AlreadyDisputed), + 405 => Some(Error::DisputeVoteExpired), + 406 => Some(Error::DisputeVoteDenied), + 407 => Some(Error::DisputeAlreadyVoted), + 408 => Some(Error::DisputeCondNotMet), + 409 => Some(Error::DisputeFeeFailed), + 410 => Some(Error::DisputeError), + 411 => Some(Error::SweepAlreadyDone), + 412 => Some(Error::FeeArithmeticOverflow), + 413 => Some(Error::FeeAlreadyCollected), + 414 => Some(Error::NoFeesToCollect), + 415 => Some(Error::InvalidExtensionDays), + 416 => Some(Error::ExtensionDenied), + 417 => Some(Error::GasBudgetExceeded), + 418 => Some(Error::AdminNotSet), + 420 => Some(Error::QuestionTooLong), + 421 => Some(Error::OutcomeTooLong), + 422 => Some(Error::TooManyOutcomes), + 423 => Some(Error::FeedIdTooLong), + 424 => Some(Error::ComparisonTooLong), + 425 => Some(Error::CategoryTooLong), + 426 => Some(Error::TagTooLong), + 427 => Some(Error::TooManyTags), + 428 => Some(Error::ExtensionReasonTooLong), + 429 => Some(Error::SourceTooLong), + 430 => Some(Error::ErrorMessageTooLong), + 431 => Some(Error::SignatureTooLong), + 432 => Some(Error::TooManyExtensions), + 433 => Some(Error::TooManyOracleResults), + 434 => Some(Error::TooManyWinningOutcomes), + 435 => Some(Error::ForceResolveAlreadyUsed), + 436 => Some(Error::CategoryTooShort), + 437 => Some(Error::TagTooShort), + 438 => Some(Error::DisputerCannotVote), + 439 => Some(Error::AssetDecimalsMismatch), + 440 => Some(Error::ArchiveFull), + 441 => Some(Error::DuplicateMarketId), + 442 => Some(Error::CannotArchiveFromState), + 443 => Some(Error::AdminActionTimelocked), + 444 => Some(Error::OperationWouldExceedBudget), + 445 => Some(Error::MarketAlreadyArchived), + 446 => Some(Error::MarketAlreadyRestored), + 447 => Some(Error::CannotRestoreFromState), + 500 => Some(Error::CBNotInitialized), + 501 => Some(Error::CBAlreadyOpen), + 502 => Some(Error::CBNotOpen), + 503 => Some(Error::CBOpen), + 504 => Some(Error::CBError), + 505 => Some(Error::RateLimitExceeded), + 506 => Some(Error::CumulativeExtensionCapHit), + 507 => Some(Error::IllegalMarketStateTransition), + 508 => Some(Error::FeeExceedsMax), + 517 => Some(Error::ForceResolveReplayed), + 518 => Some(Error::ForceResolveReasonEmpty), + 519 => Some(Error::NoPendingFeeCommit), + 520 => Some(Error::FeeRevealTooEarly), + 521 => Some(Error::FeePreimageMismatch), + 522 => Some(Error::DisputeStakeCapExceeded), + 523 => Some(Error::InsufficientStorageRentBudget), + 524 => Some(Error::ExtensionCapExceeded), + 525 => Some(Error::UpgradeChainMismatch), + 526 => Some(Error::ReplayedOverride), + 527 => Some(Error::OracleQuoteOutlier), + 528 => Some(Error::MaxParticipantsReached), + 545 => Some(Error::BatchEmpty), + 546 => Some(Error::BatchSizeExceeded), + 660 => Some(Error::IdempotentBatchAlreadyApplied), + 670 => Some(Error::ReasonTableFull), + 672 => Some(Error::Overflow), + 673 => Some(Error::MaxBetCapExceeded), + 674 => Some(Error::InvalidCap), + 675 => Some(Error::BetExceedsCap), + 676 => Some(Error::OracleAdminCooldownActive), + 677 => Some(Error::SignerRotationCooldown), + 678 => Some(Error::UserNotWhitelisted), + 679 => Some(Error::UserBlacklisted), + 680 => Some(Error::CreatorBlacklisted), + 681 => Some(Error::AlreadyInitialized), + 682 => Some(Error::InvalidTimeLockDelay), + 683 => Some(Error::TimeLockNotExpired), + 684 => Some(Error::NoPendingUpdate), + 685 => Some(Error::PendingUpdateExists), + 686 => Some(Error::InvalidStakeAmount), + 687 => Some(Error::PerLedgerBetCapExceeded), + 688 => Some(Error::RegistryFull), + 689 => Some(Error::TreasuryUpdateTimelocked), + 690 => Some(Error::NoPendingTreasuryUpdate), + 691 => Some(Error::PendingTreasuryUpdateExists), + 700 => Some(Error::InvalidInitializationParams), + _ => None, + } + } + + /// Resolves an `Error` variant from an off-chain `client_code` (`u32`). + pub fn from_client_code(code: u32) -> Option { + match code { + 1000 => Some(Error::OracleUnavailable), + 1001 => Some(Error::InvalidOracleConfig), + 1002 => Some(Error::OracleStale), + 1003 => Some(Error::OracleNoConsensus), + 1004 => Some(Error::OracleVerified), + 1005 => Some(Error::FallbackOracleUnavailable), + 1006 => Some(Error::ResolutionTimeoutReached), + 1007 => Some(Error::OracleConfidenceTooWide), + 1008 => Some(Error::InvalidOracleFeed), + 1009 => Some(Error::OracleCallbackAuthFailed), + 1010 => Some(Error::OracleCallbackUnauthorized), + 1011 => Some(Error::OracleCallbackInvalidSignature), + 1012 => Some(Error::OracleCallbackReplayDetected), + 1013 => Some(Error::OracleCallbackTimeout), + 1014 => Some(Error::OracleQuoteOutlier), + 1100 => Some(Error::MarketNotFound), + 1101 => Some(Error::MarketClosed), + 1102 => Some(Error::MarketResolved), + 1103 => Some(Error::MarketNotResolved), + 1104 => Some(Error::MarketNotReady), + 1105 => Some(Error::InvalidState), + 1106 => Some(Error::IllegalMarketStateTransition), + 1107 => Some(Error::DuplicateMarketId), + 1108 => Some(Error::MaxParticipantsReached), + 1109 => Some(Error::CannotArchiveFromState), + 1110 => Some(Error::CannotRestoreFromState), + 1111 => Some(Error::MarketAlreadyArchived), + 1112 => Some(Error::MarketAlreadyRestored), + 1200 => Some(Error::InvalidQuestion), + 1201 => Some(Error::InvalidOutcomes), + 1202 => Some(Error::InvalidDuration), + 1203 => Some(Error::InvalidThreshold), + 1204 => Some(Error::InvalidComparison), + 1205 => Some(Error::InvalidInput), + 1206 => Some(Error::InvalidOutcome), + 1207 => Some(Error::AssetDecimalsMismatch), + 1208 => Some(Error::InvalidExtensionDays), + 1209 => Some(Error::ExtensionDenied), + 1210 => Some(Error::CumulativeExtensionCapHit), + 1211 => Some(Error::ExtensionCapExceeded), + 1212 => Some(Error::InvalidInitializationParams), + 1213 => Some(Error::InvalidNonce), + 1214 => Some(Error::BetLimitsInverted), + 1215 => Some(Error::BetLimitAboveMaximum), + 1216 => Some(Error::BetCapOutOfRange), + 1217 => Some(Error::BetAboveMaximum), + 1218 => Some(Error::BetBelowMarketMin), + 1300 => Some(Error::InsufficientStake), + 1301 => Some(Error::InsufficientBalance), + 1302 => Some(Error::NothingToClaim), + 1303 => Some(Error::AlreadyClaimed), + 1304 => Some(Error::FeeArithmeticOverflow), + 1305 => Some(Error::FeeAlreadyCollected), + 1306 => Some(Error::NoFeesToCollect), + 1307 => Some(Error::InvalidFeeConfig), + 1308 => Some(Error::FeeExceedsMax), + 1309 => Some(Error::SweepAlreadyDone), + 1310 => Some(Error::DisputeFeeFailed), + 1311 => Some(Error::NoPendingFeeCommit), + 1312 => Some(Error::FeeRevealTooEarly), + 1313 => Some(Error::FeePreimageMismatch), + 1314 => Some(Error::BetExceedsCap), + 1315 => Some(Error::MaxBetCapExceeded), + 1316 => Some(Error::TreasuryUpdateTimelocked), + 1317 => Some(Error::NoPendingTreasuryUpdate), + 1318 => Some(Error::PendingTreasuryUpdateExists), + 1400 => Some(Error::AlreadyDisputed), + 1401 => Some(Error::DisputeVoteExpired), + 1402 => Some(Error::DisputeVoteDenied), + 1403 => Some(Error::DisputeAlreadyVoted), + 1404 => Some(Error::DisputeCondNotMet), + 1405 => Some(Error::DisputeError), + 1406 => Some(Error::DisputerCannotVote), + 1407 => Some(Error::DisputeStakeCapExceeded), + 1500 => Some(Error::Unauthorized), + 1501 => Some(Error::ReplayedOverride), + 1502 => Some(Error::UserNotWhitelisted), + 1503 => Some(Error::UserBlacklisted), + 1504 => Some(Error::CreatorBlacklisted), + 1600 => Some(Error::CBNotInitialized), + 1601 => Some(Error::CBAlreadyOpen), + 1602 => Some(Error::CBNotOpen), + 1603 => Some(Error::CBOpen), + 1604 => Some(Error::CBError), + 1605 => Some(Error::RateLimitExceeded), + 1606 => Some(Error::PerLedgerBetCapExceeded), + 1700 => Some(Error::ConfigNotFound), + 1701 => Some(Error::AdminNotSet), + 1702 => Some(Error::GasBudgetExceeded), + 1703 => Some(Error::OperationWouldExceedBudget), + 1704 => Some(Error::InsufficientStorageRentBudget), + 1705 => Some(Error::UpgradeChainMismatch), + 1706 => Some(Error::AlreadyInitialized), + 1707 => Some(Error::InvalidTimeLockDelay), + 1708 => Some(Error::TimeLockNotExpired), + 1709 => Some(Error::NoPendingUpdate), + 1710 => Some(Error::PendingUpdateExists), + 1711 => Some(Error::AdminActionTimelocked), + 1712 => Some(Error::OracleAdminCooldownActive), + 1713 => Some(Error::SignerRotationCooldown), + 1714 => Some(Error::Overflow), + 1715 => Some(Error::InvalidCap), + 1716 => Some(Error::BatchEmpty), + 1717 => Some(Error::BatchSizeExceeded), + 1800 => Some(Error::AlreadyVoted), + 1801 => Some(Error::AlreadyBet), + 1802 => Some(Error::BetsAlreadyPlaced), + 1803 => Some(Error::ForceResolveAlreadyUsed), + 1804 => Some(Error::ForceResolveReplayed), + 1805 => Some(Error::ForceResolveReasonEmpty), + 1806 => Some(Error::IdempotentBatchAlreadyApplied), + 1807 => Some(Error::InvalidStakeAmount), + 1900 => Some(Error::QuestionTooLong), + 1901 => Some(Error::OutcomeTooLong), + 1902 => Some(Error::TooManyOutcomes), + 1903 => Some(Error::FeedIdTooLong), + 1904 => Some(Error::ComparisonTooLong), + 1905 => Some(Error::CategoryTooLong), + 1906 => Some(Error::CategoryTooShort), + 1907 => Some(Error::TagTooLong), + 1908 => Some(Error::TagTooShort), + 1909 => Some(Error::TooManyTags), + 1910 => Some(Error::ExtensionReasonTooLong), + 1911 => Some(Error::SourceTooLong), + 1912 => Some(Error::ErrorMessageTooLong), + 1913 => Some(Error::SignatureTooLong), + 1914 => Some(Error::TooManyExtensions), + 1915 => Some(Error::TooManyOracleResults), + 1916 => Some(Error::TooManyWinningOutcomes), + 1917 => Some(Error::ArchiveFull), + 1918 => Some(Error::ReasonTableFull), + 1919 => Some(Error::RegistryFull), + _ => None, + } + } + + /// Resolves an `Error` variant from its canonical uppercase string code. + pub fn from_code_str(code: &str) -> Option { + match code { + "IDEMPOTENT_BATCH_ALREADY_APPLIED" => Some(Error::IdempotentBatchAlreadyApplied), + "REASON_TABLE_FULL" => Some(Error::ReasonTableFull), + "OVERFLOW" => Some(Error::Overflow), + "MAX_BET_CAP_EXCEEDED" => Some(Error::MaxBetCapExceeded), + "INVALID_CAP" => Some(Error::InvalidCap), + "UNAUTHORIZED" => Some(Error::Unauthorized), + "MARKET_NOT_FOUND" => Some(Error::MarketNotFound), + "MARKET_CLOSED" => Some(Error::MarketClosed), + "MARKET_ALREADY_RESOLVED" => Some(Error::MarketResolved), + "MARKET_NOT_RESOLVED" => Some(Error::MarketNotResolved), + "NOTHING_TO_CLAIM" => Some(Error::NothingToClaim), + "ALREADY_CLAIMED" => Some(Error::AlreadyClaimed), + "INSUFFICIENT_STAKE" => Some(Error::InsufficientStake), + "INVALID_OUTCOME" => Some(Error::InvalidOutcome), + "ALREADY_VOTED" => Some(Error::AlreadyVoted), + "ALREADY_BET" => Some(Error::AlreadyBet), + "BETS_ALREADY_PLACED" => Some(Error::BetsAlreadyPlaced), + "INSUFFICIENT_BALANCE" => Some(Error::InsufficientBalance), + "INVALID_NONCE" => Some(Error::InvalidNonce), + "BET_ABOVE_MAXIMUM" => Some(Error::BetAboveMaximum), + "BET_BELOW_MARKET_MIN" => Some(Error::BetBelowMarketMin), + "BET_LIMITS_INVERTED" => Some(Error::BetLimitsInverted), + "BET_LIMIT_ABOVE_MAXIMUM" => Some(Error::BetLimitAboveMaximum), + "BET_CAP_OUT_OF_RANGE" => Some(Error::BetCapOutOfRange), + "ORACLE_UNAVAILABLE" => Some(Error::OracleUnavailable), + "INVALID_ORACLE_CONFIG" => Some(Error::InvalidOracleConfig), + "ORACLE_STALE" => Some(Error::OracleStale), + "ORACLE_NO_CONSENSUS" => Some(Error::OracleNoConsensus), + "ORACLE_VERIFIED" => Some(Error::OracleVerified), + "MARKET_NOT_READY" => Some(Error::MarketNotReady), + "FALLBACK_ORACLE_UNAVAILABLE" => Some(Error::FallbackOracleUnavailable), + "RESOLUTION_TIMEOUT_REACHED" => Some(Error::ResolutionTimeoutReached), + "ORACLE_CONFIDENCE_TOO_WIDE" => Some(Error::OracleConfidenceTooWide), + "INVALID_ORACLE_FEED" => Some(Error::InvalidOracleFeed), + "ORACLE_CALLBACK_AUTH_FAILED" => Some(Error::OracleCallbackAuthFailed), + "ORACLE_CALLBACK_UNAUTHORIZED" => Some(Error::OracleCallbackUnauthorized), + "ORACLE_CALLBACK_INVALID_SIGNATURE" => Some(Error::OracleCallbackInvalidSignature), + "ORACLE_CALLBACK_REPLAY_DETECTED" => Some(Error::OracleCallbackReplayDetected), + "ORACLE_CALLBACK_TIMEOUT" => Some(Error::OracleCallbackTimeout), + "INVALID_QUESTION" => Some(Error::InvalidQuestion), + "INVALID_OUTCOMES" => Some(Error::InvalidOutcomes), + "INVALID_DURATION" => Some(Error::InvalidDuration), + "INVALID_THRESHOLD" => Some(Error::InvalidThreshold), + "INVALID_COMPARISON" => Some(Error::InvalidComparison), + "INVALID_STATE" => Some(Error::InvalidState), + "INVALID_INPUT" => Some(Error::InvalidInput), + "INVALID_FEE_CONFIG" => Some(Error::InvalidFeeConfig), + "CONFIGURATION_NOT_FOUND" => Some(Error::ConfigNotFound), + "ALREADY_DISPUTED" => Some(Error::AlreadyDisputed), + "DISPUTE_VOTING_PERIOD_EXPIRED" => Some(Error::DisputeVoteExpired), + "DISPUTE_VOTING_NOT_ALLOWED" => Some(Error::DisputeVoteDenied), + "DISPUTE_ALREADY_VOTED" => Some(Error::DisputeAlreadyVoted), + "DISPUTE_RESOLUTION_CONDITIONS_NOT_MET" => Some(Error::DisputeCondNotMet), + "DISPUTE_FEE_DISTRIBUTION_FAILED" => Some(Error::DisputeFeeFailed), + "INVALID_INITIALIZATION_PARAMS" => Some(Error::InvalidInitializationParams), + "DISPUTE_ERROR" => Some(Error::DisputeError), + "DISPUTER_CANNOT_VOTE" => Some(Error::DisputerCannotVote), + "SWEEP_ALREADY_DONE" => Some(Error::SweepAlreadyDone), + "FEE_ARITHMETIC_OVERFLOW" => Some(Error::FeeArithmeticOverflow), + "FEE_ALREADY_COLLECTED" => Some(Error::FeeAlreadyCollected), + "NO_FEES_TO_COLLECT" => Some(Error::NoFeesToCollect), + "INVALID_EXTENSION_DAYS" => Some(Error::InvalidExtensionDays), + "EXTENSION_DENIED" => Some(Error::ExtensionDenied), + "GAS_BUDGET_EXCEEDED" => Some(Error::GasBudgetExceeded), + "ADMIN_NOT_SET" => Some(Error::AdminNotSet), + "ASSET_DECIMALS_MISMATCH" => Some(Error::AssetDecimalsMismatch), + "ADMIN_ACTION_TIMELOCKED" => Some(Error::AdminActionTimelocked), + "OPERATION_WOULD_EXCEED_BUDGET" => Some(Error::OperationWouldExceedBudget), + "QUESTION_TOO_LONG" => Some(Error::QuestionTooLong), + "OUTCOME_TOO_LONG" => Some(Error::OutcomeTooLong), + "TOO_MANY_OUTCOMES" => Some(Error::TooManyOutcomes), + "FEED_ID_TOO_LONG" => Some(Error::FeedIdTooLong), + "COMPARISON_TOO_LONG" => Some(Error::ComparisonTooLong), + "CATEGORY_TOO_LONG" => Some(Error::CategoryTooLong), + "TAG_TOO_LONG" => Some(Error::TagTooLong), + "TOO_MANY_TAGS" => Some(Error::TooManyTags), + "EXTENSION_REASON_TOO_LONG" => Some(Error::ExtensionReasonTooLong), + "SOURCE_TOO_LONG" => Some(Error::SourceTooLong), + "ERROR_MESSAGE_TOO_LONG" => Some(Error::ErrorMessageTooLong), + "SIGNATURE_TOO_LONG" => Some(Error::SignatureTooLong), + "TOO_MANY_EXTENSIONS" => Some(Error::TooManyExtensions), + "TOO_MANY_ORACLE_RESULTS" => Some(Error::TooManyOracleResults), + "TOO_MANY_WINNING_OUTCOMES" => Some(Error::TooManyWinningOutcomes), + "FORCE_RESOLVE_ALREADY_USED" => Some(Error::ForceResolveAlreadyUsed), + "ARCHIVE_FULL" => Some(Error::ArchiveFull), + "CATEGORY_TOO_SHORT" => Some(Error::CategoryTooShort), + "TAG_TOO_SHORT" => Some(Error::TagTooShort), + "DUPLICATE_MARKET_ID" => Some(Error::DuplicateMarketId), + "CANNOT_ARCHIVE_FROM_STATE" => Some(Error::CannotArchiveFromState), + "CANNOT_RESTORE_FROM_STATE" => Some(Error::CannotRestoreFromState), + "MARKET_ALREADY_ARCHIVED" => Some(Error::MarketAlreadyArchived), + "MARKET_ALREADY_RESTORED" => Some(Error::MarketAlreadyRestored), + "CIRCUIT_BREAKER_NOT_INITIALIZED" => Some(Error::CBNotInitialized), + "CIRCUIT_BREAKER_ALREADY_OPEN" => Some(Error::CBAlreadyOpen), + "CIRCUIT_BREAKER_NOT_OPEN" => Some(Error::CBNotOpen), + "CIRCUIT_BREAKER_OPEN" => Some(Error::CBOpen), + "CIRCUIT_BREAKER_ERROR" => Some(Error::CBError), + "RATE_LIMIT_EXCEEDED" => Some(Error::RateLimitExceeded), + "CUMULATIVE_EXTENSION_CAP_HIT" => Some(Error::CumulativeExtensionCapHit), + "ILLEGAL_MARKET_STATE_TRANSITION" => Some(Error::IllegalMarketStateTransition), + "FEE_ABOVE_ACCEPTABLE" => Some(Error::FeeExceedsMax), + "FORCE_RESOLVE_REPLAYED" => Some(Error::ForceResolveReplayed), + "FORCE_RESOLVE_REASON_EMPTY" => Some(Error::ForceResolveReasonEmpty), + "NO_PENDING_FEE_COMMIT" => Some(Error::NoPendingFeeCommit), + "FEE_REVEAL_TOO_EARLY" => Some(Error::FeeRevealTooEarly), + "FEE_PREIMAGE_MISMATCH" => Some(Error::FeePreimageMismatch), + "DISPUTE_STAKE_CAP_EXCEEDED" => Some(Error::DisputeStakeCapExceeded), + "INSUFFICIENT_STORAGE_RENT_BUDGET" => Some(Error::InsufficientStorageRentBudget), + "EXTENSION_CAP_EXCEEDED" => Some(Error::ExtensionCapExceeded), + "UPGRADE_CHAIN_MISMATCH" => Some(Error::UpgradeChainMismatch), + "ORACLE_QUOTE_OUTLIER" => Some(Error::OracleQuoteOutlier), + "MAX_PARTICIPANTS_REACHED" => Some(Error::MaxParticipantsReached), + "BET_EXCEEDS_CAP" => Some(Error::BetExceedsCap), + "REPLAYED_OVERRIDE" => Some(Error::ReplayedOverride), + "ORACLE_ADMIN_COOLDOWN_ACTIVE" => Some(Error::OracleAdminCooldownActive), + "SIGNER_ROTATION_COOLDOWN" => Some(Error::SignerRotationCooldown), + "USER_NOT_WHITELISTED" => Some(Error::UserNotWhitelisted), + "USER_BLACKLISTED" => Some(Error::UserBlacklisted), + "CREATOR_BLACKLISTED" => Some(Error::CreatorBlacklisted), + "ALREADY_INITIALIZED" => Some(Error::AlreadyInitialized), + "INVALID_TIME_LOCK_DELAY" => Some(Error::InvalidTimeLockDelay), + "TIME_LOCK_NOT_EXPIRED" => Some(Error::TimeLockNotExpired), + "NO_PENDING_UPDATE" => Some(Error::NoPendingUpdate), + "PENDING_UPDATE_EXISTS" => Some(Error::PendingUpdateExists), + "INVALID_STAKE_AMOUNT" => Some(Error::InvalidStakeAmount), + "PER_LEDGER_BET_CAP_EXCEEDED" => Some(Error::PerLedgerBetCapExceeded), + "REGISTRY_FULL" => Some(Error::RegistryFull), + "BATCH_EMPTY" => Some(Error::BatchEmpty), + "BATCH_SIZE_EXCEEDED" => Some(Error::BatchSizeExceeded), + "TREASURY_UPDATE_TIMELOCKED" => Some(Error::TreasuryUpdateTimelocked), + "NO_PENDING_TREASURY_UPDATE" => Some(Error::NoPendingTreasuryUpdate), + "PENDING_TREASURY_UPDATE_EXISTS" => Some(Error::PendingTreasuryUpdateExists), + _ => None, + } + } + + /// Generates full public error mapping metadata for this error. + pub fn public_mapping(&self) -> PublicErrorMapping { + PublicErrorMapping { + contract_code: *self as u32, + client_code: self.client_code(), + code_str: self.code(), + description: self.description(), + category: self.category(), + severity: self.severity(), + recoverability: self.recoverability(), + is_known: true, + } + } + + /// Returns the category for this error based on its client code range. + pub fn category(&self) -> ErrorCategory { + match self.client_code() { + 1000..=1099 => ErrorCategory::Oracle, + 1100..=1199 => ErrorCategory::Market, + 1200..=1299 => ErrorCategory::Validation, + 1300..=1399 => ErrorCategory::Financial, + 1400..=1499 => ErrorCategory::Dispute, + 1500..=1599 => ErrorCategory::Authentication, + 1600..=1799 => ErrorCategory::System, + 1800..=1899 => ErrorCategory::UserOperation, + 1900..=1999 => ErrorCategory::Validation, + _ => ErrorCategory::Unknown, + } + } + + /// Returns the severity level for this error. + pub fn severity(&self) -> ErrorSeverity { + ErrorHandler::get_error_classification(self).0 + } + + /// Safely decodes a contract code into a `PublicErrorMapping`. + pub fn decode_contract_code(code: u32) -> PublicErrorMapping { + match Self::from_contract_code(code) { + Some(err) => err.public_mapping(), + None => PublicErrorMapping { + contract_code: code, + client_code: 0, + code_str: "UNKNOWN_ERROR", + description: "Unknown error code", + category: ErrorCategory::Unknown, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Terminal, + is_known: false, + }, + } + } + + /// Safely decodes an off-chain client code into a `PublicErrorMapping`. + pub fn decode_client_code(code: u32) -> PublicErrorMapping { + match Self::from_client_code(code) { + Some(err) => err.public_mapping(), + None => PublicErrorMapping { + contract_code: 0, + client_code: code, + code_str: "UNKNOWN_ERROR", + description: "Unknown client code", + category: ErrorCategory::Unknown, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Terminal, + is_known: false, + }, + } + } + } // ===== TESTS ===== @@ -2052,6 +2671,12 @@ mod tests { Error::AlreadyBet, Error::BetsAlreadyPlaced, Error::InsufficientBalance, + Error::InvalidNonce, + Error::BetAboveMaximum, + Error::BetBelowMarketMin, + Error::BetLimitsInverted, + Error::BetLimitAboveMaximum, + Error::BetCapOutOfRange, Error::OracleUnavailable, Error::InvalidOracleConfig, Error::OracleStale, @@ -2061,6 +2686,12 @@ mod tests { Error::FallbackOracleUnavailable, Error::ResolutionTimeoutReached, Error::OracleConfidenceTooWide, + Error::InvalidOracleFeed, + Error::OracleCallbackAuthFailed, + Error::OracleCallbackUnauthorized, + Error::OracleCallbackInvalidSignature, + Error::OracleCallbackReplayDetected, + Error::OracleCallbackTimeout, Error::InvalidQuestion, Error::InvalidOutcomes, Error::InvalidDuration, @@ -2085,19 +2716,13 @@ mod tests { Error::ExtensionDenied, Error::GasBudgetExceeded, Error::AdminNotSet, - Error::AssetDecimalsMismatch, - Error::InvalidOracleFeed, - Error::FeeExceedsMax, - // Metadata length limit errors Error::QuestionTooLong, Error::OutcomeTooLong, Error::TooManyOutcomes, Error::FeedIdTooLong, Error::ComparisonTooLong, Error::CategoryTooLong, - Error::CategoryTooShort, Error::TagTooLong, - Error::TagTooShort, Error::TooManyTags, Error::ExtensionReasonTooLong, Error::SourceTooLong, @@ -2106,8 +2731,19 @@ mod tests { Error::TooManyExtensions, Error::TooManyOracleResults, Error::TooManyWinningOutcomes, + Error::ForceResolveAlreadyUsed, + Error::CategoryTooShort, + Error::TagTooShort, + Error::DisputerCannotVote, + Error::AssetDecimalsMismatch, Error::ArchiveFull, - // Circuit breaker errors + Error::DuplicateMarketId, + Error::CannotArchiveFromState, + Error::AdminActionTimelocked, + Error::OperationWouldExceedBudget, + Error::MarketAlreadyArchived, + Error::MarketAlreadyRestored, + Error::CannotRestoreFromState, Error::CBNotInitialized, Error::CBAlreadyOpen, Error::CBNotOpen, @@ -2115,10 +2751,10 @@ mod tests { Error::CBError, Error::RateLimitExceeded, Error::CumulativeExtensionCapHit, - Error::DuplicateMarketId, - Error::ForceResolveAlreadyUsed, Error::IllegalMarketStateTransition, Error::FeeExceedsMax, + Error::ForceResolveReplayed, + Error::ForceResolveReasonEmpty, Error::NoPendingFeeCommit, Error::FeeRevealTooEarly, Error::FeePreimageMismatch, @@ -2128,9 +2764,32 @@ mod tests { Error::UpgradeChainMismatch, Error::ReplayedOverride, Error::OracleQuoteOutlier, + Error::MaxParticipantsReached, + Error::BatchEmpty, + Error::BatchSizeExceeded, + Error::IdempotentBatchAlreadyApplied, + Error::ReasonTableFull, + Error::Overflow, + Error::MaxBetCapExceeded, + Error::InvalidCap, + Error::BetExceedsCap, + Error::OracleAdminCooldownActive, + Error::SignerRotationCooldown, + Error::UserNotWhitelisted, + Error::UserBlacklisted, + Error::CreatorBlacklisted, + Error::AlreadyInitialized, + Error::InvalidTimeLockDelay, + Error::TimeLockNotExpired, + Error::NoPendingUpdate, + Error::PendingUpdateExists, + Error::InvalidStakeAmount, + Error::PerLedgerBetCapExceeded, + Error::RegistryFull, Error::TreasuryUpdateTimelocked, Error::NoPendingTreasuryUpdate, Error::PendingTreasuryUpdateExists, + Error::InvalidInitializationParams, ] } diff --git a/contracts/predictify-hybrid/src/error_code_tests.rs b/contracts/predictify-hybrid/src/error_code_tests.rs index d54aba60..eb543f56 100644 --- a/contracts/predictify-hybrid/src/error_code_tests.rs +++ b/contracts/predictify-hybrid/src/error_code_tests.rs @@ -1,38 +1,19 @@ -//! Tests for the error taxonomy refinement (#602). -//! -//! Acceptance criteria enforced here: -//! - No duplicate `client_code()` values across all variants. -//! - Every variant has a `recoverability()` label (enum is exhaustive). -//! - `client_code()` falls within the disjoint range reserved for its category. -//! - `client_code()` is exposed as a public method (off-chain mapping). -//! - No `unwrap()` in the mapping table (method returns `u32`, not `Option`). - #![cfg(test)] +use crate::err::{Error, ErrorCategory, ErrorSeverity, PublicErrorMapping, Recoverability}; +use alloc::collections::BTreeSet; +use alloc::format; +use alloc::string::String; use alloc::vec; -use alloc::vec::Vec as StdVec; - -use crate::err::{ - Error, ErrorCategory, ErrorHandler, ErrorSeverity, Recoverability, RecoveryStrategy, -}; -use soroban_sdk::{Env, Map, String}; +use alloc::vec::Vec; -// ─── helpers ──────────────────────────────────────────────────────────────── - -fn make_ctx(env: &Env) -> crate::err::ErrorContext { - crate::err::ErrorContext { - operation: String::from_str(env, "test_op"), - user_address: None, - market_id: None, - context_data: Map::new(env), - timestamp: env.ledger().timestamp(), - call_chain: None, - } -} - -/// Every concrete `Error` variant — used for exhaustive property checks. -fn all_errors() -> StdVec { +fn all_errors() -> Vec { vec![ + Error::IdempotentBatchAlreadyApplied, + Error::ReasonTableFull, + Error::Overflow, + Error::MaxBetCapExceeded, + Error::InvalidCap, Error::Unauthorized, Error::MarketNotFound, Error::MarketClosed, @@ -46,6 +27,12 @@ fn all_errors() -> StdVec { Error::AlreadyBet, Error::BetsAlreadyPlaced, Error::InsufficientBalance, + Error::InvalidNonce, + Error::BetAboveMaximum, + Error::BetBelowMarketMin, + Error::BetLimitsInverted, + Error::BetLimitAboveMaximum, + Error::BetCapOutOfRange, Error::OracleUnavailable, Error::InvalidOracleConfig, Error::OracleStale, @@ -76,7 +63,9 @@ fn all_errors() -> StdVec { Error::DisputeAlreadyVoted, Error::DisputeCondNotMet, Error::DisputeFeeFailed, + Error::InvalidInitializationParams, Error::DisputeError, + Error::DisputerCannotVote, Error::SweepAlreadyDone, Error::FeeArithmeticOverflow, Error::FeeAlreadyCollected, @@ -85,6 +74,9 @@ fn all_errors() -> StdVec { Error::ExtensionDenied, Error::GasBudgetExceeded, Error::AdminNotSet, + Error::AssetDecimalsMismatch, + Error::AdminActionTimelocked, + Error::OperationWouldExceedBudget, Error::QuestionTooLong, Error::OutcomeTooLong, Error::TooManyOutcomes, @@ -100,542 +92,489 @@ fn all_errors() -> StdVec { Error::TooManyExtensions, Error::TooManyOracleResults, Error::TooManyWinningOutcomes, + Error::ForceResolveAlreadyUsed, Error::ArchiveFull, Error::CategoryTooShort, Error::TagTooShort, + Error::DuplicateMarketId, + Error::CannotArchiveFromState, + Error::CannotRestoreFromState, + Error::MarketAlreadyArchived, + Error::MarketAlreadyRestored, Error::CBNotInitialized, Error::CBAlreadyOpen, Error::CBNotOpen, Error::CBOpen, Error::CBError, Error::RateLimitExceeded, + Error::CumulativeExtensionCapHit, + Error::IllegalMarketStateTransition, + Error::FeeExceedsMax, + Error::ForceResolveReplayed, + Error::ForceResolveReasonEmpty, + Error::NoPendingFeeCommit, + Error::FeeRevealTooEarly, + Error::FeePreimageMismatch, + Error::DisputeStakeCapExceeded, + Error::InsufficientStorageRentBudget, + Error::ExtensionCapExceeded, + Error::UpgradeChainMismatch, + Error::OracleQuoteOutlier, + Error::MaxParticipantsReached, + Error::BetExceedsCap, + Error::ReplayedOverride, + Error::OracleAdminCooldownActive, + Error::SignerRotationCooldown, + Error::UserNotWhitelisted, + Error::UserBlacklisted, + Error::CreatorBlacklisted, + Error::AlreadyInitialized, + Error::InvalidTimeLockDelay, + Error::TimeLockNotExpired, + Error::NoPendingUpdate, + Error::PendingUpdateExists, + Error::InvalidStakeAmount, + Error::PerLedgerBetCapExceeded, + Error::RegistryFull, + Error::BatchEmpty, + Error::BatchSizeExceeded, + Error::TreasuryUpdateTimelocked, + Error::NoPendingTreasuryUpdate, + Error::PendingTreasuryUpdateExists, ] } -/// O(n²) uniqueness check — sufficient for ~75 variants, avoids HashSet (no_std). -fn all_unique_u32(values: &[u32]) -> bool { - for i in 0..values.len() { - for j in (i + 1)..values.len() { - if values[i] == values[j] { - return false; - } - } - } - true -} - -fn all_unique_str(values: &[&'static str]) -> bool { - for i in 0..values.len() { - for j in (i + 1)..values.len() { - if values[i] == values[j] { - return false; - } - } - } - true -} - -// ═══════════════════════════════════════════════════════════════════════════ -// AC1 – No duplicate client_code() values -// ═══════════════════════════════════════════════════════════════════════════ +// ========================================================================= +// 1. Acceptance Criteria: Existing codes remain stable & distinct +// ========================================================================= #[test] -fn test_client_codes_are_unique() { - let codes: StdVec = all_errors().iter().map(|e| e.client_code()).collect(); - assert!( - all_unique_u32(&codes), - "Duplicate client_code detected" - ); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// AC2 – Every variant has a Recoverability label -// ═══════════════════════════════════════════════════════════════════════════ - -#[test] -fn test_every_variant_has_recoverability() { +fn test_all_variants_have_unique_contract_codes() { + let mut seen = BTreeSet::new(); for err in all_errors() { - let r = err.recoverability(); + let code = err as u32; assert!( - matches!( - r, - Recoverability::Retryable - | Recoverability::RequiresAdmin - | Recoverability::Terminal - ), - "{:?} returned an unexpected Recoverability value", + seen.insert(code), + "Duplicate contract code {} found for error {:?}", + code, err ); } } -// ═══════════════════════════════════════════════════════════════════════════ -// AC3 – client_code() lies in the disjoint range for its ErrorCategory -// ═══════════════════════════════════════════════════════════════════════════ - -/// Returns the expected (lo, hi) client-code range for an ErrorCategory. -/// Errors in the catch-all `Unknown` category accept the full 1000-1999 span. -fn expected_range(cat: &ErrorCategory) -> (u32, u32) { - match cat { - ErrorCategory::Oracle => (1000, 1099), - ErrorCategory::Market => (1100, 1199), - ErrorCategory::Validation => (1200, 1299), - ErrorCategory::Financial => (1300, 1399), - ErrorCategory::Dispute => (1400, 1499), - ErrorCategory::Authentication => (1500, 1599), - // System encompasses both pure system (1700-1799) and CB (1600-1699). - ErrorCategory::System => (1600, 1799), - ErrorCategory::UserOperation => (1800, 1899), - _ => (1000, 1999), // Unknown / Metadata – wide acceptance - } -} - #[test] -fn test_client_code_in_disjoint_range_for_category() { +fn test_all_variants_have_unique_client_codes() { + let mut seen = BTreeSet::new(); for err in all_errors() { - let (_, cat, _) = ErrorHandler::get_error_classification(&err); - let (lo, hi) = expected_range(&cat); - let code = err.client_code(); + let client_code = err.client_code(); + assert_ne!(client_code, 0, "client_code for {:?} must not be 0", err); assert!( - code >= lo && code <= hi, - "{:?}: client_code {} not in [{}, {}] (category {:?})", - err, code, lo, hi, cat + seen.insert(client_code), + "Duplicate client_code {} found for error {:?}", + client_code, + err ); } } #[test] -fn test_category_ranges_are_disjoint() { - let ranges: &[(&str, u32, u32)] = &[ - ("Oracle", 1000, 1099), - ("Market", 1100, 1199), - ("Validation", 1200, 1299), - ("Financial", 1300, 1399), - ("Dispute", 1400, 1499), - ("Auth", 1500, 1599), - ("CircuitBreaker", 1600, 1699), - ("System", 1700, 1799), - ("UserOperation", 1800, 1899), - ("Metadata", 1900, 1999), - ]; - for i in 0..ranges.len() { - for j in (i + 1)..ranges.len() { - let (n1, lo1, hi1) = ranges[i]; - let (n2, lo2, hi2) = ranges[j]; - assert!( - hi1 < lo2 || hi2 < lo1, - "Ranges for {} ({}-{}) and {} ({}-{}) overlap", - n1, lo1, hi1, n2, lo2, hi2 - ); - } +fn test_all_variants_have_unique_code_strings() { + let mut seen = BTreeSet::new(); + for err in all_errors() { + let s = err.code(); + assert!(!s.is_empty(), "code string for {:?} must not be empty", err); + assert_ne!(s, "UNSPECIFIED_ERROR", "code string for {:?} must be specific", err); + assert!( + seen.insert(s), + "Duplicate code string '{}' found for error {:?}", + s, + err + ); } } -// ─── Spot-check canonical codes per category ──────────────────────────────── - #[test] -fn test_oracle_client_codes_in_1000_range() { - for err in [ - Error::OracleUnavailable, - Error::InvalidOracleConfig, - Error::OracleStale, - Error::FallbackOracleUnavailable, - Error::OracleConfidenceTooWide, - ] { - let c = err.client_code(); - assert!(c >= 1000 && c <= 1099, "{:?} -> {}", err, c); - } +fn test_contract_codes_match_documented_discriminants() { + // Spot check core discriminants across all sections + assert_eq!(Error::Unauthorized as u32, 100); + assert_eq!(Error::MarketNotFound as u32, 101); + assert_eq!(Error::NothingToClaim as u32, 105); + assert_eq!(Error::AlreadyClaimed as u32, 106); + assert_eq!(Error::OracleUnavailable as u32, 200); + assert_eq!(Error::InvalidQuestion as u32, 300); + assert_eq!(Error::InvalidState as u32, 400); + assert_eq!(Error::CBNotInitialized as u32, 500); + assert_eq!(Error::IdempotentBatchAlreadyApplied as u32, 660); + assert_eq!(Error::InvalidInitializationParams as u32, 700); } -#[test] -fn test_market_client_codes_in_1100_range() { - for err in [ - Error::MarketNotFound, - Error::MarketClosed, - Error::MarketResolved, - Error::MarketNotReady, - ] { - let c = err.client_code(); - assert!(c >= 1100 && c <= 1199, "{:?} -> {}", err, c); - } -} +// ========================================================================= +// 2. Acceptance Criteria: New codes are unique and documented +// ========================================================================= #[test] -fn test_validation_client_codes_in_1200_range() { - for err in [ - Error::InvalidQuestion, - Error::InvalidInput, - Error::InvalidDuration, - ] { - let c = err.client_code(); - assert!(c >= 1200 && c <= 1299, "{:?} -> {}", err, c); +fn test_all_descriptions_are_non_empty_and_meaningful() { + for err in all_errors() { + let desc = err.description(); + assert!( + !desc.is_empty(), + "Error {:?} has an empty description", + err + ); + assert_ne!( + desc, + "An unspecified error occurred.", + "Error {:?} has default unspecified description", + err + ); } } #[test] -fn test_financial_client_codes_in_1300_range() { - for err in [ - Error::FeeArithmeticOverflow, - Error::FeeAlreadyCollected, - Error::NoFeesToCollect, - Error::InvalidFeeConfig, - Error::SweepAlreadyDone, - Error::DisputeFeeFailed, - ] { - let c = err.client_code(); - assert!(c >= 1300 && c <= 1399, "{:?} -> {}", err, c); +fn test_client_code_in_disjoint_range_for_category() { + for err in all_errors() { + let code = err.client_code(); + let cat = err.category(); + match cat { + ErrorCategory::Oracle => { + assert!( + (1000..=1099).contains(&code), + "Oracle error {:?} client_code {} not in 1000..=1099", + err, + code + ); + } + ErrorCategory::Market => { + assert!( + (1100..=1199).contains(&code), + "Market error {:?} client_code {} not in 1100..=1199", + err, + code + ); + } + ErrorCategory::Validation => { + assert!( + (1200..=1299).contains(&code) || (1900..=1999).contains(&code), + "Validation/Metadata error {:?} client_code {} not in valid range", + err, + code + ); + } + ErrorCategory::Financial => { + assert!( + (1300..=1399).contains(&code), + "Financial error {:?} client_code {} not in 1300..=1399", + err, + code + ); + } + ErrorCategory::Dispute => { + assert!( + (1400..=1499).contains(&code), + "Dispute error {:?} client_code {} not in 1400..=1499", + err, + code + ); + } + ErrorCategory::Authentication => { + assert!( + (1500..=1599).contains(&code), + "Auth error {:?} client_code {} not in 1500..=1599", + err, + code + ); + } + ErrorCategory::System => { + assert!( + (1600..=1799).contains(&code), + "System/CB error {:?} client_code {} not in 1600..=1799", + err, + code + ); + } + ErrorCategory::UserOperation => { + assert!( + (1800..=1899).contains(&code), + "UserOp error {:?} client_code {} not in 1800..=1899", + err, + code + ); + } + ErrorCategory::Unknown => { + panic!("Error {:?} returned ErrorCategory::Unknown", err); + } + } } } -#[test] -fn test_dispute_client_codes_in_1400_range() { - for err in [ - Error::AlreadyDisputed, - Error::DisputeVoteExpired, - Error::DisputeCondNotMet, - Error::DisputeError, - ] { - let c = err.client_code(); - assert!(c >= 1400 && c <= 1499, "{:?} -> {}", err, c); - } -} +// ========================================================================= +// 3. Acceptance Criteria: Unknown values decode safely +// ========================================================================= #[test] -fn test_auth_client_code_in_1500_range() { - let c = Error::Unauthorized.client_code(); - assert!(c >= 1500 && c <= 1599, "Unauthorized -> {}", c); +fn test_decode_contract_code_known() { + let mapping = Error::decode_contract_code(100); + assert_eq!(mapping.contract_code, 100); + assert_eq!(mapping.client_code, 1500); + assert_eq!(mapping.code_str, "UNAUTHORIZED"); + assert_eq!(mapping.category, ErrorCategory::Authentication); + assert!(mapping.is_known); } #[test] -fn test_circuit_breaker_client_codes_in_1600_range() { - for err in [ - Error::CBNotInitialized, - Error::CBAlreadyOpen, - Error::CBNotOpen, - Error::CBOpen, - Error::CBError, - Error::RateLimitExceeded, - ] { - let c = err.client_code(); - assert!(c >= 1600 && c <= 1699, "{:?} -> {}", err, c); +fn test_decode_contract_code_unknown() { + let unknown_codes = [0u32, 9999, 55555, u32::MAX]; + for code in unknown_codes { + let mapping = Error::decode_contract_code(code); + assert_eq!(mapping.contract_code, code); + assert_eq!(mapping.client_code, 0); + assert_eq!(mapping.code_str, "UNKNOWN_ERROR"); + assert_eq!(mapping.category, ErrorCategory::Unknown); + assert_eq!(mapping.severity, ErrorSeverity::Medium); + assert_eq!(mapping.recoverability, Recoverability::Terminal); + assert!(!mapping.is_known); } } #[test] -fn test_system_client_codes_in_1700_range() { - for err in [ - Error::InvalidState, - Error::ConfigNotFound, - Error::AdminNotSet, - Error::GasBudgetExceeded, - ] { - let c = err.client_code(); - assert!(c >= 1700 && c <= 1799, "{:?} -> {}", err, c); - } +fn test_decode_client_code_known() { + let mapping = Error::decode_client_code(1000); + assert_eq!(mapping.contract_code, 200); + assert_eq!(mapping.client_code, 1000); + assert_eq!(mapping.code_str, "ORACLE_UNAVAILABLE"); + assert_eq!(mapping.category, ErrorCategory::Oracle); + assert_eq!(mapping.recoverability, Recoverability::Retryable); + assert!(mapping.is_known); } #[test] -fn test_user_operation_client_codes_in_1800_range() { - for err in [ - Error::AlreadyVoted, - Error::AlreadyBet, - Error::BetsAlreadyPlaced, - Error::InvalidOutcome, - Error::InsufficientStake, - Error::InsufficientBalance, - Error::NothingToClaim, - Error::AlreadyClaimed, - ] { - let c = err.client_code(); - assert!(c >= 1800 && c <= 1899, "{:?} -> {}", err, c); +fn test_decode_client_code_unknown() { + let unknown_client_codes = [0u32, 999, 9999, u32::MAX]; + for code in unknown_client_codes { + let mapping = Error::decode_client_code(code); + assert_eq!(mapping.contract_code, 0); + assert_eq!(mapping.client_code, code); + assert_eq!(mapping.code_str, "UNKNOWN_ERROR"); + assert_eq!(mapping.category, ErrorCategory::Unknown); + assert_eq!(mapping.severity, ErrorSeverity::Medium); + assert_eq!(mapping.recoverability, Recoverability::Terminal); + assert!(!mapping.is_known); } } #[test] -fn test_metadata_client_codes_in_1900_range() { - for err in [ - Error::QuestionTooLong, - Error::OutcomeTooLong, - Error::TooManyOutcomes, - Error::ArchiveFull, - Error::CategoryTooShort, - Error::TagTooShort, - ] { - let c = err.client_code(); - assert!(c >= 1900 && c <= 1999, "{:?} -> {}", err, c); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Recoverability spot-checks -// ═══════════════════════════════════════════════════════════════════════════ - -#[test] -fn test_retryable_errors() { - for err in [ - Error::OracleUnavailable, - Error::OracleStale, - Error::FallbackOracleUnavailable, - Error::OracleCallbackTimeout, - Error::ResolutionTimeoutReached, - Error::RateLimitExceeded, - Error::InvalidInput, - Error::InsufficientStake, - Error::InsufficientBalance, - ] { +fn test_from_code_str_roundtrip() { + for err in all_errors() { + let str_code = err.code(); + let resolved = Error::from_code_str(str_code); assert_eq!( - err.recoverability(), - Recoverability::Retryable, - "{:?} should be Retryable", - err + resolved, + Some(err), + "Failed roundtrip for from_code_str('{}')", + str_code ); } + assert_eq!(Error::from_code_str("NON_EXISTENT_ERROR"), None); } #[test] -fn test_requires_admin_errors() { - for err in [ - Error::AdminNotSet, - Error::DisputeFeeFailed, - Error::CBNotInitialized, - Error::InvalidOracleConfig, - Error::InvalidFeeConfig, - Error::ConfigNotFound, - Error::CBOpen, - ] { +fn test_from_contract_code_roundtrip() { + for err in all_errors() { + let code = err as u32; + let resolved = Error::from_contract_code(code); assert_eq!( - err.recoverability(), - Recoverability::RequiresAdmin, - "{:?} should be RequiresAdmin", - err + resolved, + Some(err), + "Failed roundtrip for from_contract_code({})", + code ); } + assert_eq!(Error::from_contract_code(999999), None); } #[test] -fn test_terminal_errors() { - for err in [ - Error::Unauthorized, - Error::MarketClosed, - Error::MarketResolved, - Error::AlreadyVoted, - Error::AlreadyBet, - Error::AlreadyClaimed, - Error::FeeAlreadyCollected, - Error::InvalidState, - ] { +fn test_from_client_code_roundtrip() { + for err in all_errors() { + let code = err.client_code(); + let resolved = Error::from_client_code(code); assert_eq!( - err.recoverability(), - Recoverability::Terminal, - "{:?} should be Terminal", - err + resolved, + Some(err), + "Failed roundtrip for from_client_code({})", + code ); } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Off-chain client branching -// ═══════════════════════════════════════════════════════════════════════════ - -#[test] -fn test_client_can_branch_on_client_code_category() { - fn domain(code: u32) -> &'static str { - match code { - 1000..=1099 => "oracle", - 1100..=1199 => "market", - 1200..=1299 => "validation", - 1300..=1399 => "financial", - 1400..=1499 => "dispute", - 1500..=1599 => "auth", - 1600..=1699 => "circuit_breaker", - 1700..=1799 => "system", - 1800..=1899 => "user_operation", - 1900..=1999 => "metadata", - _ => "unknown", - } - } - - assert_eq!(domain(Error::OracleUnavailable.client_code()), "oracle"); - assert_eq!(domain(Error::MarketClosed.client_code()), "market"); - assert_eq!(domain(Error::InvalidInput.client_code()), "validation"); - assert_eq!(domain(Error::FeeAlreadyCollected.client_code()), "financial"); - assert_eq!(domain(Error::DisputeError.client_code()), "dispute"); - assert_eq!(domain(Error::Unauthorized.client_code()), "auth"); - assert_eq!(domain(Error::CBOpen.client_code()), "circuit_breaker"); - assert_eq!(domain(Error::AdminNotSet.client_code()), "system"); - assert_eq!(domain(Error::AlreadyVoted.client_code()), "user_operation"); - assert_eq!(domain(Error::QuestionTooLong.client_code()), "metadata"); -} - -#[test] -fn test_client_retry_decision_from_recoverability() { - fn should_retry(err: Error) -> bool { - err.recoverability() == Recoverability::Retryable - } - assert!(should_retry(Error::OracleUnavailable)); - assert!(should_retry(Error::RateLimitExceeded)); - assert!(!should_retry(Error::Unauthorized)); - assert!(!should_retry(Error::AdminNotSet)); - assert!(!should_retry(Error::CBNotInitialized)); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Existing taxonomy: .code() strings, .description(), numeric variant codes -// ═══════════════════════════════════════════════════════════════════════════ - -#[test] -fn test_error_code_strings_are_unique() { - let codes: StdVec<&'static str> = all_errors().iter().map(|e| e.code()).collect(); - assert!(all_unique_str(&codes), "Duplicate .code() string detected"); -} - -#[test] -fn test_all_code_strings_are_upper_snake_case() { - for err in all_errors() { - let code = err.code(); - assert!(!code.is_empty(), "{:?}.code() is empty", err); - for c in code.chars() { - assert!( - c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_', - "{:?}.code() has invalid char '{}' in \"{}\"", - err, c, code - ); - } - assert!(!code.starts_with('_'), "{:?}.code() starts with _", err); - assert!(!code.ends_with('_'), "{:?}.code() ends with _", err); - } -} + assert_eq!(Error::from_client_code(999999), None); +} + +// ========================================================================= +// 4. Acceptance Criteria: Golden vectors cover public entrypoints +// ========================================================================= + +struct GoldenVector { + error: Error, + contract_code: u32, + client_code: u32, + code_str: &'static str, + category: ErrorCategory, + severity: ErrorSeverity, + recoverability: Recoverability, +} + +#[test] +fn test_golden_vectors() { + let golden_vectors: &[GoldenVector] = &[ + // Core Entrypoint: Authentication + GoldenVector { + error: Error::Unauthorized, + contract_code: 100, + client_code: 1500, + code_str: "UNAUTHORIZED", + category: ErrorCategory::Authentication, + severity: ErrorSeverity::High, + recoverability: Recoverability::Terminal, + }, + // Core Entrypoint: Market Lifecycle + GoldenVector { + error: Error::MarketNotFound, + contract_code: 101, + client_code: 1100, + code_str: "MARKET_NOT_FOUND", + category: ErrorCategory::Market, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Terminal, + }, + GoldenVector { + error: Error::MarketClosed, + contract_code: 102, + client_code: 1101, + code_str: "MARKET_CLOSED", + category: ErrorCategory::Market, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Terminal, + }, + GoldenVector { + error: Error::MarketResolved, + contract_code: 103, + client_code: 1102, + code_str: "MARKET_ALREADY_RESOLVED", + category: ErrorCategory::Market, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Terminal, + }, + // Core Entrypoint: Claims / Winnings + GoldenVector { + error: Error::NothingToClaim, + contract_code: 105, + client_code: 1302, + code_str: "NOTHING_TO_CLAIM", + category: ErrorCategory::Financial, + severity: ErrorSeverity::Low, + recoverability: Recoverability::Terminal, + }, + GoldenVector { + error: Error::AlreadyClaimed, + contract_code: 106, + client_code: 1303, + code_str: "ALREADY_CLAIMED", + category: ErrorCategory::Financial, + severity: ErrorSeverity::Low, + recoverability: Recoverability::Terminal, + }, + // Core Entrypoint: Betting / Staking + GoldenVector { + error: Error::InsufficientStake, + contract_code: 107, + client_code: 1300, + code_str: "INSUFFICIENT_STAKE", + category: ErrorCategory::Financial, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Retryable, + }, + GoldenVector { + error: Error::AlreadyBet, + contract_code: 110, + client_code: 1801, + code_str: "ALREADY_BET", + category: ErrorCategory::UserOperation, + severity: ErrorSeverity::Low, + recoverability: Recoverability::Terminal, + }, + // Core Entrypoint: Oracle Integration + GoldenVector { + error: Error::OracleUnavailable, + contract_code: 200, + client_code: 1000, + code_str: "ORACLE_UNAVAILABLE", + category: ErrorCategory::Oracle, + severity: ErrorSeverity::High, + recoverability: Recoverability::Retryable, + }, + GoldenVector { + error: Error::OracleStale, + contract_code: 202, + client_code: 1002, + code_str: "ORACLE_STALE", + category: ErrorCategory::Oracle, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Retryable, + }, + // Core Entrypoint: Validation + GoldenVector { + error: Error::InvalidQuestion, + contract_code: 300, + client_code: 1200, + code_str: "INVALID_QUESTION", + category: ErrorCategory::Validation, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Retryable, + }, + // Core Entrypoint: Circuit Breaker + GoldenVector { + error: Error::CBOpen, + contract_code: 503, + client_code: 1603, + code_str: "CIRCUIT_BREAKER_OPEN", + category: ErrorCategory::System, + severity: ErrorSeverity::High, + recoverability: Recoverability::Retryable, + }, + GoldenVector { + error: Error::RateLimitExceeded, + contract_code: 505, + client_code: 1605, + code_str: "RATE_LIMIT_EXCEEDED", + category: ErrorCategory::System, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Retryable, + }, + // Core Entrypoint: Dispute + GoldenVector { + error: Error::AlreadyDisputed, + contract_code: 404, + client_code: 1400, + code_str: "ALREADY_DISPUTED", + category: ErrorCategory::Dispute, + severity: ErrorSeverity::Medium, + recoverability: Recoverability::Terminal, + }, + ]; -#[test] -fn test_all_descriptions_are_non_empty() { - for err in all_errors() { - assert!(!err.description().is_empty(), "{:?}.description() is empty", err); + for gv in golden_vectors { + let mapping = gv.error.public_mapping(); + assert_eq!(mapping.contract_code, gv.contract_code, "contract_code mismatch for {:?}", gv.error); + assert_eq!(mapping.client_code, gv.client_code, "client_code mismatch for {:?}", gv.error); + assert_eq!(mapping.code_str, gv.code_str, "code_str mismatch for {:?}", gv.error); + assert_eq!(mapping.category, gv.category, "category mismatch for {:?}", gv.error); + assert_eq!(mapping.severity, gv.severity, "severity mismatch for {:?}", gv.error); + assert_eq!(mapping.recoverability, gv.recoverability, "recoverability mismatch for {:?}", gv.error); + assert!(mapping.is_known); + + // Also verify decode functions produce exact same golden mapping + let from_contract = Error::decode_contract_code(gv.contract_code); + assert_eq!(from_contract, mapping); + + let from_client = Error::decode_client_code(gv.client_code); + assert_eq!(from_client, mapping); } } - -#[test] -fn test_all_numeric_variant_codes_are_unique() { - let codes: StdVec = all_errors().iter().map(|e| *e as u32).collect(); - assert!(all_unique_u32(&codes), "Duplicate variant numeric code detected"); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// ErrorHandler classification spot-checks -// ═══════════════════════════════════════════════════════════════════════════ - -#[test] -fn test_classification_critical_admin_not_set() { - let env = Env::default(); - let d = ErrorHandler::categorize_error(&env, Error::AdminNotSet, make_ctx(&env)); - assert_eq!(d.severity, ErrorSeverity::Critical); - assert_eq!(d.category, ErrorCategory::System); - assert_eq!(d.recovery_strategy, RecoveryStrategy::ManualIntervention); -} - -#[test] -fn test_classification_high_unauthorized() { - let env = Env::default(); - let d = ErrorHandler::categorize_error(&env, Error::Unauthorized, make_ctx(&env)); - assert_eq!(d.severity, ErrorSeverity::High); - assert_eq!(d.category, ErrorCategory::Authentication); - assert_eq!(d.recovery_strategy, RecoveryStrategy::Abort); -} - -#[test] -fn test_classification_high_oracle_unavailable() { - let env = Env::default(); - let d = ErrorHandler::categorize_error(&env, Error::OracleUnavailable, make_ctx(&env)); - assert_eq!(d.severity, ErrorSeverity::High); - assert_eq!(d.category, ErrorCategory::Oracle); - assert_eq!(d.recovery_strategy, RecoveryStrategy::RetryWithDelay); -} - -#[test] -fn test_classification_medium_market_not_found() { - let env = Env::default(); - let d = ErrorHandler::categorize_error(&env, Error::MarketNotFound, make_ctx(&env)); - assert_eq!(d.severity, ErrorSeverity::Medium); - assert_eq!(d.category, ErrorCategory::Market); - assert_eq!(d.recovery_strategy, RecoveryStrategy::AlternativeMethod); -} - -#[test] -fn test_classification_low_already_voted() { - let env = Env::default(); - let d = ErrorHandler::categorize_error(&env, Error::AlreadyVoted, make_ctx(&env)); - assert_eq!(d.severity, ErrorSeverity::Low); - assert_eq!(d.category, ErrorCategory::UserOperation); - assert_eq!(d.recovery_strategy, RecoveryStrategy::Skip); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Recovery strategy spot-checks -// ═══════════════════════════════════════════════════════════════════════════ - -#[test] -fn test_recovery_strategy_oracle_unavailable_is_retry_with_delay() { - assert_eq!( - ErrorHandler::get_error_recovery_strategy(&Error::OracleUnavailable), - RecoveryStrategy::RetryWithDelay - ); -} - -#[test] -fn test_recovery_strategy_already_voted_is_skip() { - assert_eq!( - ErrorHandler::get_error_recovery_strategy(&Error::AlreadyVoted), - RecoveryStrategy::Skip - ); -} - -#[test] -fn test_recovery_strategy_unauthorized_is_abort() { - assert_eq!( - ErrorHandler::get_error_recovery_strategy(&Error::Unauthorized), - RecoveryStrategy::Abort - ); -} - -#[test] -fn test_recovery_strategy_admin_not_set_is_manual_intervention() { - assert_eq!( - ErrorHandler::get_error_recovery_strategy(&Error::AdminNotSet), - RecoveryStrategy::ManualIntervention - ); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Analytics and context validation -// ═══════════════════════════════════════════════════════════════════════════ - -#[test] -fn test_error_analytics_initial_state() { - let env = Env::default(); - let a = ErrorHandler::get_error_analytics(&env).unwrap(); - assert_eq!(a.total_errors, 0); - assert_eq!(a.recovery_success_rate, 0); - assert!(a.errors_by_category.get(ErrorCategory::UserOperation).is_some()); - assert!(a.errors_by_severity.get(ErrorSeverity::Low).is_some()); -} - -#[test] -fn test_error_recovery_status_initial() { - let env = Env::default(); - let s = ErrorHandler::get_error_recovery_status(&env).unwrap(); - assert_eq!(s.total_attempts, 0); - assert!(s.last_recovery_timestamp.is_none()); -} - -#[test] -fn test_context_valid() { - let env = Env::default(); - assert!(ErrorHandler::validate_error_context(&make_ctx(&env)).is_ok()); -} - -#[test] -fn test_context_empty_operation_fails() { - let env = Env::default(); - let mut ctx = make_ctx(&env); - ctx.operation = String::from_str(&env, ""); - assert!(ErrorHandler::validate_error_context(&ctx).is_err()); -} diff --git a/contracts/predictify-hybrid/src/event_topic_compat_tests.rs b/contracts/predictify-hybrid/src/event_topic_compat_tests.rs index b460c3f8..f27bc656 100644 --- a/contracts/predictify-hybrid/src/event_topic_compat_tests.rs +++ b/contracts/predictify-hybrid/src/event_topic_compat_tests.rs @@ -27,6 +27,7 @@ use soroban_sdk::{symbol_short, testutils::Events, Env, Symbol, Vec}; +use alloc::format; use crate::event_topic_compat::{ EventCompatBridge, EventNonceGuard, EventTopicRegistry, TOPIC_ALIASES, TOPIC_REGISTRY, }; diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs index b82600ba..bedaa5cb 100644 --- a/contracts/predictify-hybrid/src/lib.rs +++ b/contracts/predictify-hybrid/src/lib.rs @@ -16,8 +16,8 @@ pub use config::PERCENTAGE_DENOMINATOR; mod admin; // #[cfg(any())] // mod admin_auth_audit_tests; -// #[cfg(any())] -// mod error_code_tests; +#[cfg(test)] +mod error_code_tests; pub mod analytics; pub mod audit; mod balances; diff --git a/contracts/predictify-hybrid/src/types.rs b/contracts/predictify-hybrid/src/types.rs index 9a6626a5..e21552c0 100644 --- a/contracts/predictify-hybrid/src/types.rs +++ b/contracts/predictify-hybrid/src/types.rs @@ -4079,7 +4079,7 @@ impl Bet { /// println!("Unique bettors: {}", stats.unique_bettors); /// ``` #[contracttype] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct BetStats { /// Total number of bets placed on this market pub total_bets: u32, diff --git a/contracts/predictify-hybrid/src/validation.rs b/contracts/predictify-hybrid/src/validation.rs index 6c50d534..b119976c 100644 --- a/contracts/predictify-hybrid/src/validation.rs +++ b/contracts/predictify-hybrid/src/validation.rs @@ -5704,7 +5704,7 @@ impl ContractInitializationValidator { .map_err(|_| Error::InvalidDuration)?; // Oracle configuration must be internally consistent before storage. - OracleValidator::validate_oracle_config_all_together(oracle_config) + OracleConfigValidator::validate_oracle_config_all_together(oracle_config) .map_err(|_| Error::InvalidOracleConfig)?; Ok(())