From c51a01855a8d46aec284b62d06de5574a16a189c Mon Sep 17 00:00:00 2001 From: shemz Date: Tue, 20 Oct 2026 07:16:17 +0000 Subject: [PATCH] fix(markets): fix create-market route, missing fields, and form schema (#1753) The market creation form was posting to /api/v1/blockchain/markets which did not exist in the backend (causing 404/405), and the request body was missing all fields required by the contract's create_market entrypoint. Changes: - frontend/src/lib/validation/marketForm.ts: extend marketFormSchema with resolutionDeadline, oracleAddress, feedId, maxStalenessSeconds, maxConfidenceBps, tier, parentId, parentOutcomeIdx; add cross-field refinement enforcing resolution_deadline >= close_time + 24h - frontend/src/app/markets/create/page.tsx: add form fields for all new schema fields; fix submission body to send all contract-required params (deadline/resolution_deadline as unix seconds, oracle_config object, tier, native_token address, parent_id, parent_outcome_idx) - frontend/src/lib/api/requestSchemas.ts: add createMarketSchema + CreateMarketBody type for boundary validation of the outgoing body - services/api/src/handlers.rs: add OracleConfigRequest, CreateMarketRequest, CreateMarketResponse structs and create_market handler; handler validates all fields and returns 501 with a clear message explaining that wallet signing (tracked in #77) is required for the full on-chain path - services/api/src/main.rs: register POST /api/v1/blockchain/markets in public_routes so the route exists (no more 404) - services/api/openapi.yaml: document new endpoint and add component schemas - docs/API_SPEC.md: add POST /api/v1/blockchain/markets to public routes table Closes #1753 --- docs/API_SPEC.md | 6 + frontend/src/app/markets/create/page.tsx | 236 ++++++++++++++++++++-- frontend/src/lib/api/requestSchemas.ts | 27 +++ frontend/src/lib/validation/marketForm.ts | 146 +++++++++---- services/api/openapi.yaml | 128 ++++++++++++ services/api/src/handlers.rs | 168 +++++++++++++++ services/api/src/main.rs | 1 + 7 files changed, 665 insertions(+), 47 deletions(-) diff --git a/docs/API_SPEC.md b/docs/API_SPEC.md index 8bafdf17..02347e7e 100644 --- a/docs/API_SPEC.md +++ b/docs/API_SPEC.md @@ -12,12 +12,18 @@ | GET | `/api/v1/markets/featured` | `getFeaturedMarkets` | None | | GET | `/api/v1/content` | `getContent` | None | | GET | `/api/v1/blockchain/health` | `getBlockchainHealth` | None | +| POST | `/api/v1/blockchain/markets` | `createMarket` | None | | GET | `/api/v1/blockchain/markets/{market_id}` | `getBlockchainMarket` | None | | GET | `/api/v1/blockchain/stats` | `getBlockchainStats` | None | | GET | `/api/v1/blockchain/users/{user}/bets` | `getUserBets` | None | | GET | `/api/v1/blockchain/oracle/{market_id}` | `getOracleResult` | None | | GET | `/api/v1/blockchain/tx/{tx_hash}` | `getTransactionStatus` | None | +> **Note:** `POST /api/v1/blockchain/markets` currently returns `501 Not Implemented`. +> The full create flow requires client-side wallet signing (tracked in issue #77). +> The route is registered and validates the request body; use the Stellar SDK to +> sign and submit `CreateMarket` transactions directly until #77 is resolved. + ## Newsletter Routes | Method | Path | Operation | Auth | diff --git a/frontend/src/app/markets/create/page.tsx b/frontend/src/app/markets/create/page.tsx index 88391324..cba4f9eb 100644 --- a/frontend/src/app/markets/create/page.tsx +++ b/frontend/src/app/markets/create/page.tsx @@ -20,6 +20,14 @@ const EMPTY_FORM: MarketFormValues = { outcomes: ['', ''], closeTime: '', asset: '', + resolutionDeadline: '', + oracleAddress: '', + feedId: '', + maxStalenessSeconds: 300, + maxConfidenceBps: 100, + tier: 'Basic', + parentId: 0, + parentOutcomeIdx: 0, }; interface CreateMarketResponse { @@ -78,18 +86,33 @@ export default function CreateMarketPage() { try { const parsed = marketFormSchema.parse(values); const config = getEnvConfig(); - const res = await fetch(`${config.NEXT_PUBLIC_API_URL.replace(/\/$/, '')}/api/v1/blockchain/markets`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - creator: wallet, - title: parsed.title, - description: parsed.description, - outcomes: parsed.outcomes, - ends_at: new Date(parsed.closeTime).toISOString(), - settlement_asset: getSupportedAsset(parsed.asset)?.address, - }), - }); + // NOTE: Wallet signing for the on-chain transaction is tracked under #77. + const res = await fetch( + `${config.NEXT_PUBLIC_API_URL.replace(/\/$/, '')}/api/v1/blockchain/markets`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + creator: wallet, + description: parsed.description, + options: parsed.outcomes, + deadline: Math.floor(new Date(parsed.closeTime).getTime() / 1000), + resolution_deadline: Math.floor( + new Date(parsed.resolutionDeadline).getTime() / 1000 + ), + oracle_config: { + oracle_address: parsed.oracleAddress, + feed_id: parsed.feedId, + max_staleness_seconds: parsed.maxStalenessSeconds, + max_confidence_bps: parsed.maxConfidenceBps, + }, + tier: parsed.tier, + native_token: getSupportedAsset(parsed.asset)?.address, + parent_id: parsed.parentId ?? 0, + parent_outcome_idx: parsed.parentOutcomeIdx ?? 0, + }), + } + ); if (!res.ok) { throw new Error(`Failed to create market (${res.status})`); @@ -215,6 +238,30 @@ export default function CreateMarketPage() { )} +
+ + setField('resolutionDeadline', e.target.value)} + aria-invalid={Boolean(errors.resolutionDeadline)} + aria-describedby={ + errors.resolutionDeadline ? 'market-resolution-deadline-error' : undefined + } + /> + {errors.resolutionDeadline && ( + + {errors.resolutionDeadline} + + )} +
+
+
+ + + {errors.tier && ( + + {errors.tier} + + )} +
+ +
+ Oracle configuration + +
+ + setField('oracleAddress', e.target.value)} + aria-invalid={Boolean(errors.oracleAddress)} + aria-describedby={errors.oracleAddress ? 'market-oracle-address-error' : undefined} + /> + {errors.oracleAddress && ( + + {errors.oracleAddress} + + )} +
+ +
+ + setField('feedId', e.target.value)} + aria-invalid={Boolean(errors.feedId)} + aria-describedby={errors.feedId ? 'market-feed-id-error' : undefined} + /> + {errors.feedId && ( + + {errors.feedId} + + )} +
+ +
+ + + setField('maxStalenessSeconds', parseInt(e.target.value, 10) || 0) + } + aria-invalid={Boolean(errors.maxStalenessSeconds)} + aria-describedby={ + errors.maxStalenessSeconds ? 'market-max-staleness-error' : undefined + } + /> + {errors.maxStalenessSeconds && ( + + {errors.maxStalenessSeconds} + + )} +
+ +
+ + + setField('maxConfidenceBps', parseInt(e.target.value, 10) || 0) + } + aria-invalid={Boolean(errors.maxConfidenceBps)} + aria-describedby={ + errors.maxConfidenceBps ? 'market-max-confidence-error' : undefined + } + /> + {errors.maxConfidenceBps && ( + + {errors.maxConfidenceBps} + + )} +
+
+ +
+ Parent market (optional) + +
+ + setField('parentId', parseInt(e.target.value, 10) || 0)} + aria-invalid={Boolean(errors.parentId)} + aria-describedby={errors.parentId ? 'market-parent-id-error' : undefined} + /> + {errors.parentId && ( + + {errors.parentId} + + )} +
+ +
+ + + setField('parentOutcomeIdx', parseInt(e.target.value, 10) || 0) + } + aria-invalid={Boolean(errors.parentOutcomeIdx)} + aria-describedby={ + errors.parentOutcomeIdx ? 'market-parent-outcome-idx-error' : undefined + } + /> + {errors.parentOutcomeIdx && ( + + {errors.parentOutcomeIdx} + + )} +
+
+ diff --git a/frontend/src/lib/api/requestSchemas.ts b/frontend/src/lib/api/requestSchemas.ts index 4ff6b290..5c12e144 100644 --- a/frontend/src/lib/api/requestSchemas.ts +++ b/frontend/src/lib/api/requestSchemas.ts @@ -78,6 +78,33 @@ export const placeBetSchema = z }) .loose(); +/** + * POST /api/v1/blockchain/markets — create a new prediction market. + * Full wallet-signing flow is tracked under #77; this schema validates + * the API request body that will be sent once signing is wired. + */ +export const createMarketSchema = z + .object({ + creator: z.string().min(1, 'A creator wallet address is required.'), + description: z.string().min(1, 'A description is required.'), + options: z.array(z.string().min(1)).min(2, 'At least 2 options are required.'), + deadline: z.number().int().positive('Deadline must be a positive unix timestamp.'), + resolution_deadline: z.number().int().positive('Resolution deadline must be a positive unix timestamp.'), + oracle_config: z.object({ + oracle_address: z.string().min(1, 'Oracle address is required.'), + feed_id: z.string().min(1, 'Feed ID is required.'), + max_staleness_seconds: z.number().int().nonnegative(), + max_confidence_bps: z.number().int().nonnegative(), + }).loose(), + tier: z.enum(['Basic', 'Pro', 'Institutional']), + native_token: z.string().min(1, 'Native token address is required.'), + parent_id: z.number().int().nonnegative(), + parent_outcome_idx: z.number().int().nonnegative(), + }) + .loose(); + +export type CreateMarketBody = z.infer; + // === Compile-time contract: each schema stays assignable to its generated type // The generated types carry `email` as a plain `string` (the `format: email` diff --git a/frontend/src/lib/validation/marketForm.ts b/frontend/src/lib/validation/marketForm.ts index c1dfcb8a..5b633e93 100644 --- a/frontend/src/lib/validation/marketForm.ts +++ b/frontend/src/lib/validation/marketForm.ts @@ -26,6 +26,11 @@ export const MAX_OUTCOMES = 100; // through that window. export const MIN_CLOSE_TIME_LEAD_MS = 5 * 60 * 1000; +// The contract requires resolution_deadline >= deadline + 86400 (24 hours). +// Mirror that here so the client rejects invalid submissions before spending +// gas on a doomed transaction. +export const MIN_RESOLUTION_LEAD_SECONDS = 86400; + /** * Two outcome labels are considered duplicates if they're equal once * surrounding whitespace is trimmed and case is normalized — e.g. "Yes", @@ -45,44 +50,115 @@ export function hasDuplicateOutcome(outcomes: string[]): boolean { return false; } -export const marketFormSchema = z.object({ - title: z - .string() - .trim() - .min(1, 'Title is required.') - .max(200, 'Title must be 200 characters or fewer.'), - description: z - .string() - .trim() - .min(1, 'Description is required.') - .max(2000, 'Description must be 2000 characters or fewer.'), - outcomes: z - .array(z.string().trim().min(1, 'Outcome labels cannot be empty.')) - .min(MIN_OUTCOMES, `At least ${MIN_OUTCOMES} outcomes are required.`) - .max(MAX_OUTCOMES, `A market can have at most ${MAX_OUTCOMES} outcomes.`) - .refine((outcomes) => !hasDuplicateOutcome(outcomes), { - message: 'Outcome labels must be unique (case and whitespace insensitive).', +export const marketFormSchema = z + .object({ + title: z + .string() + .trim() + .min(1, 'Title is required.') + .max(200, 'Title must be 200 characters or fewer.'), + description: z + .string() + .trim() + .min(1, 'Description is required.') + .max(2000, 'Description must be 2000 characters or fewer.'), + outcomes: z + .array(z.string().trim().min(1, 'Outcome labels cannot be empty.')) + .min(MIN_OUTCOMES, `At least ${MIN_OUTCOMES} outcomes are required.`) + .max(MAX_OUTCOMES, `A market can have at most ${MAX_OUTCOMES} outcomes.`) + .refine((outcomes) => !hasDuplicateOutcome(outcomes), { + message: 'Outcome labels must be unique (case and whitespace insensitive).', + }), + closeTime: z + .string() + .min(1, 'Close time is required.') + .refine((value) => !Number.isNaN(Date.parse(value)), 'Close time must be a valid date.') + .refine( + (value) => Date.parse(value) >= Date.now() + MIN_CLOSE_TIME_LEAD_MS, + 'Close time must be at least 5 minutes in the future.' + ), + // An asset that's technically valid on-chain but not on the platform's + // supported list (see lib/assets.ts) must be rejected here, not left for + // the contract call to fail (#1373). + asset: z + .string() + .min(1, 'Select a settlement asset.') + .refine(isSupportedAsset, 'Select a supported settlement asset.'), + + // Resolution deadline must be a valid date and is cross-validated against + // closeTime below (must be >= closeTime + 24 h) per the contract rule. + resolutionDeadline: z + .string() + .min(1, 'Resolution deadline is required.') + .refine( + (value) => !Number.isNaN(Date.parse(value)), + 'Resolution deadline must be a valid date.' + ), + + // Oracle configuration fields (OracleConfig in the contract). + oracleAddress: z + .string() + .trim() + .min(1, 'Oracle contract address is required.'), + feedId: z + .string() + .trim() + .min(1, 'Oracle feed ID is required.'), + maxStalenessSeconds: z + .number({ invalid_type_error: 'Max staleness must be a number.' }) + .int('Max staleness must be a whole number.') + .positive('Max staleness must be greater than zero.'), + maxConfidenceBps: z + .number({ invalid_type_error: 'Max confidence must be a number.' }) + .int('Max confidence must be a whole number.') + .min(0, 'Max confidence cannot be negative.'), + + // Market tier — mirrors MarketTier in the contract. + tier: z.enum(['Basic', 'Pro', 'Institutional'], { + errorMap: () => ({ message: 'Select a market tier.' }), }), - closeTime: z - .string() - .min(1, 'Close time is required.') - .refine((value) => !Number.isNaN(Date.parse(value)), 'Close time must be a valid date.') - .refine( - (value) => Date.parse(value) >= Date.now() + MIN_CLOSE_TIME_LEAD_MS, - 'Close time must be at least 5 minutes in the future.' - ), - // An asset that's technically valid on-chain but not on the platform's - // supported list (see lib/assets.ts) must be rejected here, not left for - // the contract call to fail (#1373). - asset: z - .string() - .min(1, 'Select a settlement asset.') - .refine(isSupportedAsset, 'Select a supported settlement asset.'), -}); + + // Optional parent market linkage. Default to 0 (no parent) when omitted. + parentId: z + .number({ invalid_type_error: 'Parent ID must be a number.' }) + .int('Parent ID must be a whole number.') + .min(0, 'Parent ID cannot be negative.') + .optional() + .default(0), + parentOutcomeIdx: z + .number({ invalid_type_error: 'Parent outcome index must be a number.' }) + .int('Parent outcome index must be a whole number.') + .min(0, 'Parent outcome index cannot be negative.') + .optional() + .default(0), + }) + // Cross-field refinement: resolution deadline must be at least 24 hours + // after close time, matching the on-chain invariant in the contract. + .refine( + (data) => { + if (!data.closeTime || !data.resolutionDeadline) return true; + const closeMs = Date.parse(data.closeTime); + const resolutionMs = Date.parse(data.resolutionDeadline); + if (Number.isNaN(closeMs) || Number.isNaN(resolutionMs)) return true; + return resolutionMs >= closeMs + MIN_RESOLUTION_LEAD_SECONDS * 1000; + }, + { + message: 'Resolution deadline must be at least 24 hours after the close time.', + path: ['resolutionDeadline'], + } + ); export type MarketFormValues = z.infer; -export type MarketFormErrors = Partial>; +export type MarketFormErrors = Partial< + Record< + | keyof Omit + | 'parentId' + | 'parentOutcomeIdx' + | 'form', + string + > +>; /** * Validates raw form state and returns a flat map of the first error per @@ -95,7 +171,7 @@ export function validateMarketForm(values: MarketFormValues): MarketFormErrors { const errors: MarketFormErrors = {}; for (const issue of result.error.issues) { - const key = issue.path[0] as keyof MarketFormValues | undefined; + const key = issue.path[0] as keyof MarketFormErrors | undefined; if (key && !errors[key]) { errors[key] = issue.message; } diff --git a/services/api/openapi.yaml b/services/api/openapi.yaml index f1c3ec7e..0570e13d 100644 --- a/services/api/openapi.yaml +++ b/services/api/openapi.yaml @@ -190,6 +190,47 @@ paths: schema: $ref: "#/components/schemas/BlockchainHealth" + /api/v1/blockchain/markets: + post: + tags: [blockchain] + operationId: createMarket + summary: Create a prediction market (wallet-signing required — see #77) + description: | + Registers a new prediction market. + + **Status: 501 Not Implemented** + + The full market-creation flow requires the caller to sign the Stellar + transaction with their wallet private key. The API server does not hold + user keys, so this step cannot be completed server-side until the + wallet-signing integration described in issue #77 is implemented. + + Until then: + - The route **exists** and validates the request body (no more 404/405). + - A `501 Not Implemented` response is returned with a clear message. + - Callers should build and sign the `CreateMarket` contract-invocation + transaction directly with the Stellar SDK and submit it via Soroban RPC. + parameters: + - $ref: "#/components/parameters/apiVersion" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateMarketRequest" + responses: + "400": + description: Validation error (missing/invalid fields) + $ref: "#/components/responses/ApiError" + "429": + $ref: "#/components/responses/ApiError" + "501": + description: Not yet implemented — use the Stellar SDK for wallet-signed market creation + content: + application/json: + schema: + $ref: "#/components/schemas/CreateMarketResponse" + /api/v1/blockchain/markets/{market_id}: get: tags: [blockchain] @@ -959,6 +1000,93 @@ components: message_id: type: string + OracleConfigRequest: + type: object + required: [oracle_address, feed_id, max_staleness_seconds, max_confidence_bps] + properties: + oracle_address: + type: string + description: Stellar account address of the oracle contract. + feed_id: + type: string + description: Feed identifier used by the oracle (e.g. "BTC/USD"). + max_staleness_seconds: + type: integer + format: int64 + minimum: 0 + description: Maximum age of an accepted oracle reading, in seconds. + max_confidence_bps: + type: integer + format: int64 + minimum: 0 + description: Maximum confidence band accepted from the oracle, in basis points. + + CreateMarketRequest: + type: object + required: + - creator + - description + - options + - deadline + - resolution_deadline + - oracle_config + - tier + - native_token + - parent_id + - parent_outcome_idx + properties: + creator: + type: string + description: Stellar account address that will be recorded as the market creator. + description: + type: string + description: Human-readable description of the prediction question. + options: + type: array + items: + type: string + minItems: 2 + description: Ordered list of outcome labels (minimum 2). + deadline: + type: integer + format: int64 + minimum: 0 + description: Unix timestamp (seconds) at which the market stops accepting bets. Must be in the future. + resolution_deadline: + type: integer + format: int64 + minimum: 0 + description: Unix timestamp (seconds) after which the oracle may resolve the market. Must be at least `deadline + 86400`. + oracle_config: + $ref: "#/components/schemas/OracleConfigRequest" + tier: + type: string + enum: [Basic, Pro, Institutional] + description: Market tier. + native_token: + type: string + description: Native token contract address used for payouts. + parent_id: + type: integer + format: int64 + minimum: 0 + description: Parent market ID for conditional markets (0 = top-level market). + parent_outcome_idx: + type: integer + format: int32 + minimum: 0 + description: Outcome index within the parent market that activates this market. + + CreateMarketResponse: + type: object + required: [success, message] + properties: + success: + type: boolean + message: + type: string + description: Human-readable explanation of why the request could not be completed server-side. + responses: ApiError: description: Error response with machine-readable code diff --git a/services/api/src/handlers.rs b/services/api/src/handlers.rs index 314bc8a3..7b141e19 100644 --- a/services/api/src/handlers.rs +++ b/services/api/src/handlers.rs @@ -1214,6 +1214,174 @@ pub async fn metrics(State(state): State>) -> Result, + /// Unix timestamp (seconds) at which the market stops accepting bets. + /// Must be in the future at the time of the request. + pub deadline: u64, + /// Unix timestamp (seconds) after which the oracle may resolve the market. + /// Must be at least `deadline + 86400` (24 h after betting closes). + pub resolution_deadline: u64, + /// Oracle configuration for automated resolution. + pub oracle_config: OracleConfigRequest, + /// Market tier: `"Basic"`, `"Pro"`, or `"Institutional"`. + pub tier: String, + /// Native token contract address used for payouts. + pub native_token: String, + /// Parent market ID for conditional markets (0 = top-level market). + pub parent_id: u64, + /// Outcome index within the parent market that activates this market. + pub parent_outcome_idx: u32, +} + +/// Response body returned by `POST /api/v1/blockchain/markets`. +#[derive(Debug, Clone, Serialize, utoipa::ToSchema)] +pub struct CreateMarketResponse { + pub success: bool, + pub message: String, +} + +/// Create a new prediction market. +/// +/// **Status: 501 Not Implemented** +/// +/// The full market-creation flow requires wallet signing of the Stellar +/// transaction on the client side, which has not yet been wired into the +/// API server (tracked in issue #77). This endpoint is registered so that +/// clients receive a clear, actionable error instead of a 404 or 405. +/// +/// Until #77 is resolved, callers should build and sign the `CreateMarket` +/// contract-invocation transaction directly with the Stellar SDK and submit +/// it via the Stellar Horizon or Soroban RPC endpoint. +#[utoipa::path( + post, + path = "/api/v1/blockchain/markets", + tag = "blockchain", + request_body = CreateMarketRequest, + responses( + (status = 400, description = "Validation error", body = ApiError), + (status = 501, description = "Not yet implemented — use Stellar SDK for wallet-signed market creation", body = CreateMarketResponse), + ) +)] +pub async fn create_market( + Json(payload): Json, +) -> impl IntoResponse { + use std::time::{SystemTime, UNIX_EPOCH}; + + let now_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + // ── Validation ──────────────────────────────────────────────────────────── + + if payload.creator.trim().is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request("`creator` must not be empty")), + ) + .into_response(); + } + + if payload.description.trim().is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request("`description` must not be empty")), + ) + .into_response(); + } + + if payload.options.len() < 2 { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request("`options` must contain at least 2 entries")), + ) + .into_response(); + } + + if payload.oracle_config.oracle_address.trim().is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request("`oracle_config.oracle_address` must not be empty")), + ) + .into_response(); + } + + if payload.oracle_config.feed_id.trim().is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request("`oracle_config.feed_id` must not be empty")), + ) + .into_response(); + } + + if payload.native_token.trim().is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request("`native_token` must not be empty")), + ) + .into_response(); + } + + if payload.deadline <= now_secs { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request("`deadline` must be in the future")), + ) + .into_response(); + } + + if payload.resolution_deadline < payload.deadline + 86_400 { + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::bad_request( + "`resolution_deadline` must be at least `deadline + 86400` (24 h after betting closes)", + )), + ) + .into_response(); + } + + // ── Not yet implemented ─────────────────────────────────────────────────── + // Full market creation requires the caller to sign the Stellar transaction + // with their private key. The API server does not hold user private keys, + // so this path cannot be completed server-side until the wallet-signing + // integration tracked in issue #77 is implemented. + ( + StatusCode::NOT_IMPLEMENTED, + Json(CreateMarketResponse { + success: false, + message: "Market creation via API is not yet implemented. \ + Direct contract invocation requires wallet signing, which must be \ + performed client-side using the Stellar SDK. \ + See issue #77 for the planned implementation." + .to_string(), + }), + ) + .into_response() +} + #[utoipa::path( get, path = "/api/v1/blockchain/health", diff --git a/services/api/src/main.rs b/services/api/src/main.rs index b6af3562..5188440d 100644 --- a/services/api/src/main.rs +++ b/services/api/src/main.rs @@ -315,6 +315,7 @@ async fn main() -> anyhow::Result<()> { let public_routes = Router::new() .route("/api/v1/blockchain/health", get(handlers::blockchain_health)) + .route("/api/v1/blockchain/markets", post(handlers::create_market)) .route("/api/v1/blockchain/markets/:market_id", get(handlers::blockchain_market_data)) .route("/api/v1/blockchain/stats", get(handlers::blockchain_platform_stats)) .route("/api/v1/blockchain/users/:user/bets", get(handlers::blockchain_user_bets))