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
6 changes: 6 additions & 0 deletions docs/API_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
236 changes: 224 additions & 12 deletions frontend/src/app/markets/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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})`);
Expand Down Expand Up @@ -215,6 +238,30 @@ export default function CreateMarketPage() {
)}
</div>

<div className="market-create__field">
<label htmlFor="market-resolution-deadline">Resolution deadline</label>
<input
id="market-resolution-deadline"
name="resolutionDeadline"
type="datetime-local"
value={values.resolutionDeadline}
onChange={(e) => setField('resolutionDeadline', e.target.value)}
aria-invalid={Boolean(errors.resolutionDeadline)}
aria-describedby={
errors.resolutionDeadline ? 'market-resolution-deadline-error' : undefined
}
/>
{errors.resolutionDeadline && (
<span
id="market-resolution-deadline-error"
className="market-create__error"
role="alert"
>
{errors.resolutionDeadline}
</span>
)}
</div>

<div className="market-create__field">
<label htmlFor="market-asset">Settlement asset</label>
<TokenSelector
Expand All @@ -231,6 +278,171 @@ export default function CreateMarketPage() {
)}
</div>

<div className="market-create__field">
<label htmlFor="market-tier">Market tier</label>
<select
id="market-tier"
name="tier"
value={values.tier}
onChange={(e) => setField('tier', e.target.value as MarketFormValues['tier'])}
aria-invalid={Boolean(errors.tier)}
aria-describedby={errors.tier ? 'market-tier-error' : undefined}
>
<option value="Basic">Basic</option>
<option value="Pro">Pro</option>
<option value="Institutional">Institutional</option>
</select>
{errors.tier && (
<span id="market-tier-error" className="market-create__error" role="alert">
{errors.tier}
</span>
)}
</div>

<fieldset className="market-create__fieldset">
<legend>Oracle configuration</legend>

<div className="market-create__field">
<label htmlFor="market-oracle-address">Oracle contract address</label>
<input
id="market-oracle-address"
name="oracleAddress"
type="text"
value={values.oracleAddress}
onChange={(e) => setField('oracleAddress', e.target.value)}
aria-invalid={Boolean(errors.oracleAddress)}
aria-describedby={errors.oracleAddress ? 'market-oracle-address-error' : undefined}
/>
{errors.oracleAddress && (
<span id="market-oracle-address-error" className="market-create__error" role="alert">
{errors.oracleAddress}
</span>
)}
</div>

<div className="market-create__field">
<label htmlFor="market-feed-id">Feed ID</label>
<input
id="market-feed-id"
name="feedId"
type="text"
value={values.feedId}
onChange={(e) => setField('feedId', e.target.value)}
aria-invalid={Boolean(errors.feedId)}
aria-describedby={errors.feedId ? 'market-feed-id-error' : undefined}
/>
{errors.feedId && (
<span id="market-feed-id-error" className="market-create__error" role="alert">
{errors.feedId}
</span>
)}
</div>

<div className="market-create__field">
<label htmlFor="market-max-staleness">Max staleness (seconds)</label>
<input
id="market-max-staleness"
name="maxStalenessSeconds"
type="number"
min={1}
step={1}
value={values.maxStalenessSeconds}
onChange={(e) =>
setField('maxStalenessSeconds', parseInt(e.target.value, 10) || 0)
}
aria-invalid={Boolean(errors.maxStalenessSeconds)}
aria-describedby={
errors.maxStalenessSeconds ? 'market-max-staleness-error' : undefined
}
/>
{errors.maxStalenessSeconds && (
<span id="market-max-staleness-error" className="market-create__error" role="alert">
{errors.maxStalenessSeconds}
</span>
)}
</div>

<div className="market-create__field">
<label htmlFor="market-max-confidence">Max confidence (basis points)</label>
<input
id="market-max-confidence"
name="maxConfidenceBps"
type="number"
min={0}
step={1}
value={values.maxConfidenceBps}
onChange={(e) =>
setField('maxConfidenceBps', parseInt(e.target.value, 10) || 0)
}
aria-invalid={Boolean(errors.maxConfidenceBps)}
aria-describedby={
errors.maxConfidenceBps ? 'market-max-confidence-error' : undefined
}
/>
{errors.maxConfidenceBps && (
<span
id="market-max-confidence-error"
className="market-create__error"
role="alert"
>
{errors.maxConfidenceBps}
</span>
)}
</div>
</fieldset>

<fieldset className="market-create__fieldset">
<legend>Parent market (optional)</legend>

<div className="market-create__field">
<label htmlFor="market-parent-id">Parent market ID</label>
<input
id="market-parent-id"
name="parentId"
type="number"
min={0}
step={1}
value={values.parentId ?? 0}
onChange={(e) => setField('parentId', parseInt(e.target.value, 10) || 0)}
aria-invalid={Boolean(errors.parentId)}
aria-describedby={errors.parentId ? 'market-parent-id-error' : undefined}
/>
{errors.parentId && (
<span id="market-parent-id-error" className="market-create__error" role="alert">
{errors.parentId}
</span>
)}
</div>

<div className="market-create__field">
<label htmlFor="market-parent-outcome-idx">Parent outcome index</label>
<input
id="market-parent-outcome-idx"
name="parentOutcomeIdx"
type="number"
min={0}
step={1}
value={values.parentOutcomeIdx ?? 0}
onChange={(e) =>
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 && (
<span
id="market-parent-outcome-idx-error"
className="market-create__error"
role="alert"
>
{errors.parentOutcomeIdx}
</span>
)}
</div>
</fieldset>

<button type="submit" className="market-create__submit" disabled={submitting}>
{submitting ? 'Creating…' : 'Create market'}
</button>
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/lib/api/requestSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createMarketSchema>;

// === Compile-time contract: each schema stays assignable to its generated type

// The generated types carry `email` as a plain `string` (the `format: email`
Expand Down
Loading