Skip to content

Commit cb071e0

Browse files
authored
[codex] Add permissionless market creation flow (#77)
* Add permissionless market creation flow * Wire market planner to registry and FLM actions
1 parent c50c08b commit cb071e0

5 files changed

Lines changed: 1056 additions & 10 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { test } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { readFile } from 'node:fs/promises';
4+
import { dirname, resolve } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
7+
const here = dirname(fileURLToPath(import.meta.url));
8+
const sourcePath = resolve(here, '../../src/features/marketCreation/marketCreationWorkflow.js');
9+
const source = await readFile(sourcePath, 'utf8');
10+
const workflow = await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(source)}`);
11+
12+
const {
13+
buildMetadataDraft,
14+
buildOneStepMarketPlan,
15+
buildPermissionlessStackPlan,
16+
createMarketWizardDefaults,
17+
KNOWN_ORGANIZATIONS,
18+
MARKET_CREATION_STAGES,
19+
PERMISSIONLESS_STACK_STAGES,
20+
validateContractActionDependencies,
21+
validateOneStepMarketPlan,
22+
} = workflow;
23+
24+
const NOW = 1_782_777_600;
25+
26+
test('Kleros defaults use KIP, PNK, sDAI, and FLM liquidity', () => {
27+
const defaults = createMarketWizardDefaults({ organizationId: 'kleros', nowSeconds: NOW });
28+
29+
assert.equal(defaults.organizationName, 'Kleros DAO');
30+
assert.equal(defaults.proposalCode, 'KIP-90');
31+
assert.equal(defaults.companyToken.symbol, 'PNK');
32+
assert.equal(defaults.currencyToken.symbol, 'sDAI');
33+
assert.equal(defaults.initialLiquidityMode, 'flm');
34+
assert.equal(defaults.snapshotLinkAfterLiquidity, true);
35+
});
36+
37+
test('Gnosis defaults use GIP, GNO, sDAI, and Gnosis Snapshot space', () => {
38+
const defaults = createMarketWizardDefaults({ organizationId: 'gnosis', nowSeconds: NOW });
39+
40+
assert.equal(defaults.organizationName, 'Gnosis DAO');
41+
assert.equal(defaults.proposalCode, 'GIP-151');
42+
assert.equal(defaults.companyToken.symbol, 'GNO');
43+
assert.equal(defaults.currencyToken.symbol, 'sDAI');
44+
assert.equal(KNOWN_ORGANIZATIONS.gnosis.snapshotSpace, 'gnosis.eth');
45+
});
46+
47+
test('one-step market plan covers the operational stages in required order', () => {
48+
const plan = buildOneStepMarketPlan({ organizationId: 'kleros', nowSeconds: NOW });
49+
const stageIds = plan.stages.map((stage) => stage.id);
50+
51+
assert.deepEqual(stageIds, MARKET_CREATION_STAGES.map((stage) => stage.id));
52+
assert.ok(stageIds.indexOf('liquidity') < stageIds.indexOf('snapshot'));
53+
assert.ok(stageIds.includes('metadata'));
54+
assert.ok(stageIds.includes('indexing'));
55+
assert.ok(stageIds.includes('arbitrage'));
56+
assert.ok(stageIds.includes('publish'));
57+
});
58+
59+
test('metadata draft includes registry fields needed by market pages and proposal routing', () => {
60+
const draft = buildMetadataDraft({
61+
organizationId: 'kleros',
62+
nowSeconds: NOW,
63+
snapshotId: '0xba2749a4f1283da9d1ca925d9f17bf712fa06a23e6a07d759c54340277820932',
64+
});
65+
66+
assert.equal(draft.chain, 100);
67+
assert.equal(draft.snapshot_id, '0xba2749a4f1283da9d1ca925d9f17bf712fa06a23e6a07d759c54340277820932');
68+
assert.equal(draft.resolution_status, 'unresolved');
69+
assert.equal(draft.visibility, 'public');
70+
assert.equal(draft.companyTokens.base.tokenSymbol, 'PNK');
71+
assert.equal(draft.currencyTokens.base.tokenSymbol, 'sDAI');
72+
assert.equal(draft.flm.mode, 'flm');
73+
assert.equal(draft.snapshot.visibilityMetadata.includeOnSnapshotWebsite, true);
74+
assert.equal(
75+
draft.registry.proposalMetadataMethod,
76+
'FutarchyOrganizationMetadata.createAndAddProposalMetadata'
77+
);
78+
assert.equal(
79+
draft.registry.defaultLiquidityManagerMethod,
80+
'FutarchyOrganizationMetadata.setDefaultLiquidityManager'
81+
);
82+
});
83+
84+
test('permissionless stack plan includes org listing, owner proposals, and default FLM', () => {
85+
const plan = buildPermissionlessStackPlan();
86+
const stageIds = plan.stages.map((stage) => stage.id);
87+
88+
assert.deepEqual(stageIds, PERMISSIONLESS_STACK_STAGES.map((stage) => stage.id));
89+
assert.ok(stageIds.includes('create-organization'));
90+
assert.ok(stageIds.includes('list-organization'));
91+
assert.ok(stageIds.includes('default-flm'));
92+
assert.ok(stageIds.includes('owner-proposal'));
93+
assert.ok(stageIds.indexOf('default-flm') < stageIds.indexOf('create-organization'));
94+
assert.equal(plan.values.chainId, 10200);
95+
});
96+
97+
test('permissionless contract actions create FLM before default organization wiring', () => {
98+
const plan = buildPermissionlessStackPlan();
99+
const actions = plan.contractActions;
100+
const actionIds = actions.map((action) => action.id);
101+
const createFlm = actions.find((action) => action.id === 'create-default-flm-bundle');
102+
const createOrganization = actions.find((action) => action.id === 'create-and-list-organization');
103+
104+
assert.equal(validateContractActionDependencies(actions).ok, true);
105+
assert.equal(createFlm.contract, 'FutarchyLiquidityManagerFactory');
106+
assert.equal(createFlm.method, 'createLiquidityManager');
107+
assert.equal(createOrganization.contract, 'FutarchyAggregatorsMetadata');
108+
assert.equal(createOrganization.method, 'createAndAddOrganizationMetadataWithDefaultLiquidityManager');
109+
assert.ok(createOrganization.dependsOn.includes('create-default-flm-bundle'));
110+
assert.ok(actionIds.indexOf('create-default-flm-bundle') < actionIds.indexOf('create-and-list-organization'));
111+
});
112+
113+
test('one-step market contract actions order market, FLM, official proposal, liquidity, and Snapshot', () => {
114+
const plan = buildOneStepMarketPlan({ organizationId: 'kleros', nowSeconds: NOW });
115+
const actions = plan.contractActions;
116+
const actionIds = actions.map((action) => action.id);
117+
const createMarket = actions.find((action) => action.id === 'create-futarchy-proposal');
118+
const createFlm = actions.find((action) => action.id === 'create-flm-bundle');
119+
const setOfficialProposal = actions.find((action) => action.id === 'set-official-proposal');
120+
const linkSnapshot = actions.find((action) => action.id === 'link-snapshot-proposal');
121+
122+
assert.equal(validateContractActionDependencies(actions).ok, true);
123+
assert.equal(createMarket.contract, 'IFutarchyFactory');
124+
assert.equal(createMarket.method, 'createProposal');
125+
assert.equal(createFlm.contract, 'FutarchyLiquidityManagerFactory');
126+
assert.equal(createFlm.method, 'createLiquidityManager');
127+
assert.equal(setOfficialProposal.contract, 'FutarchyOfficialProposalSource');
128+
assert.equal(setOfficialProposal.method, 'setOfficialProposal');
129+
assert.ok(setOfficialProposal.dependsOn.includes('create-futarchy-proposal'));
130+
assert.ok(linkSnapshot.dependsOn.includes('bootstrap-flm-liquidity'));
131+
assert.ok(actionIds.indexOf('create-flm-bundle') < actionIds.indexOf('set-official-proposal'));
132+
assert.ok(actionIds.indexOf('set-official-proposal') < actionIds.indexOf('bootstrap-flm-liquidity'));
133+
assert.ok(actionIds.indexOf('bootstrap-flm-liquidity') < actionIds.indexOf('link-snapshot-proposal'));
134+
});
135+
136+
test('validation rejects flows that would link Snapshot before liquidity', () => {
137+
const validPlan = buildOneStepMarketPlan({ organizationId: 'gnosis', nowSeconds: NOW });
138+
assert.equal(validateOneStepMarketPlan(validPlan, { nowSeconds: NOW }).ok, true);
139+
140+
const invalidPlan = buildOneStepMarketPlan({
141+
organizationId: 'gnosis',
142+
nowSeconds: NOW,
143+
snapshotLinkAfterLiquidity: false,
144+
});
145+
assert.deepEqual(validateOneStepMarketPlan(invalidPlan).errors, ['snapshotLinkAfterLiquidity']);
146+
});

src/components/futarchyFi/companyList/page/CompaniesPage.jsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState, useEffect } from "react";
2+
import Link from "next/link";
23
import { useAccount } from 'wagmi';
34
import RootLayout from "../../../layout/RootLayout";
45
import { fetchEventHighlightData } from "./EventsHighlightDataTransformer";
@@ -122,10 +123,16 @@ const CompaniesPage = ({ useStorybookUrl = false }) => {
122123
)}
123124

124125
{/* Organizations Section Header */}
125-
<div className="mb-6 mt-12">
126-
<h2 className="text-2xl font-semibold text-futarchyGray12 dark:text-white mb-6">
126+
<div className="mb-6 mt-12 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
127+
<h2 className="text-2xl font-semibold text-futarchyGray12 dark:text-white">
127128
Organizations
128129
</h2>
130+
<Link
131+
href="/markets/new"
132+
className="inline-flex h-10 items-center justify-center rounded-md border border-futarchyGray6 px-4 text-sm font-medium text-futarchyGray12 hover:border-futarchyBlue9 hover:text-futarchyBlue10 dark:border-futarchyGray7 dark:text-white"
133+
>
134+
Create market
135+
</Link>
129136
</div>
130137

131138
{/* Organizations Table (Desktop) */}

0 commit comments

Comments
 (0)