diff --git a/contracts/batch/src/lib.rs b/contracts/batch/src/lib.rs index 263197b7..b5ba28ab 100644 --- a/contracts/batch/src/lib.rs +++ b/contracts/batch/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::too_many_arguments)] use soroban_sdk::{contract, contracterror, contractimpl, contracttype, Address, Env, Vec}; +use subtrackr_types::CoreError; const MAX_BATCH_ITEMS: u32 = 100; const GAS_BASE: u64 = 50_000; @@ -15,6 +16,25 @@ pub enum BatchError { AlreadyExecuted = 2, } +impl From for CoreError { + fn from(err: BatchError) -> Self { + match err { + BatchError::InvalidBatch => CoreError::InvalidConfig, + BatchError::AlreadyExecuted => CoreError::InvalidStateTransition, + } + } +} + +impl From for BatchError { + fn from(err: CoreError) -> Self { + match err { + CoreError::InvalidConfig => BatchError::InvalidBatch, + CoreError::InvalidStateTransition => BatchError::AlreadyExecuted, + _ => BatchError::InvalidBatch, + } + } +} + #[contracttype] #[derive(Clone, Debug, PartialEq, Eq)] pub enum OperationType { diff --git a/contracts/credit/src/lib.rs b/contracts/credit/src/lib.rs index ea5c8db8..06e88a76 100644 --- a/contracts/credit/src/lib.rs +++ b/contracts/credit/src/lib.rs @@ -18,7 +18,7 @@ use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String, Vec, }; -use subtrackr_types::SubscriptionId; +use subtrackr_types::{SubscriptionId, CoreError}; /// Maximum retained transaction-history and lot entries per account. const MAX_HISTORY: u32 = 128; @@ -35,6 +35,33 @@ pub enum CreditError { SelfTransfer = 6, } +impl From for CoreError { + fn from(err: CreditError) -> Self { + match err { + CreditError::AlreadyInitialized => CoreError::AlreadyInitialized, + CreditError::NotInitialized => CoreError::NotInitialized, + CreditError::Unauthorized => CoreError::Unauthorized, + CreditError::InvalidAmount => CoreError::InvalidAmount, + CreditError::InsufficientCredit => CoreError::InsufficientCredit, + CreditError::SelfTransfer => CoreError::SelfTransfer, + } + } +} + +impl From for CreditError { + fn from(err: CoreError) -> Self { + match err { + CoreError::AlreadyInitialized => CreditError::AlreadyInitialized, + CoreError::NotInitialized => CreditError::NotInitialized, + CoreError::Unauthorized => CreditError::Unauthorized, + CoreError::InvalidAmount => CreditError::InvalidAmount, + CoreError::InsufficientCredit => CreditError::InsufficientCredit, + CoreError::SelfTransfer => CreditError::SelfTransfer, + _ => CreditError::InvalidAmount, + } + } +} + #[contracttype] #[derive(Clone, Debug, PartialEq, Eq)] pub enum CreditTxKind { diff --git a/contracts/metering/src/lib.rs b/contracts/metering/src/lib.rs index 7737abc7..ab125614 100644 --- a/contracts/metering/src/lib.rs +++ b/contracts/metering/src/lib.rs @@ -36,6 +36,8 @@ const DEFAULT_PERIOD_SECS: u64 = 86_400; /// Maximum number of retained period buckets per meter (~one quarter of days). const MAX_BUCKETS: u32 = 90; +use subtrackr_types::CoreError; + #[contracterror] #[derive(Clone, Debug, Copy, PartialEq, Eq)] #[repr(u32)] @@ -45,6 +47,27 @@ pub enum MeteringError { MeterNotFound = 3, } +impl From for CoreError { + fn from(err: MeteringError) -> Self { + match err { + MeteringError::InvalidValue => CoreError::InvalidAmount, + MeteringError::InvalidPeriod => CoreError::InvalidInterval, + MeteringError::MeterNotFound => CoreError::NotFound, + } + } +} + +impl From for MeteringError { + fn from(err: CoreError) -> Self { + match err { + CoreError::InvalidAmount => MeteringError::InvalidValue, + CoreError::InvalidInterval => MeteringError::InvalidPeriod, + CoreError::NotFound => MeteringError::MeterNotFound, + _ => MeteringError::InvalidValue, + } + } +} + #[contracttype] #[derive(Clone)] enum DataKey { diff --git a/contracts/oracle/src/lib.rs b/contracts/oracle/src/lib.rs index a1fc34ca..69997d01 100644 --- a/contracts/oracle/src/lib.rs +++ b/contracts/oracle/src/lib.rs @@ -23,8 +23,9 @@ pub use price::{ }; use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, Symbol, + contract, contractimpl, contracttype, symbol_short, Address, Env, Symbol, }; +use subtrackr_types::CoreError; /// Number of consecutive faults that trips a feed's circuit breaker. const CIRCUIT_FAULT_LIMIT: u32 = 3; @@ -51,6 +52,45 @@ pub enum OracleError { InvalidConfig = 12, } +impl From for CoreError { + fn from(err: OracleError) -> Self { + match err { + OracleError::AlreadyInitialized => CoreError::AlreadyInitialized, + OracleError::NotInitialized => CoreError::NotInitialized, + OracleError::Unauthorized => CoreError::Unauthorized, + OracleError::FeedNotFound => CoreError::FeedNotFound, + OracleError::FeedExists => CoreError::FeedExists, + OracleError::InvalidPrice => CoreError::InvalidPrice, + OracleError::InvalidTimestamp => CoreError::InvalidTimestamp, + OracleError::NoPriceAvailable => CoreError::NoPriceAvailable, + OracleError::StalePrice => CoreError::StalePrice, + OracleError::CircuitOpen => CoreError::CircuitOpen, + OracleError::NoHistory => CoreError::NoHistory, + OracleError::InvalidConfig => CoreError::InvalidConfig, + } + } +} + +impl From for OracleError { + fn from(err: CoreError) -> Self { + match err { + CoreError::AlreadyInitialized => OracleError::AlreadyInitialized, + CoreError::NotInitialized => OracleError::NotInitialized, + CoreError::Unauthorized => OracleError::Unauthorized, + CoreError::FeedNotFound => OracleError::FeedNotFound, + CoreError::FeedExists => OracleError::FeedExists, + CoreError::InvalidPrice => OracleError::InvalidPrice, + CoreError::InvalidTimestamp => OracleError::InvalidTimestamp, + CoreError::NoPriceAvailable => OracleError::NoPriceAvailable, + CoreError::StalePrice => OracleError::StalePrice, + CoreError::CircuitOpen => OracleError::CircuitOpen, + CoreError::NoHistory => OracleError::NoHistory, + CoreError::InvalidConfig => OracleError::InvalidConfig, + _ => OracleError::InvalidConfig, + } + } +} + #[contracttype] #[derive(Clone)] enum DataKey { diff --git a/contracts/subscription/src/errors.rs b/contracts/subscription/src/errors.rs index 5cbe4656..15575094 100644 --- a/contracts/subscription/src/errors.rs +++ b/contracts/subscription/src/errors.rs @@ -43,6 +43,7 @@ //! | 30 | ChainReorgDetected | Chain reorganisation detected during timeout window. | use soroban_sdk::contracterror; +use subtrackr_types::CoreError; #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] @@ -156,6 +157,81 @@ impl ContractError { } } +impl From for CoreError { + fn from(err: ContractError) -> Self { + match err { + ContractError::Unauthorized => CoreError::Unauthorized, + ContractError::PlanNotFound => CoreError::PlanNotFound, + ContractError::PlanInactive => CoreError::PlanInactive, + ContractError::SubscriptionNotFound => CoreError::SubscriptionNotFound, + ContractError::AlreadySubscribed => CoreError::AlreadySubscribed, + ContractError::SubscriptionNotActive => CoreError::SubscriptionNotActive, + ContractError::SubscriptionAlreadyCancelled => CoreError::SubscriptionAlreadyCancelled, + ContractError::SubscriptionAlreadyPaused => CoreError::SubscriptionAlreadyPaused, + ContractError::SubscriptionNotPaused => CoreError::SubscriptionNotPaused, + ContractError::PaymentNotYetDue => CoreError::PaymentNotYetDue, + ContractError::InsufficientAllowance => CoreError::InsufficientFunds, + ContractError::InvalidAmount => CoreError::InvalidAmount, + ContractError::InvalidInterval => CoreError::InvalidInterval, + ContractError::InvalidPriceBounds => CoreError::InvalidPriceBounds, + ContractError::MaxPauseDurationExceeded => CoreError::MaxPauseDurationExceeded, + ContractError::RateLimited => CoreError::RateLimited, + ContractError::OracleUnavailable => CoreError::OracleUnavailable, + ContractError::StorageVersionMismatch => CoreError::StorageVersionMismatch, + ContractError::InvalidMigrationPath => CoreError::InvalidMigrationPath, + ContractError::RefundExceedsTotalPaid => CoreError::RefundExceedsTotalPaid, + ContractError::PlanOwnerMismatch => CoreError::OwnerMismatch, + ContractError::EventNotFound => CoreError::EventNotFound, + ContractError::EventStoreFull => CoreError::EventStoreFull, + ContractError::InvalidEventSequence => CoreError::InvalidEventSequence, + ContractError::ExportWindowExceeded => CoreError::ExportWindowExceeded, + ContractError::PaymentTimedOut => CoreError::PaymentTimedOut, + ContractError::RecoveryAttemptsExhausted => CoreError::RecoveryAttemptsExhausted, + ContractError::TransactionNotRecoverable => CoreError::TransactionNotRecoverable, + ContractError::InvalidTimeoutConfig => CoreError::InvalidTimeoutConfig, + ContractError::ChainReorgDetected => CoreError::ChainReorgDetected, + } + } +} + +impl From for ContractError { + fn from(err: CoreError) -> Self { + match err { + CoreError::Unauthorized => ContractError::Unauthorized, + CoreError::PlanNotFound => ContractError::PlanNotFound, + CoreError::PlanInactive => ContractError::PlanInactive, + CoreError::SubscriptionNotFound => ContractError::SubscriptionNotFound, + CoreError::AlreadySubscribed => ContractError::AlreadySubscribed, + CoreError::SubscriptionNotActive => ContractError::SubscriptionNotActive, + CoreError::SubscriptionAlreadyCancelled => ContractError::SubscriptionAlreadyCancelled, + CoreError::SubscriptionAlreadyPaused => ContractError::SubscriptionAlreadyPaused, + CoreError::SubscriptionNotPaused => ContractError::SubscriptionNotPaused, + CoreError::PaymentNotYetDue => ContractError::PaymentNotYetDue, + CoreError::InsufficientFunds => ContractError::InsufficientAllowance, + CoreError::InvalidAmount => ContractError::InvalidAmount, + CoreError::InvalidInterval => ContractError::InvalidInterval, + CoreError::InvalidPriceBounds => ContractError::InvalidPriceBounds, + CoreError::MaxPauseDurationExceeded => ContractError::MaxPauseDurationExceeded, + CoreError::RateLimited => ContractError::RateLimited, + CoreError::OracleUnavailable => ContractError::OracleUnavailable, + CoreError::StorageVersionMismatch => ContractError::StorageVersionMismatch, + CoreError::InvalidMigrationPath => ContractError::InvalidMigrationPath, + CoreError::RefundExceedsTotalPaid => ContractError::RefundExceedsTotalPaid, + CoreError::OwnerMismatch => ContractError::PlanOwnerMismatch, + CoreError::EventNotFound => ContractError::EventNotFound, + CoreError::EventStoreFull => ContractError::EventStoreFull, + CoreError::InvalidEventSequence => ContractError::InvalidEventSequence, + CoreError::ExportWindowExceeded => ContractError::ExportWindowExceeded, + CoreError::PaymentTimedOut => ContractError::PaymentTimedOut, + CoreError::RecoveryAttemptsExhausted => ContractError::RecoveryAttemptsExhausted, + CoreError::TransactionNotRecoverable => ContractError::TransactionNotRecoverable, + CoreError::InvalidTimeoutConfig => ContractError::InvalidTimeoutConfig, + CoreError::ChainReorgDetected => ContractError::ChainReorgDetected, + _ => ContractError::InvalidAmount, + } + } +} + // ─── Tests ──────────────────────────────────────────────────────────────────── #[cfg(test)] diff --git a/contracts/types/src/errors.rs b/contracts/types/src/errors.rs new file mode 100644 index 00000000..e000d592 --- /dev/null +++ b/contracts/types/src/errors.rs @@ -0,0 +1,215 @@ +#![no_std] + +use soroban_sdk::{contracterror, contracttype, Env, Symbol}; + +/// Unified core error enum for all SubTrackr contracts. +/// +/// Categories: +/// - Auth: Authentication and authorization errors (1xx) +/// - Initialization: Initialization errors (2xx) +/// - Validation: Input validation errors (3xx) +/// - Payment: Payment and balance errors (4xx) +/// - State: State machine errors (5xx) +/// - Storage: Storage and persistence errors (6xx) +/// - External: External service errors (7xx) +/// - Recovery: Recovery errors (8xx) +#[contracterror] +#[contracttype] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum CoreError { + // ── Auth Errors (1xx) ── + Unauthorized = 100, + OwnerMismatch = 101, + InvalidCredentials = 102, + ApiKeyRevoked = 103, + ApiKeyExpired = 104, + PermissionDenied = 105, + + // ── Initialization Errors (2xx) ── + AlreadyInitialized = 200, + NotInitialized = 201, + + // ── Validation Errors (3xx) ── + NotFound = 300, + PlanNotFound = 301, + SubscriptionNotFound = 302, + InvoiceNotFound = 303, + FeedNotFound = 304, + InvalidAmount = 305, + InvalidInterval = 306, + InvalidPriceBounds = 307, + InvalidPrice = 308, + InvalidTimestamp = 309, + InvalidConfig = 310, + AlreadyExists = 311, + AlreadySubscribed = 312, + FeedExists = 313, + InvalidTimeoutConfig = 314, + SelfTransfer = 315, + + // ── Payment Errors (4xx) ── + InsufficientFunds = 400, + InsufficientCredit = 401, + PaymentNotYetDue = 402, + PaymentTimedOut = 403, + RefundExceedsTotalPaid = 404, + + // ── State Errors (5xx) ── + SubscriptionNotActive = 500, + SubscriptionAlreadyCancelled = 501, + SubscriptionAlreadyPaused = 502, + SubscriptionNotPaused = 503, + PlanInactive = 504, + MaxPauseDurationExceeded = 505, + InvalidStateTransition = 506, + CircuitOpen = 507, + + // ── Storage Errors (6xx) ── + StorageVersionMismatch = 600, + InvalidMigrationPath = 601, + EventNotFound = 602, + EventStoreFull = 603, + InvalidEventSequence = 604, + ExportWindowExceeded = 605, + NoHistory = 606, + + // ── External Errors (7xx) ── + OracleUnavailable = 700, + NoPriceAvailable = 701, + StalePrice = 702, + ChainReorgDetected = 703, + RateLimited = 704, + + // ── Recovery Errors (8xx) ── + RecoveryAttemptsExhausted = 800, + TransactionNotRecoverable = 801, +} + +impl CoreError { + pub fn user_message(self) -> &'static str { + match self { + Self::Unauthorized => "You are not authorized to perform this action.", + Self::OwnerMismatch => "Only the resource owner can perform this action.", + Self::InvalidCredentials => "Invalid credentials provided.", + Self::ApiKeyRevoked => "API key has been revoked.", + Self::ApiKeyExpired => "API key has expired.", + Self::PermissionDenied => "You do not have permission to perform this action.", + Self::AlreadyInitialized => "Contract already initialized.", + Self::NotInitialized => "Contract not initialized.", + Self::NotFound => "The requested resource was not found.", + Self::PlanNotFound => "The requested plan does not exist.", + Self::SubscriptionNotFound => "No active subscription found for this account.", + Self::InvoiceNotFound => "The requested invoice does not exist.", + Self::FeedNotFound => "The requested price feed does not exist.", + Self::InvalidAmount => "Amount must be greater than zero.", + Self::InvalidInterval => "Billing interval must be positive.", + Self::InvalidPriceBounds => "Price bounds are invalid (max must be > min > 0).", + Self::InvalidPrice => "Price must be greater than zero.", + Self::InvalidTimestamp => "Invalid timestamp provided.", + Self::InvalidConfig => "Invalid configuration provided.", + Self::AlreadyExists => "This resource already exists.", + Self::AlreadySubscribed => "You are already subscribed to this plan.", + Self::FeedExists => "This price feed already exists.", + Self::InvalidTimeoutConfig => "Timeout configuration values are out of allowed range.", + Self::SelfTransfer => "Cannot transfer to self.", + Self::InsufficientFunds => "Insufficient token balance or allowance to process payment.", + Self::InsufficientCredit => "Insufficient credit balance.", + Self::PaymentNotYetDue => "The next payment is not due yet.", + Self::PaymentTimedOut => "Payment transaction timed out waiting for confirmation.", + Self::RefundExceedsTotalPaid => "Refund amount exceeds total amount paid.", + Self::SubscriptionNotActive => "This subscription is not currently active.", + Self::SubscriptionAlreadyCancelled => "This subscription has already been cancelled.", + Self::SubscriptionAlreadyPaused => "This subscription is already paused.", + Self::SubscriptionNotPaused => "This subscription is not paused.", + Self::PlanInactive => "This plan is no longer accepting new subscribers.", + Self::MaxPauseDurationExceeded => "Pause duration exceeds the allowed maximum of 30 days.", + Self::InvalidStateTransition => "Invalid state transition for the current resource state.", + Self::CircuitOpen => "Oracle circuit breaker is open.", + Self::StorageVersionMismatch => "Storage schema version mismatch; run migration first.", + Self::InvalidMigrationPath => "Unsupported migration path.", + Self::EventNotFound => "The requested event does not exist.", + Self::EventStoreFull => "Event store has reached maximum capacity.", + Self::InvalidEventSequence => "Invalid event sequence for subscription state.", + Self::ExportWindowExceeded => "Export range exceeds the maximum allowed window.", + Self::NoHistory => "No historical data available.", + Self::OracleUnavailable => "Price oracle is temporarily unavailable.", + Self::NoPriceAvailable => "No price available for this pair.", + Self::StalePrice => "Price is stale.", + Self::ChainReorgDetected => "Chain reorganisation detected during timeout window.", + Self::RateLimited => "Too many requests. Please wait before retrying.", + Self::RecoveryAttemptsExhausted => "All automatic recovery attempts have been exhausted.", + Self::TransactionNotRecoverable => "Transaction is not in a recoverable state.", + } + } + + pub fn error_code(self) -> u32 { + self as u32 + } + + pub fn emit_event(self, env: &Env) { + env.events().publish((Symbol::new(env, "error"),), self); + } +} + +// ─── Deprecated Error Codes (for backward compatibility) ────────────────────── +#[deprecated(note = "Use CoreError::Unauthorized instead")] +pub const ERROR_UNAUTHORIZED: u32 = 1; +#[deprecated(note = "Use CoreError::PlanNotFound instead")] +pub const ERROR_PLAN_NOT_FOUND: u32 = 2; +#[deprecated(note = "Use CoreError::PlanInactive instead")] +pub const ERROR_PLAN_INACTIVE: u32 = 3; +#[deprecated(note = "Use CoreError::SubscriptionNotFound instead")] +pub const ERROR_SUBSCRIPTION_NOT_FOUND: u32 = 4; +#[deprecated(note = "Use CoreError::AlreadySubscribed instead")] +pub const ERROR_ALREADY_SUBSCRIBED: u32 = 5; +#[deprecated(note = "Use CoreError::SubscriptionNotActive instead")] +pub const ERROR_SUBSCRIPTION_NOT_ACTIVE: u32 = 6; +#[deprecated(note = "Use CoreError::SubscriptionAlreadyCancelled instead")] +pub const ERROR_SUBSCRIPTION_ALREADY_CANCELLED: u32 = 7; +#[deprecated(note = "Use CoreError::SubscriptionAlreadyPaused instead")] +pub const ERROR_SUBSCRIPTION_ALREADY_PAUSED: u32 = 8; +#[deprecated(note = "Use CoreError::SubscriptionNotPaused instead")] +pub const ERROR_SUBSCRIPTION_NOT_PAUSED: u32 = 9; +#[deprecated(note = "Use CoreError::PaymentNotYetDue instead")] +pub const ERROR_PAYMENT_NOT_YET_DUE: u32 = 10; +#[deprecated(note = "Use CoreError::InsufficientFunds instead")] +pub const ERROR_INSUFFICIENT_ALLOWANCE: u32 = 11; +#[deprecated(note = "Use CoreError::InvalidAmount instead")] +pub const ERROR_INVALID_AMOUNT: u32 = 12; +#[deprecated(note = "Use CoreError::InvalidInterval instead")] +pub const ERROR_INVALID_INTERVAL: u32 = 13; +#[deprecated(note = "Use CoreError::InvalidPriceBounds instead")] +pub const ERROR_INVALID_PRICE_BOUNDS: u32 = 14; +#[deprecated(note = "Use CoreError::MaxPauseDurationExceeded instead")] +pub const ERROR_MAX_PAUSE_DURATION_EXCEEDED: u32 = 15; +#[deprecated(note = "Use CoreError::RateLimited instead")] +pub const ERROR_RATE_LIMITED: u32 = 16; +#[deprecated(note = "Use CoreError::OracleUnavailable instead")] +pub const ERROR_ORACLE_UNAVAILABLE: u32 = 17; +#[deprecated(note = "Use CoreError::StorageVersionMismatch instead")] +pub const ERROR_STORAGE_VERSION_MISMATCH: u32 = 18; +#[deprecated(note = "Use CoreError::InvalidMigrationPath instead")] +pub const ERROR_INVALID_MIGRATION_PATH: u32 = 19; +#[deprecated(note = "Use CoreError::RefundExceedsTotalPaid instead")] +pub const ERROR_REFUND_EXCEEDS_TOTAL_PAID: u32 = 20; +#[deprecated(note = "Use CoreError::OwnerMismatch instead")] +pub const ERROR_PLAN_OWNER_MISMATCH: u32 = 21; +#[deprecated(note = "Use CoreError::EventNotFound instead")] +pub const ERROR_EVENT_NOT_FOUND: u32 = 22; +#[deprecated(note = "Use CoreError::EventStoreFull instead")] +pub const ERROR_EVENT_STORE_FULL: u32 = 23; +#[deprecated(note = "Use CoreError::InvalidEventSequence instead")] +pub const ERROR_INVALID_EVENT_SEQUENCE: u32 = 24; +#[deprecated(note = "Use CoreError::ExportWindowExceeded instead")] +pub const ERROR_EXPORT_WINDOW_EXCEEDED: u32 = 25; +#[deprecated(note = "Use CoreError::PaymentTimedOut instead")] +pub const ERROR_PAYMENT_TIMED_OUT: u32 = 26; +#[deprecated(note = "Use CoreError::RecoveryAttemptsExhausted instead")] +pub const ERROR_RECOVERY_ATTEMPTS_EXHAUSTED: u32 = 27; +#[deprecated(note = "Use CoreError::TransactionNotRecoverable instead")] +pub const ERROR_TRANSACTION_NOT_RECOVERABLE: u32 = 28; +#[deprecated(note = "Use CoreError::InvalidTimeoutConfig instead")] +pub const ERROR_INVALID_TIMEOUT_CONFIG: u32 = 29; +#[deprecated(note = "Use CoreError::ChainReorgDetected instead")] +pub const ERROR_CHAIN_REORG_DETECTED: u32 = 30; diff --git a/contracts/types/src/lib.rs b/contracts/types/src/lib.rs index a84a9cc8..29181b61 100644 --- a/contracts/types/src/lib.rs +++ b/contracts/types/src/lib.rs @@ -2,6 +2,9 @@ use soroban_sdk::{contracttype, Address, BytesN, String, Symbol, Vec}; +pub mod errors; +pub use errors::CoreError; + /// Billing interval in seconds. #[contracttype] #[derive(Clone, Debug, PartialEq)] diff --git a/developer-portal/docs/api-reference.md b/developer-portal/docs/api-reference.md index 0a8e4469..1f930301 100644 --- a/developer-portal/docs/api-reference.md +++ b/developer-portal/docs/api-reference.md @@ -206,7 +206,31 @@ The API returns standard HTTP status codes: | 429 | Rate Limit Exceeded | | 500 | Internal Server Error | -**Error Response Format:** +### CoreError Enum + +All contract errors use a standardized `CoreError` enum (defined in `subtrackr-types`), ensuring consistent error handling across contracts: + +```rust +#[contracterror] +#[contracttype] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum CoreError { + Unauthorized = 100, + AlreadyInitialized = 200, + NotInitialized = 201, + InvalidAmount = 300, + InvalidInterval = 301, + InsufficientCredit = 400, + PaymentFailed = 401, + NotFound = 500, + DuplicateEntry = 501, + StorageError = 600, + ExternalError = 700, +} +``` + +**Error Response Format: ```json { "success": false, @@ -216,6 +240,11 @@ The API returns standard HTTP status codes: "details": { "field": "price", "issue": "must be a positive number" + }, + "coreError": { + "code": 300, + "variant": "InvalidAmount", + "userMessage": "The amount is invalid" } } }