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
4 changes: 4 additions & 0 deletions .github/workflows/auto-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ on:
- '.github/workflows/auto-qa-live.yml'
- 'auto-qa/**'
- 'package.json'
- 'src/features/marketCreation/**'
- 'src/utils/proposalLifecycle.js'
- 'test/**'
push:
branches:
- main
Expand All @@ -16,7 +18,9 @@ on:
- '.github/workflows/auto-qa-live.yml'
- 'auto-qa/**'
- 'package.json'
- 'src/features/marketCreation/**'
- 'src/utils/proposalLifecycle.js'
- 'test/**'
workflow_dispatch:

jobs:
Expand Down
93 changes: 88 additions & 5 deletions src/components/futarchyFi/createMarket/CreateMarketFlow.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import {
buildOneStepMarketPlan,
Expand All @@ -11,8 +12,10 @@ import {
stepOneBroadcastErrors,
} from '../../../features/marketCreation/marketCreationWorkflow';
import { validateMetadata } from '../../../features/marketCreation/validateMetadata';
import { fetchSnapshotVoteEnd } from '../../../features/marketCreation/snapshotTiming';
import { evaluateFloor, ZERO_TRADE_NOTICE, FLOOR_TRADE_USD, FLOOR_MAX_IMPACT } from '../../../features/marketCreation/liquidityFloor';
import useCreateProposal, { simulateProposal } from '../../debug/hooks/useCreateProposal';
import WizardSteps23 from './WizardSteps23';
import RootLayout from '../../layout/RootLayout';
import PageLayout from '../../layout/PageLayout';

Expand Down Expand Up @@ -152,8 +155,11 @@ function ReadinessPanel({ metadataDraft, bootstrap }) {

// Real, wallet-connected proposal creation. Simulate-first (no broadcast) so the
// flow is demoable end-to-end without minting a market; Broadcast sends the tx.
function ExecutePanel({ form, organization }) {
function ExecutePanel({ form, organization, onProposalCreated }) {
const { isConnected, isSubmitting, status, transactionHash, proposalAddress, createProposal } = useCreateProposal();
useEffect(() => {
if (proposalAddress) onProposalCreated(proposalAddress);
}, [proposalAddress, onProposalCreated]);
const [mode, setMode] = useState('simulate');
const [simResult, setSimResult] = useState(null);
const [isSimulating, setIsSimulating] = useState(false);
Expand Down Expand Up @@ -284,15 +290,45 @@ function ExecutePanel({ form, organization }) {
}

export default function CreateMarketFlow() {
const router = useRouter();
const [organizationId, setOrganizationId] = useState('kleros');
// Steps 2–3 unlock once a proposal exists; ?proposal=0x… resumes after a
// refresh instead of losing progress.
const [proposalAddress, setProposalAddress] = useState(null);
const [proposalOrganizationId, setProposalOrganizationId] = useState(null);
useEffect(() => {
const q = router.query?.proposal;
if (typeof q !== 'string' || !/^0x[a-fA-F0-9]{40}$/.test(q)) return;
const queryOrganization = typeof router.query?.organization === 'string'
&& KNOWN_ORGANIZATIONS[router.query.organization]
? router.query.organization
: null;
setProposalAddress(q);
setProposalOrganizationId(queryOrganization);
if (queryOrganization && queryOrganization !== organizationId) {
setOrganizationId(queryOrganization);
setForm(createMarketWizardDefaults({ organizationId: queryOrganization }));
}
}, [router.query, organizationId]);
const onProposalCreated = useCallback((address) => {
setProposalAddress(address);
setProposalOrganizationId(organizationId);
router.replace({ query: { ...router.query, proposal: address, organization: organizationId } }, undefined, { shallow: true });
}, [organizationId, router]);
// Defaults are Date.now()-derived, and wallet panels use wagmi hooks — both
// must stay out of the static export. form stays null until client mount, so
// the exported HTML carries the page frame but no build-time timestamps
// (which caused React 18 hydration mismatches and days-stale dates).
const [form, setForm] = useState(null);
useEffect(() => {
setForm(createMarketWizardDefaults({ organizationId: 'kleros' }));
}, []);
if (!router.isReady) return;
const queryOrganization = typeof router.query?.organization === 'string'
&& KNOWN_ORGANIZATIONS[router.query.organization]
? router.query.organization
: 'kleros';
setOrganizationId(queryOrganization);
setForm(createMarketWizardDefaults({ organizationId: queryOrganization }));
}, [router.isReady, router.query?.organization]);

const selectedOrganization = KNOWN_ORGANIZATIONS[organizationId];
const marketPlan = useMemo(
Expand All @@ -301,12 +337,33 @@ export default function CreateMarketFlow() {
);

const updateOrganization = (nextOrganizationId) => {
if (proposalAddress) return;
setOrganizationId(nextOrganizationId);
setForm(createMarketWizardDefaults({ organizationId: nextOrganizationId }));
};

const [snapshotNote, setSnapshotNote] = useState(null);

const updateField = (field, value) => {
setForm((previous) => ({ ...previous, [field]: value }));
// Increment D: a pasted Snapshot hash autofills the close time from the
// real vote end, anchoring the whole TWAP window to it.
if (field === 'snapshotId') {
setSnapshotNote(null);
fetchSnapshotVoteEnd(value).then((vote) => {
if (!vote) return;
setForm((previous) => {
if (previous?.snapshotId !== value) return previous; // stale response
return {
...previous,
closeTimestamp: vote.end,
closeDateTimeLocal: new Date(vote.end * 1000).toISOString().slice(0, 16),
...deriveTwapTiming(vote.end, previous.twapDurationHours),
};
});
setSnapshotNote(`Close time autofilled from the Snapshot vote end (${vote.state}).`);
});
}
};

const updateCloseDate = (value) => {
Expand Down Expand Up @@ -358,14 +415,31 @@ export default function CreateMarketFlow() {
</div>
) : (
<>
<div className="mb-2 text-xs text-futarchyGray10">
Step 1 {proposalAddress ? '✓' : '·'} create proposal → Step 2 · metadata → Step 3 · pools + invert check
</div>
<div className="grid gap-6 lg:grid-cols-2 mb-6">
<ExecutePanel form={form} organization={selectedOrganization} />
<ExecutePanel form={form} organization={selectedOrganization} onProposalCreated={onProposalCreated} />
<ReadinessPanel
metadataDraft={marketPlan.metadataDraft}
bootstrap={form.initialLiquidityBudget}
/>
</div>

{proposalAddress && proposalOrganizationId === organizationId && (
<WizardSteps23
proposalAddress={proposalAddress}
form={form}
organization={selectedOrganization}
organizationId={organizationId}
/>
)}
{proposalAddress && (!proposalOrganizationId || proposalOrganizationId !== organizationId) && (
<p className="mt-6 text-sm text-amber-600 dark:text-amber-400">
This proposal is not safely bound to the selected organization. Restart the wizard instead of writing mismatched metadata.
</p>
)}

<div className="grid gap-6 lg:grid-cols-[360px_1fr]">
<section className={`${panelClass} p-4`}>
<h2 className="text-lg font-semibold text-futarchyGray12 dark:text-white">Market Defaults</h2>
Expand All @@ -377,12 +451,18 @@ export default function CreateMarketFlow() {
id="organization"
className={`${inputClass} mt-1`}
value={organizationId}
disabled={Boolean(proposalAddress)}
onChange={(event) => updateOrganization(event.target.value)}
>
{Object.values(KNOWN_ORGANIZATIONS).map((org) => (
<option key={org.id} value={org.id}>{org.name}</option>
))}
</select>
{proposalAddress && (
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
Organization is locked after proposal creation so metadata cannot be written to a different organization.
</p>
)}
</div>

<div>
Expand Down Expand Up @@ -430,6 +510,9 @@ export default function CreateMarketFlow() {
value={form.snapshotId}
onChange={(event) => updateField('snapshotId', event.target.value)}
/>
{snapshotNote && (
<p className="mt-1 text-xs text-emerald-600 dark:text-emerald-400">{snapshotNote}</p>
)}
</div>

<div>
Expand Down
Loading
Loading