Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions API_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
97 changes: 97 additions & 0 deletions contracts/predict-iq/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
82 changes: 82 additions & 0 deletions docs/CONTRACT_ERRORS.md
Original file line number Diff line number Diff line change
@@ -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`
82 changes: 82 additions & 0 deletions frontend/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number, string> = {
// 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.
Expand Down
Loading