From 762ca30c9f9af90eb5a5c6393485759f61cab379 Mon Sep 17 00:00:00 2001 From: Stellar Wave Developer Date: Fri, 29 May 2026 12:13:09 +0100 Subject: [PATCH] docs: document contract error codes with human-readable descriptions - Add doc comments to every ErrorCode variant in errors.rs explaining when each error is returned - Add docs/CONTRACT_ERRORS.md with full error reference table grouped by category (authorization, market lifecycle, betting, resolution, voting, upgrades, system) - Update API_SPEC.md with a Contract Error Codes section and quick- reference table showing how contract errors are surfaced via the API - Add CONTRACT_ERROR_MESSAGES map and getContractErrorMessage() helper to frontend/src/lib/api/client.ts so the frontend can map numeric contract error codes to localized user-facing messages --- API_SPEC.md | 36 +++++++++++ contracts/predict-iq/src/errors.rs | 97 ++++++++++++++++++++++++++++++ docs/CONTRACT_ERRORS.md | 82 +++++++++++++++++++++++++ frontend/src/lib/api/client.ts | 82 +++++++++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 docs/CONTRACT_ERRORS.md diff --git a/API_SPEC.md b/API_SPEC.md index 2abca7cc..7bc16be4 100644 --- a/API_SPEC.md +++ b/API_SPEC.md @@ -29,6 +29,7 @@ announcement before being removed. - [Authentication](#authentication) - [Endpoints](#endpoints) - [Error Handling](#error-handling) +- [Contract Error Codes](#contract-error-codes) - [Rate Limiting](#rate-limiting) ## Overview @@ -94,6 +95,41 @@ All errors are returned as JSON with the following structure: | RATE_LIMITED | 429 | Rate limit exceeded | | INTERNAL_ERROR | 500 | Internal server error | +## Contract Error Codes + +When a blockchain endpoint proxies a Soroban contract call that fails, the API wraps the +contract error in the standard error envelope with `code` set to `CONTRACT_ERROR` and a +`details.contract_code` field containing the numeric error code. + +```json +{ + "error": { + "code": "CONTRACT_ERROR", + "message": "The market has been closed and no longer accepts bets or updates.", + "details": { + "contract_code": 103, + "variant": "MarketClosed" + } + } +} +``` + +For the full list of contract error codes and their descriptions see +[`docs/CONTRACT_ERRORS.md`](docs/CONTRACT_ERRORS.md). + +Quick reference for the most common codes: + +| Code | Variant | Description | +|------|---------|-------------| +| 101 | `NotAuthorized` | Caller lacks required authorization. | +| 102 | `MarketNotFound` | No market exists with the given ID. | +| 103 | `MarketClosed` | Market is closed; no bets or updates accepted. | +| 107 | `InsufficientBalance` | Caller's token balance is too low. | +| 115 | `MarketNotActive` | Market is not in an active state. | +| 121 | `ContractPaused` | Contract is paused; all writes are disabled. | +| 142 | `BetNotFound` | No bet found for the given ID or caller. | +| 147 | `MarketNotResolved` | Market has not been resolved yet. | + ## Rate Limiting The API implements rate limiting to ensure fair usage: diff --git a/contracts/predict-iq/src/errors.rs b/contracts/predict-iq/src/errors.rs index ae667a4a..f533722f 100644 --- a/contracts/predict-iq/src/errors.rs +++ b/contracts/predict-iq/src/errors.rs @@ -4,53 +4,150 @@ use soroban_sdk::contracterror; #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum ErrorCode { + /// The contract has already been initialized and cannot be initialized again. AlreadyInitialized = 100, + + /// The caller does not have the required authorization to perform this action. NotAuthorized = 101, + + /// No market exists with the given ID. MarketNotFound = 102, + + /// The market has been closed and no longer accepts bets or updates. MarketClosed = 103, + + /// The market is still active and cannot be resolved or finalized yet. MarketStillActive = 104, + + /// The provided outcome index is not valid for this market. InvalidOutcome = 105, + + /// The bet amount is zero, negative, or otherwise outside the allowed range. InvalidBetAmount = 106, + + /// The caller's token balance is too low to cover the requested operation. InsufficientBalance = 107, + + /// The oracle failed to provide a result or returned an unreadable response. OracleFailure = 108, + + /// The circuit breaker is open due to repeated failures; operations are temporarily halted. CircuitBreakerOpen = 109, + + /// The dispute window for this market has already closed; disputes can no longer be filed. DisputeWindowClosed = 110, + + /// Voting on this market has not started yet. VotingNotStarted = 111, + + /// The voting period for this market has ended. VotingEnded = 112, + + /// The caller has already cast a vote on this market and cannot vote again. AlreadyVoted = 113, + + /// The requested fee exceeds the maximum allowed fee threshold. FeeTooHigh = 114, + + /// The market is not in an active state; it may be pending, closed, or resolved. MarketNotActive = 115, + + /// The submission deadline for this market has passed. DeadlinePassed = 116, + + /// The outcome for this market has already been set and cannot be changed. CannotChangeOutcome = 117, + + /// The market is not in a disputed state; dispute-specific operations are unavailable. MarketNotDisputed = 118, + + /// The market is not pending resolution; resolution cannot proceed at this time. MarketNotPendingResolution = 119, + + /// No admin address has been configured for this contract. AdminNotSet = 120, + + /// The contract is paused; all state-changing operations are disabled. ContractPaused = 121, + + /// No guardian address has been configured for this contract. GuardianNotSet = 122, + + /// The number of outcomes provided exceeds the maximum allowed per market. TooManyOutcomes = 123, + + /// The number of winning outcomes exceeds the maximum allowed for payout calculation. TooManyWinners = 124, + + /// The requested payout mode is not supported by this contract version. PayoutModeNotSupported = 125, + + /// The deposit provided is below the minimum required amount. InsufficientDeposit = 126, + + /// A timelock is currently active; the operation must wait until the timelock expires. TimelockActive = 127, + + /// No upgrade has been initiated; upgrade-related operations cannot proceed. UpgradeNotInitiated = 128, + + /// There are not enough governance votes to approve the requested action. InsufficientVotes = 129, + + /// The caller has already voted on this upgrade proposal. AlreadyVotedOnUpgrade = 130, + + /// The provided WASM hash is malformed or does not match the expected format. InvalidWasmHash = 131, + + /// The contract upgrade process failed; the new WASM could not be applied. UpgradeFailed = 132, + + /// The parent market has not been resolved yet; this conditional market cannot proceed. ParentMarketNotResolved = 133, + + /// The parent market resolved to an outcome that does not satisfy this market's condition. ParentMarketInvalidOutcome = 134, + + /// The resolution conditions have not been met yet; try again later. ResolutionNotReady = 135, + + /// The dispute window is still open; resolution must wait until it closes. DisputeWindowStillOpen = 136, + + /// No majority outcome was reached among the votes cast; resolution is inconclusive. NoMajorityReached = 137, + + /// The oracle price data is too old and considered stale; a fresh price feed is required. StalePrice = 138, + + /// The oracle's confidence score is below the minimum threshold required for resolution. ConfidenceTooLow = 139, + + /// The caller's governance token balance is too low to meet the minimum voting weight. InsufficientVotingWeight = 140, + + /// The market was not cancelled; refund or cancellation-specific operations are unavailable. MarketNotCancelled = 141, + + /// No bet was found for the given bet ID or caller address. BetNotFound = 142, + + /// An upgrade is already pending approval; only one upgrade can be in flight at a time. UpgradeAlreadyPending = 143, + + /// This WASM hash was recently used and is still within its cooldown period. UpgradeHashInCooldown = 144, + + /// The provided amount is invalid (e.g. zero, negative, or exceeds allowed limits). InvalidAmount = 145, + + /// No governance token contract address has been configured. GovernanceTokenNotSet = 146, + + /// The market has not been resolved yet; payout or post-resolution operations are unavailable. MarketNotResolved = 147, + + /// The provided deadline is in the past or otherwise invalid. InvalidDeadline = 148, } diff --git a/docs/CONTRACT_ERRORS.md b/docs/CONTRACT_ERRORS.md new file mode 100644 index 00000000..a4f68397 --- /dev/null +++ b/docs/CONTRACT_ERRORS.md @@ -0,0 +1,82 @@ +# PredictIQ Contract Error Reference + +Error codes returned by the `predict-iq` Soroban smart contract. +Source of truth: [`contracts/predict-iq/src/errors.rs`](../contracts/predict-iq/src/errors.rs) + +When a contract call fails, the Soroban SDK surfaces the error as a `u32` value. +The table below maps each code to its variant name and a human-readable description. + +| Code | Variant | Description | +|------|---------|-------------| +| 100 | `AlreadyInitialized` | The contract has already been initialized and cannot be initialized again. | +| 101 | `NotAuthorized` | The caller does not have the required authorization to perform this action. | +| 102 | `MarketNotFound` | No market exists with the given ID. | +| 103 | `MarketClosed` | The market has been closed and no longer accepts bets or updates. | +| 104 | `MarketStillActive` | The market is still active and cannot be resolved or finalized yet. | +| 105 | `InvalidOutcome` | The provided outcome index is not valid for this market. | +| 106 | `InvalidBetAmount` | The bet amount is zero, negative, or otherwise outside the allowed range. | +| 107 | `InsufficientBalance` | The caller's token balance is too low to cover the requested operation. | +| 108 | `OracleFailure` | The oracle failed to provide a result or returned an unreadable response. | +| 109 | `CircuitBreakerOpen` | The circuit breaker is open due to repeated failures; operations are temporarily halted. | +| 110 | `DisputeWindowClosed` | The dispute window for this market has already closed; disputes can no longer be filed. | +| 111 | `VotingNotStarted` | Voting on this market has not started yet. | +| 112 | `VotingEnded` | The voting period for this market has ended. | +| 113 | `AlreadyVoted` | The caller has already cast a vote on this market and cannot vote again. | +| 114 | `FeeTooHigh` | The requested fee exceeds the maximum allowed fee threshold. | +| 115 | `MarketNotActive` | The market is not in an active state; it may be pending, closed, or resolved. | +| 116 | `DeadlinePassed` | The submission deadline for this market has passed. | +| 117 | `CannotChangeOutcome` | The outcome for this market has already been set and cannot be changed. | +| 118 | `MarketNotDisputed` | The market is not in a disputed state; dispute-specific operations are unavailable. | +| 119 | `MarketNotPendingResolution` | The market is not pending resolution; resolution cannot proceed at this time. | +| 120 | `AdminNotSet` | No admin address has been configured for this contract. | +| 121 | `ContractPaused` | The contract is paused; all state-changing operations are disabled. | +| 122 | `GuardianNotSet` | No guardian address has been configured for this contract. | +| 123 | `TooManyOutcomes` | The number of outcomes provided exceeds the maximum allowed per market. | +| 124 | `TooManyWinners` | The number of winning outcomes exceeds the maximum allowed for payout calculation. | +| 125 | `PayoutModeNotSupported` | The requested payout mode is not supported by this contract version. | +| 126 | `InsufficientDeposit` | The deposit provided is below the minimum required amount. | +| 127 | `TimelockActive` | A timelock is currently active; the operation must wait until the timelock expires. | +| 128 | `UpgradeNotInitiated` | No upgrade has been initiated; upgrade-related operations cannot proceed. | +| 129 | `InsufficientVotes` | There are not enough governance votes to approve the requested action. | +| 130 | `AlreadyVotedOnUpgrade` | The caller has already voted on this upgrade proposal. | +| 131 | `InvalidWasmHash` | The provided WASM hash is malformed or does not match the expected format. | +| 132 | `UpgradeFailed` | The contract upgrade process failed; the new WASM could not be applied. | +| 133 | `ParentMarketNotResolved` | The parent market has not been resolved yet; this conditional market cannot proceed. | +| 134 | `ParentMarketInvalidOutcome` | The parent market resolved to an outcome that does not satisfy this market's condition. | +| 135 | `ResolutionNotReady` | The resolution conditions have not been met yet; try again later. | +| 136 | `DisputeWindowStillOpen` | The dispute window is still open; resolution must wait until it closes. | +| 137 | `NoMajorityReached` | No majority outcome was reached among the votes cast; resolution is inconclusive. | +| 138 | `StalePrice` | The oracle price data is too old and considered stale; a fresh price feed is required. | +| 139 | `ConfidenceTooLow` | The oracle's confidence score is below the minimum threshold required for resolution. | +| 140 | `InsufficientVotingWeight` | The caller's governance token balance is too low to meet the minimum voting weight. | +| 141 | `MarketNotCancelled` | The market was not cancelled; refund or cancellation-specific operations are unavailable. | +| 142 | `BetNotFound` | No bet was found for the given bet ID or caller address. | +| 143 | `UpgradeAlreadyPending` | An upgrade is already pending approval; only one upgrade can be in flight at a time. | +| 144 | `UpgradeHashInCooldown` | This WASM hash was recently used and is still within its cooldown period. | +| 145 | `InvalidAmount` | The provided amount is invalid (e.g. zero, negative, or exceeds allowed limits). | +| 146 | `GovernanceTokenNotSet` | No governance token contract address has been configured. | +| 147 | `MarketNotResolved` | The market has not been resolved yet; payout or post-resolution operations are unavailable. | +| 148 | `InvalidDeadline` | The provided deadline is in the past or otherwise invalid. | + +## Error Groups + +### Authorization & Setup +100 `AlreadyInitialized`, 101 `NotAuthorized`, 120 `AdminNotSet`, 121 `ContractPaused`, 122 `GuardianNotSet`, 146 `GovernanceTokenNotSet` + +### Market Lifecycle +102 `MarketNotFound`, 103 `MarketClosed`, 104 `MarketStillActive`, 115 `MarketNotActive`, 116 `DeadlinePassed`, 148 `InvalidDeadline` + +### Betting +105 `InvalidOutcome`, 106 `InvalidBetAmount`, 107 `InsufficientBalance`, 126 `InsufficientDeposit`, 142 `BetNotFound`, 145 `InvalidAmount` + +### Resolution & Disputes +108 `OracleFailure`, 110 `DisputeWindowClosed`, 117 `CannotChangeOutcome`, 118 `MarketNotDisputed`, 119 `MarketNotPendingResolution`, 133 `ParentMarketNotResolved`, 134 `ParentMarketInvalidOutcome`, 135 `ResolutionNotReady`, 136 `DisputeWindowStillOpen`, 137 `NoMajorityReached`, 138 `StalePrice`, 139 `ConfidenceTooLow`, 141 `MarketNotCancelled`, 147 `MarketNotResolved` + +### Voting & Governance +111 `VotingNotStarted`, 112 `VotingEnded`, 113 `AlreadyVoted`, 114 `FeeTooHigh`, 129 `InsufficientVotes`, 130 `AlreadyVotedOnUpgrade`, 140 `InsufficientVotingWeight` + +### Upgrades +127 `TimelockActive`, 128 `UpgradeNotInitiated`, 131 `InvalidWasmHash`, 132 `UpgradeFailed`, 143 `UpgradeAlreadyPending`, 144 `UpgradeHashInCooldown` + +### System +109 `CircuitBreakerOpen`, 123 `TooManyOutcomes`, 124 `TooManyWinners`, 125 `PayoutModeNotSupported` diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 71b1d904..5ced67ab 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -17,6 +17,88 @@ interface RequestOptions { cacheTtl?: number; } +/** + * Maps Soroban contract error codes (u32) to localized user-facing messages. + * + * When the API returns a CONTRACT_ERROR, read `details.contract_code` and pass + * it to `getContractErrorMessage` to get a display-ready string. + * + * Source of truth: contracts/predict-iq/src/errors.rs + * Full reference: docs/CONTRACT_ERRORS.md + */ +export const CONTRACT_ERROR_MESSAGES: Record = { + // Authorization & Setup + 100: "This contract has already been set up.", + 101: "You are not authorized to perform this action.", + 120: "No admin has been configured for this contract.", + 121: "The platform is currently paused. Please try again later.", + 122: "No guardian has been configured for this contract.", + 146: "The governance token contract has not been configured.", + + // Market Lifecycle + 102: "Market not found.", + 103: "This market is closed and no longer accepts activity.", + 104: "This market is still active and cannot be finalized yet.", + 115: "This market is not currently active.", + 116: "The deadline for this market has passed.", + 148: "The provided deadline is invalid.", + + // Betting + 105: "The selected outcome is not valid for this market.", + 106: "The bet amount is invalid. Please enter a valid amount.", + 107: "Insufficient balance to complete this transaction.", + 126: "Your deposit is below the minimum required amount.", + 142: "Bet not found.", + 145: "The amount provided is invalid.", + + // Resolution & Disputes + 108: "The oracle failed to provide a result. Please try again later.", + 110: "The dispute window for this market has closed.", + 117: "The outcome for this market has already been set.", + 118: "This market is not in a disputed state.", + 119: "This market is not pending resolution.", + 133: "The parent market has not been resolved yet.", + 134: "The parent market outcome does not satisfy this market's condition.", + 135: "Resolution conditions have not been met yet. Please try again later.", + 136: "The dispute window is still open. Resolution must wait.", + 137: "No majority outcome was reached. Resolution is inconclusive.", + 138: "Price data is stale. A fresh oracle feed is required.", + 139: "Oracle confidence is too low to resolve this market.", + 141: "This market was not cancelled.", + 147: "This market has not been resolved yet.", + + // Voting & Governance + 111: "Voting on this market has not started yet.", + 112: "The voting period for this market has ended.", + 113: "You have already voted on this market.", + 114: "The requested fee is too high.", + 129: "Not enough governance votes to approve this action.", + 130: "You have already voted on this upgrade.", + 140: "Your governance token balance is too low to vote.", + + // Upgrades + 127: "A timelock is active. Please wait before retrying.", + 128: "No upgrade has been initiated.", + 131: "The provided WASM hash is invalid.", + 132: "The contract upgrade failed.", + 143: "An upgrade is already pending. Only one upgrade can be in progress at a time.", + 144: "This WASM hash is in cooldown. Please wait before reusing it.", + + // System + 109: "The system circuit breaker is open. Operations are temporarily halted.", + 123: "Too many outcomes provided for this market.", + 124: "Too many winners specified for payout calculation.", + 125: "This payout mode is not supported.", +}; + +/** + * Returns a user-facing message for a contract error code. + * Falls back to a generic message if the code is not recognized. + */ +export function getContractErrorMessage(code: number): string { + return CONTRACT_ERROR_MESSAGES[code] ?? `An unexpected contract error occurred (code ${code}).`; +} + /** * Structured API error with HTTP status code and user-friendly message. * Thrown for both network failures and non-2xx responses.