From 3ed59ae4dbfe6534400987ed2e5e92bf429dfc1d Mon Sep 17 00:00:00 2001 From: krandder Date: Tue, 30 Jun 2026 04:24:07 +0100 Subject: [PATCH 1/2] Add permissionless market creation flow --- .../tests/market-creation-workflow.test.mjs | 96 ++++++ .../companyList/page/CompaniesPage.jsx | 11 +- .../createMarket/CreateMarketFlow.jsx | 249 ++++++++++++++ .../marketCreation/marketCreationWorkflow.js | 317 ++++++++++++++++++ src/pages/markets/new/index.jsx | 20 +- 5 files changed, 683 insertions(+), 10 deletions(-) create mode 100644 auto-qa/tests/market-creation-workflow.test.mjs create mode 100644 src/components/futarchyFi/createMarket/CreateMarketFlow.jsx create mode 100644 src/features/marketCreation/marketCreationWorkflow.js diff --git a/auto-qa/tests/market-creation-workflow.test.mjs b/auto-qa/tests/market-creation-workflow.test.mjs new file mode 100644 index 0000000..c14d1ab --- /dev/null +++ b/auto-qa/tests/market-creation-workflow.test.mjs @@ -0,0 +1,96 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const sourcePath = resolve(here, '../../src/features/marketCreation/marketCreationWorkflow.js'); +const source = await readFile(sourcePath, 'utf8'); +const workflow = await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}`); + +const { + buildMetadataDraft, + buildOneStepMarketPlan, + buildPermissionlessStackPlan, + createMarketWizardDefaults, + KNOWN_ORGANIZATIONS, + MARKET_CREATION_STAGES, + PERMISSIONLESS_STACK_STAGES, + validateOneStepMarketPlan, +} = workflow; + +const NOW = 1_782_777_600; + +test('Kleros defaults use KIP, PNK, sDAI, and FLM liquidity', () => { + const defaults = createMarketWizardDefaults({ organizationId: 'kleros', nowSeconds: NOW }); + + assert.equal(defaults.organizationName, 'Kleros DAO'); + assert.equal(defaults.proposalCode, 'KIP-90'); + assert.equal(defaults.companyToken.symbol, 'PNK'); + assert.equal(defaults.currencyToken.symbol, 'sDAI'); + assert.equal(defaults.initialLiquidityMode, 'flm'); + assert.equal(defaults.snapshotLinkAfterLiquidity, true); +}); + +test('Gnosis defaults use GIP, GNO, sDAI, and Gnosis Snapshot space', () => { + const defaults = createMarketWizardDefaults({ organizationId: 'gnosis', nowSeconds: NOW }); + + assert.equal(defaults.organizationName, 'Gnosis DAO'); + assert.equal(defaults.proposalCode, 'GIP-151'); + assert.equal(defaults.companyToken.symbol, 'GNO'); + assert.equal(defaults.currencyToken.symbol, 'sDAI'); + assert.equal(KNOWN_ORGANIZATIONS.gnosis.snapshotSpace, 'gnosis.eth'); +}); + +test('one-step market plan covers the operational stages in required order', () => { + const plan = buildOneStepMarketPlan({ organizationId: 'kleros', nowSeconds: NOW }); + const stageIds = plan.stages.map((stage) => stage.id); + + assert.deepEqual(stageIds, MARKET_CREATION_STAGES.map((stage) => stage.id)); + assert.ok(stageIds.indexOf('liquidity') < stageIds.indexOf('snapshot')); + assert.ok(stageIds.includes('metadata')); + assert.ok(stageIds.includes('indexing')); + assert.ok(stageIds.includes('arbitrage')); + assert.ok(stageIds.includes('publish')); +}); + +test('metadata draft includes registry fields needed by market pages and proposal routing', () => { + const draft = buildMetadataDraft({ + organizationId: 'kleros', + nowSeconds: NOW, + snapshotId: '0xba2749a4f1283da9d1ca925d9f17bf712fa06a23e6a07d759c54340277820932', + }); + + assert.equal(draft.chain, 100); + assert.equal(draft.snapshot_id, '0xba2749a4f1283da9d1ca925d9f17bf712fa06a23e6a07d759c54340277820932'); + assert.equal(draft.resolution_status, 'unresolved'); + assert.equal(draft.visibility, 'public'); + assert.equal(draft.companyTokens.base.tokenSymbol, 'PNK'); + assert.equal(draft.currencyTokens.base.tokenSymbol, 'sDAI'); + assert.equal(draft.flm.mode, 'flm'); +}); + +test('permissionless stack plan includes org listing, owner proposals, and default FLM', () => { + const plan = buildPermissionlessStackPlan(); + const stageIds = plan.stages.map((stage) => stage.id); + + assert.deepEqual(stageIds, PERMISSIONLESS_STACK_STAGES.map((stage) => stage.id)); + assert.ok(stageIds.includes('create-organization')); + assert.ok(stageIds.includes('list-organization')); + assert.ok(stageIds.includes('default-flm')); + assert.ok(stageIds.includes('owner-proposal')); + assert.equal(plan.values.chainId, 10200); +}); + +test('validation rejects flows that would link Snapshot before liquidity', () => { + const validPlan = buildOneStepMarketPlan({ organizationId: 'gnosis', nowSeconds: NOW }); + assert.equal(validateOneStepMarketPlan(validPlan, { nowSeconds: NOW }).ok, true); + + const invalidPlan = buildOneStepMarketPlan({ + organizationId: 'gnosis', + nowSeconds: NOW, + snapshotLinkAfterLiquidity: false, + }); + assert.deepEqual(validateOneStepMarketPlan(invalidPlan).errors, ['snapshotLinkAfterLiquidity']); +}); diff --git a/src/components/futarchyFi/companyList/page/CompaniesPage.jsx b/src/components/futarchyFi/companyList/page/CompaniesPage.jsx index 72de7eb..c0228c0 100644 --- a/src/components/futarchyFi/companyList/page/CompaniesPage.jsx +++ b/src/components/futarchyFi/companyList/page/CompaniesPage.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import Link from "next/link"; import { useAccount } from 'wagmi'; import RootLayout from "../../../layout/RootLayout"; import { fetchEventHighlightData } from "./EventsHighlightDataTransformer"; @@ -122,10 +123,16 @@ const CompaniesPage = ({ useStorybookUrl = false }) => { )} {/* Organizations Section Header */} -
-

+
+

Organizations

+ + Create market +
{/* Organizations Table (Desktop) */} diff --git a/src/components/futarchyFi/createMarket/CreateMarketFlow.jsx b/src/components/futarchyFi/createMarket/CreateMarketFlow.jsx new file mode 100644 index 0000000..b40b175 --- /dev/null +++ b/src/components/futarchyFi/createMarket/CreateMarketFlow.jsx @@ -0,0 +1,249 @@ +import React, { useMemo, useState } from 'react'; +import Link from 'next/link'; +import { + buildOneStepMarketPlan, + buildPermissionlessStackPlan, + createMarketWizardDefaults, + KNOWN_ORGANIZATIONS, +} from '../../../features/marketCreation/marketCreationWorkflow'; +import RootLayout from '../../layout/RootLayout'; +import PageLayout from '../../layout/PageLayout'; + +const panelClass = 'border border-futarchyGray6 dark:border-futarchyGray7 bg-white dark:bg-futarchyGray2 rounded-lg'; +const inputClass = 'w-full px-3 py-2 bg-futarchyGray2 dark:bg-futarchyGray3 border border-futarchyGray6 dark:border-futarchyGray7 rounded-md text-sm text-futarchyGray12 dark:text-white focus:outline-none focus:ring-2 focus:ring-futarchyBlue9'; +const labelClass = 'text-xs font-semibold uppercase tracking-wide text-futarchyGray10 dark:text-futarchyGray11'; + +function formatDate(timestamp) { + if (!timestamp) return 'Not set'; + return new Date(Number(timestamp) * 1000).toISOString().replace('T', ' ').slice(0, 16) + ' UTC'; +} + +function StageList({ stages }) { + return ( +
    + {stages.map((stage) => ( +
  1. +
    {String(stage.order).padStart(2, '0')}
    +
    +
    {stage.title}
    + {stage.dependsOn?.length ? ( +
    After: {stage.dependsOn.join(', ')}
    + ) : null} +
    +
    +

    {stage.summary}

    + {stage.requiredEvidence?.length ? ( +

    + Evidence: {stage.requiredEvidence.join(', ')} +

    + ) : null} +
    +
  2. + ))} +
+ ); +} + +function MetadataPreview({ metadata }) { + return ( +
+      {JSON.stringify(metadata, null, 2)}
+    
+ ); +} + +export default function CreateMarketFlow() { + const [organizationId, setOrganizationId] = useState('kleros'); + const defaults = useMemo( + () => createMarketWizardDefaults({ organizationId }), + [organizationId] + ); + const [form, setForm] = useState(defaults); + + const selectedOrganization = KNOWN_ORGANIZATIONS[organizationId]; + const marketPlan = useMemo(() => buildOneStepMarketPlan({ ...form, organizationId }), [form, organizationId]); + const permissionlessPlan = useMemo(() => buildPermissionlessStackPlan(), []); + + const updateOrganization = (nextOrganizationId) => { + setOrganizationId(nextOrganizationId); + setForm(createMarketWizardDefaults({ organizationId: nextOrganizationId })); + }; + + const updateField = (field, value) => { + setForm((previous) => ({ ...previous, [field]: value })); + }; + + const updateCloseDate = (value) => { + const nextTimestamp = Math.floor(new Date(value).getTime() / 1000); + setForm((previous) => ({ + ...previous, + closeDateTimeLocal: value, + closeTimestamp: nextTimestamp, + twapStartTimestamp: nextTimestamp - (48 * 60 * 60), + startCandleUnix: nextTimestamp - (49 * 60 * 60), + })); + }; + + return ( + + +
+
+
+

Create Market

+

+ A single operational flow for organization setup, proposal metadata, market creation, + FLM liquidity, Snapshot linking, candle readiness, arbitrage setup, and publishing. +

+
+ + Companies + +
+ +
+
+

Permissionless Chiado Stack

+

+ This is the target testnet lifecycle: any wallet creates an organization, it is listed + automatically, and the organization receives a default FLM for proposal liquidity. +

+
+ +
+ +
+
+

Market Defaults

+ +
+
+ + +
+ +
+ + updateField('proposalCode', event.target.value)} + /> +
+ +
+ + updateField('displayTitle0', event.target.value)} + /> + updateField('displayTitle1', event.target.value)} + /> +
+ +
+ +